Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a76bd11d9 | |||
| f1e91f7639 | |||
| 4feca77924 | |||
| b12f359da1 | |||
| 8901792561 |
@@ -17,8 +17,8 @@ android {
|
|||||||
applicationId = "com.aiosman.ravenow"
|
applicationId = "com.aiosman.ravenow"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1000019
|
versionCode = 1000021
|
||||||
versionName = "1.0.000.19"
|
versionName = "1.0.000.21"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -140,5 +140,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
|
|
||||||
|
// Google Play Billing
|
||||||
|
implementation(libs.billing.ktx)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
android:theme="@style/Theme.App.Starting"
|
android:theme="@style/Theme.App.Starting"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:configChanges="fontScale|orientation|screenSize|keyboardHidden|uiMode">
|
android:configChanges="fontScale|orientation|screenSize|keyboardHidden">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@@ -73,10 +73,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
val config = Configuration(newConfig)
|
val config = Configuration(newConfig)
|
||||||
config.fontScale = 1.0f
|
config.fontScale = 1.0f
|
||||||
super.onConfigurationChanged(config)
|
super.onConfigurationChanged(config)
|
||||||
val isNightMode = (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
|
||||||
if (AppState.darkMode != isNightMode) {
|
|
||||||
syncDarkModeWithSystem(isNightMode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求通知权限
|
// 请求通知权限
|
||||||
@@ -133,7 +129,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
JPushInterface.init(this)
|
JPushInterface.init(this)
|
||||||
|
|
||||||
updateWindowBackground(AppState.darkMode)
|
if (AppState.darkMode) {
|
||||||
|
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
|
||||||
|
}
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -271,22 +269,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun syncDarkModeWithSystem(isNightMode: Boolean) {
|
|
||||||
AppState.darkMode = isNightMode
|
|
||||||
AppState.appTheme = if (isNightMode) DarkThemeColors() else LightThemeColors()
|
|
||||||
AppStore.saveDarkMode(isNightMode)
|
|
||||||
updateWindowBackground(isNightMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateWindowBackground(isDarkMode: Boolean) {
|
|
||||||
window.decorView.setBackgroundColor(
|
|
||||||
if (isDarkMode) android.graphics.Color.BLACK else android.graphics.Color.WHITE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val LocalNavController = compositionLocalOf<NavHostController> {
|
val LocalNavController = compositionLocalOf<NavHostController> {
|
||||||
error("NavController not provided")
|
error("NavController not provided")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.android.billingclient.api.BillingClient
|
||||||
|
import com.android.billingclient.api.BillingClientStateListener
|
||||||
|
import com.android.billingclient.api.BillingResult
|
||||||
|
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||||
import com.google.firebase.FirebaseApp
|
import com.google.firebase.FirebaseApp
|
||||||
import com.google.firebase.perf.FirebasePerformance
|
import com.google.firebase.perf.FirebasePerformance
|
||||||
|
|
||||||
@@ -12,6 +16,8 @@ import com.google.firebase.perf.FirebasePerformance
|
|||||||
*/
|
*/
|
||||||
class RaveNowApplication : Application() {
|
class RaveNowApplication : Application() {
|
||||||
|
|
||||||
|
private var billingClient: BillingClient? = null
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
// 禁用字体缩放,固定字体大小为系统默认大小
|
// 禁用字体缩放,固定字体大小为系统默认大小
|
||||||
val configuration = Configuration(base.resources.configuration)
|
val configuration = Configuration(base.resources.configuration)
|
||||||
@@ -48,6 +54,53 @@ class RaveNowApplication : Application() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("RaveNowApplication", "Firebase初始化失败在进程 $processName", e)
|
Log.e("RaveNowApplication", "Firebase初始化失败在进程 $processName", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化 Google Play Billing
|
||||||
|
initBillingClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Google Play Billing Client
|
||||||
|
*/
|
||||||
|
private fun initBillingClient() {
|
||||||
|
val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
|
||||||
|
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
|
||||||
|
// 处理购买成功
|
||||||
|
Log.d("RaveNowApplication", "购买成功: ${purchases.size} 个商品")
|
||||||
|
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
|
||||||
|
// 用户取消购买
|
||||||
|
Log.d("RaveNowApplication", "用户取消购买")
|
||||||
|
} else {
|
||||||
|
// 处理其他错误
|
||||||
|
Log.e("RaveNowApplication", "购买失败: ${billingResult.debugMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
billingClient = BillingClient.newBuilder(this)
|
||||||
|
.setListener(purchasesUpdatedListener)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
billingClient?.startConnection(object : BillingClientStateListener {
|
||||||
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||||
|
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||||
|
Log.d("RaveNowApplication", "BillingClient 初始化成功")
|
||||||
|
} else {
|
||||||
|
Log.e("RaveNowApplication", "BillingClient 初始化失败: ${billingResult.debugMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBillingServiceDisconnected() {
|
||||||
|
Log.w("RaveNowApplication", "BillingClient 连接断开,尝试重新连接")
|
||||||
|
// 可以在这里实现重连逻辑
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 BillingClient 实例
|
||||||
|
*/
|
||||||
|
fun getBillingClient(): BillingClient? {
|
||||||
|
return billingClient
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -545,13 +545,7 @@ class AccountServiceImpl : AccountService {
|
|||||||
val bannerField: MultipartBody.Part? = banner?.let {
|
val bannerField: MultipartBody.Part? = banner?.let {
|
||||||
createMultipartBody(it.file, it.filename, "banner")
|
createMultipartBody(it.file, it.filename, "banner")
|
||||||
}
|
}
|
||||||
val resp = ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
|
ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
|
||||||
if (!resp.isSuccessful) {
|
|
||||||
parseErrorResponse(resp.errorBody())?.let {
|
|
||||||
throw it.toServiceException()
|
|
||||||
}
|
|
||||||
throw ServiceException("Failed to update profile")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun registerUserWithPassword(loginName: String, password: String) {
|
override suspend fun registerUserWithPassword(loginName: String, password: String) {
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ data class Moment(
|
|||||||
val commentCount: Long,
|
val commentCount: Long,
|
||||||
@SerializedName("time")
|
@SerializedName("time")
|
||||||
val time: String?,
|
val time: String?,
|
||||||
|
@SerializedName("createdAt")
|
||||||
|
val createdAt: String? = null,
|
||||||
|
@SerializedName("location")
|
||||||
|
val location: String? = null,
|
||||||
@SerializedName("isFollowed")
|
@SerializedName("isFollowed")
|
||||||
val isFollowed: Boolean,
|
val isFollowed: Boolean,
|
||||||
// 新闻相关字段
|
// 新闻相关字段
|
||||||
@@ -70,11 +74,11 @@ data class Moment(
|
|||||||
"" // 如果头像为空,使用空字符串
|
"" // 如果头像为空,使用空字符串
|
||||||
},
|
},
|
||||||
nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值
|
nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值
|
||||||
location = "Worldwide",
|
location = location ?: "Worldwide",
|
||||||
time = if (time != null && time.isNotEmpty()) {
|
time = when {
|
||||||
ApiClient.dateFromApiString(time)
|
createdAt != null && createdAt.isNotEmpty() -> ApiClient.dateFromApiString(createdAt)
|
||||||
} else {
|
time != null && time.isNotEmpty() -> ApiClient.dateFromApiString(time)
|
||||||
java.util.Date() // 如果时间为空,使用当前时间作为默认值
|
else -> java.util.Date() // 如果时间为空,使用当前时间作为默认值
|
||||||
},
|
},
|
||||||
followStatus = isFollowed,
|
followStatus = isFollowed,
|
||||||
momentTextContent = textContent,
|
momentTextContent = textContent,
|
||||||
@@ -204,7 +208,7 @@ data class Video(
|
|||||||
data class User(
|
data class User(
|
||||||
@SerializedName("id")
|
@SerializedName("id")
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@SerializedName("nickName")
|
@SerializedName("nickname")
|
||||||
val nickName: String?,
|
val nickName: String?,
|
||||||
@SerializedName("avatar")
|
@SerializedName("avatar")
|
||||||
val avatar: String?,
|
val avatar: String?,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.icu.util.Calendar
|
import android.icu.util.Calendar
|
||||||
import com.aiosman.ravenow.ConstVars
|
import com.aiosman.ravenow.ConstVars
|
||||||
import com.aiosman.ravenow.exp.formatChatTime
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
|
import com.aiosman.ravenow.utils.NotificationMessageHelper
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import io.openim.android.sdk.models.Message
|
import io.openim.android.sdk.models.Message
|
||||||
import io.openim.android.sdk.models.PictureElem
|
import io.openim.android.sdk.models.PictureElem
|
||||||
@@ -21,7 +22,8 @@ data class ChatItem(
|
|||||||
val textDisplay: String = "",
|
val textDisplay: String = "",
|
||||||
val msgId: String, // Add this property
|
val msgId: String, // Add this property
|
||||||
var showTimestamp: Boolean = false,
|
var showTimestamp: Boolean = false,
|
||||||
var showTimeDivider: Boolean = false
|
var showTimeDivider: Boolean = false,
|
||||||
|
val isNotification: Boolean = false // 标识是否为通知类型消息
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
// OpenIM 消息类型常量
|
// OpenIM 消息类型常量
|
||||||
@@ -36,6 +38,32 @@ data class ChatItem(
|
|||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
calendar.timeInMillis = timestamp
|
calendar.timeInMillis = timestamp
|
||||||
|
|
||||||
|
// 检查是否为通知类型消息
|
||||||
|
// 1. 检查消息类型是否为通知类型
|
||||||
|
// 2. 检查发送者ID是否为系统账户(如 "imAdmin"、"administrator" 等)
|
||||||
|
val sendID = message.sendID ?: ""
|
||||||
|
val isSystemAccount = sendID == "imAdmin" || sendID == "administrator" || sendID.isEmpty()
|
||||||
|
val isNotificationType = OpenIMMessageType.isNotification(message.contentType)
|
||||||
|
val isNotification = isNotificationType || isSystemAccount
|
||||||
|
|
||||||
|
// 如果是通知类型消息,使用特殊处理
|
||||||
|
if (isNotification) {
|
||||||
|
val notificationText = NotificationMessageHelper.getNotificationText(message)
|
||||||
|
return ChatItem(
|
||||||
|
message = notificationText,
|
||||||
|
avatar = "", // 通知消息不显示头像
|
||||||
|
time = calendar.time.formatChatTime(context),
|
||||||
|
userId = sendID.ifEmpty { "system" },
|
||||||
|
nickname = "", // 通知消息不显示昵称
|
||||||
|
timestamp = timestamp,
|
||||||
|
imageList = emptyList<PictureInfo>().toMutableList(),
|
||||||
|
messageType = message.contentType,
|
||||||
|
textDisplay = notificationText,
|
||||||
|
msgId = message.clientMsgID,
|
||||||
|
isNotification = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var faceAvatar = avatar
|
var faceAvatar = avatar
|
||||||
if (faceAvatar == null) {
|
if (faceAvatar == null) {
|
||||||
faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}"
|
faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}"
|
||||||
@@ -62,7 +90,8 @@ data class ChatItem(
|
|||||||
).toMutableList(),
|
).toMutableList(),
|
||||||
messageType = MESSAGE_TYPE_IMAGE,
|
messageType = MESSAGE_TYPE_IMAGE,
|
||||||
textDisplay = "Image",
|
textDisplay = "Image",
|
||||||
msgId = message.clientMsgID
|
msgId = message.clientMsgID,
|
||||||
|
isNotification = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -79,7 +108,8 @@ data class ChatItem(
|
|||||||
imageList = emptyList<PictureInfo>().toMutableList(),
|
imageList = emptyList<PictureInfo>().toMutableList(),
|
||||||
messageType = MESSAGE_TYPE_TEXT,
|
messageType = MESSAGE_TYPE_TEXT,
|
||||||
textDisplay = message.textElem?.content ?: "Unsupported message type",
|
textDisplay = message.textElem?.content ?: "Unsupported message type",
|
||||||
msgId = message.clientMsgID
|
msgId = message.clientMsgID,
|
||||||
|
isNotification = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -410,16 +410,7 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
fun updateMomentLike(id: Int,isLike:Boolean) {
|
fun updateMomentLike(id: Int,isLike:Boolean) {
|
||||||
this.list = this.list.map { momentItem ->
|
this.list = this.list.map { momentItem ->
|
||||||
if (momentItem.id == id) {
|
if (momentItem.id == id) {
|
||||||
// 只有当状态发生变化时才更新计数,避免重复更新
|
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike)
|
||||||
val countDelta = if (momentItem.liked != isLike) {
|
|
||||||
if (isLike) 1 else -1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
momentItem.copy(
|
|
||||||
likeCount = (momentItem.likeCount + countDelta).coerceAtLeast(0),
|
|
||||||
liked = isLike
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
momentItem
|
momentItem
|
||||||
}
|
}
|
||||||
@@ -430,16 +421,7 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
|
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
|
||||||
this.list = this.list.map { momentItem ->
|
this.list = this.list.map { momentItem ->
|
||||||
if (momentItem.id == id) {
|
if (momentItem.id == id) {
|
||||||
// 只有当状态发生变化时才更新计数,避免重复更新
|
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
|
||||||
val countDelta = if (momentItem.isFavorite != isFavorite) {
|
|
||||||
if (isFavorite) 1 else -1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
momentItem.copy(
|
|
||||||
favoriteCount = (momentItem.favoriteCount + countDelta).coerceAtLeast(0),
|
|
||||||
isFavorite = isFavorite
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
momentItem
|
momentItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.aiosman.ravenow.entity
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.aiosman.ravenow.data.ListContainer
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
@@ -273,41 +272,19 @@ class RoomRemoteDataSource {
|
|||||||
pageSize: Int = 20,
|
pageSize: Int = 20,
|
||||||
search: String
|
search: String
|
||||||
): ListContainer<RoomEntity>? {
|
): ListContainer<RoomEntity>? {
|
||||||
return try {
|
|
||||||
val resp = ApiClient.api.getRooms(
|
val resp = ApiClient.api.getRooms(
|
||||||
page = pageNumber,
|
page = pageNumber,
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
search = search,
|
search = search,
|
||||||
roomType = "public" // 搜索时只显示公有房间
|
roomType = "public" // 搜索时只显示公有房间
|
||||||
)
|
)
|
||||||
if (!resp.isSuccessful) {
|
|
||||||
// API 调用失败,返回 null
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val body = resp.body() ?: return null
|
val body = resp.body() ?: return null
|
||||||
|
return ListContainer(
|
||||||
// 安全地转换数据,过滤掉转换失败的项目
|
|
||||||
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,
|
total = body.total,
|
||||||
page = pageNumber,
|
page = pageNumber,
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
list = roomList
|
list = body.list.map { it.toRoomtEntity() }
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
|
||||||
// 捕获所有异常,返回 null 让 PagingSource 处理
|
|
||||||
Log.e("RoomRemoteDataSource", "searchRooms error", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,31 +303,17 @@ class RoomSearchPagingSource(
|
|||||||
pageSize = params.loadSize,
|
pageSize = params.loadSize,
|
||||||
search = keyword
|
search = keyword
|
||||||
)
|
)
|
||||||
if (rooms == null) {
|
|
||||||
// API 调用失败,返回空列表
|
|
||||||
LoadResult.Page(
|
LoadResult.Page(
|
||||||
data = emptyList(),
|
data = rooms?.list ?: listOf(),
|
||||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||||
nextKey = null
|
nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null
|
||||||
)
|
)
|
||||||
} else {
|
} catch (exception: IOException) {
|
||||||
LoadResult.Page(
|
|
||||||
data = rooms.list,
|
|
||||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
|
||||||
nextKey = if (rooms.list.isNotEmpty() && rooms.list.size >= params.loadSize) currentPage + 1 else null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
// 捕获所有异常,包括 IOException、ServiceException 等
|
|
||||||
LoadResult.Error(exception)
|
LoadResult.Error(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
|
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
|
||||||
// 更健壮的实现:根据 anchorPosition 计算刷新键
|
return state.anchorPosition
|
||||||
return state.anchorPosition?.let { anchorPosition ->
|
|
||||||
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
|
||||||
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ package com.aiosman.ravenow
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Configuration
|
|
||||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,16 +24,11 @@ object AppStore {
|
|||||||
.requestEmail()
|
.requestEmail()
|
||||||
.build()
|
.build()
|
||||||
googleSignInOptions = gso
|
googleSignInOptions = gso
|
||||||
// apply dark mode - 如果用户未手动设置,优先跟随系统
|
// apply dark mode
|
||||||
val hasUserPreference = sharedPreferences.contains("darkMode")
|
if (sharedPreferences.getBoolean("darkMode", false)) {
|
||||||
val resolvedDarkMode = if (hasUserPreference) {
|
AppState.darkMode = true
|
||||||
sharedPreferences.getBoolean("darkMode", false)
|
AppState.appTheme = DarkThemeColors()
|
||||||
} else {
|
|
||||||
val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
||||||
currentNightMode == Configuration.UI_MODE_NIGHT_YES
|
|
||||||
}
|
}
|
||||||
AppState.darkMode = resolvedDarkMode
|
|
||||||
AppState.appTheme = if (resolvedDarkMode) DarkThemeColors() else LightThemeColors()
|
|
||||||
|
|
||||||
// load chat background
|
// load chat background
|
||||||
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
|
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.aiosman.ravenow.ui.about
|
|||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -41,21 +40,22 @@ fun AboutScreen() {
|
|||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(
|
NoticeScreenHeader(
|
||||||
title = stringResource(R.string.about_paipai),
|
title = stringResource(R.string.about_rave_now),
|
||||||
moreIcon = false
|
moreIcon = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
.padding(start = 24.dp),
|
||||||
verticalArrangement = Arrangement.Center
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// app icondww
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
// app icon
|
||||||
Box {
|
Box {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.invalid_name),
|
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
|
||||||
contentDescription = "app icon",
|
contentDescription = "app icon",
|
||||||
modifier = Modifier.size(80.dp)
|
modifier = Modifier.size(80.dp)
|
||||||
)
|
)
|
||||||
@@ -63,7 +63,7 @@ fun AboutScreen() {
|
|||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
// app name
|
// app name
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.paipai),
|
text = "Rave Now".uppercase(),
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
color = appColors.text,
|
color = appColors.text,
|
||||||
fontWeight = FontWeight.ExtraBold
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
|||||||
@@ -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() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,62 +1,42 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBars
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
|
||||||
// MBTI类型列表
|
// MBTI类型列表
|
||||||
val MBTI_TYPES = listOf(
|
val MBTI_TYPES = listOf(
|
||||||
@@ -66,318 +46,96 @@ val MBTI_TYPES = listOf(
|
|||||||
"ISTP", "ISFP", "ESTP", "ESFP"
|
"ISTP", "ISFP", "ESTP", "ESFP"
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getMbtiImageResId(mbti: String, isDarkMode: Boolean): Int {
|
|
||||||
return when {
|
|
||||||
isDarkMode && mbti == "ENTP" -> R.mipmap.anmbti_entp
|
|
||||||
isDarkMode && mbti == "ESTP" -> R.mipmap.anmbti_estp
|
|
||||||
isDarkMode && mbti == "ENTJ" -> R.mipmap.anmbti_entj
|
|
||||||
else -> when (mbti) {
|
|
||||||
"INTJ" -> R.mipmap.mbti_intj
|
|
||||||
"INTP" -> R.mipmap.mbti_intp
|
|
||||||
"ENTJ" -> R.mipmap.mbti_entj
|
|
||||||
"ENTP" -> R.mipmap.mbti_entp
|
|
||||||
"INFJ" -> R.mipmap.mbti_infj
|
|
||||||
"INFP" -> R.mipmap.mbti_infp
|
|
||||||
"ENFJ" -> R.mipmap.mbti_enfj
|
|
||||||
"ENFP" -> R.mipmap.mbti_enfp
|
|
||||||
"ISTJ" -> R.mipmap.mbti_istj
|
|
||||||
"ISFJ" -> R.mipmap.mbti_isfj
|
|
||||||
"ESTJ" -> R.mipmap.mbti_estj
|
|
||||||
"ESFJ" -> R.mipmap.mbti_esfj
|
|
||||||
"ISTP" -> R.mipmap.mbti_istp
|
|
||||||
"ISFP" -> R.mipmap.mbti_isfp
|
|
||||||
"ESTP" -> R.mipmap.mbti_estp
|
|
||||||
"ESFP" -> R.mipmap.mbti_esfp
|
|
||||||
else -> R.mipmap.xingzuo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MbtiSelectBottomSheet(
|
fun MbtiSelectScreen() {
|
||||||
onClose: () -> Unit
|
val navController = LocalNavController.current
|
||||||
) {
|
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
val model = AccountEditViewModel
|
val model = AccountEditViewModel
|
||||||
val currentMbti = model.mbti
|
val currentMbti = model.mbti
|
||||||
val sheetBackgroundColor = if (isDarkMode) {
|
|
||||||
appColors.secondaryBackground
|
|
||||||
} else {
|
|
||||||
Color(0xFFFFFFFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onClose,
|
|
||||||
sheetState = sheetState,
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
|
||||||
dragHandle = {}
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
|
|
||||||
color = sheetBackgroundColor,
|
|
||||||
tonalElevation = 0.dp,
|
|
||||||
shadowElevation = 0.dp,
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.fillMaxHeight()
|
.background(appColors.profileBackground)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
// 头部
|
// 头部
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(48.dp),
|
.padding(vertical = 16.dp)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
val cancelButtonGradientColors = if (isDarkMode) {
|
NoticeScreenHeader(
|
||||||
listOf(
|
title = stringResource(R.string.choose_mbti),
|
||||||
Color(0xFF3A3A3C),
|
moreIcon = false
|
||||||
Color(0xFF2C2C2E)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
Color(0xFFFFFFFF),
|
|
||||||
Color(0xFFF8F8F8)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxWidth()
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.weight(1f)
|
|
||||||
.nestedScroll(nestedScrollConnection),
|
|
||||||
contentPadding = PaddingValues(
|
|
||||||
start = 8.dp,
|
|
||||||
top = 0.dp,
|
|
||||||
end = 8.dp,
|
|
||||||
bottom = 8.dp
|
|
||||||
),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
) {
|
||||||
// 说明文字
|
items(MBTI_TYPES) { mbti ->
|
||||||
item {
|
MBTIItem(
|
||||||
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,
|
mbti = mbti,
|
||||||
isSelected = mbti == currentMbti,
|
isSelected = mbti == currentMbti,
|
||||||
onClick = {
|
onClick = {
|
||||||
model.mbti = mbti
|
model.mbti = mbti
|
||||||
onClose()
|
// 立即保存到本地存储,确保选择后立即生效
|
||||||
|
AppState.UserId?.let { uid ->
|
||||||
|
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rowItems.size == 1) {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保留原有的 MbtiSelectScreen 用于导航路由(如果需要)
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun MbtiSelectScreen() {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
MbtiSelectBottomSheet(
|
|
||||||
onClose = {
|
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MbtiItem(
|
fun MBTIItem(
|
||||||
mbti: String,
|
mbti: String,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
|
|
||||||
// 卡片背景色
|
Box(
|
||||||
val cardBackgroundColor = if (isDarkMode) {
|
|
||||||
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
|
|
||||||
} else {
|
|
||||||
Color(0xFFFAF9FB)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1.1f)
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.shadow(
|
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
|
||||||
elevation = if (isDarkMode) 8.dp else 2.dp,
|
|
||||||
shape = RoundedCornerShape(21.dp),
|
|
||||||
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(21.dp))
|
|
||||||
.background(cardBackgroundColor)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(16.dp)
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
) {
|
||||||
verticalArrangement = Arrangement.Center
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 直接把 MBTI 图标和文字放在灰色卡片内部,布局与星座保持一致
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = getMbtiImageResId(mbti, isDarkMode)),
|
|
||||||
contentDescription = mbti,
|
|
||||||
modifier = Modifier.size(96.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(0.dp))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = mbti,
|
text = mbti,
|
||||||
fontSize = 14.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.text,
|
color = if (isSelected) appColors.main else appColors.text,
|
||||||
textAlign = TextAlign.Center,
|
modifier = Modifier.weight(1f)
|
||||||
modifier = Modifier.offset(y = (-10).dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = "Selected",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = appColors.main
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -48,7 +47,6 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
@@ -137,36 +135,48 @@ fun ZodiacSelectBottomSheet(
|
|||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
// 确保弹窗展开
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
sheetState.expand()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听状态变化,确保弹窗始终展开(防止拖拽关闭和滑动)
|
||||||
|
LaunchedEffect(sheetState.currentValue, sheetState.targetValue, sheetState.isVisible) {
|
||||||
|
// 如果弹窗被拖拽关闭或位置发生变化,立即重新展开
|
||||||
|
if (!sheetState.isVisible || sheetState.targetValue != androidx.compose.material3.SheetValue.Expanded) {
|
||||||
|
kotlinx.coroutines.delay(10) // 短暂延迟确保状态更新
|
||||||
|
sheetState.expand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
|
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onClose,
|
onDismissRequest = onClose,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
// 对齐发布动态草稿箱样式:底层透明,内容区域自己绘制圆角和背景
|
containerColor = sheetBackgroundColor, // 根据主题自适应背景
|
||||||
containerColor = Color.Transparent,
|
dragHandle = null
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
|
||||||
dragHandle = {}
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight()
|
.fillMaxHeight(0.95f)
|
||||||
.padding(top = 8.dp)
|
.offset(y = offsetY)
|
||||||
) {
|
.padding(
|
||||||
Surface(
|
start = 16.dp,
|
||||||
modifier = Modifier
|
end = 16.dp,
|
||||||
.fillMaxWidth()
|
bottom = 8.dp
|
||||||
.fillMaxHeight()
|
)
|
||||||
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
|
|
||||||
color = sheetBackgroundColor,
|
|
||||||
tonalElevation = 0.dp,
|
|
||||||
shadowElevation = 0.dp,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
// 头部 - 使用 Box 实现绝对居中布局(对齐草稿箱样式)
|
// 头部 - 使用 Box 实现绝对居中布局
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -186,28 +196,24 @@ fun ZodiacSelectBottomSheet(
|
|||||||
}
|
}
|
||||||
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
|
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
|
||||||
|
|
||||||
// 左上角返回按钮:参考 iOS 设计,整体「箭头 + 取消」在 91x44 的按钮内居中
|
// 左上角返回按钮 - 根据 Swift 代码样式,带淡灰色渐变背景
|
||||||
Box(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterStart)
|
.align(Alignment.CenterStart)
|
||||||
.width(91.dp)
|
.height(36.dp)
|
||||||
.height(44.dp)
|
.clip(RoundedCornerShape(18.dp)) // 圆角 100.0 在 36dp 高度下接近完全圆角
|
||||||
.clip(RoundedCornerShape(1000.dp))
|
|
||||||
.background(
|
.background(
|
||||||
brush = Brush.linearGradient(
|
brush = Brush.linearGradient(
|
||||||
colors = cancelButtonGradientColors
|
colors = cancelButtonGradientColors
|
||||||
|
// 不指定 start 和 end,默认从左上到右下
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.noRippleClickable { onClose() },
|
.noRippleClickable { onClose() }
|
||||||
contentAlignment = Alignment.Center
|
.padding(horizontal = 8.dp), // 内部 padding 确保内容不贴边
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
// 不再固定宽度,让内容自然占位,避免裁剪掉“消”字
|
|
||||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
// 左箭头图标
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -215,20 +221,17 @@ fun ZodiacSelectBottomSheet(
|
|||||||
colorFilter = ColorFilter.tint(cancelButtonContentColor)
|
colorFilter = ColorFilter.tint(cancelButtonContentColor)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
// "取消" 文字
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "取消",
|
text = "取消",
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = cancelButtonContentColor,
|
color = cancelButtonContentColor,
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Clip
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// 中间标题 - 绝对居中
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.choose_zodiac),
|
text = stringResource(R.string.choose_zodiac),
|
||||||
color = appColors.text,
|
color = appColors.text,
|
||||||
@@ -240,34 +243,32 @@ fun ZodiacSelectBottomSheet(
|
|||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
// 创建 NestedScrollConnection:
|
// 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet
|
||||||
// 1. 不抢在列表前面消费事件,让 LazyVerticalGrid 正常滚动
|
|
||||||
// 2. 在列表滚动之后把剩余滚动吃掉,避免继续传递到 BottomSheet 去触发下拉关闭
|
|
||||||
val nestedScrollConnection = remember {
|
val nestedScrollConnection = remember {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
// 不在这里消费,先让 LazyVerticalGrid 自己处理滚动
|
// 不消费任何事件,让 LazyVerticalGrid 先处理
|
||||||
return Offset.Zero
|
return Offset.Zero
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
// 列表滚动完之后,把剩余滚动(尤其是向下拖拽)全部吃掉,防止再传给 BottomSheet
|
// 消费 LazyVerticalGrid 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
|
||||||
return available
|
return available
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
// 不抢在列表前面处理 fling,让 LazyVerticalGrid 先做惯性滚动
|
// 不消费惯性滚动,让 LazyVerticalGrid 先处理
|
||||||
return Velocity.Zero
|
return Velocity.Zero
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
// 列表惯性滚动之后,把剩余的 fling 速度吃掉,避免带动 BottomSheet 下滑关闭
|
// 消费 LazyVerticalGrid 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
|
||||||
return available
|
return available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网格列表 - 2列(与草稿箱一样放在内容区域内部滚动)
|
// 网格列表 - 2列
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(2),
|
columns = GridCells.Fixed(2),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -290,7 +291,12 @@ fun ZodiacSelectBottomSheet(
|
|||||||
zodiacResId = zodiacResId,
|
zodiacResId = zodiacResId,
|
||||||
isSelected = zodiacResId == currentZodiacResId,
|
isSelected = zodiacResId == currentZodiacResId,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
// 保存当前语言的星座文本
|
||||||
model.zodiac = zodiacText
|
model.zodiac = zodiacText
|
||||||
|
// 立即保存到本地存储,确保选择后立即生效
|
||||||
|
AppState.UserId?.let { uid ->
|
||||||
|
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
|
||||||
|
}
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -299,7 +305,6 @@ fun ZodiacSelectBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保留原有的 ZodiacSelectScreen 用于导航路由(如果需要)
|
// 保留原有的 ZodiacSelectScreen 用于导航路由(如果需要)
|
||||||
@@ -335,9 +340,9 @@ fun ZodiacItem(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1.1f)
|
.aspectRatio(1.1f) // 增加宽高比,使高度相对更低
|
||||||
.shadow(
|
.shadow(
|
||||||
elevation = if (isDarkMode) 8.dp else 2.dp,
|
elevation = if (isDarkMode) 8.dp else 2.dp, // 深色模式下更强的阴影
|
||||||
shape = RoundedCornerShape(21.dp),
|
shape = RoundedCornerShape(21.dp),
|
||||||
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
|
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
|
||||||
)
|
)
|
||||||
@@ -349,27 +354,30 @@ fun ZodiacItem(
|
|||||||
) {
|
) {
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 24.dp, vertical = 12.dp), // 减小垂直padding,确保文本不被遮挡
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
// 直接把图标和文字放在灰色卡片内部,不再额外嵌套一层 Box
|
// 星座图标 - 使用对应星座的图片
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(100.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = getZodiacImageResId(zodiacResId)),
|
painter = painterResource(id = getZodiacImageResId(zodiacResId)),
|
||||||
contentDescription = zodiac,
|
contentDescription = zodiac,
|
||||||
// 图标稍微放大一些,让视觉更聚焦在星座图标上
|
modifier = Modifier.size(100.dp)
|
||||||
modifier = Modifier.size(96.dp)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(0.dp))
|
// 星座名称 - 使用负间距让文本向上移动,与图标更靠近
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = zodiac,
|
text = zodiac,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = appColors.text,
|
color = appColors.text,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.offset(y = (-10).dp) // 再整体向上偏移 5dp(共 10dp)
|
modifier = Modifier.offset(y = (-20).dp) // 负间距,让文本进一步向上移动
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.aiosman.ravenow.AppState
|
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
@@ -70,19 +68,6 @@ fun ChangePasswordScreen() {
|
|||||||
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
||||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
// 暗色模式下的 hint 文本颜色
|
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
val hintColor = if (isDarkMode) {
|
|
||||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
|
||||||
} else {
|
|
||||||
null // 使用默认颜色
|
|
||||||
}
|
|
||||||
val labelColor = if (isDarkMode) {
|
|
||||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
|
||||||
} else {
|
|
||||||
null // 使用默认颜色
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validate(): Boolean {
|
fun validate(): Boolean {
|
||||||
// 使用通用密码校验器校验当前密码
|
// 使用通用密码校验器校验当前密码
|
||||||
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
|
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
|
||||||
@@ -127,9 +112,7 @@ fun ChangePasswordScreen() {
|
|||||||
password = true,
|
password = true,
|
||||||
label = stringResource(R.string.current_password),
|
label = stringResource(R.string.current_password),
|
||||||
hint = stringResource(R.string.current_password_tip5),
|
hint = stringResource(R.string.current_password_tip5),
|
||||||
error = oldPasswordError,
|
error = oldPasswordError
|
||||||
customHintColor = hintColor,
|
|
||||||
customLabelColor = labelColor
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
TextInputField(
|
TextInputField(
|
||||||
@@ -138,9 +121,7 @@ fun ChangePasswordScreen() {
|
|||||||
password = true,
|
password = true,
|
||||||
label = stringResource(R.string.new_password),
|
label = stringResource(R.string.new_password),
|
||||||
hint = stringResource(R.string.new_password),
|
hint = stringResource(R.string.new_password),
|
||||||
error = passwordError,
|
error = passwordError
|
||||||
customHintColor = hintColor,
|
|
||||||
customLabelColor = labelColor
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
TextInputField(
|
TextInputField(
|
||||||
@@ -149,9 +130,7 @@ fun ChangePasswordScreen() {
|
|||||||
password = true,
|
password = true,
|
||||||
label = stringResource(R.string.confirm_new_password_tip1),
|
label = stringResource(R.string.confirm_new_password_tip1),
|
||||||
hint = stringResource(R.string.new_password_tip1),
|
hint = stringResource(R.string.new_password_tip1),
|
||||||
error = confirmPasswordError,
|
error = confirmPasswordError
|
||||||
customHintColor = hintColor,
|
|
||||||
customLabelColor = labelColor
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(50.dp))
|
Spacer(modifier = Modifier.height(50.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
|
|||||||
@@ -70,8 +70,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
|
||||||
import com.aiosman.ravenow.ConstVars
|
import com.aiosman.ravenow.ConstVars
|
||||||
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -79,8 +77,6 @@ import java.io.File
|
|||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
|
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
|
||||||
import com.aiosman.ravenow.ui.account.ZodiacSheetManager
|
import com.aiosman.ravenow.ui.account.ZodiacSheetManager
|
||||||
import com.aiosman.ravenow.ui.account.MbtiBottomSheetHost
|
|
||||||
import com.aiosman.ravenow.ui.account.MbtiSheetManager
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编辑用户资料界面
|
* 编辑用户资料界面
|
||||||
@@ -198,8 +194,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
) {
|
) {
|
||||||
// 挂载星座选择弹窗
|
// 挂载星座选择弹窗
|
||||||
ZodiacBottomSheetHost()
|
ZodiacBottomSheetHost()
|
||||||
// 挂载MBTI选择弹窗
|
|
||||||
MbtiBottomSheetHost()
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -398,7 +392,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
ProfileInfoCard(
|
ProfileInfoCard(
|
||||||
label = stringResource(R.string.nickname),
|
label = stringResource(R.string.nickname),
|
||||||
value = model.name,
|
value = model.name,
|
||||||
placeholder = stringResource(R.string.nickname_placeholder),
|
placeholder = "Value",
|
||||||
onValueChange = { onNicknameChange(it) },
|
onValueChange = { onNicknameChange(it) },
|
||||||
isMultiline = false
|
isMultiline = false
|
||||||
)
|
)
|
||||||
@@ -409,7 +403,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
ProfileInfoCard(
|
ProfileInfoCard(
|
||||||
label = stringResource(R.string.personal_intro),
|
label = stringResource(R.string.personal_intro),
|
||||||
value = model.bio,
|
value = model.bio,
|
||||||
placeholder = "",
|
placeholder = "Welcome to my fantiac word i will show you something about magic",
|
||||||
onValueChange = { onBioChange(it) },
|
onValueChange = { onBioChange(it) },
|
||||||
isMultiline = true
|
isMultiline = true
|
||||||
)
|
)
|
||||||
@@ -431,7 +425,9 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
iconResDark = null, // TODO: 添加MBTI暗色模式图标
|
iconResDark = null, // TODO: 添加MBTI暗色模式图标
|
||||||
iconResLight = null, // TODO: 添加MBTI亮色模式图标
|
iconResLight = null, // TODO: 添加MBTI亮色模式图标
|
||||||
onClick = {
|
onClick = {
|
||||||
MbtiSheetManager.open()
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.MbtiSelect.route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -504,7 +500,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
// 验证通过,执行保存
|
// 验证通过,执行保存
|
||||||
model.viewModelScope.launch {
|
model.viewModelScope.launch {
|
||||||
model.isUpdating = true
|
model.isUpdating = true
|
||||||
try {
|
|
||||||
model.updateUserProfile(context)
|
model.updateUserProfile(context)
|
||||||
model.viewModelScope.launch(Dispatchers.Main) {
|
model.viewModelScope.launch(Dispatchers.Main) {
|
||||||
debouncedNavigation {
|
debouncedNavigation {
|
||||||
@@ -512,17 +507,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
}
|
}
|
||||||
model.isUpdating = false
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -566,45 +550,29 @@ fun ProfileInfoCard(
|
|||||||
isMultiline: Boolean = false
|
isMultiline: Boolean = false
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
|
||||||
var lineCount by remember { mutableStateOf(1) }
|
|
||||||
|
|
||||||
// 根据行数决定对齐方式:单行时居中,多行时顶部对齐
|
|
||||||
val verticalAlignment = if (isMultiline) {
|
|
||||||
if (lineCount <= 1) Alignment.CenterVertically else Alignment.Top
|
|
||||||
} else {
|
|
||||||
Alignment.CenterVertically
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp,个人简介66dp
|
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp,个人简介66dp
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.background(appColors.secondaryBackground),
|
.background(appColors.secondaryBackground),
|
||||||
contentAlignment = if (isMultiline && lineCount > 1) Alignment.TopStart else Alignment.CenterStart
|
contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(vertical = if (isMultiline && lineCount > 1) 11.dp else 0.dp),
|
.padding(vertical = if (isMultiline) 11.dp else 0.dp),
|
||||||
verticalAlignment = verticalAlignment
|
verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 标签
|
// 标签
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(100.dp)
|
|
||||||
.height(if (isMultiline) 44.dp else 56.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.text
|
color = appColors.text,
|
||||||
|
modifier = Modifier.width(100.dp)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
@@ -612,26 +580,10 @@ fun ProfileInfoCard(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
// 对于个人简介(isMultiline = true),当值为空且没有焦点时显示图标
|
if (value.isEmpty()) {
|
||||||
if (value.isEmpty() && isMultiline && !isFocused) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(44.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = appColors.secondaryText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (value.isEmpty() && !isMultiline && placeholder.isNotEmpty()) {
|
|
||||||
// 对于非多行输入框,仍然显示 placeholder 文字
|
|
||||||
Text(
|
Text(
|
||||||
text = placeholder,
|
text = placeholder,
|
||||||
fontSize = 17.sp,
|
fontSize = if (isMultiline) 15.sp else 17.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.secondaryText,
|
color = appColors.secondaryText,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
@@ -641,11 +593,7 @@ fun ProfileInfoCard(
|
|||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.onFocusChanged { focusState ->
|
|
||||||
isFocused = focusState.isFocused
|
|
||||||
},
|
|
||||||
textStyle = androidx.compose.ui.text.TextStyle(
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
fontSize = if (isMultiline) 15.sp else 17.sp,
|
fontSize = if (isMultiline) 15.sp else 17.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
@@ -653,12 +601,7 @@ fun ProfileInfoCard(
|
|||||||
),
|
),
|
||||||
cursorBrush = SolidColor(appColors.text),
|
cursorBrush = SolidColor(appColors.text),
|
||||||
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
|
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
|
||||||
singleLine = !isMultiline,
|
singleLine = !isMultiline
|
||||||
onTextLayout = { textLayoutResult: TextLayoutResult ->
|
|
||||||
if (isMultiline) {
|
|
||||||
lineCount = textLayoutResult.lineCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -51,14 +50,6 @@ fun RemoveAccountScreen() {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// 暗色模式下的 hint 文本颜色
|
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
val hintColor = if (isDarkMode) {
|
|
||||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
|
||||||
} else {
|
|
||||||
null // 使用默认颜色
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAccount(password: String) {
|
fun removeAccount(password: String) {
|
||||||
// 使用通用密码校验器
|
// 使用通用密码校验器
|
||||||
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
||||||
@@ -141,8 +132,7 @@ fun RemoveAccountScreen() {
|
|||||||
},
|
},
|
||||||
password = true,
|
password = true,
|
||||||
hint = stringResource(R.string.remove_account_password_hint),
|
hint = stringResource(R.string.remove_account_password_hint),
|
||||||
error = passwordError,
|
error = passwordError
|
||||||
customHintColor = hintColor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.aiosman.ravenow.data.UserService
|
|||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.entity.ChatItem
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import com.aiosman.ravenow.im.OpenIMManager
|
||||||
import io.openim.android.sdk.OpenIMClient
|
import io.openim.android.sdk.OpenIMClient
|
||||||
import io.openim.android.sdk.enums.ConversationType
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
import io.openim.android.sdk.enums.ViewType
|
import io.openim.android.sdk.enums.ViewType
|
||||||
@@ -100,6 +101,10 @@ abstract class BaseChatViewModel : ViewModel() {
|
|||||||
|
|
||||||
override fun onSuccess(data: ConversationInfo) {
|
override fun onSuccess(data: ConversationInfo) {
|
||||||
conversationID = data.conversationID
|
conversationID = data.conversationID
|
||||||
|
// 如果是群组的会话id,应该加上s修正,不知道是不是openIm的bug
|
||||||
|
if (data.conversationType == 2) {
|
||||||
|
conversationID = "s${conversationID}"
|
||||||
|
}
|
||||||
Log.d(getLogTag(), "获取会话信息成功,conversationID: $conversationID")
|
Log.d(getLogTag(), "获取会话信息成功,conversationID: $conversationID")
|
||||||
onSuccess?.invoke()
|
onSuccess?.invoke()
|
||||||
}
|
}
|
||||||
@@ -324,6 +329,7 @@ abstract class BaseChatViewModel : ViewModel() {
|
|||||||
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
||||||
object : OnBase<AdvancedMessage> {
|
object : OnBase<AdvancedMessage> {
|
||||||
override fun onSuccess(data: AdvancedMessage?) {
|
override fun onSuccess(data: AdvancedMessage?) {
|
||||||
|
|
||||||
val messages = data?.messageList ?: emptyList()
|
val messages = data?.messageList ?: emptyList()
|
||||||
val newChatItems = messages.mapNotNull {
|
val newChatItems = messages.mapNotNull {
|
||||||
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))
|
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))
|
||||||
|
|||||||
@@ -504,6 +504,12 @@ fun ChatAiOtherItem(item: ChatItem) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatAiItem(item: ChatItem, currentUserId: String) {
|
fun ChatAiItem(item: ChatItem, currentUserId: String) {
|
||||||
|
// 通知消息显示特殊布局
|
||||||
|
if (item.isNotification) {
|
||||||
|
NotificationMessageItem(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val isCurrentUser = item.userId == currentUserId
|
val isCurrentUser = item.userId == currentUserId
|
||||||
if (isCurrentUser) {
|
if (isCurrentUser) {
|
||||||
ChatAiSelfItem(item)
|
ChatAiSelfItem(item)
|
||||||
@@ -617,7 +623,7 @@ fun ChatAiInput(
|
|||||||
animationSpec = tween(300)
|
animationSpec = tween(300)
|
||||||
)
|
)
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.btn),
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.alpha(alpha)
|
.alpha(alpha)
|
||||||
|
|||||||
@@ -516,6 +516,12 @@ fun ChatOtherItem(item: ChatItem) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatItem(item: ChatItem, currentUserId: String) {
|
fun ChatItem(item: ChatItem, currentUserId: String) {
|
||||||
|
// 通知消息显示特殊布局
|
||||||
|
if (item.isNotification) {
|
||||||
|
NotificationMessageItem(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val isCurrentUser = item.userId == currentUserId
|
val isCurrentUser = item.userId == currentUserId
|
||||||
if (isCurrentUser) {
|
if (isCurrentUser) {
|
||||||
ChatSelfItem(item)
|
ChatSelfItem(item)
|
||||||
@@ -650,7 +656,7 @@ fun ChatInput(
|
|||||||
animationSpec = tween(300)
|
animationSpec = tween(300)
|
||||||
)
|
)
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.btn),
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.alpha(alpha)
|
.alpha(alpha)
|
||||||
|
|||||||
@@ -183,21 +183,45 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
if (viewModel.groupInfo?.groupAvatar?.isNotEmpty() == true) {
|
if (viewModel.groupAvatar.isNotEmpty()) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = viewModel.groupInfo!!.groupAvatar,
|
imageUrl = viewModel.groupAvatar,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(35.dp)
|
.size(32.dp)
|
||||||
.clip(RoundedCornerShape(15.dp)),
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToGroupInfo(groupId)
|
||||||
|
},
|
||||||
contentDescription = "群聊头像"
|
contentDescription = "群聊头像"
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToGroupInfo(groupId)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.groupName,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W700
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow =TextOverflow.Ellipsis,
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Column(
|
Column {
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
horizontalAlignment = Alignment.Start
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = viewModel.groupName,
|
text = viewModel.groupName,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -206,10 +230,12 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
|||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
),
|
),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow =TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -217,11 +243,12 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
|||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigateToGroupInfo(groupId)
|
navController.navigateToGroupInfo(groupId)
|
||||||
},
|
},
|
||||||
contentDescription = "更多",
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Column(
|
Column(
|
||||||
@@ -283,8 +310,13 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 获取上一个item的userId,用于判断是否显示头像和昵称
|
// 获取上一个item的userId,用于判断是否显示头像和昵称
|
||||||
|
// 通知消息不参与判断逻辑
|
||||||
val previousItem = if (index < chatList.size - 1) chatList[index + 1] else null
|
val previousItem = if (index < chatList.size - 1) chatList[index + 1] else null
|
||||||
val showAvatarAndNickname = previousItem?.userId != item.userId
|
val showAvatarAndNickname = if (item.isNotification || previousItem?.isNotification == true) {
|
||||||
|
true // 通知消息前后都显示头像和昵称
|
||||||
|
} else {
|
||||||
|
previousItem?.userId != item.userId
|
||||||
|
}
|
||||||
GroupChatItem(
|
GroupChatItem(
|
||||||
item = item,
|
item = item,
|
||||||
currentUserId = viewModel.myProfile?.trtcUserId!!,
|
currentUserId = viewModel.myProfile?.trtcUserId!!,
|
||||||
@@ -501,14 +533,14 @@ fun GroupChatOtherItem(item: ChatItem, showAvatarAndNickname: Boolean = true) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupChatItem(item: ChatItem, currentUserId: String, showAvatarAndNickname: Boolean = true) {
|
fun GroupChatItem(item: ChatItem, currentUserId: String, showAvatarAndNickname: Boolean = true) {
|
||||||
val isCurrentUser = item.userId == currentUserId
|
// 通知消息显示特殊布局(包括系统账户发送的消息)
|
||||||
|
if (item.isNotification) {
|
||||||
// 管理员消息显示特殊布局
|
NotificationMessageItem(item)
|
||||||
if (item.userId == "administrator") {
|
|
||||||
GroupChatAdminItem(item)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isCurrentUser = item.userId == currentUserId
|
||||||
|
|
||||||
// 根据是否是当前用户显示不同样式
|
// 根据是否是当前用户显示不同样式
|
||||||
when (item.userId) {
|
when (item.userId) {
|
||||||
currentUserId -> GroupChatSelfItem(item)
|
currentUserId -> GroupChatSelfItem(item)
|
||||||
@@ -650,7 +682,7 @@ fun GroupChatInput(
|
|||||||
animationSpec = tween(300)
|
animationSpec = tween(300)
|
||||||
)
|
)
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.btn),
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.alpha(alpha)
|
.alpha(alpha)
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.aiosman.ravenow.ui.chat
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.aiosman.ravenow.AppStore
|
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
|
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
|
||||||
import io.openim.android.sdk.enums.ConversationType
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
@@ -52,34 +50,7 @@ class GroupChatViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getGroupInfo() {
|
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(
|
groupInfo = GroupInfo(
|
||||||
groupId = groupId,
|
groupId = groupId,
|
||||||
groupName = name,
|
groupName = name,
|
||||||
@@ -87,12 +58,10 @@ class GroupChatViewModel(
|
|||||||
memberCount = 0,
|
memberCount = 0,
|
||||||
ownerId = ""
|
ownerId = ""
|
||||||
)
|
)
|
||||||
} finally {
|
|
||||||
groupName = groupInfo?.groupName ?: ""
|
groupName = groupInfo?.groupName ?: ""
|
||||||
groupAvatar = groupInfo?.groupAvatar ?: ""
|
groupAvatar = groupInfo?.groupAvatar ?: ""
|
||||||
memberCount = groupInfo?.memberCount ?: 0
|
memberCount = groupInfo?.memberCount ?: 0
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||||
// 根据群组类型决定ConversationType,这里假设是普通群聊
|
// 根据群组类型决定ConversationType,这里假设是普通群聊
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,9 +15,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.ime
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -30,11 +28,9 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@@ -42,7 +38,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -84,9 +79,8 @@ class CommentModalViewModel(
|
|||||||
fun CommentModalContent(
|
fun CommentModalContent(
|
||||||
postId: Int? = null,
|
postId: Int? = null,
|
||||||
commentCount: Int = 0,
|
commentCount: Int = 0,
|
||||||
onDismiss: () -> Unit = {},
|
onCommentAdded: () -> Unit = {},
|
||||||
showTitle: Boolean = true,
|
onDismiss: () -> Unit = {}
|
||||||
onCommentAdded: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
val model = viewModel<CommentModalViewModel>(
|
val model = viewModel<CommentModalViewModel>(
|
||||||
key = "CommentModalViewModel_$postId",
|
key = "CommentModalViewModel_$postId",
|
||||||
@@ -110,8 +104,6 @@ fun CommentModalContent(
|
|||||||
var softwareKeyboardController = LocalSoftwareKeyboardController.current
|
var softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
var shouldAutoFocus by remember { mutableStateOf(false) }
|
var shouldAutoFocus by remember { mutableStateOf(false) }
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(imePadding) {
|
LaunchedEffect(imePadding) {
|
||||||
bottomPadding = imePadding.dp
|
bottomPadding = imePadding.dp
|
||||||
@@ -169,23 +161,6 @@ fun CommentModalContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
// 拖动手柄
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp, bottom = 12.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(40.dp)
|
|
||||||
.height(4.dp)
|
|
||||||
.clip(RoundedCornerShape(50))
|
|
||||||
.background(AppColors.divider)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showTitle) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -200,11 +175,14 @@ fun CommentModalContent(
|
|||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
HorizontalDivider(
|
||||||
|
color = AppColors.divider
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 20.dp, vertical = 8.dp),
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
@@ -213,14 +191,9 @@ fun CommentModalContent(
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = AppColors.secondaryText
|
color = AppColors.secondaryText
|
||||||
)
|
)
|
||||||
OrderSelectionComponent(
|
OrderSelectionComponent {
|
||||||
selectedOrder = commentViewModel.order
|
|
||||||
) {
|
|
||||||
commentViewModel.order = it
|
commentViewModel.order = it
|
||||||
commentViewModel.reloadComment()
|
commentViewModel.reloadComment()
|
||||||
scope.launch {
|
|
||||||
listState.scrollToItem(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
@@ -231,8 +204,7 @@ fun CommentModalContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp)
|
||||||
state = listState
|
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
CommentContent(
|
CommentContent(
|
||||||
|
|||||||
@@ -41,15 +41,16 @@ import com.aiosman.ravenow.R
|
|||||||
import com.aiosman.ravenow.entity.CommentEntity
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.aiosman.ravenow.exp.timeAgo
|
import com.aiosman.ravenow.exp.timeAgo
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.navigateToPost
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
@Composable
|
@Composable
|
||||||
fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
|
fun CommentNoticeScreen() {
|
||||||
val viewModel = viewModel<CommentNoticeListViewModel>(
|
val viewModel = viewModel<CommentNoticeListViewModel>(
|
||||||
key = "CommentNotice",
|
key = "CommentNotice",
|
||||||
factory = object : ViewModelProvider.Factory {
|
factory = object : ViewModelProvider.Factory {
|
||||||
@@ -67,22 +68,50 @@ fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
StatusBarMaskLayout(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().background(color = AppColors.background)
|
||||||
.background(color = AppColors.background)
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
darkIcons = !AppState.darkMode,
|
|
||||||
maskBoxBackgroundColor = AppColors.background,
|
|
||||||
includeStatusBarPadding = includeStatusBarPadding
|
|
||||||
) {
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
modifier = Modifier
|
||||||
|
.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)
|
viewModel.initData(context, force = true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
|
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -112,8 +141,7 @@ fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
|
|||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxSize().padding(horizontal = 16.dp)
|
||||||
.background(color = AppColors.background)
|
|
||||||
) {
|
) {
|
||||||
items(comments.itemCount) { index ->
|
items(comments.itemCount) { index ->
|
||||||
comments[index]?.let { comment ->
|
comments[index]?.let { comment ->
|
||||||
@@ -187,18 +215,17 @@ fun CommentNoticeItem(
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val commentPrefix = stringResource(R.string.comment_notice)
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(vertical = 12.dp)
|
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
// 左侧头像区域
|
Box {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context = context,
|
context = context,
|
||||||
imageUrl = commentItem.avatar,
|
imageUrl = commentItem.avatar,
|
||||||
contentDescription = commentItem.name,
|
contentDescription = commentItem.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@@ -209,36 +236,32 @@ fun CommentNoticeItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
// 右侧内容区域
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 8.dp)
|
.padding(start = 12.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
onPostClick()
|
onPostClick()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// 主要信息列
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = commentItem.name,
|
text = commentItem.name,
|
||||||
fontSize = 14.sp,
|
fontSize = 18.sp,
|
||||||
color = AppColors.text,
|
modifier = Modifier,
|
||||||
maxLines = 1,
|
color = AppColors.text
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
// 评论内容行
|
|
||||||
Row {
|
Row {
|
||||||
var text = commentItem.comment
|
var text = commentItem.comment
|
||||||
if (commentItem.parentCommentId != null) {
|
if (commentItem.parentCommentId != null) {
|
||||||
text = "Reply you: $text"
|
text = "Reply you: $text"
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "$commentPrefix $text",
|
text = text,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
@@ -252,20 +275,25 @@ fun CommentNoticeItem(
|
|||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
// 右侧帖子图片
|
|
||||||
commentItem.post?.let {
|
commentItem.post?.let {
|
||||||
Box {
|
Box {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(4.dp)
|
||||||
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context = context,
|
context = context,
|
||||||
imageUrl = it.images[0].thumbnail,
|
imageUrl = it.images[0].thumbnail,
|
||||||
contentDescription = "Post Image",
|
contentDescription = "Post Image",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(48.dp).clip(RoundedCornerShape(8.dp))
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
)
|
)
|
||||||
// 未读指示器
|
// unread indicator
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (commentItem.unread) {
|
if (commentItem.unread) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -275,7 +303,11 @@ fun CommentNoticeItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ fun StatusBarMaskLayout(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
darkIcons: Boolean = true,
|
darkIcons: Boolean = true,
|
||||||
useNavigationBarMask: Boolean = true,
|
useNavigationBarMask: Boolean = true,
|
||||||
includeStatusBarPadding: Boolean = true,
|
|
||||||
maskBoxBackgroundColor: Color = Color.Transparent,
|
maskBoxBackgroundColor: Color = Color.Transparent,
|
||||||
content: @Composable ColumnScope.() -> Unit
|
content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -51,13 +50,13 @@ fun StatusBarMaskLayout(
|
|||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (includeStatusBarPadding) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(paddingValues.calculateTopPadding())
|
.height(paddingValues.calculateTopPadding())
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(maskBoxBackgroundColor)
|
.background(maskBoxBackgroundColor)
|
||||||
)
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
content()
|
content()
|
||||||
if (navigationBarPaddings > 24.dp && useNavigationBarMask) {
|
if (navigationBarPaddings > 24.dp && useNavigationBarMask) {
|
||||||
|
|||||||
@@ -17,19 +17,15 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -46,7 +42,7 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
|||||||
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager
|
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.navigateToPost
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@@ -88,11 +84,43 @@ fun FavouriteListPage() {
|
|||||||
var moments = dataFlow.collectAsLazyPagingItems()
|
var moments = dataFlow.collectAsLazyPagingItems()
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
modifier = Modifier
|
||||||
|
.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)
|
model.refreshPager(force = true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if(moments.itemCount == 0) {
|
} else if(moments.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -159,10 +187,7 @@ fun FavouriteListPage() {
|
|||||||
.clip(RoundedCornerShape(8.dp)),
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
|
if (momentItem.images.size > 1) {
|
||||||
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
|
|
||||||
|
|
||||||
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp, end = 8.dp)
|
.padding(top = 8.dp, end = 8.dp)
|
||||||
@@ -175,31 +200,6 @@ fun FavouriteListPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideoMoment) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 8.dp, end = 8.dp)
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.background(
|
|
||||||
color = Color.Black.copy(alpha = 0.4f),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.PlayArrow,
|
|
||||||
contentDescription = "",
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
|||||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -76,11 +76,43 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
modifier = Modifier
|
||||||
|
.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)
|
model.loadData(userId, true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (users.itemCount == 0) {
|
} else if (users.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -41,7 +40,7 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|||||||
import com.aiosman.ravenow.ui.composables.FollowButton
|
import com.aiosman.ravenow.ui.composables.FollowButton
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@@ -49,14 +48,13 @@ import com.aiosman.ravenow.utils.NetworkUtils
|
|||||||
* 关注消息列表
|
* 关注消息列表
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
fun FollowerNoticeScreen() {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
StatusBarMaskLayout(
|
StatusBarMaskLayout(
|
||||||
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
|
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
|
||||||
darkIcons = !AppState.darkMode,
|
darkIcons = !AppState.darkMode,
|
||||||
maskBoxBackgroundColor = AppColors.background,
|
maskBoxBackgroundColor = AppColors.background
|
||||||
includeStatusBarPadding = includeStatusBarPadding
|
|
||||||
) {
|
) {
|
||||||
val model = FollowerNoticeViewModel
|
val model = FollowerNoticeViewModel
|
||||||
var dataFlow = model.followerItemsFlow
|
var dataFlow = model.followerItemsFlow
|
||||||
@@ -68,11 +66,43 @@ fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
|||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
modifier = Modifier
|
||||||
|
.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)
|
model.reload(force = true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (followers.itemCount == 0) {
|
} else if (followers.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -141,22 +171,9 @@ fun FollowItem(
|
|||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val followText = stringResource(R.string.followed_you)
|
Box(
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.padding(vertical = 16.dp)
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 左侧头像区域
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = context,
|
|
||||||
imageUrl = avatar,
|
|
||||||
contentDescription = nickname,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.AccountProfile.route.replace(
|
NavigationRoute.AccountProfile.route.replace(
|
||||||
@@ -165,33 +182,24 @@ fun FollowItem(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
// 右侧内容区域
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
|
Row(
|
||||||
|
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(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
|
||||||
text = nickname,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = AppColors.text,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = followText,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = AppColors.secondaryText,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (!isFollowing && userId != AppState.UserId) {
|
if (!isFollowing && userId != AppState.UserId) {
|
||||||
FollowButton(
|
FollowButton(
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import com.aiosman.ravenow.exp.viewModelFactory
|
|||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@@ -78,11 +78,43 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
modifier = Modifier
|
||||||
|
.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)
|
model.loadData(userId, true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if(users.itemCount == 0) {
|
} else if(users.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -99,15 +99,6 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
var showSelectTipsDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// 自动隐藏“请选择群成员并输入群名称”提示弹窗
|
|
||||||
LaunchedEffect(showSelectTipsDialog) {
|
|
||||||
if (showSelectTipsDialog) {
|
|
||||||
kotlinx.coroutines.delay(2000)
|
|
||||||
showSelectTipsDialog = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取费用和余额信息
|
// 获取费用和余额信息
|
||||||
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
|
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
|
||||||
@@ -504,48 +495,14 @@ 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(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
// 创建群聊逻辑
|
// 创建群聊逻辑
|
||||||
if (selectedMembers.isNotEmpty()) {
|
if (selectedMembers.isNotEmpty()) {
|
||||||
// 检查是否超过上限
|
// 检查是否超过上限
|
||||||
if (selectedMembers.size > maxMemberLimit) {
|
if (selectedMembers.size > maxMemberLimit) {
|
||||||
CreateGroupChatViewModel.showError(
|
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
|
||||||
context.getString(
|
|
||||||
R.string.create_group_chat_exceed_limit,
|
|
||||||
maxMemberLimit
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return@Button
|
return@Button
|
||||||
}
|
}
|
||||||
// 如果费用大于0,显示确认弹窗
|
// 如果费用大于0,显示确认弹窗
|
||||||
@@ -567,15 +524,16 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = if (isCreateEnabled) Color.Transparent else AppColors.main,
|
containerColor = AppColors.main,
|
||||||
contentColor = AppColors.mainText,
|
contentColor = AppColors.mainText,
|
||||||
disabledContainerColor = AppColors.disabledBackground,
|
disabledContainerColor = AppColors.disabledBackground,
|
||||||
disabledContentColor = AppColors.text
|
disabledContentColor = AppColors.text
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
enabled = isCreateEnabled
|
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
|
||||||
) {
|
) {
|
||||||
if (CreateGroupChatViewModel.isLoading) {
|
if (CreateGroupChatViewModel.isLoading) {
|
||||||
Text(
|
Text(
|
||||||
@@ -592,44 +550,6 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 禁用状态下拦截点击并弹出提示
|
|
||||||
if (!isCreateEnabled) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.noRippleClickable {
|
|
||||||
showSelectTipsDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请选择群成员并输入群名称 提示弹窗
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消费确认弹窗
|
// 消费确认弹窗
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.aiosman.ravenow.ui.group
|
|||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -45,7 +44,6 @@ import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
|
|||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.ravenow.ui.index.NavItem
|
import com.aiosman.ravenow.ui.index.NavItem
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.navigateToAddGroupMember
|
|
||||||
import com.aiosman.ravenow.ui.navigateToGroupMembers
|
import com.aiosman.ravenow.ui.navigateToGroupMembers
|
||||||
import com.aiosman.ravenow.ui.navigateToGroupProfileSettings
|
import com.aiosman.ravenow.ui.navigateToGroupProfileSettings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -175,7 +173,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${viewModel.groupInfo?.memberCount ?: 0}${stringResource(R.string.people)}",
|
text = "${viewModel.groupInfo?.memberCount ?: 0}人",
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
color = AppColors.text.copy(alpha = 0.7f),
|
color = AppColors.text.copy(alpha = 0.7f),
|
||||||
fontSize = 11.sp
|
fontSize = 11.sp
|
||||||
@@ -189,17 +187,14 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.padding(horizontal = 24.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
if (viewModel.groupInfo?.isCreator == true) {
|
|
||||||
// 添加其他人
|
// 添加其他人
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
// TODO: 实现添加其他人功能
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
@@ -225,7 +220,6 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 通知设置
|
// 通知设置
|
||||||
Column(
|
Column(
|
||||||
@@ -264,11 +258,11 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分享群聊
|
// 退出群聊
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
// TODO: 实现分享功能
|
// TODO: 实现退出群聊功能
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
@@ -278,7 +272,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.icon_share),
|
painter = painterResource(R.drawable.group_info_edit),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
@@ -303,16 +297,16 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.border(1.dp, AppColors.decentBackground, RoundedCornerShape(12.dp))
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(AppColors.background)
|
.background(AppColors.decentBackground.copy(alpha = 0.28f))
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.icons_brain),
|
painter = painterResource(R.drawable.group_info_edit),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
@@ -321,6 +315,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
@@ -342,7 +337,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(AppColors.decentBackground)
|
.background(AppColors.background)
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
showAddMemoryDialog = true
|
showAddMemoryDialog = true
|
||||||
@@ -361,7 +356,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(AppColors.decentBackground)
|
.background(AppColors.background)
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
showMemoryManageDialog = true
|
showMemoryManageDialog = true
|
||||||
@@ -384,8 +379,6 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(13.dp))
|
Spacer(modifier = Modifier.height(13.dp))
|
||||||
|
|
||||||
// 仅当当前用户是群聊创建者时显示以下组件
|
|
||||||
if (viewModel.groupInfo?.isCreator == true) {
|
|
||||||
// 群资料设置
|
// 群资料设置
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -398,7 +391,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.fengm),
|
painter = painterResource(R.drawable.group_info_edit),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
@@ -462,7 +455,6 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 成员管理
|
// 成员管理
|
||||||
Row(
|
Row(
|
||||||
@@ -476,21 +468,15 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.icons_users),
|
painter = painterResource(R.drawable.group_info_users),
|
||||||
modifier = Modifier.size(25.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
AppColors.text)
|
AppColors.text)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = stringResource(R.string.group_chat_info_member_manage),
|
||||||
if (viewModel.groupInfo?.isCreator == true) {
|
|
||||||
R.string.group_chat_info_member_manage
|
|
||||||
} else {
|
|
||||||
R.string.group_members_title
|
|
||||||
}
|
|
||||||
),
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 15.sp
|
fontSize = 15.sp
|
||||||
@@ -513,8 +499,8 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.iconsgallery),
|
painter = painterResource(R.drawable.group_info_edit),
|
||||||
modifier = Modifier.size(25.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
AppColors.text)
|
AppColors.text)
|
||||||
@@ -534,8 +520,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (viewModel.groupInfo?.isCreator == true) {
|
// 解散群聊
|
||||||
// 解散群聊(仅群主)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -544,14 +529,13 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
.noRippleClickable { },
|
.noRippleClickable { },
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(3.dp))
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.iconslogout),
|
painter = painterResource(R.drawable.group_info_exit),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(11.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.group_chat_info_dissolve),
|
text = stringResource(R.string.group_chat_info_dissolve),
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
@@ -563,41 +547,9 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||||
modifier = Modifier.size(18.dp),
|
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,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,16 +107,16 @@ fun GroupMembersScreen(groupId: String) {
|
|||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
// androidx.compose.foundation.Image(
|
androidx.compose.foundation.Image(
|
||||||
// painter = painterResource(R.drawable.rider_pro_add_other),
|
painter = painterResource(R.drawable.rider_pro_add_other),
|
||||||
// contentDescription = stringResource(R.string.group_chat_info_add_member),
|
contentDescription = stringResource(R.string.group_chat_info_add_member),
|
||||||
// colorFilter = ColorFilter.tint(AppColors.text),
|
colorFilter = ColorFilter.tint(AppColors.text),
|
||||||
// modifier = Modifier
|
modifier = Modifier
|
||||||
// .size(24.dp)
|
.size(24.dp)
|
||||||
// .noRippleClickable {
|
.noRippleClickable {
|
||||||
// navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
||||||
// }
|
}
|
||||||
// )
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +391,7 @@ private fun MemberItem(
|
|||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
// 菜单按钮
|
// 菜单按钮
|
||||||
if (isAdmin) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { onMenuClick(itemPosition, itemHeight) },
|
onClick = { onMenuClick(itemPosition, itemHeight) },
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
@@ -404,7 +404,6 @@ private fun MemberItem(
|
|||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package com.aiosman.ravenow.ui.index
|
|||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||||
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@@ -136,7 +140,6 @@ fun IndexScreen() {
|
|||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
gesturesEnabled = drawerState.isOpen,
|
gesturesEnabled = drawerState.isOpen,
|
||||||
scrimColor = Color.Black.copy(alpha = 0.6f),
|
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
SideMenuContent(
|
SideMenuContent(
|
||||||
@@ -522,6 +525,8 @@ fun SideMenuContent(
|
|||||||
} else {
|
} else {
|
||||||
Color(0xFFFAF9FB) // 亮色模式:浅灰色
|
Color(0xFFFAF9FB) // 亮色模式:浅灰色
|
||||||
}
|
}
|
||||||
|
// 遮罩颜色 黑色透明度0.6
|
||||||
|
val overlayColor = Color.Black.copy(alpha = 0.6f)
|
||||||
// 卡片背景色 - 根据暗色模式适配
|
// 卡片背景色 - 根据暗色模式适配
|
||||||
val cardBackgroundColor = if (darkModeEnabled) {
|
val cardBackgroundColor = if (darkModeEnabled) {
|
||||||
appColors.background // 暗色模式:深色背景
|
appColors.background // 暗色模式:深色背景
|
||||||
@@ -541,6 +546,24 @@ fun SideMenuContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
// 左侧半透明遮罩(平滑淡入淡出)
|
||||||
|
val overlayTransition = updateTransition(targetState = isDrawerOpen, label = "overlay")
|
||||||
|
val overlayAlpha by overlayTransition.animateFloat(
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState) {
|
||||||
|
tween(durationMillis = 400, easing = LinearOutSlowInEasing)
|
||||||
|
} else {
|
||||||
|
tween(durationMillis = 300, easing = FastOutLinearInEasing)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = "overlayAlpha"
|
||||||
|
) { open -> if (open) 0.6f else 0f }
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = overlayAlpha))
|
||||||
|
)
|
||||||
|
|
||||||
// 右侧菜单面板
|
// 右侧菜单面板
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
@@ -24,22 +23,18 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -63,33 +58,24 @@ import com.aiosman.ravenow.LocalNavController
|
|||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgent
|
|
||||||
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.hot.HotAgent
|
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.composables.TabItem
|
|
||||||
import com.aiosman.ravenow.ui.composables.TabSpacer
|
import com.aiosman.ravenow.ui.composables.TabSpacer
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.CustomTabItem
|
import com.aiosman.ravenow.ui.index.tabs.moment.CustomTabItem
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.ExploreViewModel
|
|
||||||
import com.aiosman.ravenow.utils.DebounceUtils
|
import com.aiosman.ravenow.utils.DebounceUtils
|
||||||
import com.aiosman.ravenow.utils.ResourceCleanupManager
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.compose.foundation.lazy.grid.items as gridItems
|
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
import com.airbnb.lottie.compose.LottieAnimation
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||||
@@ -113,10 +99,6 @@ fun Agent() {
|
|||||||
val navigationBarPaddings =
|
val navigationBarPaddings =
|
||||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
// 游客模式下只显示热门Agent,正常用户显示我的Agent和热门Agent
|
|
||||||
val tabCount = if (AppStore.isGuest) 1 else 2
|
|
||||||
var pagerState = rememberPagerState { tabCount }
|
|
||||||
var scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val viewModel: AgentViewModel = AgentViewModel
|
val viewModel: AgentViewModel = AgentViewModel
|
||||||
|
|
||||||
@@ -125,16 +107,6 @@ fun Agent() {
|
|||||||
viewModel.ensureDataLoaded()
|
viewModel.ensureDataLoaded()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防抖状态
|
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
|
||||||
|
|
||||||
// 页面退出时只清理必要的资源,不清理推荐Agent数据
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
// 只清理子页面的资源,保留推荐Agent数据
|
|
||||||
// ResourceCleanupManager.cleanupPageResources("ai")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val agentItems = viewModel.agentItems
|
val agentItems = viewModel.agentItems
|
||||||
var selectedTabIndex by remember { mutableStateOf(0) }
|
var selectedTabIndex by remember { mutableStateOf(0) }
|
||||||
@@ -165,7 +137,7 @@ fun Agent() {
|
|||||||
contentDescription = "Rave AI Logo",
|
contentDescription = "Rave AI Logo",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(44.dp)
|
.height(44.dp)
|
||||||
.padding(top =9.dp,bottom=9.dp)
|
.padding(top = 9.dp, bottom = 9.dp)
|
||||||
.wrapContentSize(),
|
.wrapContentSize(),
|
||||||
// colorFilter = ColorFilter.tint(AppColors.text)
|
// colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
@@ -176,7 +148,7 @@ fun Agent() {
|
|||||||
contentDescription = "search",
|
contentDescription = "search",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(44.dp)
|
.size(44.dp)
|
||||||
.padding(top = 9.dp,bottom=9.dp)
|
.padding(top = 9.dp, bottom = 9.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(NavigationRoute.Search.route)
|
navController.navigate(NavigationRoute.Search.route)
|
||||||
},
|
},
|
||||||
@@ -267,11 +239,19 @@ fun Agent() {
|
|||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
selectedTabIndex == 0 -> {
|
selectedTabIndex == 0 -> {
|
||||||
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
AgentViewPagerSection(
|
||||||
|
agentItems = viewModel.agentItems.take(15),
|
||||||
|
viewModel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedTabIndex in 1..viewModel.categories.size -> {
|
selectedTabIndex in 1..viewModel.categories.size -> {
|
||||||
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
AgentViewPagerSection(
|
||||||
|
agentItems = viewModel.agentItems.take(15),
|
||||||
|
viewModel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
|
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
|
||||||
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
|
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
|
||||||
@@ -329,7 +309,6 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 只有当热门聊天室有数据时,才展示“发现更多”区域
|
// 只有当热门聊天室有数据时,才展示“发现更多”区域
|
||||||
if (viewModel.chatRooms.isNotEmpty()) {
|
|
||||||
item { Spacer(modifier = Modifier.height(20.dp)) }
|
item { Spacer(modifier = Modifier.height(20.dp)) }
|
||||||
|
|
||||||
// "发现更多" 标题 - 吸顶
|
// "发现更多" 标题 - 吸顶
|
||||||
@@ -404,58 +383,7 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AgentGridLayout(
|
|
||||||
agentItems: List<AgentItem>,
|
|
||||||
viewModel: AgentViewModel,
|
|
||||||
navController: NavHostController
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
// 将agentItems按两列分组
|
|
||||||
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
|
|
||||||
bottom = 20.dp
|
|
||||||
),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
// 第一列
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
AgentCardSquare(
|
|
||||||
agentItem = rowItems[0],
|
|
||||||
viewModel = viewModel,
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第二列(如果存在)
|
|
||||||
if (rowItems.size > 1) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
AgentCardSquare(
|
|
||||||
agentItem = rowItems[1],
|
|
||||||
viewModel = viewModel,
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果只有一列,添加空白占位
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,9 +490,10 @@ fun AgentCardSquare(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
|
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
if (agentItems.isEmpty()) return
|
if (agentItems.isEmpty()) return
|
||||||
|
|
||||||
@@ -711,186 +640,6 @@ fun AgentLargeCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier,navController: NavHostController) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 0.dp)
|
|
||||||
) {
|
|
||||||
// 显示3个agent
|
|
||||||
agentItems.forEachIndexed { index, agentItem ->
|
|
||||||
AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current)
|
|
||||||
if (index < agentItems.size - 1) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SuspiciousIndentation")
|
|
||||||
@Composable
|
|
||||||
fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
|
|
||||||
// 防抖状态
|
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 3.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 左侧头像
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.background(Color(0x00F5F5F5), RoundedCornerShape(24.dp))
|
|
||||||
.clickable {
|
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
|
||||||
viewModel.goToProfile(agentItem.openId, navController)
|
|
||||||
}) {
|
|
||||||
lastClickTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = agentItem.avatar,
|
|
||||||
contentDescription = "Agent头像",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(RoundedCornerShape(24.dp)),
|
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
|
|
||||||
defaultRes = R.mipmap.group_copy
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
|
|
||||||
// 中间文字内容
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 8.dp)
|
|
||||||
) {
|
|
||||||
// 标题
|
|
||||||
androidx.compose.material3.Text(
|
|
||||||
text = agentItem.title,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
|
||||||
color = AppColors.text,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// 描述
|
|
||||||
androidx.compose.material3.Text(
|
|
||||||
text = agentItem.desc,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = AppColors.secondaryText,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 右侧聊天按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(width = 60.dp, height = 32.dp)
|
|
||||||
.background(
|
|
||||||
color = Color(0X147c7480),
|
|
||||||
shape = RoundedCornerShape(
|
|
||||||
topStart = 14.dp,
|
|
||||||
topEnd = 14.dp,
|
|
||||||
bottomStart = 0.dp,
|
|
||||||
bottomEnd = 14.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.clickable {
|
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
|
||||||
// 检查游客模式,如果是游客则跳转登录
|
|
||||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
|
||||||
navController.navigate(NavigationRoute.Login.route)
|
|
||||||
} else {
|
|
||||||
viewModel.createSingleChat(agentItem.openId)
|
|
||||||
viewModel.goToChatAi(
|
|
||||||
agentItem.openId,
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
lastClickTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
androidx.compose.material3.Text(
|
|
||||||
text = stringResource(R.string.chat),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = AppColors.text,
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Composable
|
|
||||||
fun ChatRoomsSection(
|
|
||||||
chatRooms: List<ChatRoom>,
|
|
||||||
navController: NavHostController
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
// 标题
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.mipmap.rider_pro_hot_room),
|
|
||||||
contentDescription = "chat room",
|
|
||||||
modifier = Modifier.size(28.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
androidx.compose.material3.Text(
|
|
||||||
text = stringResource(R.string.hot_rooms),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
chatRooms.chunked(2).forEach { rowRooms ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
rowRooms.forEach { chatRoom ->
|
|
||||||
ChatRoomCard(
|
|
||||||
chatRoom = chatRoom,
|
|
||||||
navController = navController,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatRoomCard(
|
fun ChatRoomCard(
|
||||||
chatRoom: ChatRoom,
|
chatRoom: ChatRoom,
|
||||||
@@ -938,7 +687,10 @@ fun ChatRoomCard(
|
|||||||
.size(cardSize)
|
.size(cardSize)
|
||||||
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
|
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
|
||||||
.clickable(enabled = !viewModel.isJoiningRoom) {
|
.clickable(enabled = !viewModel.isJoiningRoom) {
|
||||||
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(
|
||||||
|
lastClickTime,
|
||||||
|
500L
|
||||||
|
) {
|
||||||
// 加入群聊房间
|
// 加入群聊房间
|
||||||
viewModel.joinRoom(
|
viewModel.joinRoom(
|
||||||
id = chatRoom.id,
|
id = chatRoom.id,
|
||||||
@@ -953,7 +705,8 @@ fun ChatRoomCard(
|
|||||||
// 处理错误,可以显示Toast或其他提示
|
// 处理错误,可以显示Toast或其他提示
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
lastClickTime = System.currentTimeMillis()
|
lastClickTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -967,11 +720,14 @@ fun ChatRoomCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(cardSize)
|
.width(cardSize)
|
||||||
.height(120.dp)
|
.height(120.dp)
|
||||||
.clip(RoundedCornerShape(
|
.clip(
|
||||||
|
RoundedCornerShape(
|
||||||
topStart = 12.dp,
|
topStart = 12.dp,
|
||||||
topEnd = 12.dp,
|
topEnd = 12.dp,
|
||||||
bottomStart = 0.dp,
|
bottomStart = 0.dp,
|
||||||
bottomEnd = 0.dp)),
|
bottomEnd = 0.dp
|
||||||
|
)
|
||||||
|
),
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
|
||||||
defaultRes = R.mipmap.rider_pro_agent
|
defaultRes = R.mipmap.rider_pro_agent
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ object AgentViewModel: ViewModel() {
|
|||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
withWorkflow = 1,
|
withWorkflow = 1,
|
||||||
categoryIds = listOf(categoryId),
|
categoryIds = listOf(categoryId),
|
||||||
random = 1
|
// random = 1
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// 获取推荐智能体,使用random=1
|
// 获取推荐智能体,使用random=1
|
||||||
@@ -143,7 +143,7 @@ object AgentViewModel: ViewModel() {
|
|||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
withWorkflow = 1,
|
withWorkflow = 1,
|
||||||
categoryIds = null,
|
categoryIds = null,
|
||||||
random = 1
|
// random = 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() {
|
|||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 20,
|
pageSize = 20,
|
||||||
isRecommended = 1,
|
isRecommended = 1,
|
||||||
random = "1"
|
// random = "1"
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val allRooms = response.body()?.list ?: emptyList()
|
val allRooms = response.body()?.list ?: emptyList()
|
||||||
@@ -332,18 +332,17 @@ object AgentViewModel: ViewModel() {
|
|||||||
openId: String,
|
openId: String,
|
||||||
navController: NavHostController
|
navController: NavHostController
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
// 直接使用openId导航,页面内的AiProfileViewModel会处理数据加载
|
||||||
|
// 避免重复请求,因为AiProfileViewModel.loadProfile已经支持通过openId加载
|
||||||
try {
|
try {
|
||||||
val profile = userService.getUserProfileByOpenId(openId)
|
|
||||||
// 从Agent列表点击进去的一定是智能体,直接传递isAiAccount = true
|
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.AccountProfile.route
|
NavigationRoute.AccountProfile.route
|
||||||
.replace("{id}", profile.id.toString())
|
.replace("{id}", openId)
|
||||||
.replace("{isAiAccount}", "true")
|
.replace("{isAiAccount}", "true")
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// swallow error to avoid crash on navigation attempt failures
|
Log.e("AgentViewModel", "Navigation failed", e)
|
||||||
}
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -49,12 +48,11 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
import com.airbnb.lottie.compose.LottieAnimation
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||||
import com.airbnb.lottie.compose.LottieConstants
|
import com.airbnb.lottie.compose.LottieConstants
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 智能体聊天列表页面
|
* 智能体聊天列表页面
|
||||||
@@ -91,30 +89,74 @@ fun AgentChatListScreen() {
|
|||||||
.pullRefresh(state)
|
.pullRefresh(state)
|
||||||
) {
|
) {
|
||||||
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
|
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// 空状态
|
// 空状态
|
||||||
Column(
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.offset(y = (-40).dp)
|
|
||||||
) {
|
) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
if (isNetworkAvailable) {
|
if (isNetworkAvailable) {
|
||||||
ChatEmptyStateView()
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
} else {
|
Image(
|
||||||
NetworkErrorContentCompact(
|
painter = painterResource(id = R.mipmap.invalid_name_3),
|
||||||
onReload = {
|
contentDescription = "null data",
|
||||||
|
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)
|
AgentChatListViewModel.refreshPager(context = context)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
|
|||||||
@@ -42,11 +42,10 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
|
||||||
|
|
||||||
data class CombinedConversation(
|
data class CombinedConversation(
|
||||||
val type: String, // "agent", "group", or "friend"
|
val type: String, // "agent", "group", or "friend"
|
||||||
@@ -218,23 +217,66 @@ fun AllChatListScreen() {
|
|||||||
.pullRefresh(state)
|
.pullRefresh(state)
|
||||||
) {
|
) {
|
||||||
if (allConversations.isEmpty() && !isLoading) {
|
if (allConversations.isEmpty() && !isLoading) {
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.offset(y = (-40).dp)
|
|
||||||
) {
|
) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
if (isNetworkAvailable) {
|
if (isNetworkAvailable) {
|
||||||
ChatEmptyStateView()
|
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 {
|
} else {
|
||||||
NetworkErrorContentCompact(
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
onReload = {
|
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
|
isLoading = true
|
||||||
// 重新加载所有类型的数据
|
// 重新加载所有类型的数据
|
||||||
AgentChatListViewModel.refreshPager(context = context)
|
AgentChatListViewModel.refreshPager(context = context)
|
||||||
@@ -244,7 +286,6 @@ fun AllChatListScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import com.aiosman.ravenow.LocalNavController
|
|||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
import com.aiosman.ravenow.ui.index.tabs.search.ReloadButton
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -41,7 +41,6 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -73,29 +72,72 @@ fun FriendChatListScreen() {
|
|||||||
.pullRefresh(state)
|
.pullRefresh(state)
|
||||||
) {
|
) {
|
||||||
if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) {
|
if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) {
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.offset(y = (-40).dp)
|
//verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
if (isNetworkAvailable) {
|
if (isNetworkAvailable) {
|
||||||
ChatEmptyStateView()
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
} else {
|
Image(
|
||||||
NetworkErrorContentCompact(
|
painter = painterResource(id = R.mipmap.invalid_name_3),
|
||||||
onReload = {
|
contentDescription = "null data",
|
||||||
|
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)
|
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
|||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -27,6 +26,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
@@ -35,8 +35,7 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -68,29 +67,71 @@ fun GroupChatListScreen() {
|
|||||||
.pullRefresh(state)
|
.pullRefresh(state)
|
||||||
) {
|
) {
|
||||||
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
|
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.offset(y = (-40).dp)
|
|
||||||
) {
|
) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
if (isNetworkAvailable) {
|
if (isNetworkAvailable) {
|
||||||
ChatEmptyStateView()
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
} else {
|
Image(
|
||||||
NetworkErrorContentCompact(
|
painter = painterResource(id = R.mipmap.invalid_name_3),
|
||||||
onReload = {
|
contentDescription = "null data",
|
||||||
|
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)
|
GroupChatListViewModel.refreshPager(context = context)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
@@ -112,13 +153,6 @@ fun GroupChatListScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (index < GroupChatListViewModel.groupChatList.size - 1) {
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
|
||||||
color = AppColors.divider
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) {
|
if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) {
|
||||||
@@ -172,16 +206,16 @@ fun GroupChatItem(
|
|||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val chatDebouncer = rememberDebouncer()
|
val chatDebouncer = rememberDebouncer()
|
||||||
val avatarDebouncer = rememberDebouncer()
|
val avatarDebouncer = rememberDebouncer()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp, vertical = 12.dp)
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
chatDebouncer {
|
chatDebouncer {
|
||||||
onChatClick(conversation)
|
onChatClick(conversation)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
@@ -201,9 +235,9 @@ fun GroupChatItem(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 12.dp)
|
.padding(start = 12.dp, top = 2.dp),
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -211,22 +245,22 @@ fun GroupChatItem(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = conversation.groupName,
|
text = conversation.groupName,
|
||||||
fontSize = 16.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = conversation.lastMessageTime,
|
text = conversation.lastMessageTime,
|
||||||
fontSize = 12.sp,
|
fontSize = 11.sp,
|
||||||
color = AppColors.secondaryText
|
color = AppColors.secondaryText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -234,29 +268,29 @@ fun GroupChatItem(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
|
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
|
||||||
fontSize = 14.sp,
|
fontSize = 12.sp,
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
|
||||||
if (conversation.unreadCount > 0) {
|
if (conversation.unreadCount > 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
|
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
|
||||||
.background(
|
.background(
|
||||||
color = AppColors.main,
|
color = Color(0xFFFF3B30),
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
|
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
|
||||||
color = AppColors.mainText,
|
color = Color.White,
|
||||||
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp,
|
fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,36 +64,12 @@ open class BaseMomentModel :ViewModel(){
|
|||||||
momentLoader.updateMomentLike(event.postId, event.isLike)
|
momentLoader.updateMomentLike(event.postId, event.isLike)
|
||||||
}
|
}
|
||||||
suspend fun likeMoment(id: Int) {
|
suspend fun likeMoment(id: Int) {
|
||||||
// 获取当前动态信息,用于计算新的点赞数
|
|
||||||
val currentMoment = momentLoader.list.find { it.id == id }
|
|
||||||
val newLikeCount = (currentMoment?.likeCount ?: 0) + 1
|
|
||||||
|
|
||||||
momentService.likeMoment(id)
|
momentService.likeMoment(id)
|
||||||
|
momentLoader.updateMomentLike(id, true)
|
||||||
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
|
|
||||||
EventBus.getDefault().post(
|
|
||||||
MomentLikeChangeEvent(
|
|
||||||
postId = id,
|
|
||||||
likeCount = newLikeCount,
|
|
||||||
isLike = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
suspend fun dislikeMoment(id: Int) {
|
suspend fun dislikeMoment(id: Int) {
|
||||||
// 获取当前动态信息,用于计算新的点赞数
|
|
||||||
val currentMoment = momentLoader.list.find { it.id == id }
|
|
||||||
val newLikeCount = ((currentMoment?.likeCount ?: 0) - 1).coerceAtLeast(0)
|
|
||||||
|
|
||||||
momentService.dislikeMoment(id)
|
momentService.dislikeMoment(id)
|
||||||
|
momentLoader.updateMomentLike(id, false)
|
||||||
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
|
|
||||||
EventBus.getDefault().post(
|
|
||||||
MomentLikeChangeEvent(
|
|
||||||
postId = id,
|
|
||||||
likeCount = newLikeCount,
|
|
||||||
isLike = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -114,27 +90,14 @@ open class BaseMomentModel :ViewModel(){
|
|||||||
|
|
||||||
suspend fun favoriteMoment(id: Int) {
|
suspend fun favoriteMoment(id: Int) {
|
||||||
momentService.favoriteMoment(id)
|
momentService.favoriteMoment(id)
|
||||||
|
momentLoader.updateFavoriteCount(id, true)
|
||||||
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
|
|
||||||
EventBus.getDefault().post(
|
|
||||||
MomentFavouriteChangeEvent(
|
|
||||||
postId = id,
|
|
||||||
isFavourite = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun unfavoriteMoment(id: Int) {
|
suspend fun unfavoriteMoment(id: Int) {
|
||||||
momentService.unfavoriteMoment(id)
|
momentService.unfavoriteMoment(id)
|
||||||
|
momentLoader.updateFavoriteCount(id, false)
|
||||||
|
|
||||||
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
|
|
||||||
EventBus.getDefault().post(
|
|
||||||
MomentFavouriteChangeEvent(
|
|
||||||
postId = id,
|
|
||||||
isFavourite = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
@@ -31,7 +30,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -146,8 +144,6 @@ fun NewsCommentModal(
|
|||||||
var showCommentMenu by remember { mutableStateOf(false) }
|
var showCommentMenu by remember { mutableStateOf(false) }
|
||||||
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
// 菜单弹窗
|
// 菜单弹窗
|
||||||
if (showCommentMenu) {
|
if (showCommentMenu) {
|
||||||
@@ -230,14 +226,9 @@ fun NewsCommentModal(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End
|
horizontalArrangement = Arrangement.End
|
||||||
) {
|
) {
|
||||||
OrderSelectionComponent(
|
OrderSelectionComponent {
|
||||||
selectedOrder = commentViewModel.order
|
|
||||||
) {
|
|
||||||
commentViewModel.order = it
|
commentViewModel.order = it
|
||||||
commentViewModel.reloadComment()
|
commentViewModel.reloadComment()
|
||||||
scope.launch {
|
|
||||||
listState.scrollToItem(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,9 +267,7 @@ fun NewsCommentModal(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn {
|
||||||
state = listState
|
|
||||||
) {
|
|
||||||
item {
|
item {
|
||||||
CommentContent(
|
CommentContent(
|
||||||
viewModel = commentViewModel,
|
viewModel = commentViewModel,
|
||||||
|
|||||||
@@ -35,10 +35,16 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.comment.CommentModalContent
|
import com.aiosman.ravenow.ui.comment.CommentModalContent
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.data.UserService
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态推荐Item组件(post_normal)
|
* 动态推荐Item组件(post_normal)
|
||||||
@@ -50,16 +56,37 @@ fun PostRecommendationItem(
|
|||||||
moment: MomentEntity,
|
moment: MomentEntity,
|
||||||
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||||
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onCommentAdded: ((MomentEntity) -> Unit)? = null,
|
||||||
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
|
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
|
||||||
onShareClick: ((MomentEntity) -> Unit)? = null,
|
onShareClick: ((MomentEntity) -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val userService: UserService = UserServiceImpl()
|
||||||
var showCommentModal by remember { mutableStateOf(false) }
|
var showCommentModal by remember { mutableStateOf(false) }
|
||||||
var sheetState = rememberModalBottomSheetState(
|
var sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 导航到个人资料
|
||||||
|
fun navigateToProfile() {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val profile = userService.getUserProfile(moment.authorId.toString())
|
||||||
|
navController.navigate(
|
||||||
|
NavigationRoute.AccountProfile.route
|
||||||
|
.replace("{id}", profile.id.toString())
|
||||||
|
.replace("{isAiAccount}", if (profile.aiAccount) "true" else "false")
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 处理错误,避免崩溃
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 图片列表
|
// 图片列表
|
||||||
val images = moment.images
|
val images = moment.images
|
||||||
val imageCount = images.size
|
val imageCount = images.size
|
||||||
@@ -71,8 +98,9 @@ fun PostRecommendationItem(
|
|||||||
) {
|
) {
|
||||||
// 图片显示区域(替代视频播放器)
|
// 图片显示区域(替代视频播放器)
|
||||||
if (imageCount > 0) {
|
if (imageCount > 0) {
|
||||||
// 只显示第一张图片,优先使用 thumbnailDirectUrl
|
// 只显示第一张图片,优先使用 smallDirectUrl
|
||||||
val imageUrl = images[0].thumbnailDirectUrl
|
val imageUrl = images[0].smallDirectUrl
|
||||||
|
?: images[0].thumbnailDirectUrl
|
||||||
?: images[0].directUrl
|
?: images[0].directUrl
|
||||||
?: images[0].url
|
?: images[0].url
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
@@ -104,32 +132,16 @@ fun PostRecommendationItem(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, bottom = 16.dp)
|
.padding(start = 16.dp, bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
// 用户头像和昵称
|
// 用户昵称
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(Color.White.copy(alpha = 0.2f))
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = moment.avatar,
|
|
||||||
contentDescription = "用户头像",
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
defaultRes = R.drawable.default_avatar
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
text = "@${moment.nickname}",
|
text = "@${moment.nickname}",
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
modifier = Modifier
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
.noRippleClickable { navigateToProfile() },
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// 文字内容
|
// 文字内容
|
||||||
if (!moment.momentTextContent.isNullOrEmpty()) {
|
if (!moment.momentTextContent.isNullOrEmpty()) {
|
||||||
@@ -160,7 +172,10 @@ fun PostRecommendationItem(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// 用户头像
|
// 用户头像
|
||||||
UserAvatar(avatarUrl = moment.avatar)
|
UserAvatar(
|
||||||
|
avatarUrl = moment.avatar,
|
||||||
|
onClick = { navigateToProfile() }
|
||||||
|
)
|
||||||
|
|
||||||
// 点赞
|
// 点赞
|
||||||
VideoBtn(
|
VideoBtn(
|
||||||
@@ -205,21 +220,35 @@ fun PostRecommendationItem(
|
|||||||
containerColor = Color.White,
|
containerColor = Color.White,
|
||||||
sheetState = sheetState
|
sheetState = sheetState
|
||||||
) {
|
) {
|
||||||
CommentModalContent(postId = moment.id) {
|
CommentModalContent(
|
||||||
// 评论添加后的回调
|
postId = moment.id,
|
||||||
|
commentCount = moment.commentCount,
|
||||||
|
onCommentAdded = {
|
||||||
|
onCommentAdded?.invoke(moment)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UserAvatar(avatarUrl: String? = null) {
|
private fun UserAvatar(
|
||||||
|
avatarUrl: String? = null,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 16.dp)
|
.padding(bottom = 16.dp)
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color.White.copy(alpha = 0.2f))
|
.background(Color.White.copy(alpha = 0.2f))
|
||||||
|
.then(
|
||||||
|
if (onClick != null) {
|
||||||
|
Modifier.noRippleClickable { onClick() }
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (avatarUrl != null && avatarUrl.isNotEmpty()) {
|
if (avatarUrl != null && avatarUrl.isNotEmpty()) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
|
|||||||
@@ -239,11 +239,13 @@ fun RecommendScreen() {
|
|||||||
onCommentClick = { m ->
|
onCommentClick = { m ->
|
||||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||||
navController.navigate(NavigationRoute.Login.route)
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
} else {
|
}
|
||||||
|
// 注意:不在这里增加评论数,应该在评论真正提交成功后再增加
|
||||||
|
},
|
||||||
|
onCommentAdded = { m ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
RecommendViewModel.onAddComment(m.id)
|
RecommendViewModel.onAddComment(m.id)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onFavoriteClick = { m ->
|
onFavoriteClick = { m ->
|
||||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@@ -30,11 +33,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
@@ -97,10 +97,6 @@ fun VideoRecommendationItem(
|
|||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
)
|
)
|
||||||
var pauseIconVisibleState by remember { mutableStateOf(false) }
|
var pauseIconVisibleState by remember { mutableStateOf(false) }
|
||||||
var shouldResumeAfterLifecyclePause by remember { mutableStateOf(false) }
|
|
||||||
// 防抖:记录上次双击时间,防止快速重复双击
|
|
||||||
val lastDoubleTapTime = remember { mutableStateOf(0L) }
|
|
||||||
val doubleTapDebounceTime = 500L // 500ms 防抖时间
|
|
||||||
|
|
||||||
val exoPlayer = remember(videoUrl) {
|
val exoPlayer = remember(videoUrl) {
|
||||||
ExoPlayer.Builder(context)
|
ExoPlayer.Builder(context)
|
||||||
@@ -171,19 +167,7 @@ fun VideoRecommendationItem(
|
|||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.pointerInput(videoUrl, moment.liked) {
|
.noRippleClickable {
|
||||||
detectTapGestures(
|
|
||||||
onDoubleTap = { offset ->
|
|
||||||
// 双击点赞/取消点赞
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
|
|
||||||
lastDoubleTapTime.value = currentTime
|
|
||||||
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
|
|
||||||
onLikeClick?.invoke(moment)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap = {
|
|
||||||
// 单击播放/暂停
|
|
||||||
pauseIconVisibleState = true
|
pauseIconVisibleState = true
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -197,17 +181,15 @@ fun VideoRecommendationItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (pauseIconVisibleState) {
|
if (pauseIconVisibleState) {
|
||||||
Image(
|
Icon(
|
||||||
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
|
imageVector = Icons.Default.PlayArrow,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.size(80.dp),
|
.size(80.dp),
|
||||||
colorFilter = ColorFilter.tint(Color.White)
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,9 +300,7 @@ fun VideoRecommendationItem(
|
|||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { showCommentModal = false },
|
onDismissRequest = { showCommentModal = false },
|
||||||
containerColor = Color.White,
|
containerColor = Color.White,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState
|
||||||
dragHandle = {},
|
|
||||||
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
|
|
||||||
) {
|
) {
|
||||||
CommentModalContent(postId = moment.id) {
|
CommentModalContent(postId = moment.id) {
|
||||||
// 评论添加后的回调
|
// 评论添加后的回调
|
||||||
@@ -340,18 +320,11 @@ fun VideoRecommendationItem(
|
|||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
Lifecycle.Event.ON_PAUSE -> {
|
Lifecycle.Event.ON_PAUSE -> {
|
||||||
shouldResumeAfterLifecyclePause = exoPlayer.isPlaying && !pauseIconVisibleState
|
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
}
|
}
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
if (isVisible && shouldResumeAfterLifecyclePause) {
|
if (isVisible) {
|
||||||
pauseIconVisibleState = false
|
|
||||||
exoPlayer.play()
|
exoPlayer.play()
|
||||||
} else {
|
|
||||||
// 未自动恢复播放时,如果当前可见且视频已暂停,则显示暂停图标
|
|
||||||
if (isVisible && !exoPlayer.isPlaying) {
|
|
||||||
pauseIconVisibleState = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -426,4 +399,3 @@ private fun VideoBtn(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态列表
|
* 动态列表
|
||||||
@@ -91,11 +90,38 @@ fun TimelineMomentsList() {
|
|||||||
.padding(top = 188.dp),
|
.padding(top = 188.dp),
|
||||||
contentAlignment = Alignment.TopCenter
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
NetworkErrorContentInline(
|
Column(
|
||||||
onReload = {
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
model.refreshPager(pullRefresh = true)
|
modifier = Modifier.fillMaxWidth()
|
||||||
}
|
) {
|
||||||
|
val exploreDebouncer = rememberDebouncer()
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(140.dp)
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
ExploreButton(
|
||||||
|
onClick = {
|
||||||
|
exploreDebouncer {
|
||||||
|
/* TODO: 添加点击事件处理 */
|
||||||
|
} }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (moments.isEmpty()) {
|
} else if (moments.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
|||||||
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||||
import com.aiosman.ravenow.event.MomentRemoveEvent
|
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||||
import com.aiosman.ravenow.data.PointService
|
import com.aiosman.ravenow.data.PointService
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
@@ -310,16 +308,14 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
* 加载房间列表
|
* 加载房间列表
|
||||||
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
||||||
* @param pullRefresh 是否下拉刷新
|
* @param pullRefresh 是否下拉刷新
|
||||||
* @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为null,则显示当前用户创建或加入的房间
|
|
||||||
*/
|
*/
|
||||||
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false, ownerSessionId: String? = null) {
|
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false) {
|
||||||
// 游客模式下不加载房间列表
|
// 游客模式下不加载房间列表
|
||||||
if (AppStore.isGuest) {
|
if (AppStore.isGuest) {
|
||||||
Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表")
|
Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
|
|
||||||
if (roomsLoading && !pullRefresh) return
|
if (roomsLoading && !pullRefresh) return
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -335,51 +331,43 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
roomsCurrentPage
|
roomsCurrentPage
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据filterType确定roomType
|
val response = when (filterType) {
|
||||||
val roomType = when (filterType) {
|
0 -> {
|
||||||
1 -> "public"
|
// 全部:显示自己创建或加入的所有房间
|
||||||
2 -> "private"
|
apiClient.getRooms(
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
val effectiveRoomType = if (normalizedOwnerId != null) "public" else roomType
|
|
||||||
|
|
||||||
// 构建API调用参数
|
|
||||||
if (normalizedOwnerId != null) {
|
|
||||||
// 查看其他用户的房间:仅显示该用户创建的公开房间
|
|
||||||
val createdResponse = apiClient.getRooms(
|
|
||||||
page = currentPage,
|
page = currentPage,
|
||||||
pageSize = roomsPageSize,
|
pageSize = roomsPageSize,
|
||||||
roomType = effectiveRoomType,
|
|
||||||
ownerSessionId = normalizedOwnerId,
|
|
||||||
includeUsers = false
|
|
||||||
)
|
|
||||||
|
|
||||||
val createdRooms = if (createdResponse.isSuccessful) {
|
|
||||||
createdResponse.body()?.list?.map { it.toRoomtEntity() } ?: emptyList()
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pullRefresh || currentPage == 1) {
|
|
||||||
rooms = createdRooms
|
|
||||||
} else {
|
|
||||||
rooms = rooms + createdRooms
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
showCreated = true,
|
||||||
showJoined = if (filterType == 2) null else 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) {
|
if (response.isSuccessful) {
|
||||||
val roomList = response.body()?.list ?: emptyList()
|
val roomList = response.body()?.list ?: emptyList()
|
||||||
@@ -398,7 +386,6 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
} else {
|
} else {
|
||||||
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
|
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("MyProfileViewModel", "loadRooms error: ", e)
|
Log.e("MyProfileViewModel", "loadRooms error: ", e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -411,29 +398,20 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
/**
|
/**
|
||||||
* 加载更多房间
|
* 加载更多房间
|
||||||
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
||||||
* @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间
|
|
||||||
*/
|
*/
|
||||||
fun loadMoreRooms(filterType: Int = 0, ownerSessionId: String? = null) {
|
fun loadMoreRooms(filterType: Int = 0) {
|
||||||
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
|
|
||||||
if (roomsLoading || !roomsHasMore) return
|
if (roomsLoading || !roomsHasMore) return
|
||||||
loadRooms(filterType = filterType, pullRefresh = false, ownerSessionId = normalizedOwnerId)
|
loadRooms(filterType = filterType, pullRefresh = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新房间列表
|
* 刷新房间列表
|
||||||
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
||||||
* @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间
|
|
||||||
*/
|
*/
|
||||||
fun refreshRooms(filterType: Int = 0, ownerSessionId: String? = null) {
|
fun refreshRooms(filterType: Int = 0) {
|
||||||
rooms = emptyList()
|
rooms = emptyList()
|
||||||
roomsCurrentPage = 1
|
roomsCurrentPage = 1
|
||||||
roomsHasMore = true
|
roomsHasMore = true
|
||||||
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
|
loadRooms(filterType = filterType, pullRefresh = true)
|
||||||
loadRooms(filterType = filterType, pullRefresh = true, ownerSessionId = normalizedOwnerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun normalizeOwnerSessionId(ownerSessionId: String?): String? {
|
|
||||||
val trimmed = ownerSessionId?.trim()
|
|
||||||
return if (trimmed.isNullOrEmpty()) null else trimmed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -60,8 +59,6 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -88,8 +85,6 @@ import com.aiosman.ravenow.ui.index.IndexViewModel
|
|||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SegmentedControl
|
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.AgentSegmentedControl
|
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
|
||||||
@@ -173,41 +168,6 @@ fun ProfileV3(
|
|||||||
initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset
|
initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset
|
||||||
)
|
)
|
||||||
val scrollState = rememberScrollState(model.profileScrollOffset)
|
val scrollState = rememberScrollState(model.profileScrollOffset)
|
||||||
var tabIndicatorContentOffset by remember { mutableStateOf<Float?>(null) }
|
|
||||||
var tabIndicatorHeightPx by remember { mutableStateOf(0) }
|
|
||||||
val topNavigationBarHeightPx = with(density) { (statusBarPaddingValues.calculateTopPadding() + 56.dp).toPx() }
|
|
||||||
val stickyTopPadding = statusBarPaddingValues.calculateTopPadding() + 56.dp
|
|
||||||
var agentSegmentOffset by remember { mutableStateOf<Float?>(null) }
|
|
||||||
var agentSegmentHeightPx by remember { mutableStateOf(0) }
|
|
||||||
var groupSegmentOffset by remember { mutableStateOf<Float?>(null) }
|
|
||||||
var groupSegmentHeightPx by remember { mutableStateOf(0) }
|
|
||||||
var agentSegmentSelected by remember { mutableStateOf(0) }
|
|
||||||
var groupSegmentSelected by remember { mutableStateOf(0) }
|
|
||||||
val tabIndicatorHeightDp = with(density) { tabIndicatorHeightPx.toDp() }
|
|
||||||
val tabBarBottomPx = topNavigationBarHeightPx + tabIndicatorHeightPx
|
|
||||||
val tabBarBottomPadding = stickyTopPadding + tabIndicatorHeightDp
|
|
||||||
val tabStickyThreshold = remember(tabIndicatorContentOffset, topNavigationBarHeightPx) {
|
|
||||||
tabIndicatorContentOffset?.minus(topNavigationBarHeightPx)
|
|
||||||
}
|
|
||||||
val agentSegmentThreshold = remember(agentSegmentOffset, tabBarBottomPx) {
|
|
||||||
agentSegmentOffset?.minus(tabBarBottomPx)
|
|
||||||
}
|
|
||||||
val groupSegmentThreshold = remember(groupSegmentOffset, tabBarBottomPx) {
|
|
||||||
groupSegmentOffset?.minus(tabBarBottomPx)
|
|
||||||
}
|
|
||||||
val agentTabIndex = if (isAiAccount) -1 else 1
|
|
||||||
val groupTabIndex = if (isAiAccount) 1 else 2
|
|
||||||
val shouldStickTabBar = tabStickyThreshold?.let { scrollState.value >= it } ?: false
|
|
||||||
val shouldStickAgentSegments = isSelf && !isAiAccount && agentSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == agentTabIndex
|
|
||||||
val shouldStickGroupSegments = isSelf && groupSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == groupTabIndex
|
|
||||||
val externalOwnerSessionId = remember(isSelf, profile?.chatAIId, profile?.trtcUserId) {
|
|
||||||
if (isSelf) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
profile?.chatAIId?.takeIf { it.isNotBlank() }
|
|
||||||
?: profile?.trtcUserId?.takeIf { it.isNotBlank() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) {
|
val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
@@ -531,20 +491,11 @@ fun ProfileV3(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(AppColors.profileBackground)
|
.background(AppColors.profileBackground)
|
||||||
.padding(top = 8.dp)
|
.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(
|
UserContentPageIndicator(
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
showAgentTab = !isAiAccount
|
showAgentTab = !isAiAccount
|
||||||
)
|
)
|
||||||
}
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.height(650.dp) // 固定滚动高度
|
modifier = Modifier.height(650.dp) // 固定滚动高度
|
||||||
@@ -582,53 +533,22 @@ fun ProfileV3(
|
|||||||
showNoMoreText = isSelf,
|
showNoMoreText = isSelf,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = listState,
|
state = listState,
|
||||||
nestedScrollConnection = nestedScrollConnection,
|
nestedScrollConnection = nestedScrollConnection
|
||||||
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
|
|
||||||
segmentSelectedIndex = agentSegmentSelected,
|
|
||||||
onSegmentSelected = { agentSegmentSelected = it },
|
|
||||||
onSegmentMeasured = { offset, height ->
|
|
||||||
agentSegmentOffset = offset
|
|
||||||
agentSegmentHeightPx = height
|
|
||||||
},
|
|
||||||
isSegmentSticky = shouldStickAgentSegments,
|
|
||||||
parentScrollProvider = { scrollState.value }
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// 查看其他用户的主页时,传递该用户的会话ID以显示其创建的群聊;查看自己的主页时传递null
|
|
||||||
GroupChatPlaceholder(
|
GroupChatPlaceholder(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
listState = groupChatListState,
|
listState = groupChatListState,
|
||||||
nestedScrollConnection = nestedScrollConnection,
|
nestedScrollConnection = nestedScrollConnection
|
||||||
ownerSessionId = externalOwnerSessionId,
|
|
||||||
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
|
|
||||||
selectedSegmentIndex = groupSegmentSelected,
|
|
||||||
onSegmentSelected = { groupSegmentSelected = it },
|
|
||||||
onSegmentMeasured = { offset, height ->
|
|
||||||
groupSegmentOffset = offset
|
|
||||||
groupSegmentHeightPx = height
|
|
||||||
},
|
|
||||||
isSegmentSticky = shouldStickGroupSegments,
|
|
||||||
parentScrollProvider = { scrollState.value }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
if (!isAiAccount) {
|
if (!isAiAccount) {
|
||||||
// 查看其他用户的主页时,传递该用户的会话ID以显示其创建的群聊;查看自己的主页时传递null
|
|
||||||
GroupChatPlaceholder(
|
GroupChatPlaceholder(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
listState = groupChatListState,
|
listState = groupChatListState,
|
||||||
nestedScrollConnection = nestedScrollConnection,
|
nestedScrollConnection = nestedScrollConnection
|
||||||
ownerSessionId = externalOwnerSessionId,
|
|
||||||
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
|
|
||||||
selectedSegmentIndex = groupSegmentSelected,
|
|
||||||
onSegmentSelected = { groupSegmentSelected = it },
|
|
||||||
onSegmentMeasured = { offset, height ->
|
|
||||||
groupSegmentOffset = offset
|
|
||||||
groupSegmentHeightPx = height
|
|
||||||
},
|
|
||||||
isSegmentSticky = shouldStickGroupSegments,
|
|
||||||
parentScrollProvider = { scrollState.value }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,55 +560,6 @@ fun ProfileV3(
|
|||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldStickTabBar && tabIndicatorHeightPx > 0) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.TopCenter)
|
|
||||||
.padding(top = stickyTopPadding)
|
|
||||||
.background(AppColors.profileBackground)
|
|
||||||
) {
|
|
||||||
UserContentPageIndicator(
|
|
||||||
pagerState = pagerState,
|
|
||||||
showAgentTab = !isAiAccount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldStickAgentSegments && agentSegmentHeightPx > 0) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.TopCenter)
|
|
||||||
.padding(top = tabBarBottomPadding)
|
|
||||||
.background(AppColors.profileBackground)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
AgentSegmentedControl(
|
|
||||||
selectedIndex = agentSegmentSelected,
|
|
||||||
onSegmentSelected = { agentSegmentSelected = it },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldStickGroupSegments && groupSegmentHeightPx > 0) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.TopCenter)
|
|
||||||
.padding(top = tabBarBottomPadding)
|
|
||||||
.background(AppColors.profileBackground)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
SegmentedControl(
|
|
||||||
selectedIndex = groupSegmentSelected,
|
|
||||||
onSegmentSelected = { groupSegmentSelected = it },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 顶部导航栏
|
// 顶部导航栏
|
||||||
TopNavigationBar(
|
TopNavigationBar(
|
||||||
isMain = isMain,
|
isMain = isMain,
|
||||||
@@ -785,26 +656,12 @@ fun ProfileV3(
|
|||||||
private fun GroupChatPlaceholder(
|
private fun GroupChatPlaceholder(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
listState: androidx.compose.foundation.lazy.LazyListState,
|
listState: androidx.compose.foundation.lazy.LazyListState,
|
||||||
nestedScrollConnection: NestedScrollConnection? = null,
|
nestedScrollConnection: NestedScrollConnection? = null
|
||||||
ownerSessionId: String? = null, // 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为null,则显示当前用户创建或加入的房间
|
|
||||||
showSegments: Boolean = true,
|
|
||||||
selectedSegmentIndex: Int = 0,
|
|
||||||
onSegmentSelected: (Int) -> Unit = {},
|
|
||||||
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
|
|
||||||
isSegmentSticky: Boolean = false,
|
|
||||||
parentScrollProvider: () -> Int = { 0 }
|
|
||||||
) {
|
) {
|
||||||
GroupChatEmptyContent(
|
GroupChatEmptyContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
nestedScrollConnection = nestedScrollConnection,
|
nestedScrollConnection = nestedScrollConnection
|
||||||
ownerSessionId = ownerSessionId,
|
|
||||||
showSegments = showSegments,
|
|
||||||
selectedSegmentIndex = selectedSegmentIndex,
|
|
||||||
onSegmentSelected = onSegmentSelected,
|
|
||||||
onSegmentMeasured = onSegmentMeasured,
|
|
||||||
isSegmentSticky = isSegmentSticky,
|
|
||||||
parentScrollProvider = parentScrollProvider
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,50 +1,47 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
|
||||||
import com.aiosman.ravenow.LocalNavController
|
|
||||||
import com.aiosman.ravenow.R
|
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|
||||||
import com.aiosman.ravenow.ui.navigateToPost
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
@@ -158,8 +155,7 @@ fun GalleryGrid(
|
|||||||
modifier = baseModifier
|
modifier = baseModifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 60.dp)
|
.padding(vertical = 60.dp),
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
@@ -202,8 +198,24 @@ fun GalleryGrid(
|
|||||||
.padding(vertical = 60.dp),
|
.padding(vertical = 60.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
EmptyStateView(
|
Image(
|
||||||
contentDescription = "暂无图片"
|
painter = painterResource(id = R.mipmap.l_empty_img),
|
||||||
|
contentDescription = "暂无图片",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 181.dp, height = 153.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(9.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cosmos_awaits),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -215,20 +227,8 @@ fun GalleryGrid(
|
|||||||
.padding(bottom = 8.dp),
|
.padding(bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(moments) { idx, moment ->
|
itemsIndexed(moments) { idx, moment ->
|
||||||
moment?.let { momentItem ->
|
if (moment != null && moment.images.isNotEmpty()) {
|
||||||
val itemDebouncer = rememberDebouncer()
|
val itemDebouncer = rememberDebouncer()
|
||||||
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
|
|
||||||
val previewUrl = when {
|
|
||||||
momentItem.images.isNotEmpty() -> momentItem.images[0].thumbnail
|
|
||||||
isVideoMoment -> {
|
|
||||||
val firstVideo = momentItem.videos!!.first()
|
|
||||||
firstVideo.thumbnailDirectUrl
|
|
||||||
?: firstVideo.thumbnailUrl
|
|
||||||
?: firstVideo.directUrl
|
|
||||||
?: firstVideo.url
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -237,32 +237,20 @@ fun GalleryGrid(
|
|||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
itemDebouncer {
|
itemDebouncer {
|
||||||
navController.navigateToPost(
|
navController.navigateToPost(
|
||||||
id = momentItem.id,
|
id = moment.id,
|
||||||
highlightCommentId = 0,
|
highlightCommentId = 0,
|
||||||
initImagePagerIndex = 0
|
initImagePagerIndex = 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (previewUrl != null) {
|
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = previewUrl,
|
imageUrl = moment.images[0].thumbnail,
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
context = LocalContext.current
|
context = LocalContext.current
|
||||||
)
|
)
|
||||||
} else {
|
if (moment.images.size > 1) {
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(
|
|
||||||
color = AppColors.basicMain.copy(alpha = 0.2f),
|
|
||||||
shape = RoundedCornerShape(10.dp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp, end = 8.dp)
|
.padding(top = 8.dp, end = 8.dp)
|
||||||
@@ -275,31 +263,6 @@ fun GalleryGrid(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideoMoment) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 8.dp, end = 8.dp)
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.background(
|
|
||||||
color = Color.Black.copy(alpha = 0.4f),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.PlayArrow,
|
|
||||||
contentDescription = "",
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,15 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
import com.airbnb.lottie.compose.LottieAnimation
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||||
import com.airbnb.lottie.compose.LottieConstants
|
import com.airbnb.lottie.compose.LottieConstants
|
||||||
@@ -41,8 +40,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -66,47 +63,37 @@ import com.aiosman.ravenow.ui.navigateToGroupChat
|
|||||||
import com.aiosman.ravenow.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils.isNetworkAvailable
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupChatEmptyContent(
|
fun GroupChatEmptyContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
nestedScrollConnection: NestedScrollConnection? = null,
|
nestedScrollConnection: NestedScrollConnection? = null
|
||||||
ownerSessionId: String? = null, // 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为null,则显示当前用户创建或加入的房间
|
|
||||||
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
|
|
||||||
selectedSegmentIndex: Int = 0,
|
|
||||||
onSegmentSelected: (Int) -> Unit = {},
|
|
||||||
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
|
|
||||||
isSegmentSticky: Boolean = false,
|
|
||||||
parentScrollProvider: () -> Int = { 0 }
|
|
||||||
) {
|
) {
|
||||||
|
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val viewModel = MyProfileViewModel
|
val viewModel = MyProfileViewModel
|
||||||
val normalizedOwnerSessionId = ownerSessionId?.takeIf { it.isNotBlank() }
|
|
||||||
val canLoadRooms = showSegments || normalizedOwnerSessionId != null
|
|
||||||
val networkAvailable = isNetworkAvailable(context)
|
|
||||||
|
|
||||||
// 如果查看其他用户的房间,固定使用全部类型(filterType = 0)
|
|
||||||
val filterType = if (showSegments) selectedSegmentIndex else 0
|
|
||||||
|
|
||||||
val state = rememberPullRefreshState(
|
val state = rememberPullRefreshState(
|
||||||
refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false,
|
refreshing = viewModel.roomsRefreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
if (canLoadRooms) {
|
viewModel.refreshRooms(filterType = selectedSegment)
|
||||||
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 当分段或用户ID改变时,重新加载数据
|
// 当分段改变时,重新加载数据
|
||||||
LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) {
|
LaunchedEffect(selectedSegment) {
|
||||||
if (canLoadRooms) {
|
// 切换分段时重新加载
|
||||||
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
|
viewModel.refreshRooms(filterType = selectedSegment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
|
||||||
|
viewModel.loadRooms(filterType = selectedSegment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,74 +110,56 @@ fun GroupChatEmptyContent(
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 只在查看自己的房间时显示分段控制器
|
// 分段控制器
|
||||||
if (showSegments) {
|
|
||||||
SegmentedControl(
|
SegmentedControl(
|
||||||
selectedIndex = selectedSegmentIndex,
|
selectedIndex = selectedSegment,
|
||||||
onSegmentSelected = onSegmentSelected,
|
onSegmentSelected = {
|
||||||
modifier = Modifier
|
selectedSegment = it
|
||||||
.fillMaxWidth()
|
// LaunchedEffect 会监听 selectedSegment 的变化并自动刷新
|
||||||
.onGloballyPositioned { coordinates ->
|
},
|
||||||
onSegmentMeasured?.invoke(
|
modifier = Modifier.fillMaxWidth()
|
||||||
coordinates.positionInRoot().y + parentScrollProvider(),
|
|
||||||
coordinates.size.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.alpha(if (isSegmentSticky) 0f else 1f)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = nestedScrollModifier
|
modifier = nestedScrollModifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.pullRefresh(state)
|
.pullRefresh(state)
|
||||||
) {
|
) {
|
||||||
if (!canLoadRooms) {
|
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
} else if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
|
|
||||||
// 空状态内容(居中)
|
// 空状态内容(居中)
|
||||||
Column(
|
Column(
|
||||||
modifier = nestedScrollModifier.fillMaxWidth(),
|
modifier = nestedScrollModifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// 空状态插图
|
// 空状态插图
|
||||||
EmptyStateIllustration(
|
EmptyStateIllustration()
|
||||||
isNetworkAvailable = networkAvailable,
|
|
||||||
onReload = {
|
|
||||||
MyProfileViewModel.ResetModel()
|
|
||||||
MyProfileViewModel.loadProfile(pullRefresh = true)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(9.dp))
|
Spacer(modifier = Modifier.height(9.dp))
|
||||||
|
|
||||||
|
// 空状态文本
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cosmos_awaits),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = AppColors.text,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = nestedScrollModifier.fillMaxSize()
|
modifier = nestedScrollModifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
// 网格布局:每行显示2个房间卡片
|
itemsIndexed(
|
||||||
items(
|
items = viewModel.rooms,
|
||||||
items = viewModel.rooms.chunked(2),
|
key = { _, item -> item.id }
|
||||||
key = { rowRooms -> rowRooms.firstOrNull()?.id?.toString() ?: "" }
|
) { index, room ->
|
||||||
) { rowRooms ->
|
RoomItem(
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
rowRooms.forEach { room ->
|
|
||||||
RoomCard(
|
|
||||||
room = room,
|
room = room,
|
||||||
onRoomClick = { roomEntity ->
|
onRoomClick = { roomEntity ->
|
||||||
// 导航到群聊聊天界面
|
// 导航到群聊聊天界面
|
||||||
@@ -199,14 +168,14 @@ fun GroupChatEmptyContent(
|
|||||||
name = roomEntity.name,
|
name = roomEntity.name,
|
||||||
avatar = roomEntity.avatar
|
avatar = roomEntity.avatar
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
modifier = Modifier.weight(1f)
|
)
|
||||||
|
|
||||||
|
if (index < viewModel.rooms.size - 1) {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
color = AppColors.divider
|
||||||
)
|
)
|
||||||
}
|
|
||||||
// 如果这一行只有一个房间,添加一个空的占位符
|
|
||||||
if (rowRooms.size == 1) {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,17 +201,13 @@ fun GroupChatEmptyContent(
|
|||||||
if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
|
if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
|
||||||
item {
|
item {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadMoreRooms(
|
viewModel.loadMoreRooms(filterType = selectedSegment)
|
||||||
filterType = filterType,
|
|
||||||
ownerSessionId = normalizedOwnerSessionId
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canLoadRooms) {
|
|
||||||
PullRefreshIndicator(
|
PullRefreshIndicator(
|
||||||
refreshing = viewModel.roomsRefreshing,
|
refreshing = viewModel.roomsRefreshing,
|
||||||
state = state,
|
state = state,
|
||||||
@@ -250,123 +215,8 @@ fun GroupChatEmptyContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RoomCard(
|
|
||||||
room: RoomEntity,
|
|
||||||
onRoomClick: (RoomEntity) -> Unit = {},
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
val roomDebouncer = rememberDebouncer()
|
|
||||||
val cardSize = 180.dp
|
|
||||||
|
|
||||||
// 构建头像URL
|
|
||||||
val avatarUrl = if (room.avatar.isNotEmpty()) {
|
|
||||||
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
|
|
||||||
} else {
|
|
||||||
// 如果头像为空,使用群头像接口
|
|
||||||
val groupIdBase64 = Base64.encodeToString(
|
|
||||||
room.trtcType.toByteArray(),
|
|
||||||
Base64.NO_WRAP
|
|
||||||
)
|
|
||||||
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=${groupIdBase64}&token=${AppStore.token}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先显示cover,如果没有cover则显示recommendBanner,最后显示avatar
|
|
||||||
val imageUrl = when {
|
|
||||||
room.cover.isNotEmpty() -> "${ConstVars.BASE_SERVER}/api/v1/outside/${room.cover}?token=${AppStore.token}"
|
|
||||||
room.recommendBanner.isNotEmpty() -> "${ConstVars.BASE_SERVER}/api/v1/outside/${room.recommendBanner}?token=${AppStore.token}"
|
|
||||||
else -> avatarUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
// 正方形卡片,文字重叠在底部
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(cardSize)
|
|
||||||
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
|
|
||||||
.noRippleClickable {
|
|
||||||
roomDebouncer {
|
|
||||||
onRoomClick(room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = context,
|
|
||||||
imageUrl = imageUrl,
|
|
||||||
contentDescription = room.name,
|
|
||||||
modifier = Modifier
|
|
||||||
.width(cardSize)
|
|
||||||
.height(120.dp)
|
|
||||||
.clip(RoundedCornerShape(
|
|
||||||
topStart = 12.dp,
|
|
||||||
topEnd = 12.dp,
|
|
||||||
bottomStart = 0.dp,
|
|
||||||
bottomEnd = 0.dp)),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
defaultRes = R.mipmap.rider_pro_agent
|
|
||||||
)
|
|
||||||
|
|
||||||
// 房间名称,重叠在底部
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 32.dp, start = 10.dp, end = 10.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = room.name,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.W900,
|
|
||||||
color = AppColors.text,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
textAlign = TextAlign.Left
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示人数
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 10.dp, start = 10.dp, end = 10.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.rider_pro_nav_profile),
|
|
||||||
contentDescription = "chat",
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "${room.userCount} ${stringResource(R.string.chatting_now)}",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.6f)
|
|
||||||
.weight(1f),
|
|
||||||
color = AppColors.text,
|
|
||||||
fontWeight = FontWeight.W500,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列表样式的房间项,供搜索等场景复用
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomItem(
|
fun RoomItem(
|
||||||
room: RoomEntity,
|
room: RoomEntity,
|
||||||
@@ -380,6 +230,7 @@ fun RoomItem(
|
|||||||
val avatarUrl = if (room.avatar.isNotEmpty()) {
|
val avatarUrl = if (room.avatar.isNotEmpty()) {
|
||||||
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
|
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
|
||||||
} else {
|
} else {
|
||||||
|
// 如果头像为空,使用群头像接口
|
||||||
val groupIdBase64 = Base64.encodeToString(
|
val groupIdBase64 = Base64.encodeToString(
|
||||||
room.trtcType.toByteArray(),
|
room.trtcType.toByteArray(),
|
||||||
Base64.NO_WRAP
|
Base64.NO_WRAP
|
||||||
@@ -456,7 +307,7 @@ fun RoomItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SegmentedControl(
|
private fun SegmentedControl(
|
||||||
selectedIndex: Int,
|
selectedIndex: Int,
|
||||||
onSegmentSelected: (Int) -> Unit,
|
onSegmentSelected: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -521,7 +372,7 @@ private fun SegmentButton(
|
|||||||
},
|
},
|
||||||
shape = RoundedCornerShape(1000.dp)
|
shape = RoundedCornerShape(1000.dp)
|
||||||
)
|
)
|
||||||
.noRippleClickable { onClick() },
|
.clickable(onClick = onClick),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -538,43 +389,14 @@ private fun SegmentButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyStateIllustration(
|
private fun EmptyStateIllustration() {
|
||||||
isNetworkAvailable: Boolean,
|
|
||||||
onReload: () -> Unit
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
if (isNetworkAvailable) {
|
|
||||||
EmptyStateView(
|
|
||||||
contentDescription = "空状态",
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
painter = painterResource(id = R.mipmap.l_empty_img),
|
||||||
contentDescription = "network error",
|
contentDescription = "空状态",
|
||||||
modifier = Modifier.size(181.dp),
|
modifier = Modifier
|
||||||
|
.width(181.dp)
|
||||||
|
.height(153.dp),
|
||||||
|
contentScale = ContentScale.Fit
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -55,7 +52,6 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
|||||||
import com.aiosman.ravenow.entity.AgentEntity
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|
||||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import com.aiosman.ravenow.utils.DebounceUtils
|
import com.aiosman.ravenow.utils.DebounceUtils
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
@@ -70,13 +66,7 @@ fun UserAgentsList(
|
|||||||
showNoMoreText: Boolean = false,
|
showNoMoreText: Boolean = false,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: LazyListState,
|
state: LazyListState,
|
||||||
nestedScrollConnection: NestedScrollConnection? = null,
|
nestedScrollConnection: NestedScrollConnection? = null
|
||||||
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
|
|
||||||
segmentSelectedIndex: Int = 0,
|
|
||||||
onSegmentSelected: (Int) -> Unit = {},
|
|
||||||
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
|
|
||||||
isSegmentSticky: Boolean = false,
|
|
||||||
parentScrollProvider: () -> Int = { 0 }
|
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val listModifier = if (nestedScrollConnection != null) {
|
val listModifier = if (nestedScrollConnection != null) {
|
||||||
@@ -90,14 +80,7 @@ fun UserAgentsList(
|
|||||||
Box(
|
Box(
|
||||||
modifier = listModifier.fillMaxSize()
|
modifier = listModifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
AgentEmptyContentWithSegments(
|
AgentEmptyContentWithSegments()
|
||||||
showSegments = showSegments,
|
|
||||||
segmentSelectedIndex = segmentSelectedIndex,
|
|
||||||
onSegmentSelected = onSegmentSelected,
|
|
||||||
onSegmentMeasured = onSegmentMeasured,
|
|
||||||
isSegmentSticky = isSegmentSticky,
|
|
||||||
parentScrollProvider = parentScrollProvider
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -265,14 +248,8 @@ fun UserAgentCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentEmptyContentWithSegments(
|
fun AgentEmptyContentWithSegments() {
|
||||||
showSegments: Boolean = true,
|
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
|
||||||
segmentSelectedIndex: Int = 0,
|
|
||||||
onSegmentSelected: (Int) -> Unit = {},
|
|
||||||
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
|
|
||||||
isSegmentSticky: Boolean = false,
|
|
||||||
parentScrollProvider: () -> Int = { 0 }
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
@@ -283,24 +260,14 @@ fun AgentEmptyContentWithSegments(
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 只在查看自己的智能体时显示分段控制器
|
// 分段控制器
|
||||||
if (showSegments) {
|
|
||||||
AgentSegmentedControl(
|
AgentSegmentedControl(
|
||||||
selectedIndex = segmentSelectedIndex,
|
selectedIndex = selectedSegment,
|
||||||
onSegmentSelected = onSegmentSelected,
|
onSegmentSelected = { selectedSegment = it },
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
|
||||||
.onGloballyPositioned { coordinates ->
|
|
||||||
onSegmentMeasured?.invoke(
|
|
||||||
coordinates.positionInRoot().y + parentScrollProvider(),
|
|
||||||
coordinates.size.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.alpha(if (isSegmentSticky) 0f else 1f)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
|
||||||
|
|
||||||
// 空状态内容(与动态、群聊保持一致)
|
// 空状态内容(与动态、群聊保持一致)
|
||||||
Column(
|
Column(
|
||||||
@@ -308,8 +275,25 @@ fun AgentEmptyContentWithSegments(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
if (isNetworkAvailable) {
|
if (isNetworkAvailable) {
|
||||||
EmptyStateView(
|
Image(
|
||||||
contentDescription = "暂无Agent"
|
painter = painterResource(id = R.mipmap.l_empty_img),
|
||||||
|
contentDescription = "暂无Agent",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 181.dp, height = 153.dp)
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(9.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cosmos_awaits),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Image(
|
Image(
|
||||||
@@ -348,7 +332,7 @@ fun AgentEmptyContentWithSegments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentSegmentedControl(
|
private fun AgentSegmentedControl(
|
||||||
selectedIndex: Int,
|
selectedIndex: Int,
|
||||||
onSegmentSelected: (Int) -> Unit,
|
onSegmentSelected: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -413,7 +397,7 @@ private fun AgentSegmentButton(
|
|||||||
},
|
},
|
||||||
shape = RoundedCornerShape(1000.dp)
|
shape = RoundedCornerShape(1000.dp)
|
||||||
)
|
)
|
||||||
.noRippleClickable { onClick() },
|
.clickable(onClick = onClick),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
|||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@@ -416,17 +415,64 @@ fun MomentResultTab() {
|
|||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
if (moments.itemCount == 0 && model.showResult) {
|
if (moments.itemCount == 0 && model.showResult) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
Column(
|
||||||
SearchPlaceholderContent(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
isNetworkAvailable = isNetworkAvailable,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
onReload = {
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
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.ResetModel()
|
||||||
SearchViewModel.search()
|
SearchViewModel.search()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -520,17 +566,64 @@ fun UserResultTab() {
|
|||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (users.itemCount == 0 && model.showResult) {
|
if (users.itemCount == 0 && model.showResult) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
Column(
|
||||||
SearchPlaceholderContent(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
isNetworkAvailable = isNetworkAvailable,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
onReload = {
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
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.ResetModel()
|
||||||
SearchViewModel.search()
|
SearchViewModel.search()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -641,17 +734,64 @@ fun AiResultTab() {
|
|||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
if (agents.itemCount == 0 && model.showResult) {
|
if (agents.itemCount == 0 && model.showResult) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
Column(
|
||||||
SearchPlaceholderContent(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
isNetworkAvailable = isNetworkAvailable,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
onReload = {
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
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.ResetModel()
|
||||||
SearchViewModel.search()
|
SearchViewModel.search()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -723,17 +863,65 @@ fun RoomResultTab() {
|
|||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
if (rooms.itemCount == 0 && model.showResult) {
|
if (rooms.itemCount == 0 && model.showResult) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
Column(
|
||||||
SearchPlaceholderContent(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
isNetworkAvailable = isNetworkAvailable,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
onReload = {
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
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.ResetModel()
|
||||||
SearchViewModel.search()
|
SearchViewModel.search()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -757,32 +945,41 @@ fun RoomResultTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchPlaceholderContent(
|
fun ReloadButton(
|
||||||
modifier: Modifier = Modifier,
|
onClick: () -> Unit
|
||||||
isNetworkAvailable: Boolean,
|
|
||||||
onReload: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val gradientBrush = Brush.linearGradient(
|
||||||
Column(
|
colors = listOf(
|
||||||
modifier = modifier,
|
Color(0xFF7c45ed),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
Color(0xFF7c68ef),
|
||||||
verticalArrangement = Arrangement.Center
|
Color(0xFF7bd8f8)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 120.dp)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(30.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = Color.Transparent
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(0.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(gradientBrush),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (isNetworkAvailable) {
|
|
||||||
androidx.compose.foundation.Image(
|
|
||||||
painter = painterResource(id = R.mipmap.empty_img),
|
|
||||||
contentDescription = "No Comment",
|
|
||||||
modifier = Modifier.size(168.dp)
|
|
||||||
)
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.null_search),
|
text = stringResource(R.string.Reload),
|
||||||
color = appColors.text,
|
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
NetworkErrorContentInline(onReload = onReload)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ import com.aiosman.ravenow.entity.MomentEntity
|
|||||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||||
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
|
||||||
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
|
||||||
import com.aiosman.ravenow.entity.AgentEntity
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
import com.aiosman.ravenow.entity.AgentRemoteDataSource
|
import com.aiosman.ravenow.entity.AgentRemoteDataSource
|
||||||
import com.aiosman.ravenow.entity.AgentSearchPagingSource
|
import com.aiosman.ravenow.entity.AgentSearchPagingSource
|
||||||
@@ -33,7 +31,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
|
|
||||||
object SearchViewModel : ViewModel() {
|
object SearchViewModel : ViewModel() {
|
||||||
var searchText by mutableStateOf("")
|
var searchText by mutableStateOf("")
|
||||||
@@ -207,14 +204,7 @@ object SearchViewModel : ViewModel() {
|
|||||||
suspend fun likeMoment(id: Int) {
|
suspend fun likeMoment(id: Int) {
|
||||||
try {
|
try {
|
||||||
momentService.likeMoment(id)
|
momentService.likeMoment(id)
|
||||||
val likeCount = updateMomentLike(id, true)
|
updateMomentLike(id, true)
|
||||||
EventBus.getDefault().post(
|
|
||||||
MomentLikeChangeEvent(
|
|
||||||
postId = id,
|
|
||||||
likeCount = likeCount,
|
|
||||||
isLike = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -223,14 +213,7 @@ object SearchViewModel : ViewModel() {
|
|||||||
suspend fun dislikeMoment(id: Int) {
|
suspend fun dislikeMoment(id: Int) {
|
||||||
try {
|
try {
|
||||||
momentService.dislikeMoment(id)
|
momentService.dislikeMoment(id)
|
||||||
val likeCount = updateMomentLike(id, false)
|
updateMomentLike(id, false)
|
||||||
EventBus.getDefault().post(
|
|
||||||
MomentLikeChangeEvent(
|
|
||||||
postId = id,
|
|
||||||
likeCount = likeCount,
|
|
||||||
isLike = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -240,12 +223,6 @@ object SearchViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
momentService.favoriteMoment(id)
|
momentService.favoriteMoment(id)
|
||||||
updateMomentFavorite(id, true)
|
updateMomentFavorite(id, true)
|
||||||
EventBus.getDefault().post(
|
|
||||||
MomentFavouriteChangeEvent(
|
|
||||||
postId = id,
|
|
||||||
isFavourite = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -255,12 +232,6 @@ object SearchViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
momentService.unfavoriteMoment(id)
|
momentService.unfavoriteMoment(id)
|
||||||
updateMomentFavorite(id, false)
|
updateMomentFavorite(id, false)
|
||||||
EventBus.getDefault().post(
|
|
||||||
MomentFavouriteChangeEvent(
|
|
||||||
postId = id,
|
|
||||||
isFavourite = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -270,23 +241,19 @@ object SearchViewModel : ViewModel() {
|
|||||||
updateMomentCommentCount(id, 1)
|
updateMomentCommentCount(id, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMomentLike(id: Int, isLike: Boolean): Int? {
|
private fun updateMomentLike(id: Int, isLike: Boolean) {
|
||||||
var latestLikeCount: Int? = null
|
|
||||||
val currentPagingData = _momentsFlow.value
|
val currentPagingData = _momentsFlow.value
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||||
if (momentItem.id == id) {
|
if (momentItem.id == id) {
|
||||||
val nextCount = (momentItem.likeCount + if (isLike) 1 else -1).coerceAtLeast(0)
|
|
||||||
latestLikeCount = nextCount
|
|
||||||
momentItem.copy(
|
momentItem.copy(
|
||||||
liked = isLike,
|
liked = isLike,
|
||||||
likeCount = nextCount
|
likeCount = momentItem.likeCount + if (isLike) 1 else -1
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
momentItem
|
momentItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_momentsFlow.value = updatedPagingData
|
_momentsFlow.value = updatedPagingData
|
||||||
return latestLikeCount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMomentFavorite(id: Int, isFavorite: Boolean) {
|
private fun updateMomentFavorite(id: Int, isFavorite: Boolean) {
|
||||||
@@ -295,7 +262,7 @@ object SearchViewModel : ViewModel() {
|
|||||||
if (momentItem.id == id) {
|
if (momentItem.id == id) {
|
||||||
momentItem.copy(
|
momentItem.copy(
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
favoriteCount = (momentItem.favoriteCount + if (isFavorite) 1 else -1).coerceAtLeast(0)
|
favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
momentItem
|
momentItem
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -26,7 +25,10 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@@ -54,7 +56,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
@@ -318,9 +319,9 @@ private fun SingleVideoItemContent(
|
|||||||
isPageVisible: Boolean = true
|
isPageVisible: Boolean = true
|
||||||
) {
|
) {
|
||||||
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
|
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
|
||||||
val pauseIconVisibleState = remember(pager) { mutableStateOf(false) }
|
val pauseIconVisibleState = remember(pager) {
|
||||||
// 记录进入后台前是否在播放,用于决定是否需要自动恢复播放
|
mutableStateOf(false)
|
||||||
val shouldResumeAfterLifecyclePause = remember(pager) { mutableStateOf(false) }
|
}
|
||||||
|
|
||||||
// 当页面切换时,重置暂停状态
|
// 当页面切换时,重置暂停状态
|
||||||
LaunchedEffect(pager, pagerState.currentPage) {
|
LaunchedEffect(pager, pagerState.currentPage) {
|
||||||
@@ -340,7 +341,6 @@ private fun SingleVideoItemContent(
|
|||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
pager = pager,
|
pager = pager,
|
||||||
pauseIconVisibleState = pauseIconVisibleState,
|
pauseIconVisibleState = pauseIconVisibleState,
|
||||||
shouldResumeAfterLifecyclePause = shouldResumeAfterLifecyclePause,
|
|
||||||
onLikeClick = onLikeClick,
|
onLikeClick = onLikeClick,
|
||||||
onCommentClick = onCommentClick,
|
onCommentClick = onCommentClick,
|
||||||
onCommentAdded = onCommentAdded,
|
onCommentAdded = onCommentAdded,
|
||||||
@@ -373,7 +373,6 @@ fun VideoPlayer(
|
|||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
pager: Int,
|
pager: Int,
|
||||||
pauseIconVisibleState: MutableState<Boolean>,
|
pauseIconVisibleState: MutableState<Boolean>,
|
||||||
shouldResumeAfterLifecyclePause: MutableState<Boolean>,
|
|
||||||
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||||
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||||
onCommentAdded: ((MomentEntity) -> Unit)? = null,
|
onCommentAdded: ((MomentEntity) -> Unit)? = null,
|
||||||
@@ -468,9 +467,6 @@ fun VideoPlayer(
|
|||||||
.clip(RectangleShape)
|
.clip(RectangleShape)
|
||||||
) {
|
) {
|
||||||
var playerView by remember { mutableStateOf<PlayerView?>(null) }
|
var playerView by remember { mutableStateOf<PlayerView?>(null) }
|
||||||
// 防抖:记录上次双击时间,防止快速重复双击
|
|
||||||
val lastDoubleTapTime = remember { mutableStateOf(0L) }
|
|
||||||
val doubleTapDebounceTime = 500L // 500ms 防抖时间
|
|
||||||
|
|
||||||
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
|
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
|
||||||
androidx.compose.runtime.key(videoUrl) {
|
androidx.compose.runtime.key(videoUrl) {
|
||||||
@@ -483,31 +479,15 @@ fun VideoPlayer(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(RectangleShape)
|
.clip(RectangleShape)
|
||||||
.pointerInput(videoUrl, moment?.liked) {
|
.noRippleClickable {
|
||||||
detectTapGestures(
|
|
||||||
onDoubleTap = { offset ->
|
|
||||||
// 双击点赞/取消点赞
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
|
|
||||||
lastDoubleTapTime.value = currentTime
|
|
||||||
moment?.let {
|
|
||||||
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
|
|
||||||
onLikeClick?.invoke(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap = {
|
|
||||||
// 单击播放/暂停
|
|
||||||
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
|
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pauseIconVisibleState.value) {
|
if (pauseIconVisibleState.value) {
|
||||||
Image(
|
Icon(
|
||||||
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
|
imageVector = Icons.Default.PlayArrow,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
@@ -534,26 +514,15 @@ fun VideoPlayer(
|
|||||||
when (event) {
|
when (event) {
|
||||||
Lifecycle.Event.ON_PAUSE -> {
|
Lifecycle.Event.ON_PAUSE -> {
|
||||||
// 应用进入后台时暂停
|
// 应用进入后台时暂停
|
||||||
shouldResumeAfterLifecyclePause.value = exoPlayer.isPlaying && !pauseIconVisibleState.value
|
|
||||||
exoPlayer.playWhenReady = false
|
exoPlayer.playWhenReady = false
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
// 返回前台且为当前页面时恢复播放
|
// 返回前台且为当前页面时恢复播放
|
||||||
if (
|
if (pager == pagerState.currentPage) {
|
||||||
pager == pagerState.currentPage &&
|
|
||||||
isPageVisible &&
|
|
||||||
shouldResumeAfterLifecyclePause.value
|
|
||||||
) {
|
|
||||||
exoPlayer.playWhenReady = true
|
exoPlayer.playWhenReady = true
|
||||||
exoPlayer.play()
|
exoPlayer.play()
|
||||||
pauseIconVisibleState.value = false
|
|
||||||
} else {
|
|
||||||
// 未自动恢复播放时,如果当前页面视频处于暂停状态,则显示暂停图标
|
|
||||||
if (!exoPlayer.isPlaying) {
|
|
||||||
pauseIconVisibleState.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,8 +660,7 @@ fun VideoPlayer(
|
|||||||
},
|
},
|
||||||
containerColor = AppColors.background,
|
containerColor = AppColors.background,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
dragHandle = {},
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||||
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -702,7 +670,6 @@ fun VideoPlayer(
|
|||||||
CommentModalContent(
|
CommentModalContent(
|
||||||
postId = moment.id,
|
postId = moment.id,
|
||||||
commentCount = moment.commentCount,
|
commentCount = moment.commentCount,
|
||||||
showTitle = false,
|
|
||||||
onCommentAdded = {
|
onCommentAdded = {
|
||||||
onCommentAdded?.invoke(moment)
|
onCommentAdded?.invoke(moment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -47,10 +46,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|||||||
import com.aiosman.ravenow.ui.navigateToPost
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
fun LikeNoticeScreen() {
|
||||||
val model = LikeNoticeViewModel
|
val model = LikeNoticeViewModel
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
var dataFlow = model.likeItemsFlow
|
var dataFlow = model.likeItemsFlow
|
||||||
@@ -64,8 +63,7 @@ fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
|||||||
|
|
||||||
StatusBarMaskLayout(
|
StatusBarMaskLayout(
|
||||||
darkIcons = !AppState.darkMode,
|
darkIcons = !AppState.darkMode,
|
||||||
maskBoxBackgroundColor = AppColors.background,
|
maskBoxBackgroundColor = AppColors.background
|
||||||
includeStatusBarPadding = includeStatusBarPadding
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -77,11 +75,42 @@ fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
|||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
modifier = Modifier.fillMaxSize()
|
||||||
|
.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)
|
LikeNoticeViewModel.reload(force = true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (likes.itemCount == 0) {
|
} else if (likes.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
@@ -155,22 +184,18 @@ fun ActionPostNoticeItem(
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
val actionLabel = when (action) {
|
Box(
|
||||||
"favourite" -> stringResource(R.string.favourite_your_post)
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
else -> stringResource(R.string.like_your_post)
|
) {
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
verticalAlignment = Alignment.Top,
|
||||||
.padding(vertical = 12.dp, horizontal = 0.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context = context,
|
context,
|
||||||
imageUrl = avatar,
|
imageUrl = avatar,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@@ -182,56 +207,34 @@ fun ActionPostNoticeItem(
|
|||||||
},
|
},
|
||||||
contentDescription = action,
|
contentDescription = action,
|
||||||
)
|
)
|
||||||
Row(
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 8.dp)
|
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigateToPost(
|
navController.navigateToPost(
|
||||||
id = postId,
|
id = postId,
|
||||||
highlightCommentId = 0,
|
highlightCommentId = 0,
|
||||||
initImagePagerIndex = 0
|
initImagePagerIndex = 0
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Text(nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
|
||||||
modifier = Modifier.weight(1f)
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
) {
|
when (action) {
|
||||||
Text(
|
"like" -> Text(stringResource(R.string.like_your_post), color = AppColors.text)
|
||||||
text = nickName,
|
"favourite" -> Text(stringResource(R.string.favourite_your_post), color = AppColors.text)
|
||||||
fontWeight = FontWeight.Bold,
|
}
|
||||||
fontSize = 14.sp,
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
color = AppColors.text,
|
Row {
|
||||||
maxLines = 1,
|
Text(likeTime.timeAgo(context), fontSize = 12.sp, color = AppColors.secondaryText)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context = context,
|
context,
|
||||||
imageUrl = thumbnail,
|
imageUrl = thumbnail,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(48.dp)
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
contentDescription = action,
|
contentDescription = action,
|
||||||
)
|
)
|
||||||
@@ -246,11 +249,10 @@ fun LikeCommentNoticeItem(
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val previewPost = item.comment?.replyComment?.post ?: item.comment?.post
|
|
||||||
|
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 12.dp)
|
.padding(vertical = 16.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
item.comment?.postId.let {
|
item.comment?.postId.let {
|
||||||
navController.navigateToPost(
|
navController.navigateToPost(
|
||||||
@@ -260,69 +262,41 @@ fun LikeCommentNoticeItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
) {
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = item.user.avatar,
|
imageUrl = item.user.avatar,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape),
|
.clip(CircleShape),
|
||||||
contentDescription = stringResource(R.string.like_your_comment)
|
contentDescription = stringResource(R.string.like_your_comment)
|
||||||
)
|
)
|
||||||
Row(
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
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(
|
Text(
|
||||||
text = item.user.nickName,
|
item.likeTime.timeAgo(context),
|
||||||
fontWeight = FontWeight.Bold,
|
fontSize = 12.sp,
|
||||||
fontSize = 14.sp,
|
|
||||||
color = AppColors.text,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.like_your_comment),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = AppColors.secondaryText,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = item.likeTime.timeAgo(context),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = AppColors.secondaryText
|
color = AppColors.secondaryText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
previewPost?.let {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = context,
|
|
||||||
imageUrl = it.images[0].thumbnail,
|
|
||||||
contentDescription = "Post Thumbnail",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(start = 48.dp)
|
modifier = Modifier.padding(start = 48.dp)
|
||||||
) {
|
) {
|
||||||
@@ -360,4 +334,34 @@ fun LikeCommentNoticeItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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(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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -90,7 +89,7 @@ fun NotificationScreen() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.padding(start = 16.dp, top = 8.dp),
|
.padding(start = 16.dp, top = 8.dp, bottom = 16.dp),
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.Bottom
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
@@ -107,7 +106,7 @@ fun NotificationScreen() {
|
|||||||
TabSpacer()
|
TabSpacer()
|
||||||
|
|
||||||
TabItem(
|
TabItem(
|
||||||
text = stringResource(R.string.follow_upper),
|
text = stringResource(R.string.followers_upper),
|
||||||
isSelected = pagerState.currentPage == 1,
|
isSelected = pagerState.currentPage == 1,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -136,9 +135,9 @@ fun NotificationScreen() {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
) { page ->
|
) { page ->
|
||||||
when (page) {
|
when (page) {
|
||||||
0 -> LikeNoticeScreen(includeStatusBarPadding = false)
|
0 -> LikeNoticeScreen()
|
||||||
1 -> FollowerNoticeScreen(includeStatusBarPadding = false)
|
1 -> FollowerNoticeScreen()
|
||||||
2 -> CommentNoticeScreen(includeStatusBarPadding = false)
|
2 -> CommentNoticeScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@@ -54,6 +56,7 @@ import androidx.compose.ui.geometry.CornerRadius
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
@@ -103,36 +106,32 @@ fun PointsBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
|
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮)
|
onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮)
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
containerColor = Color.Transparent,
|
containerColor = AppColors.background,
|
||||||
dragHandle = null, // 移除拖动手柄
|
dragHandle = null // 移除拖动手柄
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 10.dp)
|
.fillMaxHeight(0.95f)
|
||||||
) {
|
.offset(y = offsetY)
|
||||||
Surface(
|
|
||||||
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(
|
.padding(
|
||||||
start = 16.dp,
|
start = 16.dp,
|
||||||
end = 16.dp,
|
end = 16.dp,
|
||||||
bottom = 8.dp
|
bottom = 8.dp
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
) {
|
) {
|
||||||
// 头部 - 使用 Box 实现绝对居中布局
|
// 头部 - 使用 Box 实现绝对居中布局
|
||||||
Box(
|
Box(
|
||||||
@@ -344,7 +343,6 @@ fun PointsBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -13,30 +13,37 @@ import kotlinx.coroutines.launch
|
|||||||
class CommentsViewModel(
|
class CommentsViewModel(
|
||||||
var postId: String = 0.toString(),
|
var postId: String = 0.toString(),
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
companion object {
|
|
||||||
private const val ORDER_ALL = "all"
|
|
||||||
private const val COMMENTS_PAGE_SIZE = 50
|
|
||||||
}
|
|
||||||
|
|
||||||
var commentService: CommentService = CommentServiceImpl()
|
var commentService: CommentService = CommentServiceImpl()
|
||||||
var commentsList by mutableStateOf<List<CommentEntity>>(emptyList())
|
var commentsList by mutableStateOf<List<CommentEntity>>(emptyList())
|
||||||
var order: String by mutableStateOf(ORDER_ALL)
|
var order: String by mutableStateOf("like")
|
||||||
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
|
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
|
||||||
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
|
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
|
||||||
var highlightCommentId by mutableStateOf<Int?>(null)
|
var highlightCommentId by mutableStateOf<Int?>(null)
|
||||||
var highlightComment by mutableStateOf<CommentEntity?>(null)
|
var highlightComment by mutableStateOf<CommentEntity?>(null)
|
||||||
var isLoading by mutableStateOf(false)
|
var isLoading by mutableStateOf(false)
|
||||||
var hasError by mutableStateOf(false)
|
var hasError by mutableStateOf(false)
|
||||||
var isLoadingMore by mutableStateOf(false)
|
|
||||||
var hasMore by mutableStateOf(false)
|
|
||||||
private var currentPage by mutableStateOf(0)
|
|
||||||
private var totalComments by mutableStateOf(0)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预加载,在跳转到 PostScreen 之前设置好内容
|
* 预加载,在跳转到 PostScreen 之前设置好内容
|
||||||
*/
|
*/
|
||||||
fun preTransit() {
|
fun preTransit() {
|
||||||
reloadComment()
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isLoading = true
|
||||||
|
val response = commentService.getComments(
|
||||||
|
pageNumber = 1,
|
||||||
|
postId = postId.toInt(),
|
||||||
|
pageSize = 10
|
||||||
|
)
|
||||||
|
commentsList = response.list
|
||||||
|
hasError = false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
hasError = true
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,61 +51,25 @@ class CommentsViewModel(
|
|||||||
*/
|
*/
|
||||||
fun reloadComment() {
|
fun reloadComment() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
loadComments(page = 1, reset = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadMoreComments() {
|
|
||||||
if (isLoading || isLoadingMore || !hasMore) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
loadComments(page = currentPage + 1, reset = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadComments(page: Int, reset: Boolean) {
|
|
||||||
try {
|
try {
|
||||||
if (reset) {
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
hasError = false
|
|
||||||
} else {
|
|
||||||
isLoadingMore = true
|
|
||||||
}
|
|
||||||
val response = commentService.getComments(
|
val response = commentService.getComments(
|
||||||
pageNumber = page,
|
pageNumber = 1,
|
||||||
postId = postId.toInt(),
|
postId = postId.toInt(),
|
||||||
order = normalizeOrder(order),
|
order = order,
|
||||||
pageSize = COMMENTS_PAGE_SIZE
|
pageSize = 50
|
||||||
)
|
)
|
||||||
val total = response.total.coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
|
commentsList = response.list
|
||||||
totalComments = total
|
hasError = false
|
||||||
currentPage = response.page
|
|
||||||
commentsList = if (reset) {
|
|
||||||
response.list
|
|
||||||
} else {
|
|
||||||
commentsList + response.list
|
|
||||||
}
|
|
||||||
hasMore = commentsList.size < totalComments
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
if (reset) {
|
|
||||||
hasError = true
|
hasError = true
|
||||||
commentsList = emptyList()
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
if (reset) {
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
} else {
|
|
||||||
isLoadingMore = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun normalizeOrder(currentOrder: String): String? {
|
|
||||||
return currentOrder.takeUnless { it.equals(ORDER_ALL, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun highlightComment(commentId: Int) {
|
suspend fun highlightComment(commentId: Int) {
|
||||||
highlightCommentId = commentId
|
highlightCommentId = commentId
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import androidx.compose.material3.Divider
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -35,7 +34,6 @@ import android.graphics.BitmapFactory
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -80,28 +78,14 @@ fun DraftBoxBottomSheet(
|
|||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
containerColor = Color.Transparent,
|
containerColor = AppColors.background,
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
dragHandle = {}
|
dragHandle = {}
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
|
|
||||||
color = AppColors.background,
|
|
||||||
tonalElevation = 0.dp,
|
|
||||||
shadowElevation = 0.dp,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight()
|
.fillMaxHeight(0.9f)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
// 标题
|
// 标题
|
||||||
@@ -183,8 +167,6 @@ fun DraftBoxBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package com.aiosman.ravenow.ui.post
|
package com.aiosman.ravenow.ui.post
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@@ -591,34 +588,6 @@ fun AddImageGrid() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 摄像头权限请求
|
|
||||||
val requestCameraPermissionLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.RequestPermission()
|
|
||||||
) { isGranted ->
|
|
||||||
if (isGranted) {
|
|
||||||
// 权限已授予,打开相机
|
|
||||||
if (model.imageList.size < 9) {
|
|
||||||
val photoFile = File(context.cacheDir, "photo.jpg")
|
|
||||||
val photoUri: Uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.fileprovider",
|
|
||||||
photoFile
|
|
||||||
)
|
|
||||||
model.currentPhotoUri = photoUri
|
|
||||||
takePictureLauncher.launch(photoUri)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 权限被拒绝,提示用户
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"需要摄像头权限才能拍摄照片,请在设置中开启",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val addImageDebouncer = rememberDebouncer()
|
val addImageDebouncer = rememberDebouncer()
|
||||||
val canAddMoreImages = model.imageList.size < 9
|
val canAddMoreImages = model.imageList.size < 9
|
||||||
|
|
||||||
@@ -673,13 +642,6 @@ fun AddImageGrid() {
|
|||||||
.background(Color(0xFFFAF9FB))
|
.background(Color(0xFFFAF9FB))
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
if (model.imageList.size < 9) {
|
if (model.imageList.size < 9) {
|
||||||
// 检查摄像头权限
|
|
||||||
when {
|
|
||||||
ContextCompat.checkSelfPermission(
|
|
||||||
context,
|
|
||||||
Manifest.permission.CAMERA
|
|
||||||
) == PackageManager.PERMISSION_GRANTED -> {
|
|
||||||
// 已有权限,直接打开相机
|
|
||||||
val photoFile = File(context.cacheDir, "photo.jpg")
|
val photoFile = File(context.cacheDir, "photo.jpg")
|
||||||
val photoUri: Uri = FileProvider.getUriForFile(
|
val photoUri: Uri = FileProvider.getUriForFile(
|
||||||
context,
|
context,
|
||||||
@@ -688,12 +650,6 @@ fun AddImageGrid() {
|
|||||||
)
|
)
|
||||||
model.currentPhotoUri = photoUri
|
model.currentPhotoUri = photoUri
|
||||||
takePictureLauncher.launch(photoUri)
|
takePictureLauncher.launch(photoUri)
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// 没有权限,请求权限
|
|
||||||
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import androidx.compose.animation.slideInVertically
|
|||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
@@ -26,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -47,7 +44,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -304,26 +300,13 @@ fun PostScreen(
|
|||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showReportDialog = false
|
showReportDialog = false
|
||||||
},
|
},
|
||||||
containerColor = Color.Transparent,
|
containerColor = AppColors.background,
|
||||||
sheetState = rememberModalBottomSheetState(
|
sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
),
|
),
|
||||||
dragHandle = {},
|
dragHandle = {},
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
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(
|
ReportModal(
|
||||||
momentId = viewModel.moment!!.id,
|
momentId = viewModel.moment!!.id,
|
||||||
@@ -333,8 +316,6 @@ fun PostScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
@@ -512,9 +493,7 @@ fun PostScreen(
|
|||||||
color = AppColors.nonActiveText
|
color = AppColors.nonActiveText
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
OrderSelectionComponent(
|
OrderSelectionComponent() {
|
||||||
selectedOrder = commentsViewModel.order
|
|
||||||
) {
|
|
||||||
commentsViewModel.order = it
|
commentsViewModel.order = it
|
||||||
viewModel.reloadComment()
|
viewModel.reloadComment()
|
||||||
}
|
}
|
||||||
@@ -761,33 +740,6 @@ fun CommentContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.isLoadingMore || viewModel.hasMore) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
if (viewModel.isLoadingMore) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
color = AppColors.main,
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.load_more),
|
|
||||||
color = AppColors.main,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
modifier = Modifier.noRippleClickable {
|
|
||||||
viewModel.loadMoreComments()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载状态处理
|
// 加载状态处理
|
||||||
if (viewModel.isLoading) {
|
if (viewModel.isLoading) {
|
||||||
Box(
|
Box(
|
||||||
@@ -1207,26 +1159,14 @@ fun ImageViewerDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PostImageView(
|
fun PostImageView(
|
||||||
images: List<MomentImageEntity>,
|
images: List<MomentImageEntity>,
|
||||||
initialPage: Int? = 0
|
initialPage: Int? = 0
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
var isImageViewerDialog by remember { mutableStateOf(false) }
|
var isImageViewerDialog by remember { mutableStateOf(false) }
|
||||||
val initialPageIndex = initialPage ?: 0
|
var currentImageIndex by remember { mutableStateOf(initialPage ?: 0) }
|
||||||
val pagerState = rememberPagerState(
|
|
||||||
pageCount = { images.size },
|
|
||||||
initialPage = initialPageIndex.coerceIn(0, maxOf(0, images.size - 1))
|
|
||||||
)
|
|
||||||
var currentImageIndex by remember { mutableStateOf(pagerState.currentPage) }
|
|
||||||
|
|
||||||
// 同步 pagerState 的当前页面到 currentImageIndex
|
|
||||||
LaunchedEffect(pagerState.currentPage) {
|
|
||||||
currentImageIndex = pagerState.currentPage
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -1247,21 +1187,14 @@ fun PostImageView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (images.isNotEmpty()) {
|
if (images.isNotEmpty()) {
|
||||||
HorizontalPager(
|
CustomAsyncImage(
|
||||||
state = pagerState,
|
context,
|
||||||
|
images[currentImageIndex].thumbnail,
|
||||||
|
contentDescription = "Image",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) { page ->
|
|
||||||
val image = images[page]
|
|
||||||
CustomAsyncImage(
|
|
||||||
context,
|
|
||||||
image.thumbnail,
|
|
||||||
contentDescription = "Image",
|
|
||||||
blurHash = image.blurHash,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = {
|
onTap = {
|
||||||
@@ -1272,7 +1205,6 @@ fun PostImageView(
|
|||||||
.background(Color.Gray.copy(alpha = 0.1f))
|
.background(Color.Gray.copy(alpha = 0.1f))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 图片导航控件
|
// 图片导航控件
|
||||||
if (images.size > 1) {
|
if (images.size > 1) {
|
||||||
@@ -1280,17 +1212,33 @@ fun PostImageView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
// Previous button
|
||||||
|
Text(
|
||||||
|
text = "Previous",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (currentImageIndex > 0) {
|
||||||
|
currentImageIndex--
|
||||||
|
}
|
||||||
|
},
|
||||||
|
color = if (currentImageIndex > 0) Color.Blue else Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
// Indicators
|
// Indicators
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
images.forEachIndexed { index, _ ->
|
images.forEachIndexed { index, _ ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(4.dp)
|
.size(4.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
|
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
|
||||||
alpha = 0.5f
|
alpha = 0.5f
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1301,6 +1249,20 @@ fun PostImageView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
Text(
|
||||||
|
text = "Next",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (currentImageIndex < images.size - 1) {
|
||||||
|
currentImageIndex++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
color = if (currentImageIndex < images.size - 1) Color.Blue else Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1955,15 +1917,15 @@ fun CommentMenuModal(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OrderSelectionComponent(
|
fun OrderSelectionComponent(
|
||||||
selectedOrder: String,
|
|
||||||
onSelected: (String) -> Unit = {}
|
onSelected: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
var selectedOrder by remember { mutableStateOf("like") }
|
||||||
val orders = listOf(
|
val orders = listOf(
|
||||||
"all" to stringResource(R.string.order_comment_default),
|
"like" to stringResource(R.string.order_comment_default),
|
||||||
"latest" to stringResource(R.string.order_comment_latest),
|
"earliest" to stringResource(R.string.order_comment_earliest),
|
||||||
"like" to stringResource(R.string.order_comment_hot)
|
"latest" to stringResource(R.string.order_comment_latest)
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -1981,10 +1943,9 @@ fun OrderSelectionComponent(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
if (selectedOrder != order.first) {
|
selectedOrder = order.first
|
||||||
onSelected(order.first)
|
onSelected(order.first)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.background(
|
.background(
|
||||||
if (
|
if (
|
||||||
selectedOrder == order.first
|
selectedOrder == order.first
|
||||||
@@ -2050,8 +2011,8 @@ fun ReportModal(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight()
|
.background(AppColors.background)
|
||||||
.padding(start = 24.dp, end = 24.dp)
|
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp),
|
||||||
|
|||||||
@@ -44,12 +44,21 @@ class AiProfileViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 检查id是否是纯数字(用户ID),如果不是则当作openId处理
|
||||||
|
val isUserId = id.toIntOrNull() != null
|
||||||
|
|
||||||
|
if (isUserId) {
|
||||||
// 先通过用户ID获取基本信息,获取chatAIId
|
// 先通过用户ID获取基本信息,获取chatAIId
|
||||||
val basicProfile = userService.getUserProfile(id)
|
val basicProfile = userService.getUserProfile(id)
|
||||||
profileId = id.toInt()
|
profileId = id.toInt()
|
||||||
|
|
||||||
// 使用chatAIId通过getUserProfileByOpenId获取完整信息(包含creatorProfile)
|
// 使用chatAIId通过getUserProfileByOpenId获取完整信息(包含creatorProfile)
|
||||||
profile = userService.getUserProfileByOpenId(basicProfile.chatAIId)
|
profile = userService.getUserProfileByOpenId(basicProfile.chatAIId)
|
||||||
|
} else {
|
||||||
|
// 直接通过openId获取完整信息
|
||||||
|
profile = userService.getUserProfileByOpenId(id)
|
||||||
|
profileId = profile?.id ?: 0
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("AiProfileViewModel", "Error loading profile", e)
|
Log.e("AiProfileViewModel", "Error loading profile", e)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.aiosman.ravenow.utils
|
package com.aiosman.ravenow.utils
|
||||||
|
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.entity.OpenIMMessageType
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.JsonSyntaxException
|
import com.google.gson.JsonSyntaxException
|
||||||
import io.openim.android.sdk.models.Message
|
import io.openim.android.sdk.models.Message
|
||||||
@@ -25,8 +26,21 @@ object MessageParser {
|
|||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
val message = gson.fromJson(latestMsgJson, Message::class.java)
|
val message = gson.fromJson(latestMsgJson, Message::class.java)
|
||||||
|
|
||||||
|
// 检查是否为通知类型消息
|
||||||
|
// 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 = message.sendID == AppState.profile?.trtcUserId
|
isSelf = sendID == AppState.profile?.trtcUserId
|
||||||
|
}
|
||||||
|
|
||||||
// 根据消息类型生成显示文本
|
// 根据消息类型生成显示文本
|
||||||
displayText = getMessageDisplayText(message)
|
displayText = getMessageDisplayText(message)
|
||||||
@@ -50,6 +64,19 @@ object MessageParser {
|
|||||||
* @return 消息的显示文本
|
* @return 消息的显示文本
|
||||||
*/
|
*/
|
||||||
private fun getMessageDisplayText(message: Message): String {
|
private fun getMessageDisplayText(message: Message): String {
|
||||||
|
// 检查是否为通知类型消息
|
||||||
|
// 1. 检查消息类型是否为通知类型
|
||||||
|
// 2. 检查发送者ID是否为系统账户(如 "imAdmin"、"administrator" 等)
|
||||||
|
val sendID = message.sendID ?: ""
|
||||||
|
val isSystemAccount = sendID == "imAdmin" || sendID == "administrator" || sendID.isEmpty()
|
||||||
|
val isNotificationType = OpenIMMessageType.isNotification(message.contentType)
|
||||||
|
val isNotification = isNotificationType || isSystemAccount
|
||||||
|
|
||||||
|
if (isNotification) {
|
||||||
|
// 使用 NotificationMessageHelper 生成通知文本
|
||||||
|
return NotificationMessageHelper.getNotificationText(message)
|
||||||
|
}
|
||||||
|
|
||||||
return when (message.contentType) {
|
return when (message.contentType) {
|
||||||
101 -> { // TEXT
|
101 -> { // TEXT
|
||||||
message.textElem?.content ?: "[文本消息]"
|
message.textElem?.content ?: "[文本消息]"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1006 B |
|
Before Width: | Height: | Size: 866 B |
|
Before Width: | Height: | Size: 798 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 935 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 886 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 724 B |
|
Before Width: | Height: | Size: 656 B |
|
Before Width: | Height: | Size: 618 B |
|
Before Width: | Height: | Size: 916 B |
|
Before Width: | Height: | Size: 715 B |
|
Before Width: | Height: | Size: 882 B |
|
Before Width: | Height: | Size: 800 B |
|
Before Width: | Height: | Size: 860 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 864 B |
|
Before Width: | Height: | Size: 675 B |