Compare commits
85 Commits
open_im
...
revert-48-
| Author | SHA1 | Date | |
|---|---|---|---|
| 721b7aa9ab | |||
| f16be90cc3 | |||
| 0bbfd9b739 | |||
| 13593212df | |||
| 56f225702e | |||
| 0a5b81cc5c | |||
| 10fc40373d | |||
| f18ee9e360 | |||
| 9262288bc9 | |||
| 3273b17d15 | |||
| 4ef5a94d46 | |||
| 4a684886fa | |||
| d7f87c7c55 | |||
| 00933dadb8 | |||
| d5cc186e27 | |||
| 90156745ad | |||
| 7095832722 | |||
| 658b337d22 | |||
| f6a760371a | |||
| 13ed16078b | |||
| 2a5174cbb6 | |||
| ea26cb40a0 | |||
| eb8119b775 | |||
| 66da741eda | |||
| 18dd52e193 | |||
| f839a793a3 | |||
| 54df6c088b | |||
| 28fb94a824 | |||
| 4bdbbb0231 | |||
| 58a2013a8f | |||
| 2f2da0a159 | |||
| 4ffaf3c3a8 | |||
| 29490d288b | |||
| a99ab30c4e | |||
| 0442925ae9 | |||
| 9d3d13a22d | |||
| f0a9704e2d | |||
| 7f2c103ada | |||
| bd01ae39d0 | |||
| d94e3b5c20 | |||
| 44cc76d2e3 | |||
| fac6f23356 | |||
| 39928abc46 | |||
| 4d0d7004b0 | |||
| 28a6e3fef3 | |||
| b275d88ef7 | |||
| 595ef7f942 | |||
| 1202b55c74 | |||
| 074009d256 | |||
| ad1de9e3f7 | |||
| 359bcfdfd7 | |||
| 1901fddb2e | |||
| b96ae94bdb | |||
| dedd356896 | |||
| bb9fda75ae | |||
| ea911f113b | |||
| 88f379fe5b | |||
| 1a24136c35 | |||
| 742410223c | |||
| bd5aff7564 | |||
| b43c1585c4 | |||
| cb582393f1 | |||
| a200d00587 | |||
| 6d2133545f | |||
| 2aad126010 | |||
| b7b777d2d0 | |||
| e804c8be0c | |||
| 228a74695e | |||
| 41a51b85da | |||
| e74e8615a5 | |||
| 349d39daf2 | |||
| eca85c8377 | |||
| 8154a0ddc4 | |||
| f8be622ba6 | |||
| f3c841779b | |||
| 57e4614ce8 | |||
| 922d6e72d6 | |||
| c41c097d41 | |||
| 5218ca7046 | |||
| ce6ee7bf82 | |||
| e00deb5661 | |||
| 95d6522a54 | |||
| d231f3678c | |||
| cd35562244 | |||
| 21cb512237 |
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2025-09-08T06:52:32.669239Z">
|
<DropdownSelection timestamp="2025-09-17T06:25:35.585100400Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/liudikang/.android/avd/Pixel_8_API_30.avd" />
|
<DeviceId pluginId="Default" identifier="serial=192.168.0.216:5555;connection=698a7727" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
isDebuggable = true
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
@@ -49,6 +52,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.3"
|
kotlinCompilerExtensionVersion = "1.5.3"
|
||||||
@@ -121,7 +125,7 @@ dependencies {
|
|||||||
// 添加 lifecycle-runtime-ktx 依赖
|
// 添加 lifecycle-runtime-ktx 依赖
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx.v262)
|
implementation(libs.androidx.lifecycle.runtime.ktx.v262)
|
||||||
implementation (libs.eventbus)
|
implementation (libs.eventbus)
|
||||||
|
implementation(libs.lottie)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
app/proguard-rules.pro
vendored
@@ -20,3 +20,23 @@
|
|||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
# OpenIM SDK ProGuard rules
|
||||||
|
-keep class io.openim.android.sdk.** { *; }
|
||||||
|
-keep class io.openim.core.** { *; }
|
||||||
|
-keepclassmembers class io.openim.android.sdk.** { *; }
|
||||||
|
-keepclassmembers class io.openim.core.** { *; }
|
||||||
|
|
||||||
|
# Keep OpenIM models and listeners
|
||||||
|
-keep class io.openim.android.sdk.models.** { *; }
|
||||||
|
-keep class io.openim.android.sdk.listener.** { *; }
|
||||||
|
-keep class io.openim.android.sdk.enums.** { *; }
|
||||||
|
|
||||||
|
# Keep OpenIM Client and managers
|
||||||
|
-keep class io.openim.android.sdk.OpenIMClient { *; }
|
||||||
|
-keep class io.openim.android.sdk.manager.** { *; }
|
||||||
|
|
||||||
|
# Prevent obfuscation of callback methods
|
||||||
|
-keepclassmembers class * implements io.openim.android.sdk.listener.** {
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/rider_pro_logo_next"
|
android:icon="@mipmap/invalid_name"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/rider_pro_logo_next_round"
|
android:roundIcon="@mipmap/rider_pro_logo_next_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
|||||||
BIN
app/src/main/assets/loading.lottie
Normal file
BIN
app/src/main/assets/login.lottie
Normal file
BIN
app/src/main/assets/login_light.lottie
Normal file
@@ -45,6 +45,8 @@ object AppState {
|
|||||||
var googleClientId: String? = null
|
var googleClientId: String? = null
|
||||||
var enableGoogleLogin: Boolean = false
|
var enableGoogleLogin: Boolean = false
|
||||||
var enableChat = false
|
var enableChat = false
|
||||||
|
var agentCreatedSuccess by mutableStateOf(false)
|
||||||
|
var chatBackgroundUrl by mutableStateOf<String?>(null)
|
||||||
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
||||||
// 如果是游客模式,使用简化的初始化流程
|
// 如果是游客模式,使用简化的初始化流程
|
||||||
if (AppStore.isGuest) {
|
if (AppStore.isGuest) {
|
||||||
@@ -67,8 +69,13 @@ object AppState {
|
|||||||
)
|
)
|
||||||
// 设置当前登录用户 ID
|
// 设置当前登录用户 ID
|
||||||
UserId = resp.id
|
UserId = resp.id
|
||||||
|
try {
|
||||||
var profileResult = accountService.getMyAccountProfile()
|
var profileResult = accountService.getMyAccountProfile()
|
||||||
profile = profileResult
|
profile = profileResult
|
||||||
|
|
||||||
|
} catch (e:Exception) {
|
||||||
|
Log.e("AppState", "getMyAccountProfile Error:"+ e.message )
|
||||||
|
}
|
||||||
// 获取当前用户资料
|
// 获取当前用户资料
|
||||||
|
|
||||||
// 注册 JPush
|
// 注册 JPush
|
||||||
@@ -101,10 +108,11 @@ object AppState {
|
|||||||
|
|
||||||
val initConfig = InitConfig(
|
val initConfig = InitConfig(
|
||||||
"https://im.ravenow.ai/api",//SDK api地址
|
"https://im.ravenow.ai/api",//SDK api地址
|
||||||
"wss:///im.ravenow.ai/msg_gateway",//SDK WebSocket地址
|
"wss://im.ravenow.ai/msg_gateway",//SDK WebSocket地址
|
||||||
OpenIMManager.getStorageDir(context),//SDK数据库存储目录
|
OpenIMManager.getStorageDir(context),//SDK数据库存储目录
|
||||||
)
|
)
|
||||||
|
// initConfig.isLogStandardOutput = true;
|
||||||
|
// initConfig.logLevel = 6
|
||||||
// 使用 OpenIMManager 初始化 SDK
|
// 使用 OpenIMManager 初始化 SDK
|
||||||
OpenIMManager.initSDK(context, initConfig)
|
OpenIMManager.initSDK(context, initConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.aiosman.ravenow
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
object ConstVars {
|
object ConstVars {
|
||||||
// api 地址
|
// api 地址 - 根据构建类型自动选择
|
||||||
// const val BASE_SERVER = "http://192.168.31.131:8088"
|
// Debug: http://192.168.0.201:8088
|
||||||
// const val BASE_SERVER = "http://192.168.142.141:8088"
|
// Release: https://rider-pro.aiosman.com/beta_api
|
||||||
// const val BASE_SERVER = "http://192.168.0.228:8088"
|
val BASE_SERVER = if (BuildConfig.DEBUG) {
|
||||||
const val BASE_SERVER = "https://rider-pro.aiosman.com/beta_api"
|
// "http://47.109.137.67:6363" // Debug环境
|
||||||
|
"https://rider-pro.aiosman.com/beta_api" // Release环境
|
||||||
|
} else {
|
||||||
|
"https://rider-pro.aiosman.com/beta_api" // Release环境
|
||||||
|
}
|
||||||
|
|
||||||
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
|
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
|
||||||
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
|
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
|
||||||
|
|||||||
@@ -43,7 +43,12 @@ import com.google.firebase.analytics.analytics
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.aiosman.ravenow.ui.splash.SplashScreen
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
// Firebase Analytics
|
// Firebase Analytics
|
||||||
@@ -122,11 +127,22 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
var showSplash by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
kotlinx.coroutines.delay(2000)
|
||||||
|
showSplash = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSplash) {
|
||||||
|
SplashScreen()
|
||||||
|
} else {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAppTheme provides AppState.appTheme
|
LocalAppTheme provides AppState.appTheme
|
||||||
) {
|
) {
|
||||||
CheckUpdateDialog()
|
CheckUpdateDialog()
|
||||||
Navigation(startDestination) { navController ->
|
Navigation(startDestination) { navController ->
|
||||||
|
|
||||||
// 处理带有 postId 的通知点击
|
// 处理带有 postId 的通知点击
|
||||||
val postId = intent.getStringExtra("POST_ID")
|
val postId = intent.getStringExtra("POST_ID")
|
||||||
var commentId = intent.getStringExtra("COMMENT_ID")
|
var commentId = intent.getStringExtra("COMMENT_ID")
|
||||||
@@ -179,17 +195,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
|
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
|
||||||
navController.navigate(NavigationRoute.NewPost.route)
|
navController.navigate(NavigationRoute.NewPost.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求通知权限
|
* 请求通知权限
|
||||||
|
|||||||
@@ -430,9 +430,17 @@ interface AccountService {
|
|||||||
|
|
||||||
class AccountServiceImpl : AccountService {
|
class AccountServiceImpl : AccountService {
|
||||||
override suspend fun getMyAccountProfile(): AccountProfileEntity {
|
override suspend fun getMyAccountProfile(): AccountProfileEntity {
|
||||||
|
// 如果已有缓存,直接返回缓存结果
|
||||||
|
AppState.profile?.let { return it }
|
||||||
|
|
||||||
|
// 第一次调用,获取数据并缓存
|
||||||
val resp = ApiClient.api.getMyAccount()
|
val resp = ApiClient.api.getMyAccount()
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
return body.data.toAccountProfileEntity()
|
val profile = body.data.toAccountProfileEntity()
|
||||||
|
|
||||||
|
// 缓存结果到共享状态
|
||||||
|
AppState.profile = profile
|
||||||
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMyAccount(): UserAuth {
|
override suspend fun getMyAccount(): UserAuth {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ interface AgentService {
|
|||||||
pageNumber: Int,
|
pageNumber: Int,
|
||||||
pageSize: Int = 20,
|
pageSize: Int = 20,
|
||||||
authorId: Int? = null
|
authorId: Int? = null
|
||||||
): ListContainer<AgentEntity>
|
): ListContainer<AgentEntity>?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,21 @@ data class Moment(
|
|||||||
val time: String,
|
val time: String,
|
||||||
@SerializedName("isFollowed")
|
@SerializedName("isFollowed")
|
||||||
val isFollowed: Boolean,
|
val isFollowed: Boolean,
|
||||||
|
// 新闻相关字段
|
||||||
|
@SerializedName("isNews")
|
||||||
|
val isNews: Boolean = false,
|
||||||
|
@SerializedName("newsTitle")
|
||||||
|
val newsTitle: String? = null,
|
||||||
|
@SerializedName("newsUrl")
|
||||||
|
val newsUrl: String? = null,
|
||||||
|
@SerializedName("newsSource")
|
||||||
|
val newsSource: String? = null,
|
||||||
|
@SerializedName("newsCategory")
|
||||||
|
val newsCategory: String? = null,
|
||||||
|
@SerializedName("newsLanguage")
|
||||||
|
val newsLanguage: String? = null,
|
||||||
|
@SerializedName("newsContent")
|
||||||
|
val newsContent: String? = null,
|
||||||
) {
|
) {
|
||||||
fun toMomentItem(): MomentEntity {
|
fun toMomentItem(): MomentEntity {
|
||||||
return MomentEntity(
|
return MomentEntity(
|
||||||
@@ -60,6 +75,14 @@ data class Moment(
|
|||||||
authorId = user.id.toInt(),
|
authorId = user.id.toInt(),
|
||||||
liked = isLiked,
|
liked = isLiked,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
|
// 新闻相关字段
|
||||||
|
isNews = isNews,
|
||||||
|
newsTitle = newsTitle ?: "",
|
||||||
|
newsUrl = newsUrl ?: "",
|
||||||
|
newsSource = newsSource ?: "",
|
||||||
|
newsCategory = newsCategory ?: "",
|
||||||
|
newsLanguage = newsLanguage ?: "",
|
||||||
|
newsContent = newsContent ?: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class AuthInterceptor() : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestBuilder.addHeader("Authorization", "Bearer ${AppStore.token}")
|
requestBuilder.addHeader("Authorization", "Bearer ${AppStore.token}")
|
||||||
|
requestBuilder.addHeader("DEVICE-OS", "Android")
|
||||||
|
|
||||||
val response = chain.proceed(requestBuilder.build())
|
val response = chain.proceed(requestBuilder.build())
|
||||||
return response
|
return response
|
||||||
@@ -69,9 +70,9 @@ class AuthInterceptor() : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object ApiClient {
|
object ApiClient {
|
||||||
const val BASE_SERVER = ConstVars.BASE_SERVER
|
val BASE_SERVER = ConstVars.BASE_SERVER
|
||||||
const val BASE_API_URL = "${BASE_SERVER}/api/v1"
|
val BASE_API_URL = "${BASE_SERVER}/api/v1"
|
||||||
const val RETROFIT_URL = "${BASE_API_URL}/"
|
val RETROFIT_URL = "${BASE_API_URL}/"
|
||||||
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
|
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
|
||||||
private val okHttpClient: OkHttpClient by lazy {
|
private val okHttpClient: OkHttpClient by lazy {
|
||||||
getSafeOkHttpClient(authInterceptor = AuthInterceptor())
|
getSafeOkHttpClient(authInterceptor = AuthInterceptor())
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ data class CreateGroupChatRequestBody(
|
|||||||
|
|
||||||
data class JoinGroupChatRequestBody(
|
data class JoinGroupChatRequestBody(
|
||||||
@SerializedName("trtcId")
|
@SerializedName("trtcId")
|
||||||
val trtcId: String,
|
val trtcId: String? = null,
|
||||||
|
@SerializedName("roomId")
|
||||||
|
val roomId: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class LoginUserRequestBody(
|
data class LoginUserRequestBody(
|
||||||
@@ -271,6 +273,350 @@ data class RemoveAccountRequestBody(
|
|||||||
val password: String,
|
val password: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// API 错误响应(用于加入房间等接口的错误处理)
|
||||||
|
data class ApiErrorResponse(
|
||||||
|
@SerializedName("err")
|
||||||
|
val error: String,
|
||||||
|
@SerializedName("success")
|
||||||
|
val success: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
// 群聊中的用户信息
|
||||||
|
data class GroupChatUser(
|
||||||
|
@SerializedName("ID")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("CreatedAt")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("UpdatedAt")
|
||||||
|
val updatedAt: String,
|
||||||
|
@SerializedName("DeletedAt")
|
||||||
|
val deletedAt: String?,
|
||||||
|
@SerializedName("userSessionId")
|
||||||
|
val userSessionId: String,
|
||||||
|
@SerializedName("sessions")
|
||||||
|
val sessions: Any?, // 根据实际需要可以定义具体类型
|
||||||
|
@SerializedName("prompts")
|
||||||
|
val prompts: Any?, // 根据实际需要可以定义具体类型
|
||||||
|
@SerializedName("isAgent")
|
||||||
|
val isAgent: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
// 智能体角色信息
|
||||||
|
data class GroupChatPrompt(
|
||||||
|
@SerializedName("ID")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("CreatedAt")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("UpdatedAt")
|
||||||
|
val updatedAt: String,
|
||||||
|
@SerializedName("DeletedAt")
|
||||||
|
val deletedAt: String?,
|
||||||
|
@SerializedName("Title")
|
||||||
|
val title: String,
|
||||||
|
@SerializedName("Desc")
|
||||||
|
val desc: String,
|
||||||
|
@SerializedName("Value")
|
||||||
|
val value: String,
|
||||||
|
@SerializedName("Enable")
|
||||||
|
val enable: Boolean,
|
||||||
|
@SerializedName("UserSessions")
|
||||||
|
val userSessions: Any?, // 根据实际需要可以定义具体类型
|
||||||
|
@SerializedName("Avatar")
|
||||||
|
val avatar: String,
|
||||||
|
@SerializedName("AuthorId")
|
||||||
|
val authorId: Int?,
|
||||||
|
@SerializedName("Author")
|
||||||
|
val author: Any?, // 根据实际需要可以定义具体类型
|
||||||
|
@SerializedName("TokenCount")
|
||||||
|
val tokenCount: Int,
|
||||||
|
@SerializedName("OpenId")
|
||||||
|
val openId: String,
|
||||||
|
@SerializedName("Public")
|
||||||
|
val public: Boolean,
|
||||||
|
@SerializedName("BreakMode")
|
||||||
|
val breakMode: Boolean,
|
||||||
|
@SerializedName("DocNamespace")
|
||||||
|
val docNamespace: String,
|
||||||
|
@SerializedName("UseRag")
|
||||||
|
val useRag: Boolean,
|
||||||
|
@SerializedName("RagThreshold")
|
||||||
|
val ragThreshold: Double,
|
||||||
|
@SerializedName("WorkflowId")
|
||||||
|
val workflowId: Int?,
|
||||||
|
@SerializedName("Workflow")
|
||||||
|
val workflow: Any?, // 根据实际需要可以定义具体类型
|
||||||
|
@SerializedName("WorkflowInputs")
|
||||||
|
val workflowInputs: Any?, // 根据实际需要可以定义具体类型
|
||||||
|
@SerializedName("Source")
|
||||||
|
val source: String,
|
||||||
|
@SerializedName("categories")
|
||||||
|
val categories: Any? // 根据实际需要可以定义具体类型
|
||||||
|
)
|
||||||
|
|
||||||
|
// 群聊详细信息响应
|
||||||
|
data class GroupChatResponse(
|
||||||
|
@SerializedName("ID")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("CreatedAt")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("UpdatedAt")
|
||||||
|
val updatedAt: String,
|
||||||
|
@SerializedName("DeletedAt")
|
||||||
|
val deletedAt: String?,
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String,
|
||||||
|
@SerializedName("creatorId")
|
||||||
|
val creatorId: Int,
|
||||||
|
@SerializedName("creator")
|
||||||
|
val creator: Any?, // 根据实际需要可以定义具体类型
|
||||||
|
@SerializedName("trtcRoomId")
|
||||||
|
val trtcRoomId: String,
|
||||||
|
@SerializedName("trtcType")
|
||||||
|
val trtcType: String,
|
||||||
|
@SerializedName("cover")
|
||||||
|
val cover: String,
|
||||||
|
@SerializedName("avatar")
|
||||||
|
val avatar: String,
|
||||||
|
@SerializedName("recommendBanner")
|
||||||
|
val recommendBanner: String,
|
||||||
|
@SerializedName("isRecommended")
|
||||||
|
val isRecommended: Boolean,
|
||||||
|
@SerializedName("allowInHot")
|
||||||
|
val allowInHot: Boolean,
|
||||||
|
@SerializedName("users")
|
||||||
|
val users: List<GroupChatUser>,
|
||||||
|
@SerializedName("prompts")
|
||||||
|
val prompts: List<GroupChatPrompt>,
|
||||||
|
@SerializedName("source")
|
||||||
|
val source: String
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class CategoryTranslation(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String?,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CategoryTemplate(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String,
|
||||||
|
@SerializedName("avatar")
|
||||||
|
val avatar: String,
|
||||||
|
@SerializedName("parentId")
|
||||||
|
val parentId: Int?,
|
||||||
|
@SerializedName("parent")
|
||||||
|
val parent: CategoryTemplate?,
|
||||||
|
@SerializedName("children")
|
||||||
|
val children: List<CategoryTemplate>?,
|
||||||
|
@SerializedName("sort")
|
||||||
|
val sort: Int,
|
||||||
|
@SerializedName("isActive")
|
||||||
|
val isActive: Boolean,
|
||||||
|
@SerializedName("promptCount")
|
||||||
|
val promptCount: Int?,
|
||||||
|
@SerializedName("createdAt")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("updatedAt")
|
||||||
|
val updatedAt: String,
|
||||||
|
@SerializedName("translations")
|
||||||
|
val translations: Map<String, CategoryTranslation>?
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* 根据语言代码获取翻译后的名称,如果没有翻译则返回默认名称
|
||||||
|
*/
|
||||||
|
fun getLocalizedName(lang: String): String {
|
||||||
|
// 尝试获取完整的语言标记(如 "zh-CN")
|
||||||
|
val translation = translations?.get(lang)
|
||||||
|
if (translation?.name != null && translation.name.isNotEmpty()) {
|
||||||
|
return translation.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到,尝试语言代码的前缀(如 "zh")
|
||||||
|
val langPrefix = lang.split("-", "_").firstOrNull()
|
||||||
|
if (langPrefix != null) {
|
||||||
|
translations?.entries?.forEach { (key, value) ->
|
||||||
|
if (key.startsWith(langPrefix) && value.name != null && value.name.isNotEmpty()) {
|
||||||
|
return value.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有翻译,返回默认名称
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据语言代码获取翻译后的描述,如果没有翻译则返回默认描述
|
||||||
|
*/
|
||||||
|
fun getLocalizedDescription(lang: String): String {
|
||||||
|
// 尝试获取完整的语言标记(如 "zh-CN")
|
||||||
|
val translation = translations?.get(lang)
|
||||||
|
if (translation?.description != null && translation.description.isNotEmpty()) {
|
||||||
|
return translation.description
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到,尝试语言代码的前缀(如 "zh")
|
||||||
|
val langPrefix = lang.split("-", "_").firstOrNull()
|
||||||
|
if (langPrefix != null) {
|
||||||
|
translations?.entries?.forEach { (key, value) ->
|
||||||
|
if (key.startsWith(langPrefix) && value.description != null && value.description.isNotEmpty()) {
|
||||||
|
return value.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有翻译,返回默认描述
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CategoryListResponse(
|
||||||
|
@SerializedName("page")
|
||||||
|
val page: Int,
|
||||||
|
@SerializedName("pageSize")
|
||||||
|
val pageSize: Int,
|
||||||
|
@SerializedName("total")
|
||||||
|
val total: Int,
|
||||||
|
@SerializedName("list")
|
||||||
|
val list: List<CategoryTemplate>
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========== Prompt Rule 相关数据类 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建规则请求体
|
||||||
|
* @param rule 规则内容,不能为空
|
||||||
|
* @param promptId 智能体ID,与 openId 二选一,promptId 优先
|
||||||
|
* @param openId 智能体的 OpenID(UUID格式),与 promptId 二选一
|
||||||
|
*/
|
||||||
|
data class CreatePromptRuleRequestBody(
|
||||||
|
@SerializedName("rule")
|
||||||
|
val rule: String,
|
||||||
|
@SerializedName("promptId")
|
||||||
|
val promptId: Int? = null,
|
||||||
|
@SerializedName("openId")
|
||||||
|
val openId: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改规则请求体
|
||||||
|
* @param id 规则ID,必填
|
||||||
|
* @param rule 新的规则内容,不能为空
|
||||||
|
* @param promptId 要更改关联的智能体ID(可选)
|
||||||
|
* @param openId 要更改关联的智能体 OpenID(可选)
|
||||||
|
*/
|
||||||
|
data class UpdatePromptRuleRequestBody(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("rule")
|
||||||
|
val rule: String,
|
||||||
|
@SerializedName("promptId")
|
||||||
|
val promptId: Int? = null,
|
||||||
|
@SerializedName("openId")
|
||||||
|
val openId: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则关联的智能体信息
|
||||||
|
* @param id 智能体ID
|
||||||
|
* @param title 智能体标题
|
||||||
|
* @param avatar 智能体头像URL
|
||||||
|
*/
|
||||||
|
data class PromptRuleAgent(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("title")
|
||||||
|
val title: String,
|
||||||
|
@SerializedName("avatar")
|
||||||
|
val avatar: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则详情
|
||||||
|
* @param id 规则ID
|
||||||
|
* @param rule 规则内容
|
||||||
|
* @param creator 创建者名称
|
||||||
|
* @param creatorType 创建者类型(如 "user")
|
||||||
|
* @param scope 作用域(如 "personal")
|
||||||
|
* @param prompt 关联的智能体信息
|
||||||
|
* @param createdAt 创建时间(ISO 8601 格式)
|
||||||
|
* @param updatedAt 更新时间(ISO 8601 格式)
|
||||||
|
*/
|
||||||
|
data class PromptRule(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("rule")
|
||||||
|
val rule: String,
|
||||||
|
@SerializedName("creator")
|
||||||
|
val creator: String,
|
||||||
|
@SerializedName("creator_type")
|
||||||
|
val creatorType: String,
|
||||||
|
@SerializedName("scope")
|
||||||
|
val scope: String,
|
||||||
|
@SerializedName("prompt")
|
||||||
|
val prompt: PromptRuleAgent,
|
||||||
|
@SerializedName("created_at")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("updated_at")
|
||||||
|
val updatedAt: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则列表响应
|
||||||
|
* @param page 当前页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @param total 总记录数
|
||||||
|
* @param list 规则列表
|
||||||
|
*/
|
||||||
|
data class PromptRuleListResponse(
|
||||||
|
@SerializedName("page")
|
||||||
|
val page: Int,
|
||||||
|
@SerializedName("pageSize")
|
||||||
|
val pageSize: Int,
|
||||||
|
@SerializedName("total")
|
||||||
|
val total: Int,
|
||||||
|
@SerializedName("list")
|
||||||
|
val list: List<PromptRule>
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规则配额信息
|
||||||
|
* @param promptId 智能体ID
|
||||||
|
* @param promptTitle 智能体标题
|
||||||
|
* @param baseMaxCount 基础条数限制(免费配额)
|
||||||
|
* @param purchasedCount 用户购买的额外条数
|
||||||
|
* @param totalMaxCount 总可用条数(基础+购买)
|
||||||
|
* @param currentCount 当前已创建的规则条数
|
||||||
|
* @param remainingCount 剩余可用条数
|
||||||
|
* @param usagePercent 使用百分比(0-100)
|
||||||
|
*/
|
||||||
|
data class PromptRuleQuota(
|
||||||
|
@SerializedName("promptId")
|
||||||
|
val promptId: Int,
|
||||||
|
@SerializedName("promptTitle")
|
||||||
|
val promptTitle: String,
|
||||||
|
@SerializedName("baseMaxCount")
|
||||||
|
val baseMaxCount: Int,
|
||||||
|
@SerializedName("purchasedCount")
|
||||||
|
val purchasedCount: Int,
|
||||||
|
@SerializedName("totalMaxCount")
|
||||||
|
val totalMaxCount: Int,
|
||||||
|
@SerializedName("currentCount")
|
||||||
|
val currentCount: Int,
|
||||||
|
@SerializedName("remainingCount")
|
||||||
|
val remainingCount: Int,
|
||||||
|
@SerializedName("usagePercent")
|
||||||
|
val usagePercent: Double
|
||||||
|
)
|
||||||
|
|
||||||
interface RaveNowAPI {
|
interface RaveNowAPI {
|
||||||
@GET("membership/config")
|
@GET("membership/config")
|
||||||
@retrofit2.http.Headers("X-Requires-Auth: true")
|
@retrofit2.http.Headers("X-Requires-Auth: true")
|
||||||
@@ -309,6 +655,7 @@ interface RaveNowAPI {
|
|||||||
@Query("trend") trend: String? = null,
|
@Query("trend") trend: String? = null,
|
||||||
@Query("favouriteUserId") favouriteUserId: Int? = null,
|
@Query("favouriteUserId") favouriteUserId: Int? = null,
|
||||||
@Query("explore") explore: String? = null,
|
@Query("explore") explore: String? = null,
|
||||||
|
@Query("newsFilter") newsFilter: String? = null,
|
||||||
): Response<ListContainer<Moment>>
|
): Response<ListContainer<Moment>>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@@ -552,6 +899,8 @@ interface RaveNowAPI {
|
|||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("withWorkflow") withWorkflow: Int = 1,
|
@Query("withWorkflow") withWorkflow: Int = 1,
|
||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
|
@Query("random") random: Int? = null,
|
||||||
): Response<DataContainer<ListContainer<Agent>>>
|
): Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
@GET("outside/my/prompts")
|
@GET("outside/my/prompts")
|
||||||
@@ -580,7 +929,10 @@ interface RaveNowAPI {
|
|||||||
suspend fun agentMoment(@Body body: AgentMomentRequestBody): Response<DataContainer<String>>
|
suspend fun agentMoment(@Body body: AgentMomentRequestBody): Response<DataContainer<String>>
|
||||||
|
|
||||||
@GET("outside/rooms/open")
|
@GET("outside/rooms/open")
|
||||||
suspend fun createGroupChatAi(@Query("trtcGroupId") trtcGroupId: String): Response<DataContainer<Unit>>
|
suspend fun createGroupChatAi(
|
||||||
|
@Query("trtcGroupId") trtcGroupId: String? = null,
|
||||||
|
@Query("roomId") roomId: Int? = null
|
||||||
|
): Response<DataContainer<GroupChatResponse>>
|
||||||
|
|
||||||
@POST("outside/rooms/create-single-chat")
|
@POST("outside/rooms/create-single-chat")
|
||||||
suspend fun createSingleChat(@Body body: SingleChatRequestBody): Response<DataContainer<Unit>>
|
suspend fun createSingleChat(@Body body: SingleChatRequestBody): Response<DataContainer<Unit>>
|
||||||
@@ -595,6 +947,7 @@ interface RaveNowAPI {
|
|||||||
suspend fun getRooms(@Query("page") page: Int = 1,
|
suspend fun getRooms(@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("isRecommended") isRecommended: Int = 1,
|
@Query("isRecommended") isRecommended: Int = 1,
|
||||||
|
@Query("random") random: Int? = null,
|
||||||
): Response<ListContainer<Room>>
|
): Response<ListContainer<Room>>
|
||||||
|
|
||||||
@GET("outside/rooms/detail")
|
@GET("outside/rooms/detail")
|
||||||
@@ -605,7 +958,240 @@ interface RaveNowAPI {
|
|||||||
suspend fun joinRoom(@Body body: JoinGroupChatRequestBody,
|
suspend fun joinRoom(@Body body: JoinGroupChatRequestBody,
|
||||||
): Response<DataContainer<Room>>
|
): Response<DataContainer<Room>>
|
||||||
|
|
||||||
|
@GET("outside/categories")
|
||||||
|
suspend fun getCategories(
|
||||||
|
@Query("page") page: Int? = null,
|
||||||
|
@Query("pageSize") pageSize: Int? = null,
|
||||||
|
@Query("parentId") parentId: Int? = null,
|
||||||
|
@Query("isActive") isActive: Boolean? = null,
|
||||||
|
@Query("name") name: String? = null,
|
||||||
|
@Query("withChildren") withChildren: Boolean? = null,
|
||||||
|
@Query("withParent") withParent: Boolean? = null,
|
||||||
|
@Query("withCount") withCount: Boolean? = null,
|
||||||
|
@Query("hideEmpty") hideEmpty: Boolean? = null,
|
||||||
|
@Query("lang") lang: String? = null
|
||||||
|
): Response<CategoryListResponse>
|
||||||
|
|
||||||
|
@GET("outside/categories/tree")
|
||||||
|
suspend fun getCategoryTree(
|
||||||
|
@Query("withCount") withCount: Boolean? = null,
|
||||||
|
@Query("hideEmpty") hideEmpty: Boolean? = null
|
||||||
|
): Response<DataContainer<List<CategoryTemplate>>>
|
||||||
|
|
||||||
|
@GET("outside/categories/{id}")
|
||||||
|
suspend fun getCategoryById(
|
||||||
|
@Path("id") id: Int
|
||||||
|
): Response<DataContainer<CategoryTemplate>>
|
||||||
|
|
||||||
|
@GET("outside/prompts")
|
||||||
|
suspend fun getPromptsByCategory(
|
||||||
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
|
@Query("categoryName") categoryName: String? = null,
|
||||||
|
@Query("uncategorized") uncategorized: String? = null,
|
||||||
|
@Query("page") page: Int? = null,
|
||||||
|
@Query("pageSize") pageSize: Int? = null
|
||||||
|
): Response<ListContainer<Agent>>
|
||||||
|
|
||||||
|
// ========== Prompt Rule API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建智能体规则
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 为指定的智能体创建一条新规则
|
||||||
|
* - 规则必须关联到存在的智能体(通过 promptId 或 openId 指定)
|
||||||
|
* - 创建前会自动检查配额限制,如果超出会尝试自动扩容
|
||||||
|
* - 只有创建者可以修改和删除该规则
|
||||||
|
*
|
||||||
|
* @param body 创建规则请求体
|
||||||
|
* - rule: 规则内容,不能为空
|
||||||
|
* - promptId: 智能体ID(与 openId 二选一,promptId 优先)
|
||||||
|
* - openId: 智能体的 OpenID(UUID格式,与 promptId 二选一)
|
||||||
|
*
|
||||||
|
* @return 成功时返回空 Unit,失败时返回错误信息
|
||||||
|
*
|
||||||
|
* 示例:
|
||||||
|
* ```kotlin
|
||||||
|
* val request = CreatePromptRuleRequestBody(
|
||||||
|
* rule = "禁止讨论政治话题",
|
||||||
|
* promptId = 123
|
||||||
|
* )
|
||||||
|
* val response = api.createPromptRule(request)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@POST("outside/prompt/rule")
|
||||||
|
suspend fun createPromptRule(
|
||||||
|
@Body body: CreatePromptRuleRequestBody
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改智能体规则
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 修改已存在的规则内容或关联的智能体
|
||||||
|
* - 只有规则的创建者可以修改
|
||||||
|
* - 可以同时修改规则内容和关联的智能体
|
||||||
|
* - 修改关联智能体时会重新验证配额限制
|
||||||
|
*
|
||||||
|
* @param body 修改规则请求体
|
||||||
|
* - id: 规则ID,必填
|
||||||
|
* - rule: 新的规则内容,不能为空
|
||||||
|
* - promptId: 要更改关联的智能体ID(可选)
|
||||||
|
* - openId: 要更改关联的智能体 OpenID(可选)
|
||||||
|
*
|
||||||
|
* @return 成功时返回空 Unit,失败时返回错误信息
|
||||||
|
*
|
||||||
|
* 权限要求:
|
||||||
|
* - 必须是规则的创建者(creator_type 为 "user" 且 create_id 匹配)
|
||||||
|
*
|
||||||
|
* 示例:
|
||||||
|
* ```kotlin
|
||||||
|
* val request = UpdatePromptRuleRequestBody(
|
||||||
|
* id = 456,
|
||||||
|
* rule = "禁止讨论政治和敏感话题"
|
||||||
|
* )
|
||||||
|
* val response = api.updatePromptRule(request)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@retrofit2.http.PUT("outside/prompt/rule")
|
||||||
|
suspend fun updatePromptRule(
|
||||||
|
@Body body: UpdatePromptRuleRequestBody
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除智能体规则
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 删除指定的规则
|
||||||
|
* - 只有规则的创建者可以删除
|
||||||
|
* - 删除操作不可恢复,请谨慎操作
|
||||||
|
*
|
||||||
|
* @param id 规则ID
|
||||||
|
*
|
||||||
|
* @return 成功时返回空 Unit,失败时返回错误信息
|
||||||
|
*
|
||||||
|
* 权限要求:
|
||||||
|
* - 必须是规则的创建者(creator_type 为 "user" 且 create_id 匹配)
|
||||||
|
*
|
||||||
|
* 示例:
|
||||||
|
* ```kotlin
|
||||||
|
* val response = api.deletePromptRule(456)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@DELETE("outside/prompt/rule/{id}")
|
||||||
|
suspend fun deletePromptRule(
|
||||||
|
@Path("id") id: Int
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询智能体规则列表
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 查询指定智能体下当前用户创建的规则列表
|
||||||
|
* - 支持分页和关键词模糊搜索
|
||||||
|
* - 只返回当前用户创建的规则,不会返回其他用户的规则
|
||||||
|
*
|
||||||
|
* @param promptId 智能体ID,支持数字ID或UUID格式的 openId
|
||||||
|
* @param rule 规则内容关键词(模糊搜索),可选
|
||||||
|
* @param page 页码,默认 1
|
||||||
|
* @param pageSize 每页数量,默认 10
|
||||||
|
*
|
||||||
|
* @return 返回分页的规则列表,包含规则详情和关联的智能体信息
|
||||||
|
*
|
||||||
|
* 响应数据说明:
|
||||||
|
* - page: 当前页码
|
||||||
|
* - pageSize: 每页数量
|
||||||
|
* - total: 总记录数
|
||||||
|
* - list: 规则列表
|
||||||
|
* - id: 规则ID
|
||||||
|
* - rule: 规则内容
|
||||||
|
* - creator: 创建者名称
|
||||||
|
* - creator_type: 创建者类型
|
||||||
|
* - scope: 作用域
|
||||||
|
* - prompt: 关联的智能体信息(id, title, avatar)
|
||||||
|
* - created_at: 创建时间
|
||||||
|
* - updated_at: 更新时间
|
||||||
|
*
|
||||||
|
* 示例:
|
||||||
|
* ```kotlin
|
||||||
|
* // 使用数字ID查询
|
||||||
|
* val response1 = api.getPromptRuleList("123", page = 1, pageSize = 10)
|
||||||
|
*
|
||||||
|
* // 使用 OpenID 查询
|
||||||
|
* val response2 = api.getPromptRuleList(
|
||||||
|
* "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
* page = 1,
|
||||||
|
* pageSize = 10
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // 带关键词搜索
|
||||||
|
* val response3 = api.getPromptRuleList(
|
||||||
|
* "123",
|
||||||
|
* rule = "政治",
|
||||||
|
* page = 1,
|
||||||
|
* pageSize = 10
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@GET("outside/prompt/{promptId}/rule/list")
|
||||||
|
suspend fun getPromptRuleList(
|
||||||
|
@Path("promptId") promptId: String,
|
||||||
|
@Query("rule") rule: String? = null,
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("pageSize") pageSize: Int = 10
|
||||||
|
): Response<DataContainer<PromptRuleListResponse>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询智能体规则配额信息
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 查询指定智能体的规则条数使用情况
|
||||||
|
* - 包括基础配额、已购买配额、已使用数量等完整信息
|
||||||
|
* - 用于判断是否还能创建新规则
|
||||||
|
*
|
||||||
|
* @param promptId 智能体ID,支持数字ID或UUID格式的 openId
|
||||||
|
*
|
||||||
|
* @return 返回配额详细信息
|
||||||
|
*
|
||||||
|
* 响应数据说明:
|
||||||
|
* - promptId: 智能体ID
|
||||||
|
* - promptTitle: 智能体标题
|
||||||
|
* - baseMaxCount: 基础条数限制(免费配额,由智能体等级决定)
|
||||||
|
* - purchasedCount: 用户购买的额外条数
|
||||||
|
* - totalMaxCount: 总可用条数(baseMaxCount + purchasedCount)
|
||||||
|
* - currentCount: 当前已创建的规则条数(只统计当前用户创建的)
|
||||||
|
* - remainingCount: 剩余可用条数(totalMaxCount - currentCount)
|
||||||
|
* - usagePercent: 使用百分比,0-100(currentCount / totalMaxCount * 100)
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* 1. 创建规则前检查是否有足够配额
|
||||||
|
* 2. 展示规则使用情况统计
|
||||||
|
* 3. 提示用户购买额外配额
|
||||||
|
*
|
||||||
|
* 示例:
|
||||||
|
* ```kotlin
|
||||||
|
* // 使用数字ID查询
|
||||||
|
* val response1 = api.getPromptRuleQuota("123")
|
||||||
|
*
|
||||||
|
* // 使用 OpenID 查询
|
||||||
|
* val response2 = api.getPromptRuleQuota("550e8400-e29b-41d4-a716-446655440000")
|
||||||
|
*
|
||||||
|
* // 处理响应
|
||||||
|
* response1.body()?.data?.let { quota ->
|
||||||
|
* if (quota.remainingCount > 0) {
|
||||||
|
* // 可以创建新规则
|
||||||
|
* println("还可以创建 ${quota.remainingCount} 条规则")
|
||||||
|
* } else {
|
||||||
|
* // 配额已用完
|
||||||
|
* println("规则配额已用完,已使用 ${quota.currentCount}/${quota.totalMaxCount}")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@GET("outside/prompt/{promptId}/rule/count")
|
||||||
|
suspend fun getPromptRuleQuota(
|
||||||
|
@Path("promptId") promptId: String
|
||||||
|
): Response<DataContainer<PromptRuleQuota>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ class AgentPagingSource(
|
|||||||
authorId = authorId
|
authorId = authorId
|
||||||
)
|
)
|
||||||
LoadResult.Page(
|
LoadResult.Page(
|
||||||
data = users.list,
|
data = users?.list ?: listOf(),
|
||||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||||
nextKey = if (users.list.isEmpty()) null else users.page + 1
|
nextKey = if (users?.list?.isNotEmpty() == true) users.page + 1 else null
|
||||||
)
|
)
|
||||||
} catch (exception: IOException) {
|
} catch (exception: IOException) {
|
||||||
return LoadResult.Error(exception)
|
return LoadResult.Error(exception)
|
||||||
@@ -102,7 +102,7 @@ class AgentRemoteDataSource(
|
|||||||
suspend fun getAgent(
|
suspend fun getAgent(
|
||||||
pageNumber: Int,
|
pageNumber: Int,
|
||||||
authorId: Int? = null
|
authorId: Int? = null
|
||||||
): ListContainer<AgentEntity> {
|
): ListContainer<AgentEntity>? {
|
||||||
return agentService.getAgent(
|
return agentService.getAgent(
|
||||||
pageNumber = pageNumber,
|
pageNumber = pageNumber,
|
||||||
authorId = authorId
|
authorId = authorId
|
||||||
@@ -117,7 +117,7 @@ class AgentServiceImpl() : AgentService {
|
|||||||
pageNumber: Int,
|
pageNumber: Int,
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
authorId: Int?
|
authorId: Int?
|
||||||
): ListContainer<AgentEntity> {
|
): ListContainer<AgentEntity>? {
|
||||||
return agentBackend.getAgent(
|
return agentBackend.getAgent(
|
||||||
pageNumber = pageNumber,
|
pageNumber = pageNumber,
|
||||||
authorId = authorId
|
authorId = authorId
|
||||||
@@ -130,7 +130,7 @@ class AgentBackend {
|
|||||||
suspend fun getAgent(
|
suspend fun getAgent(
|
||||||
pageNumber: Int,
|
pageNumber: Int,
|
||||||
authorId: Int? = null
|
authorId: Int? = null
|
||||||
): ListContainer<AgentEntity> {
|
): ListContainer<AgentEntity>? {
|
||||||
// 如果是游客模式且获取我的Agent(authorId为null),返回空列表
|
// 如果是游客模式且获取我的Agent(authorId为null),返回空列表
|
||||||
if (authorId == null && AppStore.isGuest) {
|
if (authorId == null && AppStore.isGuest) {
|
||||||
return ListContainer(
|
return ListContainer(
|
||||||
@@ -154,7 +154,7 @@ class AgentBackend {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get agents")
|
val body = resp.body() ?: return null
|
||||||
|
|
||||||
// 处理不同的返回类型
|
// 处理不同的返回类型
|
||||||
return if (authorId != null) {
|
return if (authorId != null) {
|
||||||
|
|||||||
@@ -299,12 +299,21 @@ data class MomentEntity(
|
|||||||
// 关联动态
|
// 关联动态
|
||||||
var relMoment: MomentEntity? = null,
|
var relMoment: MomentEntity? = null,
|
||||||
// 是否收藏
|
// 是否收藏
|
||||||
var isFavorite: Boolean = false
|
var isFavorite: Boolean = false,
|
||||||
|
// 新闻相关字段
|
||||||
|
val isNews: Boolean = false,
|
||||||
|
val newsTitle: String = "",
|
||||||
|
val newsUrl: String = "",
|
||||||
|
val newsSource: String = "",
|
||||||
|
val newsCategory: String = "",
|
||||||
|
val newsLanguage: String = "",
|
||||||
|
val newsContent: String = ""
|
||||||
)
|
)
|
||||||
class MomentLoaderExtraArgs(
|
class MomentLoaderExtraArgs(
|
||||||
val explore: Boolean? = false,
|
val explore: Boolean? = false,
|
||||||
val timelineId: Int? = null,
|
val timelineId: Int? = null,
|
||||||
val authorId : Int? = null
|
val authorId : Int? = null,
|
||||||
|
val newsOnly: Boolean? = null
|
||||||
)
|
)
|
||||||
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||||
override suspend fun fetchData(
|
override suspend fun fetchData(
|
||||||
@@ -317,7 +326,8 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
explore = if (extra.explore == true) "true" else "",
|
explore = if (extra.explore == true) "true" else "",
|
||||||
timelineId = extra.timelineId,
|
timelineId = extra.timelineId,
|
||||||
authorId = extra.authorId
|
authorId = extra.authorId,
|
||||||
|
newsFilter = if (extra.newsOnly == true) "news_only" else ""
|
||||||
)
|
)
|
||||||
val data = result.body()?.let {
|
val data = result.body()?.let {
|
||||||
ListContainer(
|
ListContainer(
|
||||||
@@ -355,6 +365,18 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
onListChanged?.invoke(this.list)
|
onListChanged?.invoke(this.list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateCommentCount(id: Int, delta: Int) {
|
||||||
|
this.list = this.list.map { momentItem ->
|
||||||
|
if (momentItem.id == id) {
|
||||||
|
val newCount = (momentItem.commentCount + delta).coerceAtLeast(0)
|
||||||
|
momentItem.copy(commentCount = newCount)
|
||||||
|
} else {
|
||||||
|
momentItem
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
fun removeMoment(id: Int) {
|
fun removeMoment(id: Int) {
|
||||||
this.list = this.list.filter { it.id != id }.toMutableList()
|
this.list = this.list.filter { it.id != id }.toMutableList()
|
||||||
onListChanged?.invoke(this.list)
|
onListChanged?.invoke(this.list)
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ object AppStore {
|
|||||||
AppState.appTheme = DarkThemeColors()
|
AppState.appTheme = DarkThemeColors()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// load chat background
|
||||||
|
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
|
||||||
|
if (savedBgUrl != null) {
|
||||||
|
AppState.chatBackgroundUrl = savedBgUrl
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveData() {
|
suspend fun saveData() {
|
||||||
@@ -54,5 +60,15 @@ object AppStore {
|
|||||||
}.apply()
|
}.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveChatBackgroundUrl(url: String?) {
|
||||||
|
sharedPreferences.edit().apply {
|
||||||
|
if (url != null) {
|
||||||
|
putString("chatBackgroundUrl", url)
|
||||||
|
} else {
|
||||||
|
remove("chatBackgroundUrl")
|
||||||
|
}
|
||||||
|
}.apply()
|
||||||
|
AppState.chatBackgroundUrl = url
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -41,10 +41,12 @@ import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
|||||||
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
|
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
|
||||||
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
||||||
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
||||||
|
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
|
||||||
import com.aiosman.ravenow.ui.chat.ChatScreen
|
import com.aiosman.ravenow.ui.chat.ChatScreen
|
||||||
import com.aiosman.ravenow.ui.chat.GroupChatScreen
|
import com.aiosman.ravenow.ui.chat.GroupChatScreen
|
||||||
import com.aiosman.ravenow.ui.comment.CommentsScreen
|
import com.aiosman.ravenow.ui.comment.CommentsScreen
|
||||||
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
||||||
|
import com.aiosman.ravenow.ui.composables.AgentCreatedSuccessIndicator
|
||||||
import com.aiosman.ravenow.ui.crop.ImageCropScreen
|
import com.aiosman.ravenow.ui.crop.ImageCropScreen
|
||||||
import com.aiosman.ravenow.ui.favourite.FavouriteListPage
|
import com.aiosman.ravenow.ui.favourite.FavouriteListPage
|
||||||
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen
|
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen
|
||||||
@@ -70,6 +72,7 @@ import com.aiosman.ravenow.ui.post.NewPostScreen
|
|||||||
import com.aiosman.ravenow.ui.post.PostScreen
|
import com.aiosman.ravenow.ui.post.PostScreen
|
||||||
import com.aiosman.ravenow.ui.profile.AccountProfileV2
|
import com.aiosman.ravenow.ui.profile.AccountProfileV2
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
|
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
|
||||||
|
import com.aiosman.ravenow.ui.notification.NotificationScreen
|
||||||
|
|
||||||
sealed class NavigationRoute(
|
sealed class NavigationRoute(
|
||||||
val route: String,
|
val route: String,
|
||||||
@@ -104,6 +107,7 @@ sealed class NavigationRoute(
|
|||||||
data object FavouriteList : NavigationRoute("FavouriteList")
|
data object FavouriteList : NavigationRoute("FavouriteList")
|
||||||
data object Chat : NavigationRoute("Chat/{id}")
|
data object Chat : NavigationRoute("Chat/{id}")
|
||||||
data object ChatAi : NavigationRoute("ChatAi/{id}")
|
data object ChatAi : NavigationRoute("ChatAi/{id}")
|
||||||
|
data object ChatSetting : NavigationRoute("ChatSetting")
|
||||||
data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
|
data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
|
||||||
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
||||||
data object ImageCrop : NavigationRoute("ImageCrop")
|
data object ImageCrop : NavigationRoute("ImageCrop")
|
||||||
@@ -115,6 +119,7 @@ sealed class NavigationRoute(
|
|||||||
data object GroupInfo : NavigationRoute("GroupInfo/{id}")
|
data object GroupInfo : NavigationRoute("GroupInfo/{id}")
|
||||||
data object VipSelPage : NavigationRoute("VipSelPage")
|
data object VipSelPage : NavigationRoute("VipSelPage")
|
||||||
data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
|
data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
|
||||||
|
data object NotificationScreen : NavigationRoute("NotificationScreen")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -488,6 +493,14 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(route = NavigationRoute.ChatSetting.route) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
ChatSettingScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.ChatGroup.route,
|
route = NavigationRoute.ChatGroup.route,
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType },
|
arguments = listOf(navArgument("id") { type = NavType.StringType },
|
||||||
@@ -590,6 +603,13 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(route = NavigationRoute.NotificationScreen.route) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
NotificationScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,6 +635,7 @@ fun Navigation(
|
|||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = startDestination
|
startDestination = startDestination
|
||||||
)
|
)
|
||||||
|
AgentCreatedSuccessIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.background
|
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.Column
|
||||||
@@ -47,12 +50,24 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.ConstVars
|
||||||
|
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编辑用户资料界面
|
* 编辑用户资料界面
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountEditScreen2() {
|
fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,) {
|
||||||
val model = AccountEditViewModel
|
val model = AccountEditViewModel
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -61,6 +76,19 @@ fun AccountEditScreen2() {
|
|||||||
|
|
||||||
// 防抖导航器
|
// 防抖导航器
|
||||||
val debouncedNavigation = rememberDebouncedNavigation()
|
val debouncedNavigation = rememberDebouncedNavigation()
|
||||||
|
|
||||||
|
// 添加图片选择启动器
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
||||||
|
context,
|
||||||
|
scope,
|
||||||
|
maxSize = ConstVars.BANNER_IMAGE_MAX_SIZE,
|
||||||
|
quality = 100
|
||||||
|
) { uri, file ->
|
||||||
|
// 处理选中的图片
|
||||||
|
onUpdateBanner?.invoke(uri, file, context)
|
||||||
|
}
|
||||||
|
|
||||||
fun onNicknameChange(value: String) {
|
fun onNicknameChange(value: String) {
|
||||||
// 去除换行符,确保昵称不包含换行
|
// 去除换行符,确保昵称不包含换行
|
||||||
val cleanValue = value.replace("\n", "").replace("\r", "")
|
val cleanValue = value.replace("\n", "").replace("\r", "")
|
||||||
@@ -152,7 +180,60 @@ fun AccountEditScreen2() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(44.dp))
|
|
||||||
|
|
||||||
|
// 添加横幅图片区域
|
||||||
|
val banner = model.profile?.banner
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(300.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
) {
|
||||||
|
if (banner != null) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = LocalContext.current,
|
||||||
|
imageUrl = banner,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "Banner",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Gray.copy(alpha = 0.1f))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(120.dp)
|
||||||
|
.height(42.dp)
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 12.dp, bottom = 12.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.4f),
|
||||||
|
shape = RoundedCornerShape(9.dp)
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
Intent(Intent.ACTION_PICK).apply {
|
||||||
|
type = "image/*"
|
||||||
|
pickBannerImageLauncher.launch(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "change",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
// 显示内容或加载状态
|
// 显示内容或加载状态
|
||||||
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
|
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
|
||||||
@@ -180,7 +261,15 @@ fun AccountEditScreen2() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(32.dp)
|
.size(32.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(appColors.main)
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7c45ed),
|
||||||
|
Color(0x997c68ef),
|
||||||
|
Color(0xFF7bd8f8)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.debouncedClickable(
|
.debouncedClickable(
|
||||||
debounceTime = 800L
|
debounceTime = 800L
|
||||||
@@ -198,7 +287,7 @@ fun AccountEditScreen2() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(58.dp))
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ import androidx.activity.compose.BackHandler
|
|||||||
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.focus.FocusRequester
|
||||||
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.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
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 androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -61,7 +63,28 @@ import com.aiosman.ravenow.ui.composables.form.FormTextInput2
|
|||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.StartOffset
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import com.aiosman.ravenow.ui.agent.AddAgentViewModel.showManualCreation
|
||||||
|
import com.airbnb.lottie.compose.LottieAnimation
|
||||||
|
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||||
|
import com.airbnb.lottie.compose.LottieConstants
|
||||||
|
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||||
/**
|
/**
|
||||||
* 添加智能体界面
|
* 添加智能体界面
|
||||||
*/
|
*/
|
||||||
@@ -73,8 +96,12 @@ fun AddAgentScreen() {
|
|||||||
var agnetNameError by remember { mutableStateOf<String?>(null) }
|
var agnetNameError by remember { mutableStateOf<String?>(null) }
|
||||||
var agnetDescError by remember { mutableStateOf<String?>(null) }
|
var agnetDescError by remember { mutableStateOf<String?>(null) }
|
||||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
var isProcessing by remember { mutableStateOf(false) }
|
||||||
|
var showWaveAnimation by remember { mutableStateOf(false) }
|
||||||
|
var isCreatingAgent by remember { mutableStateOf(false) } // 控制是否处于创建状态
|
||||||
|
var showManualCreationForm by remember { mutableStateOf(false) } // 控制是否显示手动创建表单
|
||||||
|
var tempDesc by remember { mutableStateOf("") } // 独立的临时描述变量
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
fun onNameChange(value: String) {
|
fun onNameChange(value: String) {
|
||||||
model.name = value.trim()
|
model.name = value.trim()
|
||||||
@@ -88,15 +115,37 @@ fun AddAgentScreen() {
|
|||||||
fun onDescChange(value: String) {
|
fun onDescChange(value: String) {
|
||||||
model.desc = value.trim()
|
model.desc = value.trim()
|
||||||
agnetDescError = when {
|
agnetDescError = when {
|
||||||
value.length > 100 -> "简介长度不能大于100"
|
value.length > 512 -> "简介长度不能大于512"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onTempDescChange(value: String) {
|
||||||
|
tempDesc = value.trim()
|
||||||
|
agnetDescError = when {
|
||||||
|
value.length > 512 -> "简介长度不能大于512"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validate(): Boolean {
|
fun validate(): Boolean {
|
||||||
return agnetNameError == null && agnetDescError == null
|
return agnetNameError == null && agnetDescError == null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI文案优化
|
||||||
|
suspend fun optimizeTextWithAI(content: String): String? {
|
||||||
|
return try {
|
||||||
|
val sessionId = ""
|
||||||
|
val response = com.aiosman.ravenow.data.api.ApiClient.api.agentMoment(
|
||||||
|
com.aiosman.ravenow.data.api.AgentMomentRequestBody(
|
||||||
|
generateText = content,
|
||||||
|
sessionId = sessionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
response.body()?.data
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理系统返回键
|
// 处理系统返回键
|
||||||
BackHandler {
|
BackHandler {
|
||||||
@@ -110,7 +159,22 @@ fun AddAgentScreen() {
|
|||||||
// 页面进入时重置头像选择状态
|
// 页面进入时重置头像选择状态
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.isSelectingAvatar = false
|
model.isSelectingAvatar = false
|
||||||
|
// 根据标记恢复相应的状态
|
||||||
|
if (model.isAutoModeManualForm) {
|
||||||
|
// 恢复自动模式下的手动表单状态
|
||||||
|
showManualCreationForm = model.showManualCreationForm
|
||||||
|
isCreatingAgent = model.isCreatingAgent
|
||||||
|
showWaveAnimation = model.showWaveAnimation
|
||||||
|
showManualCreation = model.showManualCreation
|
||||||
|
} else {
|
||||||
|
// 恢复手动模式下的状态
|
||||||
|
showManualCreation = model.showManualCreation
|
||||||
|
showManualCreationForm = model.showManualCreationForm
|
||||||
|
isCreatingAgent = model.isCreatingAgent
|
||||||
|
showWaveAnimation = model.showWaveAnimation
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -118,9 +182,12 @@ fun AddAgentScreen() {
|
|||||||
.background(color = appColors.decentBackground),
|
.background(color = appColors.decentBackground),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
var showManualCreation by remember {
|
||||||
|
mutableStateOf(model.showManualCreation)
|
||||||
|
}
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 16.dp)
|
||||||
.background(color = appColors.decentBackground)
|
.background(color = appColors.decentBackground)
|
||||||
) {
|
) {
|
||||||
// 自定义header,控制返回按钮行为
|
// 自定义header,控制返回按钮行为
|
||||||
@@ -148,80 +215,298 @@ fun AddAgentScreen() {
|
|||||||
stringResource(R.string.agent_add),
|
stringResource(R.string.agent_add),
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.W600,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
color = appColors.text
|
color = appColors.text
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(12.dp))
|
}
|
||||||
Icon(
|
}
|
||||||
|
Spacer(modifier = Modifier.height(1.dp))
|
||||||
|
|
||||||
|
if (!isCreatingAgent) {
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.fillMaxWidth()
|
||||||
.noRippleClickable {
|
.height(50.dp)
|
||||||
// 提交创建智能体的逻辑可以在这里实现
|
.padding(horizontal = 20.dp),
|
||||||
},
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = "Add",
|
|
||||||
tint = appColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(44.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.size(88.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
CustomAsyncImage(
|
Image(
|
||||||
context,
|
painter = painterResource(id = R.mipmap.group_copy),
|
||||||
model.croppedBitmap,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(88.dp)
|
|
||||||
.clip(
|
|
||||||
RoundedCornerShape(88.dp)
|
|
||||||
),
|
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
contentScale = ContentScale.Crop,
|
modifier = Modifier
|
||||||
placeholderRes = R.mipmap.rider_pro_agent_avatar
|
.size(48.dp)
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(48.dp)
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(start = 20.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${AppState.profile?.nickName ?: "User"} ${stringResource(R.string.welcome_1)}",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!isCreatingAgent) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(start = 20.dp)
|
||||||
|
) {
|
||||||
|
if (!showManualCreation) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.welcome_2),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = appColors.text.copy(alpha = 0.6f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
if (!showManualCreation) {
|
||||||
|
//自动创造AI界面
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(32.dp)
|
.fillMaxWidth()
|
||||||
.clip(CircleShape)
|
.height(95.dp)
|
||||||
.background(appColors.main)
|
.shadow(
|
||||||
.align(Alignment.BottomEnd)
|
elevation = 10.dp,
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
spotColor = Color(0x33F563FF),
|
||||||
|
ambientColor = Color(0x99F563FF),
|
||||||
|
clip = false
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
Color(0xFF6246FF),
|
||||||
|
Color(0xFF7C45ED)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
)
|
||||||
|
.padding(0.5.dp)
|
||||||
|
.background(
|
||||||
|
color = appColors.inputBackground2,
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
|
model.viewModelScope.launch {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
keyboardController?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = tempDesc,
|
||||||
|
onValueChange = { value -> onTempDescChange(value) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(95.dp)
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.agent_desc_hint_auto),
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = LocalAppTheme.current.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
focusedContainerColor = appColors.inputBackground2,
|
||||||
|
unfocusedContainerColor = appColors.inputBackground2,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
supportingText = null,
|
||||||
|
trailingIcon = null,
|
||||||
|
leadingIcon = null
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 12.dp, bottom = 12.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
// 只有在有内容且未处理中且未创建中时才可点击
|
||||||
|
if (tempDesc.isNotEmpty() && !isProcessing && !isCreatingAgent) {
|
||||||
|
isProcessing = true
|
||||||
|
showWaveAnimation = true // 显示构思动画
|
||||||
|
keyboardController?.hide()
|
||||||
|
model.viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val optimizedText = optimizeTextWithAI(tempDesc)
|
||||||
|
if (optimizedText != null) {
|
||||||
|
onTempDescChange(optimizedText)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
isProcessing = false
|
||||||
|
showWaveAnimation = false
|
||||||
|
isCreatingAgent = true
|
||||||
|
showManualCreationForm = true
|
||||||
|
onDescChange(tempDesc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
if (tempDesc.isEmpty() || isProcessing || isCreatingAgent) {
|
||||||
|
Modifier.alpha(0.5f)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_info_magic),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.agent_text_beautify),
|
||||||
|
color = Color(0xFF6246FF),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
if ((isCreatingAgent && showWaveAnimation) || (isProcessing && showWaveAnimation)) {
|
||||||
|
// 显示构思动画
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.padding(start = 20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
) {
|
||||||
|
LottieAnimation(
|
||||||
|
composition = rememberLottieComposition(LottieCompositionSpec.Asset("loading.lottie")).value,
|
||||||
|
iterations = LottieConstants.IterateForever,
|
||||||
|
modifier = Modifier.matchParentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.ideaing),
|
||||||
|
color = appColors.text.copy(alpha = 0.6f),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (isCreatingAgent && !showWaveAnimation && showManualCreationForm) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.avatar),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0x777c45ed),
|
||||||
|
Color(0x777c68ef),
|
||||||
|
Color(0x557bd8f8)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.noRippleClickable {
|
||||||
|
// 保存当前状态
|
||||||
|
model.showManualCreationForm = showManualCreationForm
|
||||||
|
model.isCreatingAgent = isCreatingAgent
|
||||||
|
model.showWaveAnimation = showWaveAnimation
|
||||||
|
model.showManualCreation = showManualCreation
|
||||||
|
model.isAutoModeManualForm = true // 标记为自动模式下的手动表单
|
||||||
// 设置正在选择头像的标志
|
// 设置正在选择头像的标志
|
||||||
model.isSelectingAvatar = true
|
model.isSelectingAvatar = true
|
||||||
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
){
|
||||||
|
// 如果已有裁剪后的头像,则显示头像,否则显示编辑图标
|
||||||
|
if (model.croppedBitmap != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = model.croppedBitmap!!.asImageBitmap(),
|
||||||
|
contentDescription = "Avatar",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Add,
|
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
||||||
contentDescription = "Add",
|
contentDescription = "Edit",
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(58.dp))
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
Column(
|
// 原版两个输入框
|
||||||
modifier = Modifier
|
Text(
|
||||||
.padding(horizontal = 16.dp)
|
text = stringResource(R.string.agent_name),
|
||||||
) {
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
FormTextInput(
|
FormTextInput(
|
||||||
value = model.name,
|
value = model.name,
|
||||||
label = stringResource(R.string.agent_name),
|
hint = stringResource(R.string.agent_name_hint_1),
|
||||||
hint = stringResource(R.string.agent_name_hint),
|
|
||||||
background = appColors.inputBackground2,
|
background = appColors.inputBackground2,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) { value ->
|
) { value ->
|
||||||
onNameChange(value)
|
onNameChange(value)
|
||||||
}
|
}
|
||||||
// Spacer(modifier = Modifier.height(16.dp))
|
Text(
|
||||||
|
text = stringResource(R.string.agent_desc),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
FormTextInput2(
|
FormTextInput2(
|
||||||
value = model.desc,
|
value = model.desc,
|
||||||
label = stringResource(R.string.agent_desc),
|
|
||||||
hint = stringResource(R.string.agent_desc_hint),
|
hint = stringResource(R.string.agent_desc_hint),
|
||||||
background = appColors.inputBackground2,
|
background = appColors.inputBackground2,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -229,38 +514,243 @@ fun AddAgentScreen() {
|
|||||||
onDescChange(value)
|
onDescChange(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(58.dp))
|
} else if (!isCreatingAgent && !showWaveAnimation) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.padding(start = 20.dp)
|
||||||
|
.width(136.dp)
|
||||||
|
.height(40.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = Color(0x33858B98),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = appColors.background,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
showManualCreation = true
|
||||||
|
tempDesc = ""
|
||||||
|
agnetDescError = null
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 18.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = appColors.text,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.create_agent_hand),
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
//手动创造AI界面
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.width(140.dp)
|
||||||
|
.height(40.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 10.dp,
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
spotColor = Color(0x33F563FF),
|
||||||
|
ambientColor = Color(0x99F563FF),
|
||||||
|
clip = false
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
Color(0xFF6246FF),
|
||||||
|
Color(0xFF7C45ED)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
)
|
||||||
|
.padding(0.5.dp)
|
||||||
|
.background(
|
||||||
|
color = appColors.background,
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
showManualCreation = false
|
||||||
|
model.name = ""
|
||||||
|
model.desc = ""
|
||||||
|
model.croppedBitmap = null
|
||||||
|
isCreatingAgent = false
|
||||||
|
showManualCreationForm = false
|
||||||
|
showWaveAnimation = false
|
||||||
|
isProcessing = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_info_magic),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = appColors.text,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.create_agent_auto),
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.avatar),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0x777c45ed),
|
||||||
|
Color(0x777c68ef),
|
||||||
|
Color(0x557bd8f8)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.noRippleClickable {
|
||||||
|
// 保存当前状态
|
||||||
|
model.showManualCreation = showManualCreation
|
||||||
|
model.showManualCreationForm = showManualCreationForm
|
||||||
|
model.isCreatingAgent = isCreatingAgent
|
||||||
|
model.showWaveAnimation = showWaveAnimation
|
||||||
|
model.isAutoModeManualForm = false // 标记为手动模式下的手动表单
|
||||||
|
// 设置正在选择头像的标志
|
||||||
|
model.isSelectingAvatar = true
|
||||||
|
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
// 如果已有裁剪后的头像,则显示头像,否则显示编辑图标
|
||||||
|
if (model.croppedBitmap != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = model.croppedBitmap!!.asImageBitmap(),
|
||||||
|
contentDescription = "Avatar",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
||||||
|
contentDescription = "Edit",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
// 原版两个输入框
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.agent_name),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
FormTextInput(
|
||||||
|
value = model.name,
|
||||||
|
hint = stringResource(R.string.agent_name_hint_1),
|
||||||
|
background = appColors.inputBackground2,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { value ->
|
||||||
|
onNameChange(value)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.agent_desc),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
FormTextInput2(
|
||||||
|
value = model.desc,
|
||||||
|
hint = stringResource(R.string.agent_desc_hint),
|
||||||
|
background = appColors.inputBackground2,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { value ->
|
||||||
|
onDescChange(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//手动创造AI界面
|
||||||
|
}
|
||||||
|
|
||||||
// 错误信息显示
|
// 错误信息显示
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
errorMessage?.let { error ->
|
errorMessage?.let { error ->
|
||||||
Text(
|
Text(
|
||||||
text = error,
|
text = error,
|
||||||
color = Color.Red,
|
color = Color.Red,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier
|
||||||
|
.padding(bottom = 20.dp)
|
||||||
|
.align(Alignment.Center),
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(345.dp)
|
.width(345.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(bottom = 40.dp)
|
||||||
.background(
|
.background(
|
||||||
brush = Brush.linearGradient(
|
brush = Brush.linearGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color(0xFFEE2A33),
|
Color(0x777c45ed),
|
||||||
Color(0xFFD80264),
|
Color(0x777c68ef),
|
||||||
Color(0xFF8468BC)
|
Color(0x557bd8f8)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(24.dp)
|
shape = RoundedCornerShape(24.dp)
|
||||||
),
|
),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
backgroundColor = Color.Transparent,
|
backgroundColor = Color.Transparent,
|
||||||
text = stringResource(R.string.agent_create),
|
text = stringResource(R.string.create_confirm),
|
||||||
isLoading = model.isUpdating,
|
isLoading = model.isUpdating,
|
||||||
loadingText = stringResource(R.string.agent_createing),
|
|
||||||
enabled = !model.isUpdating && validate()
|
enabled = !model.isUpdating && validate()
|
||||||
) {
|
) {
|
||||||
// 验证输入
|
// 验证输入
|
||||||
@@ -268,6 +758,10 @@ fun AddAgentScreen() {
|
|||||||
if (validationError != null) {
|
if (validationError != null) {
|
||||||
// 显示验证错误
|
// 显示验证错误
|
||||||
errorMessage = validationError
|
errorMessage = validationError
|
||||||
|
model.viewModelScope.launch {
|
||||||
|
kotlinx.coroutines.delay(3000)
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
return@ActionButton
|
return@ActionButton
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,11 +776,15 @@ fun AddAgentScreen() {
|
|||||||
// 创建成功,清空数据并关闭页面
|
// 创建成功,清空数据并关闭页面
|
||||||
model.clearData()
|
model.clearData()
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
|
AppState.agentCreatedSuccess = true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// 显示错误信息
|
// 显示错误信息
|
||||||
errorMessage = "创建智能体失败: ${e.message}"
|
errorMessage = "创建智能体失败: ${e.message}"
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
// 3秒后清除错误信息
|
||||||
|
kotlinx.coroutines.delay(3000)
|
||||||
|
errorMessage = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ object AddAgentViewModel : ViewModel() {
|
|||||||
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
||||||
var isUpdating by mutableStateOf(false)
|
var isUpdating by mutableStateOf(false)
|
||||||
var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像
|
var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像
|
||||||
|
var showManualCreationForm by mutableStateOf(false)
|
||||||
|
var isCreatingAgent by mutableStateOf(false)
|
||||||
|
var showWaveAnimation by mutableStateOf(false)
|
||||||
|
var showManualCreation by mutableStateOf(false)
|
||||||
|
// 添加一个标志来区分两种手动表单状态
|
||||||
|
var isAutoModeManualForm by mutableStateOf(false)
|
||||||
suspend fun updateAgentAvatar(context: Context) {
|
suspend fun updateAgentAvatar(context: Context) {
|
||||||
croppedBitmap?.let {
|
croppedBitmap?.let {
|
||||||
val file = File(context.cacheDir, "agent_avatar.jpg")
|
val file = File(context.cacheDir, "agent_avatar.jpg")
|
||||||
@@ -70,7 +75,7 @@ object AddAgentViewModel : ViewModel() {
|
|||||||
name.length < 2 -> "智能体名称长度不能少于2个字符"
|
name.length < 2 -> "智能体名称长度不能少于2个字符"
|
||||||
name.length > 20 -> "智能体名称长度不能超过20个字符"
|
name.length > 20 -> "智能体名称长度不能超过20个字符"
|
||||||
desc.isEmpty() -> "智能体描述不能为空"
|
desc.isEmpty() -> "智能体描述不能为空"
|
||||||
desc.length > 100 -> "智能体描述长度不能超过100个字符"
|
desc.length > 512 -> "智能体描述长度不能超过512个字符"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,5 +89,10 @@ object AddAgentViewModel : ViewModel() {
|
|||||||
croppedBitmap = null
|
croppedBitmap = null
|
||||||
isUpdating = false
|
isUpdating = false
|
||||||
isSelectingAvatar = false
|
isSelectingAvatar = false
|
||||||
|
showManualCreationForm = false
|
||||||
|
isCreatingAgent = false
|
||||||
|
showWaveAnimation = false
|
||||||
|
showManualCreation = false
|
||||||
|
isAutoModeManualForm = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.UserService
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import io.openim.android.sdk.OpenIMClient
|
||||||
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
|
import io.openim.android.sdk.enums.ViewType
|
||||||
|
import io.openim.android.sdk.listener.OnAdvanceMsgListener
|
||||||
|
import io.openim.android.sdk.listener.OnBase
|
||||||
|
import io.openim.android.sdk.listener.OnMsgSendCallback
|
||||||
|
import io.openim.android.sdk.models.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天ViewModel基类,包含所有聊天功能的通用实现
|
||||||
|
* 子类需要实现抽象方法来处理特定的聊天类型(单聊/群聊)
|
||||||
|
*/
|
||||||
|
abstract class BaseChatViewModel : ViewModel() {
|
||||||
|
|
||||||
|
// 通用状态属性
|
||||||
|
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
|
||||||
|
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
var hasMore by mutableStateOf(true)
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var lastMessage: Message? = null
|
||||||
|
val showTimestampMap = mutableMapOf<String, Boolean>()
|
||||||
|
var goToNew by mutableStateOf(false)
|
||||||
|
var conversationID: String = "" // 会话ID,通过getOneConversation初始化
|
||||||
|
|
||||||
|
// 通用服务
|
||||||
|
val userService: UserService = UserServiceImpl()
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
var textMessageListener: OnAdvanceMsgListener? = null
|
||||||
|
|
||||||
|
val fetchHistorySize = 20
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化方法,子类需要实现具体的初始化逻辑
|
||||||
|
*/
|
||||||
|
abstract fun init(context: Context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志标签,子类需要实现
|
||||||
|
*/
|
||||||
|
abstract fun getLogTag(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话参数,子类需要实现
|
||||||
|
* @return Triple(targetId, conversationType, isSingleChat)
|
||||||
|
*/
|
||||||
|
abstract fun getConversationParams(): Triple<String, Int, Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理接收到的新消息,子类可以重写以添加特定逻辑
|
||||||
|
*/
|
||||||
|
open fun handleNewMessage(message: Message, context: Context): Boolean {
|
||||||
|
return false // 默认不处理,子类重写
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取发送消息时的接收者ID,子类需要实现
|
||||||
|
*/
|
||||||
|
abstract fun getReceiverInfo(): Pair<String?, String?> // (recvID, groupID)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息成功后的额外处理,子类可以重写
|
||||||
|
*/
|
||||||
|
open fun onMessageSentSuccess(message: String, sentMessage: Message?) {
|
||||||
|
// 默认无额外处理,子类可以重写
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话信息并初始化conversationID
|
||||||
|
*/
|
||||||
|
fun getOneConversation(onSuccess: (() -> Unit)? = null) {
|
||||||
|
val (targetId, conversationType, isSingleChat) = getConversationParams()
|
||||||
|
|
||||||
|
OpenIMClient.getInstance().conversationManager.getOneConversation(
|
||||||
|
object : OnBase<ConversationInfo> {
|
||||||
|
override fun onError(code: Int, error: String) {
|
||||||
|
Log.e(getLogTag(), "getOneConversation error: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: ConversationInfo) {
|
||||||
|
conversationID = data.conversationID
|
||||||
|
Log.d(getLogTag(), "获取会话信息成功,conversationID: $conversationID")
|
||||||
|
onSuccess?.invoke()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
targetId,
|
||||||
|
conversationType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册消息监听器
|
||||||
|
*/
|
||||||
|
fun RegistListener(context: Context) {
|
||||||
|
// 检查 OpenIM 是否已登录
|
||||||
|
if (!com.aiosman.ravenow.AppState.enableChat) {
|
||||||
|
Log.w(getLogTag(), "OpenIM 未登录,跳过注册消息监听器")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
textMessageListener = object : OnAdvanceMsgListener {
|
||||||
|
override fun onRecvNewMessage(msg: Message?) {
|
||||||
|
msg?.let { message ->
|
||||||
|
if (handleNewMessage(message, context)) {
|
||||||
|
val chatItem = ChatItem.convertToChatItem(message, context, avatar = getMessageAvatar(message))
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
Log.i(getLogTag(), "收到来自 ${message.sendID} 的消息,更新聊天列表")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息头像,子类可以重写
|
||||||
|
*/
|
||||||
|
open fun getMessageAvatar(message: Message): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消注册消息监听器
|
||||||
|
*/
|
||||||
|
fun UnRegistListener() {
|
||||||
|
textMessageListener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除未读消息
|
||||||
|
*/
|
||||||
|
fun clearUnRead() {
|
||||||
|
if (conversationID.isEmpty()) {
|
||||||
|
Log.w(getLogTag(), "conversationID为空,无法清除未读消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
|
||||||
|
conversationID,
|
||||||
|
object : OnBase<String> {
|
||||||
|
override fun onSuccess(data: String?) {
|
||||||
|
Log.i("openim", "清除未读消息成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.i("openim", "清除未读消息失败, code:$code, error:$error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载更多历史消息
|
||||||
|
*/
|
||||||
|
fun onLoadMore(context: Context) {
|
||||||
|
if (!hasMore || isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadHistoryMessages(context, isLoadMore = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送文本消息
|
||||||
|
*/
|
||||||
|
fun sendMessage(message: String, context: Context) {
|
||||||
|
// 检查 OpenIM 是否已登录
|
||||||
|
if (!com.aiosman.ravenow.AppState.enableChat) {
|
||||||
|
Log.w(getLogTag(), "OpenIM 未登录,无法发送消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
|
||||||
|
val (recvID, groupID) = getReceiverInfo()
|
||||||
|
|
||||||
|
OpenIMClient.getInstance().messageManager.sendMessage(
|
||||||
|
object : OnMsgSendCallback {
|
||||||
|
override fun onProgress(progress: Long) {
|
||||||
|
// 发送进度
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.e(getLogTag(), "发送消息失败: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: Message?) {
|
||||||
|
Log.d(getLogTag(), "发送消息成功")
|
||||||
|
onMessageSentSuccess(message, data)
|
||||||
|
data?.let { sentMessage ->
|
||||||
|
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textMessage,
|
||||||
|
recvID,
|
||||||
|
groupID,
|
||||||
|
OfflinePushInfo()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送图片消息
|
||||||
|
*/
|
||||||
|
fun sendImageMessage(imageUri: Uri, context: Context) {
|
||||||
|
val tempFile = createTempFile(context, imageUri)
|
||||||
|
val imagePath = tempFile?.path
|
||||||
|
if (imagePath != null) {
|
||||||
|
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
|
||||||
|
val (recvID, groupID) = getReceiverInfo()
|
||||||
|
|
||||||
|
OpenIMClient.getInstance().messageManager.sendMessage(
|
||||||
|
object : OnMsgSendCallback {
|
||||||
|
override fun onProgress(progress: Long) {
|
||||||
|
Log.d(getLogTag(), "发送图片消息进度: $progress")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.e(getLogTag(), "发送图片消息失败: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: Message?) {
|
||||||
|
Log.d(getLogTag(), "发送图片消息成功")
|
||||||
|
data?.let { sentMessage ->
|
||||||
|
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageMessage,
|
||||||
|
recvID,
|
||||||
|
groupID,
|
||||||
|
OfflinePushInfo()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建临时文件
|
||||||
|
*/
|
||||||
|
fun createTempFile(context: Context, uri: Uri): File? {
|
||||||
|
return try {
|
||||||
|
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||||
|
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||||
|
cursor?.use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||||
|
val filePath = it.getString(columnIndex)
|
||||||
|
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||||
|
val mimeType = context.contentResolver.getType(uri)
|
||||||
|
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
|
||||||
|
val tempFile =
|
||||||
|
File.createTempFile("temp_image", ".$extension", context.cacheDir)
|
||||||
|
val outputStream = FileOutputStream(tempFile)
|
||||||
|
|
||||||
|
inputStream?.use { input ->
|
||||||
|
outputStream.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempFile
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取历史消息
|
||||||
|
*/
|
||||||
|
fun fetchHistoryMessage(context: Context) {
|
||||||
|
loadHistoryMessages(context, isLoadMore = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载历史消息的通用方法
|
||||||
|
* @param context 上下文
|
||||||
|
* @param isLoadMore 是否是加载更多(true:追加到现有数据,false:替换现有数据)
|
||||||
|
*/
|
||||||
|
private fun loadHistoryMessages(context: Context, isLoadMore: Boolean) {
|
||||||
|
if (conversationID.isEmpty()) {
|
||||||
|
Log.w(getLogTag(), "conversationID为空,无法${if (isLoadMore) "加载更多" else "获取"}历史消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
isLoading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
||||||
|
object : OnBase<AdvancedMessage> {
|
||||||
|
override fun onSuccess(data: AdvancedMessage?) {
|
||||||
|
val messages = data?.messageList ?: emptyList()
|
||||||
|
val newChatItems = messages.mapNotNull {
|
||||||
|
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))
|
||||||
|
}.reversed() // 反转顺序,使最新消息在前面
|
||||||
|
|
||||||
|
// 根据是否是加载更多来决定数据处理方式
|
||||||
|
chatData = if (isLoadMore) {
|
||||||
|
chatData + newChatItems // 追加到现有数据
|
||||||
|
} else {
|
||||||
|
newChatItems // 替换现有数据
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.size < fetchHistorySize) {
|
||||||
|
hasMore = false
|
||||||
|
}
|
||||||
|
lastMessage = messages.firstOrNull()
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(getLogTag(), "${if (isLoadMore) "加载更多" else "获取"}历史消息成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.e(getLogTag(), "${if (isLoadMore) "加载更多" else "获取"}历史消息失败: $error")
|
||||||
|
if (isLoadMore) {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
conversationID,
|
||||||
|
if (isLoadMore) lastMessage else null, // 首次加载不传lastMessage
|
||||||
|
fetchHistorySize,
|
||||||
|
ViewType.History
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显示的聊天列表
|
||||||
|
*/
|
||||||
|
fun getDisplayChatList(): List<ChatItem> {
|
||||||
|
val list = chatData
|
||||||
|
// 更新每条消息的时间戳显示状态
|
||||||
|
for (item in list) {
|
||||||
|
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,11 +79,12 @@ import com.aiosman.ravenow.R
|
|||||||
import com.aiosman.ravenow.entity.ChatItem
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
import com.aiosman.ravenow.exp.formatChatTime
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
|
||||||
import com.aiosman.ravenow.ui.composables.MenuItem
|
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
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.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import io.openim.android.sdk.enums.MessageType
|
import io.openim.android.sdk.enums.MessageType
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -91,10 +92,10 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatAiScreen(userId: String) {
|
fun ChatAiScreen(userId: String) {
|
||||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalNavController.current.context
|
val context = LocalNavController.current.context
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
val chatBackgroundUrl = AppState.chatBackgroundUrl
|
||||||
var goToNewCount by remember { mutableStateOf(0) }
|
var goToNewCount by remember { mutableStateOf(0) }
|
||||||
val viewModel = viewModel<ChatAiViewModel>(
|
val viewModel = viewModel<ChatAiViewModel>(
|
||||||
key = "ChatAiViewModel_$userId",
|
key = "ChatAiViewModel_$userId",
|
||||||
@@ -158,14 +159,25 @@ fun ChatAiScreen(userId: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (chatBackgroundUrl != null && chatBackgroundUrl.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = chatBackgroundUrl,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "chat_background",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
topBar = {
|
topBar = {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(AppColors.background)
|
.background(Color.Transparent)
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
Row(
|
Row(
|
||||||
@@ -213,49 +225,24 @@ fun ChatAiScreen(userId: String) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(28.dp)
|
.size(28.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
isMenuExpanded = true
|
navController.navigate(NavigationRoute.ChatSetting.route)
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
AppColors.text)
|
AppColors.text)
|
||||||
)
|
)
|
||||||
DropdownMenu(
|
|
||||||
expanded = isMenuExpanded,
|
|
||||||
onDismissRequest = {
|
|
||||||
isMenuExpanded = false
|
|
||||||
},
|
|
||||||
menuItems = listOf(
|
|
||||||
MenuItem(
|
|
||||||
title = if (viewModel.notificationStrategy == "mute") "Unmute" else "Mute",
|
|
||||||
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
|
||||||
) {
|
|
||||||
|
|
||||||
isMenuExpanded = false
|
|
||||||
if (NetworkUtils.isNetworkAvailable(context)) {
|
|
||||||
viewModel.viewModelScope.launch {
|
|
||||||
if (viewModel.notificationStrategy == "mute") {
|
|
||||||
viewModel.updateNotificationStrategy("active")
|
|
||||||
} else {
|
|
||||||
viewModel.updateNotificationStrategy("mute")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
val hasChatBackground = AppState.chatBackgroundUrl != null && AppState.chatBackgroundUrl!!.isNotEmpty()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
) {
|
) {
|
||||||
|
if (!hasChatBackground) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -263,6 +250,7 @@ fun ChatAiScreen(userId: String) {
|
|||||||
.background(
|
.background(
|
||||||
AppColors.decentBackground)
|
AppColors.decentBackground)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
ChatAiInput(
|
ChatAiInput(
|
||||||
onSendImage = {
|
onSendImage = {
|
||||||
@@ -283,7 +271,7 @@ fun ChatAiScreen(userId: String) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(AppColors.decentBackground)
|
.background(Color.Transparent)
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -346,8 +334,7 @@ fun ChatAiScreen(userId: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,7 +558,8 @@ fun ChatAiInput(
|
|||||||
}
|
}
|
||||||
Box( modifier = Modifier
|
Box( modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
|
.background(Color.Transparent)
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 45.dp),){
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -1,57 +1,27 @@
|
|||||||
package com.aiosman.ravenow.ui.chat
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
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.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import com.aiosman.ravenow.ChatState
|
import com.aiosman.ravenow.ChatState
|
||||||
import com.aiosman.ravenow.data.AccountService
|
|
||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
|
||||||
import com.aiosman.ravenow.data.UserService
|
|
||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
|
||||||
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 com.aiosman.ravenow.data.api.SingleChatRequestBody
|
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.entity.ChatItem
|
|
||||||
import com.aiosman.ravenow.entity.ChatNotification
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
import com.aiosman.ravenow.ui.navigateToChatAi
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
// OpenIM SDK 导入
|
|
||||||
import io.openim.android.sdk.OpenIMClient
|
|
||||||
import io.openim.android.sdk.enums.ViewType
|
|
||||||
import io.openim.android.sdk.listener.OnAdvanceMsgListener
|
|
||||||
import io.openim.android.sdk.listener.OnBase
|
|
||||||
import io.openim.android.sdk.listener.OnMsgSendCallback
|
|
||||||
import io.openim.android.sdk.models.*
|
import io.openim.android.sdk.models.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
|
|
||||||
class ChatAiViewModel(
|
class ChatAiViewModel(
|
||||||
val userId: String,
|
val userId: String,
|
||||||
) : ViewModel() {
|
) : BaseChatViewModel() {
|
||||||
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
|
|
||||||
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
|
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
|
|
||||||
val userService: UserService = UserServiceImpl()
|
|
||||||
val accountService: AccountService = AccountServiceImpl()
|
|
||||||
var textMessageListener: OnAdvanceMsgListener? = null
|
|
||||||
var hasMore by mutableStateOf(true)
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
var lastMessage: Message? = null
|
|
||||||
val showTimestampMap = mutableMapOf<String, Boolean>() // Add this map
|
|
||||||
var chatNotification by mutableStateOf<ChatNotification?>(null)
|
var chatNotification by mutableStateOf<ChatNotification?>(null)
|
||||||
var goToNew by mutableStateOf(false)
|
override fun init(context: Context) {
|
||||||
fun init(context: Context) {
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val resp = userService.getUserProfile(userId)
|
val resp = userService.getUserProfile(userId)
|
||||||
@@ -59,150 +29,55 @@ class ChatAiViewModel(
|
|||||||
myProfile = accountService.getMyAccountProfile()
|
myProfile = accountService.getMyAccountProfile()
|
||||||
|
|
||||||
RegistListener(context)
|
RegistListener(context)
|
||||||
|
|
||||||
|
// 获取会话信息,然后加载历史消息
|
||||||
|
getOneConversation {
|
||||||
fetchHistoryMessage(context)
|
fetchHistoryMessage(context)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取通知信息
|
// 获取通知信息
|
||||||
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
|
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
|
||||||
chatNotification = notiStrategy
|
chatNotification = notiStrategy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||||
fun RegistListener(context: Context) {
|
return Triple(userProfile?.trtcUserId ?: userId, ConversationType.SINGLE_CHAT, true)
|
||||||
// 检查 OpenIM 是否已登录
|
|
||||||
if (!com.aiosman.ravenow.AppState.enableChat) {
|
|
||||||
android.util.Log.w("ChatAiViewModel", "OpenIM 未登录,跳过注册消息监听器")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textMessageListener = object : OnAdvanceMsgListener {
|
override fun getLogTag(): String {
|
||||||
override fun onRecvNewMessage(msg: Message?) {
|
return "ChatAiViewModel"
|
||||||
msg?.let { message ->
|
}
|
||||||
|
|
||||||
|
override fun handleNewMessage(message: Message, context: Context): Boolean {
|
||||||
// 只处理当前聊天对象的消息
|
// 只处理当前聊天对象的消息
|
||||||
val currentChatUserId = userProfile?.trtcUserId
|
val currentChatUserId = userProfile?.trtcUserId
|
||||||
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
|
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
|
||||||
|
|
||||||
if (currentChatUserId != null && currentUserId != null) {
|
if (currentChatUserId != null && currentUserId != null) {
|
||||||
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
|
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
|
||||||
if ((message.sendID == currentChatUserId || message.sendID == currentUserId) &&
|
return (message.sendID == currentChatUserId || message.sendID == currentUserId) &&
|
||||||
message.sendID != currentUserId) {
|
message.sendID != currentUserId
|
||||||
val chatItem = ChatItem.convertToChatItem(message, context, avatar = userProfile?.avatar)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
android.util.Log.i("ChatAiViewModel", "收到来自 ${message.sendID} 的消息,更新AI聊天列表")
|
|
||||||
}
|
}
|
||||||
}
|
return false
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun UnRegistListener() {
|
override fun getReceiverInfo(): Pair<String?, String?> {
|
||||||
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null
|
return Pair(userProfile?.trtcUserId, null) // (recvID, groupID)
|
||||||
textMessageListener = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearUnRead() {
|
override fun getMessageAvatar(message: Message): String? {
|
||||||
val conversationID = "single_${userProfile?.trtcUserId}"
|
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
||||||
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
|
myProfile?.avatar
|
||||||
conversationID,
|
} else {
|
||||||
object : OnBase<String> {
|
userProfile?.avatar
|
||||||
override fun onSuccess(data: String?) {
|
|
||||||
Log.i("openim", "clear unread success")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.i("openim", "clear unread failure, code:$code, error:$error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun onLoadMore(context: Context) {
|
|
||||||
if (!hasMore || isLoading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isLoading = true
|
|
||||||
viewModelScope.launch {
|
|
||||||
val conversationID = "single_${userProfile?.trtcUserId!!}"
|
|
||||||
// val options = OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList() .apply {
|
|
||||||
// conversationID = conversationID
|
|
||||||
// count = 20
|
|
||||||
// lastMinSeq = lastMessage?.seq ?: 0
|
|
||||||
// }
|
|
||||||
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
|
||||||
object : OnBase<AdvancedMessage> {
|
|
||||||
override fun onSuccess(data: AdvancedMessage?) {
|
|
||||||
val messages = data?.messageList ?: emptyList()
|
|
||||||
chatData = chatData + messages.map {
|
|
||||||
var avatar = userProfile?.avatar
|
|
||||||
if (it.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
|
||||||
avatar = myProfile?.avatar
|
|
||||||
}
|
|
||||||
ChatItem.convertToChatItem(it, context, avatar)
|
|
||||||
}.filterNotNull()
|
|
||||||
|
|
||||||
if (messages.size < 20) {
|
|
||||||
hasMore = false
|
|
||||||
}
|
|
||||||
lastMessage = messages.lastOrNull()
|
|
||||||
isLoading = false
|
|
||||||
Log.d("ChatAiViewModel", "fetch history message success")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("ChatAiViewModel", "fetch history message error: $error")
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
conversationID,
|
|
||||||
lastMessage,
|
|
||||||
20,
|
|
||||||
ViewType.History
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessage(message: String, context: Context) {
|
override fun onMessageSentSuccess(message: String, sentMessage: Message?) {
|
||||||
// 检查 OpenIM 是否已登录
|
// AI聊天特有的处理逻辑
|
||||||
if (!com.aiosman.ravenow.AppState.enableChat) {
|
|
||||||
android.util.Log.w("ChatAiViewModel", "OpenIM 未登录,无法发送消息")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.sendMessage(
|
|
||||||
object : OnMsgSendCallback {
|
|
||||||
override fun onProgress(progress: Long) {
|
|
||||||
// 发送进度
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("ChatAiViewModel", "send message error: $error")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Message?) {
|
|
||||||
Log.d("ChatAiViewModel", "send message success")
|
|
||||||
sendChatAiMessage(myProfile?.trtcUserId!!, userProfile?.trtcUserId!!, message)
|
sendChatAiMessage(myProfile?.trtcUserId!!, userProfile?.trtcUserId!!, message)
|
||||||
createGroup2ChatAi(userProfile?.trtcUserId!!, "ai_group")
|
createGroup2ChatAi(userProfile?.trtcUserId!!, "ai_group")
|
||||||
data?.let { sentMessage ->
|
|
||||||
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textMessage,
|
|
||||||
userProfile?.trtcUserId!!, // recvID
|
|
||||||
null, // groupID
|
|
||||||
null // offlinePushInfo
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
fun createGroup2ChatAi(
|
fun createGroup2ChatAi(
|
||||||
trtcUserId: String,
|
trtcUserId: String,
|
||||||
@@ -212,104 +87,6 @@ class ChatAiViewModel(
|
|||||||
Log.d("ChatAiViewModel", "OpenIM 不支持会话分组功能")
|
Log.d("ChatAiViewModel", "OpenIM 不支持会话分组功能")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendImageMessage(imageUri: Uri, context: Context) {
|
|
||||||
val tempFile = createTempFile(context, imageUri)
|
|
||||||
val imagePath = tempFile?.path
|
|
||||||
if (imagePath != null) {
|
|
||||||
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.sendMessage(
|
|
||||||
object : OnMsgSendCallback {
|
|
||||||
override fun onProgress(progress: Long) {
|
|
||||||
Log.d("ChatAiViewModel", "send image message progress: $progress")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("ChatAiViewModel", "send image message error: $error")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Message?) {
|
|
||||||
Log.d("ChatAiViewModel", "send image message success")
|
|
||||||
data?.let { sentMessage ->
|
|
||||||
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
imageMessage,
|
|
||||||
userProfile?.trtcUserId!!, // recvID
|
|
||||||
null, // groupID
|
|
||||||
null // offlinePushInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createTempFile(context: Context, uri: Uri): File? {
|
|
||||||
return try {
|
|
||||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
|
||||||
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
|
||||||
cursor?.use {
|
|
||||||
if (it.moveToFirst()) {
|
|
||||||
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
|
||||||
val filePath = it.getString(columnIndex)
|
|
||||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
|
||||||
val mimeType = context.contentResolver.getType(uri)
|
|
||||||
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
|
|
||||||
val tempFile =
|
|
||||||
File.createTempFile("temp_image", ".$extension", context.cacheDir)
|
|
||||||
val outputStream = FileOutputStream(tempFile)
|
|
||||||
|
|
||||||
inputStream?.use { input ->
|
|
||||||
outputStream.use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tempFile
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchHistoryMessage(context: Context) {
|
|
||||||
val conversationID = "single_${userProfile?.trtcUserId!!}"
|
|
||||||
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
|
||||||
object : OnBase<AdvancedMessage> {
|
|
||||||
override fun onSuccess(data: AdvancedMessage?) {
|
|
||||||
val messages = data?.messageList ?: emptyList()
|
|
||||||
chatData = messages.mapNotNull {
|
|
||||||
var avatar = userProfile?.avatar
|
|
||||||
if (it.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
|
||||||
avatar = myProfile?.avatar
|
|
||||||
}
|
|
||||||
ChatItem.convertToChatItem(it, context, avatar)
|
|
||||||
}
|
|
||||||
if (messages.size < 20) {
|
|
||||||
hasMore = false
|
|
||||||
}
|
|
||||||
lastMessage = messages.lastOrNull()
|
|
||||||
Log.d("ChatAiViewModel", "fetch history message success")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("ChatAiViewModel", "fetch history message error: $error")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
conversationID,
|
|
||||||
lastMessage,
|
|
||||||
20,
|
|
||||||
ViewType.History
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fun sendChatAiMessage(
|
fun sendChatAiMessage(
|
||||||
fromTrtcUserId: String,
|
fromTrtcUserId: String,
|
||||||
toTrtcUserId: String,
|
toTrtcUserId: String,
|
||||||
@@ -318,16 +95,6 @@ class ChatAiViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(fromTrtcUserId = fromTrtcUserId,toTrtcUserId = toTrtcUserId,message = message))
|
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(fromTrtcUserId = fromTrtcUserId,toTrtcUserId = toTrtcUserId,message = message))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDisplayChatList(): List<ChatItem> {
|
|
||||||
val list = chatData
|
|
||||||
// Update showTimestamp for each message
|
|
||||||
for (item in list) {
|
|
||||||
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateNotificationStrategy(strategy: String) {
|
suspend fun updateNotificationStrategy(strategy: String) {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.platform.SoftwareKeyboardController
|
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -74,10 +75,12 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.entity.ChatItem
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
import com.aiosman.ravenow.exp.formatChatTime
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
|
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.composables.DropdownMenu
|
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||||
import com.aiosman.ravenow.ui.composables.MenuItem
|
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||||
@@ -91,7 +94,6 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(userId: String) {
|
fun ChatScreen(userId: String) {
|
||||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalNavController.current.context
|
val context = LocalNavController.current.context
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
@@ -158,14 +160,37 @@ fun ChatScreen(userId: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 背景图层
|
||||||
|
val bgUrl = AppState.chatBackgroundUrl
|
||||||
|
if (bgUrl != null) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = bgUrl,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "chat_background",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 无背景时使用主题背景色
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
topBar = {
|
topBar = {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(AppColors.background)
|
.background(Color.Transparent)
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
Row(
|
Row(
|
||||||
@@ -214,39 +239,12 @@ fun ChatScreen(userId: String) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(28.dp)
|
.size(28.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
isMenuExpanded = true
|
navController.navigate(NavigationRoute.ChatSetting.route)
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
AppColors.text)
|
AppColors.text)
|
||||||
)
|
)
|
||||||
DropdownMenu(
|
|
||||||
expanded = isMenuExpanded,
|
|
||||||
onDismissRequest = {
|
|
||||||
isMenuExpanded = false
|
|
||||||
},
|
|
||||||
menuItems = listOf(
|
|
||||||
MenuItem(
|
|
||||||
title = if (viewModel.notificationStrategy == "mute") "取消静音" else "静音",
|
|
||||||
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
|
||||||
) {
|
|
||||||
|
|
||||||
isMenuExpanded = false
|
|
||||||
if (NetworkUtils.isNetworkAvailable(context)) {
|
|
||||||
viewModel.viewModelScope.launch {
|
|
||||||
if (viewModel.notificationStrategy == "mute") {
|
|
||||||
viewModel.updateNotificationStrategy("active")
|
|
||||||
} else {
|
|
||||||
viewModel.updateNotificationStrategy("mute")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,7 +281,7 @@ fun ChatScreen(userId: String) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(AppColors.background)
|
.background(Color.Transparent)
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -347,6 +345,7 @@ fun ChatScreen(userId: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,7 +571,7 @@ fun ChatInput(
|
|||||||
}
|
}
|
||||||
Box( modifier = Modifier
|
Box( modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
|
.padding(start = 16.dp, end = 16.dp, bottom = 45.dp),){
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatSettingScreen() {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
var showThemeSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(appColors.secondaryBackground)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Box(modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)) {
|
||||||
|
NoticeScreenHeader(title = stringResource(R.string.chat_settings), moreIcon = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
|
SettingCard(
|
||||||
|
title = stringResource(R.string.chat_theme_settings),
|
||||||
|
onClick = { showThemeSheet = true }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
SettingCard(
|
||||||
|
title = stringResource(R.string.report),
|
||||||
|
onClick = { /* TODO: 跳转举报 */ }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showThemeSheet) {
|
||||||
|
ThemePickerSheet(onClose = { showThemeSheet = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingCard(title: String, onClick: () -> Unit) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(appColors.background)
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(horizontal = 12.dp, vertical = 14.dp)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W500,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rave_now_nav_right),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = appColors.text,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ThemePickerSheet(onClose: () -> Unit) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
|
val sheetHeight = screenHeight * 0.9f
|
||||||
|
var previewUrl by remember { mutableStateOf<String?>(null) }
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onClose,
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = appColors.secondaryBackground,
|
||||||
|
dragHandle = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(sheetHeight),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp, bottom = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.custom_background),
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
modifier = Modifier.weight(1f).padding(start = 90.dp),
|
||||||
|
)
|
||||||
|
IconButton(onClick = onClose) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_close),
|
||||||
|
contentDescription = "close",
|
||||||
|
tint = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从相册选择
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(appColors.background)
|
||||||
|
.clickable { /* TODO: 打开相册选择 */ }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.select_from_gallery), color = appColors.text, fontSize = 15.sp, modifier = Modifier.weight(1f))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.group_info_edit),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = appColors.text,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(text = stringResource(R.string.featured_backgrounds), color = appColors.text, fontSize = 12.sp)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
val presets = remember {
|
||||||
|
listOf(
|
||||||
|
"https://picsum.photos/seed/ai1/400/600",
|
||||||
|
"https://picsum.photos/seed/ai2/400/600",
|
||||||
|
"https://picsum.photos/seed/ai3/400/600",
|
||||||
|
"https://picsum.photos/seed/ai4/400/600",
|
||||||
|
"https://picsum.photos/seed/ai5/400/600",
|
||||||
|
"https://picsum.photos/seed/ai6/400/600",
|
||||||
|
"https://picsum.photos/seed/ai7/400/600",
|
||||||
|
"https://picsum.photos/seed/ai8/400/600",
|
||||||
|
"https://picsum.photos/seed/ai9/400/600",
|
||||||
|
"https://picsum.photos/seed/ai10/400/600",
|
||||||
|
"https://picsum.photos/seed/ai11/400/600",
|
||||||
|
"https://picsum.photos/seed/ai12/400/600",
|
||||||
|
"https://picsum.photos/seed/ai13/400/600",
|
||||||
|
"https://picsum.photos/seed/ai14/400/600",
|
||||||
|
"https://picsum.photos/seed/ai15/400/600",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(presets) { url ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { previewUrl = url }
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(appColors.decentBackground)
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = url,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(3f / 4f),
|
||||||
|
contentDescription = "preset",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = "Heart Drive",
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览自定义背景弹窗
|
||||||
|
if (previewUrl != null) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { previewUrl = null },
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||||
|
containerColor = appColors.secondaryBackground,
|
||||||
|
dragHandle = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(sheetHeight)
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = previewUrl!!,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "preview_bg",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.previewing_custom_background), color = Color.White, fontSize = 15.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(8.dp)) {
|
||||||
|
Row {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.each_theme_unique_experience), color = Color.Black, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.select_apply_to_use_theme), color = Color.Black, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.background(Color(0xFF7C4DFF))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.tap_cancel_to_preview_other_themes), color = Color.White, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
// 底部按钮
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(bottom = 60.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.clickable { previewUrl = null }
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.cancel), color = Color.Black, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFFEE2A33),
|
||||||
|
Color(0xFFD80264),
|
||||||
|
Color(0xFF664C92)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
previewUrl?.let { url ->
|
||||||
|
com.aiosman.ravenow.AppStore.saveChatBackgroundUrl(url)
|
||||||
|
previewUrl = null
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.moment_ai_apply), color = Color.White, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,53 +1,24 @@
|
|||||||
package com.aiosman.ravenow.ui.chat
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.util.Log
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
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.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.aiosman.ravenow.ChatState
|
import com.aiosman.ravenow.ChatState
|
||||||
import com.aiosman.ravenow.data.AccountService
|
|
||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
|
||||||
import com.aiosman.ravenow.data.UserService
|
|
||||||
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.ChatNotification
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
// OpenIM SDK 导入
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
import io.openim.android.sdk.OpenIMClient
|
import io.openim.android.sdk.models.Message
|
||||||
import io.openim.android.sdk.enums.MessageType
|
|
||||||
import io.openim.android.sdk.enums.ViewType
|
|
||||||
import io.openim.android.sdk.listener.OnAdvanceMsgListener
|
|
||||||
import io.openim.android.sdk.listener.OnBase
|
|
||||||
import io.openim.android.sdk.listener.OnMsgSendCallback
|
|
||||||
import io.openim.android.sdk.models.*
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
|
|
||||||
class ChatViewModel(
|
class ChatViewModel(
|
||||||
val userId: String,
|
val userId: String,
|
||||||
) : ViewModel() {
|
) : BaseChatViewModel() {
|
||||||
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
|
|
||||||
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
|
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
|
|
||||||
val userService: UserService = UserServiceImpl()
|
|
||||||
val accountService: AccountService = AccountServiceImpl()
|
|
||||||
var textMessageListener: OnAdvanceMsgListener? = null
|
|
||||||
var hasMore by mutableStateOf(true)
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
var lastMessage: Message? = null
|
|
||||||
val showTimestampMap = mutableMapOf<String, Boolean>() // Add this map
|
|
||||||
var chatNotification by mutableStateOf<ChatNotification?>(null)
|
var chatNotification by mutableStateOf<ChatNotification?>(null)
|
||||||
var goToNew by mutableStateOf(false)
|
override fun init(context: Context) {
|
||||||
fun init(context: Context) {
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val resp = userService.getUserProfile(userId)
|
val resp = userService.getUserProfile(userId)
|
||||||
@@ -55,252 +26,50 @@ class ChatViewModel(
|
|||||||
myProfile = accountService.getMyAccountProfile()
|
myProfile = accountService.getMyAccountProfile()
|
||||||
|
|
||||||
RegistListener(context)
|
RegistListener(context)
|
||||||
|
|
||||||
|
// 获取会话信息,然后加载历史消息
|
||||||
|
getOneConversation {
|
||||||
fetchHistoryMessage(context)
|
fetchHistoryMessage(context)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取通知信息
|
// 获取通知信息
|
||||||
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
|
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
|
||||||
chatNotification = notiStrategy
|
chatNotification = notiStrategy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||||
fun RegistListener(context: Context) {
|
return Triple(userProfile?.trtcUserId ?: userId, ConversationType.SINGLE_CHAT, true)
|
||||||
// 检查 OpenIM 是否已登录
|
|
||||||
if (!com.aiosman.ravenow.AppState.enableChat) {
|
|
||||||
android.util.Log.w("ChatViewModel", "OpenIM 未登录,跳过注册消息监听器")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textMessageListener = object : OnAdvanceMsgListener {
|
override fun getLogTag(): String {
|
||||||
override fun onRecvNewMessage(msg: Message?) {
|
return "ChatViewModel"
|
||||||
msg?.let { message ->
|
}
|
||||||
|
|
||||||
|
override fun handleNewMessage(message: Message, context: Context): Boolean {
|
||||||
// 只处理当前聊天对象的消息
|
// 只处理当前聊天对象的消息
|
||||||
val currentChatUserId = userProfile?.trtcUserId
|
val currentChatUserId = userProfile?.trtcUserId
|
||||||
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
|
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
|
||||||
|
|
||||||
if (currentChatUserId != null && currentUserId != null) {
|
if (currentChatUserId != null && currentUserId != null) {
|
||||||
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
|
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
|
||||||
if ((message.sendID == currentChatUserId || message.sendID == currentUserId) &&
|
return (message.sendID == currentChatUserId || message.sendID == currentUserId) &&
|
||||||
message.sendID != currentUserId) {
|
message.sendID != currentUserId
|
||||||
val chatItem = ChatItem.convertToChatItem(message, context, avatar = userProfile?.avatar)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
android.util.Log.i("ChatViewModel", "收到来自 ${message.sendID} 的消息,更新聊天列表")
|
|
||||||
}
|
}
|
||||||
}
|
return false
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun UnRegistListener() {
|
override fun getReceiverInfo(): Pair<String?, String?> {
|
||||||
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null
|
return Pair(userProfile?.trtcUserId, null) // (recvID, groupID)
|
||||||
textMessageListener = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearUnRead() {
|
override fun getMessageAvatar(message: Message): String? {
|
||||||
val conversationID = "single_${userProfile?.trtcUserId}"
|
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
||||||
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
|
myProfile?.avatar
|
||||||
conversationID,
|
|
||||||
object : OnBase<String> {
|
|
||||||
override fun onSuccess(data: String?) {
|
|
||||||
Log.i("openim", "clear unread success")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.i("openim", "clear unread failure, code:$code, error:$error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun onLoadMore(context: Context) {
|
|
||||||
if (!hasMore || isLoading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isLoading = true
|
|
||||||
viewModelScope.launch {
|
|
||||||
val conversationID = "single_${userProfile?.trtcUserId!!}"
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
|
||||||
object : OnBase<AdvancedMessage> {
|
|
||||||
override fun onSuccess(data: AdvancedMessage?) {
|
|
||||||
val messages = data?.messageList ?: emptyList()
|
|
||||||
chatData = chatData + messages.map {
|
|
||||||
var avatar = userProfile?.avatar
|
|
||||||
if (it.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
|
||||||
avatar = myProfile?.avatar
|
|
||||||
}
|
|
||||||
ChatItem.convertToChatItem(it, context, avatar)
|
|
||||||
}.filterNotNull()
|
|
||||||
|
|
||||||
if (messages.size < 20) {
|
|
||||||
hasMore = false
|
|
||||||
}
|
|
||||||
lastMessage = messages.lastOrNull()
|
|
||||||
isLoading = false
|
|
||||||
Log.d("ChatViewModel", "fetch history message success")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("ChatViewModel", "fetch history message error: $error")
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
conversationID,
|
|
||||||
lastMessage,
|
|
||||||
20,
|
|
||||||
ViewType.History
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessage(message: String, context: Context) {
|
|
||||||
// 检查 OpenIM 是否已登录
|
|
||||||
if (!com.aiosman.ravenow.AppState.enableChat) {
|
|
||||||
android.util.Log.w("ChatViewModel", "OpenIM 未登录,无法发送消息")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.sendMessage(
|
|
||||||
object : OnMsgSendCallback {
|
|
||||||
override fun onProgress(progress: Long) {
|
|
||||||
// 发送进度
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("ChatViewModel", "send message error: $error")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Message?) {
|
|
||||||
Log.d("ChatViewModel", "send message success")
|
|
||||||
data?.let { sentMessage ->
|
|
||||||
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textMessage,
|
|
||||||
userProfile?.trtcUserId!!, // recvID
|
|
||||||
null, // groupID
|
|
||||||
null // offlinePushInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendImageMessage(imageUri: Uri, context: Context) {
|
|
||||||
val tempFile = createTempFile(context, imageUri)
|
|
||||||
val imagePath = tempFile?.path
|
|
||||||
if (imagePath != null) {
|
|
||||||
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.sendMessage(
|
|
||||||
object : OnMsgSendCallback {
|
|
||||||
override fun onProgress(progress: Long) {
|
|
||||||
Log.d("ChatViewModel", "send image message progress: $progress")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("ChatViewModel", "send image message error: $error")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Message?) {
|
|
||||||
Log.d("ChatViewModel", "send image message success")
|
|
||||||
data?.let { sentMessage ->
|
|
||||||
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
imageMessage,
|
|
||||||
userProfile?.trtcUserId!!, // recvID
|
|
||||||
null, // groupID
|
|
||||||
null // offlinePushInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createTempFile(context: Context, uri: Uri): File? {
|
|
||||||
return try {
|
|
||||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
|
||||||
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
|
||||||
cursor?.use {
|
|
||||||
if (it.moveToFirst()) {
|
|
||||||
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
|
||||||
val filePath = it.getString(columnIndex)
|
|
||||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
|
||||||
val mimeType = context.contentResolver.getType(uri)
|
|
||||||
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
|
|
||||||
val tempFile =
|
|
||||||
File.createTempFile("temp_image", ".$extension", context.cacheDir)
|
|
||||||
val outputStream = FileOutputStream(tempFile)
|
|
||||||
|
|
||||||
inputStream?.use { input ->
|
|
||||||
outputStream.use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tempFile
|
|
||||||
} else {
|
} else {
|
||||||
null
|
userProfile?.avatar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchHistoryMessage(context: Context) {
|
|
||||||
val conversationID = "single_${userProfile?.trtcUserId!!}"
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
|
||||||
object : OnBase<AdvancedMessage> {
|
|
||||||
override fun onSuccess(data: AdvancedMessage?) {
|
|
||||||
val messages = data?.messageList ?: emptyList()
|
|
||||||
chatData = messages.mapNotNull {
|
|
||||||
var avatar = userProfile?.avatar
|
|
||||||
if (it.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
|
||||||
avatar = myProfile?.avatar
|
|
||||||
}
|
|
||||||
ChatItem.convertToChatItem(it, context, avatar)
|
|
||||||
}
|
|
||||||
if (messages.size < 20) {
|
|
||||||
hasMore = false
|
|
||||||
}
|
|
||||||
lastMessage = messages.lastOrNull()
|
|
||||||
Log.d("ChatViewModel", "fetch history message success")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("ChatViewModel", "fetch history message error: $error")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
conversationID,
|
|
||||||
null,
|
|
||||||
20,
|
|
||||||
ViewType.History
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDisplayChatList(): List<ChatItem> {
|
|
||||||
val list = chatData
|
|
||||||
// Update showTimestamp for each message
|
|
||||||
for (item in list) {
|
|
||||||
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateNotificationStrategy(strategy: String) {
|
suspend fun updateNotificationStrategy(strategy: String) {
|
||||||
userProfile?.let {
|
userProfile?.let {
|
||||||
|
|||||||
@@ -1,55 +1,23 @@
|
|||||||
package com.aiosman.ravenow.ui.chat
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
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.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.aiosman.ravenow.ChatState
|
|
||||||
import com.aiosman.ravenow.data.AccountService
|
|
||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
|
||||||
import com.aiosman.ravenow.data.UserService
|
|
||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.ravenow.data.api.GroupChatRequestBody
|
|
||||||
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
|
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
|
||||||
import com.aiosman.ravenow.data.api.SingleChatRequestBody
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
|
||||||
import com.aiosman.ravenow.entity.ChatItem
|
|
||||||
import com.aiosman.ravenow.entity.ChatNotification
|
|
||||||
// OpenIM SDK 导入
|
|
||||||
import io.openim.android.sdk.OpenIMClient
|
|
||||||
import io.openim.android.sdk.enums.ViewType
|
|
||||||
import io.openim.android.sdk.listener.OnAdvanceMsgListener
|
|
||||||
import io.openim.android.sdk.listener.OnBase
|
|
||||||
import io.openim.android.sdk.listener.OnMsgSendCallback
|
|
||||||
import io.openim.android.sdk.models.*
|
import io.openim.android.sdk.models.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class GroupChatViewModel(
|
class GroupChatViewModel(
|
||||||
val groupId: String,
|
val groupId: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
) : ViewModel() {
|
) : BaseChatViewModel() {
|
||||||
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
|
|
||||||
var groupInfo by mutableStateOf<GroupInfo?>(null)
|
var groupInfo by mutableStateOf<GroupInfo?>(null)
|
||||||
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
|
|
||||||
val userService: UserService = UserServiceImpl()
|
|
||||||
val accountService: AccountService = AccountServiceImpl()
|
|
||||||
var textMessageListener: OnAdvanceMsgListener? = null
|
|
||||||
var hasMore by mutableStateOf(true)
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
var lastMessage: Message? = null
|
|
||||||
val showTimestampMap = mutableMapOf<String, Boolean>()
|
|
||||||
var goToNew by mutableStateOf(false)
|
|
||||||
|
|
||||||
// 群聊特有属性
|
// 群聊特有属性
|
||||||
var memberCount by mutableStateOf(0)
|
var memberCount by mutableStateOf(0)
|
||||||
@@ -64,13 +32,17 @@ class GroupChatViewModel(
|
|||||||
val ownerId: String
|
val ownerId: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun init(context: Context) {
|
override fun init(context: Context) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
getGroupInfo()
|
getGroupInfo()
|
||||||
myProfile = accountService.getMyAccountProfile()
|
myProfile = accountService.getMyAccountProfile()
|
||||||
RegistListener(context)
|
RegistListener(context)
|
||||||
|
|
||||||
|
// 获取会话信息,然后加载历史消息
|
||||||
|
getOneConversation {
|
||||||
fetchHistoryMessage(context)
|
fetchHistoryMessage(context)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("GroupChatViewModel", "初始化失败: ${e.message}")
|
Log.e("GroupChatViewModel", "初始化失败: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -91,120 +63,36 @@ class GroupChatViewModel(
|
|||||||
memberCount = groupInfo?.memberCount ?: 0
|
memberCount = groupInfo?.memberCount ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RegistListener(context: Context) {
|
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||||
// 检查 OpenIM 是否已登录
|
// 根据群组类型决定ConversationType,这里假设是普通群聊
|
||||||
if (!com.aiosman.ravenow.AppState.enableChat) {
|
return Triple(groupId, ConversationType.GROUP_CHAT, false)
|
||||||
android.util.Log.w("GroupChatViewModel", "OpenIM 未登录,跳过注册消息监听器")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textMessageListener = object : OnAdvanceMsgListener {
|
override fun getLogTag(): String {
|
||||||
override fun onRecvNewMessage(msg: Message?) {
|
return "GroupChatViewModel"
|
||||||
msg?.let {
|
}
|
||||||
|
|
||||||
|
override fun handleNewMessage(message: Message, context: Context): Boolean {
|
||||||
// 检查是否是当前群聊的消息
|
// 检查是否是当前群聊的消息
|
||||||
if (it.groupID == groupId) {
|
return message.groupID == groupId
|
||||||
val chatItem = ChatItem.convertToChatItem(msg, context, avatar = null)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun UnRegistListener() {
|
override fun getReceiverInfo(): Pair<String?, String?> {
|
||||||
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null
|
return Pair(null, groupId) // (recvID, groupID)
|
||||||
textMessageListener = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearUnRead() {
|
override fun getMessageAvatar(message: Message): String? {
|
||||||
val conversationID = "group_${groupId}"
|
// 群聊中,如果是自己发送的消息显示自己的头像,否则为null(由ChatItem处理)
|
||||||
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
|
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
||||||
conversationID,
|
myProfile?.avatar
|
||||||
object : OnBase<String> {
|
} else {
|
||||||
override fun onSuccess(data: String?) {
|
null
|
||||||
Log.i("openim", "清除群聊未读消息成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.i("openim", "清除群聊未读消息失败, code:$code, error:$error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onLoadMore(context: Context) {
|
|
||||||
if (!hasMore || isLoading) return
|
|
||||||
isLoading = true
|
|
||||||
viewModelScope.launch {
|
|
||||||
val conversationID = "group_${groupId}"
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
|
||||||
object : OnBase<AdvancedMessage> {
|
|
||||||
override fun onSuccess(data: AdvancedMessage?) {
|
|
||||||
val messages = data?.messageList ?: emptyList()
|
|
||||||
chatData = chatData + messages.map {
|
|
||||||
ChatItem.convertToChatItem(it, context, avatar = null)
|
|
||||||
}.filterNotNull()
|
|
||||||
|
|
||||||
if (messages.size < 20) {
|
|
||||||
hasMore = false
|
|
||||||
}
|
|
||||||
lastMessage = messages.lastOrNull()
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("GroupChatViewModel", "获取群聊历史消息失败: $error")
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
conversationID,
|
|
||||||
lastMessage,
|
|
||||||
20,
|
|
||||||
ViewType.History
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessage(message: String, context: Context) {
|
override fun onMessageSentSuccess(message: String, sentMessage: Message?) {
|
||||||
// 检查 OpenIM 是否已登录
|
// 群聊特有的处理逻辑
|
||||||
if (!com.aiosman.ravenow.AppState.enableChat) {
|
|
||||||
android.util.Log.w("GroupChatViewModel", "OpenIM 未登录,无法发送消息")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.sendMessage(
|
|
||||||
object : OnMsgSendCallback {
|
|
||||||
override fun onProgress(progress: Long) {
|
|
||||||
// 发送进度
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("GroupChatViewModel", "发送群聊消息失败: $error")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Message?) {
|
|
||||||
sendChatAiMessage(message = message, trtcGroupId = groupId)
|
sendChatAiMessage(message = message, trtcGroupId = groupId)
|
||||||
data?.let { sentMessage ->
|
|
||||||
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textMessage,
|
|
||||||
null, // recvID (群聊为 null)
|
|
||||||
groupId, // groupID
|
|
||||||
null // offlinePushInfo
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -216,107 +104,6 @@ class GroupChatViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(trtcGroupId = trtcGroupId,message = message))
|
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(trtcGroupId = trtcGroupId,message = message))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendImageMessage(imageUri: Uri, context: Context) {
|
|
||||||
val tempFile = createTempFile(context, imageUri)
|
|
||||||
val imagePath = tempFile?.path
|
|
||||||
if (imagePath != null) {
|
|
||||||
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.sendMessage(
|
|
||||||
object : OnMsgSendCallback {
|
|
||||||
override fun onProgress(progress: Long) {
|
|
||||||
Log.d("GroupChatViewModel", "发送群聊图片消息进度: $progress")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("GroupChatViewModel", "发送群聊图片消息失败: $error")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Message?) {
|
|
||||||
data?.let { sentMessage ->
|
|
||||||
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
|
||||||
chatItem?.let {
|
|
||||||
chatData = listOf(it) + chatData
|
|
||||||
goToNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
imageMessage,
|
|
||||||
null, // recvID (群聊为 null)
|
|
||||||
groupId, // groupID
|
|
||||||
null // offlinePushInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createTempFile(context: Context, uri: Uri): File? {
|
|
||||||
return try {
|
|
||||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
|
||||||
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
|
||||||
cursor?.use {
|
|
||||||
if (it.moveToFirst()) {
|
|
||||||
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
|
||||||
val filePath = it.getString(columnIndex)
|
|
||||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
|
||||||
val mimeType = context.contentResolver.getType(uri)
|
|
||||||
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
|
|
||||||
val tempFile =
|
|
||||||
File.createTempFile("temp_image", ".$extension", context.cacheDir)
|
|
||||||
val outputStream = FileOutputStream(tempFile)
|
|
||||||
|
|
||||||
inputStream?.use { input ->
|
|
||||||
outputStream.use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tempFile
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchHistoryMessage(context: Context) {
|
|
||||||
val conversationID = "group_${groupId}"
|
|
||||||
|
|
||||||
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
|
||||||
object : OnBase<AdvancedMessage> {
|
|
||||||
override fun onSuccess(data: AdvancedMessage?) {
|
|
||||||
val messages = data?.messageList ?: emptyList()
|
|
||||||
chatData = messages.mapNotNull {
|
|
||||||
ChatItem.convertToChatItem(it, context, avatar = null)
|
|
||||||
}
|
|
||||||
if (messages.size < 20) {
|
|
||||||
hasMore = false
|
|
||||||
}
|
|
||||||
lastMessage = messages.lastOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
Log.e("GroupChatViewModel", "获取群聊历史消息失败: $error")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
conversationID,
|
|
||||||
null,
|
|
||||||
20,
|
|
||||||
ViewType.History
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDisplayChatList(): List<ChatItem> {
|
|
||||||
val list = chatData
|
|
||||||
for (item in list) {
|
|
||||||
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ 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.ui.network.ReloadButton
|
||||||
@Composable
|
@Composable
|
||||||
fun CommentNoticeScreen() {
|
fun CommentNoticeScreen() {
|
||||||
val viewModel = viewModel<CommentNoticeListViewModel>(
|
val viewModel = viewModel<CommentNoticeListViewModel>(
|
||||||
@@ -71,14 +72,47 @@ fun CommentNoticeScreen() {
|
|||||||
modifier = Modifier.fillMaxSize().background(color = AppColors.background)
|
modifier = Modifier.fillMaxSize().background(color = AppColors.background)
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
|
if (!isNetworkAvailable) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.padding(16.dp)
|
.padding(top = 149.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(stringResource(R.string.comment), moreIcon = false)
|
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)
|
||||||
}
|
}
|
||||||
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
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -92,7 +126,7 @@ fun CommentNoticeScreen() {
|
|||||||
androidx.compose.foundation.Image(
|
androidx.compose.foundation.Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id =if(AppState.darkMode) R.mipmap.qst_pl_qs_as_img
|
id =if(AppState.darkMode) R.mipmap.qst_pl_qs_as_img
|
||||||
else R.mipmap.qst_pl_qs_img),
|
else R.mipmap.invalid_name_11),
|
||||||
contentDescription = "No Comment",
|
contentDescription = "No Comment",
|
||||||
modifier = Modifier.size(181.dp)
|
modifier = Modifier.size(181.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
|
|||||||
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.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
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
|
||||||
@@ -32,6 +33,7 @@ fun ActionButton(
|
|||||||
text: String,
|
text: String,
|
||||||
color: Color? = null,
|
color: Color? = null,
|
||||||
backgroundColor: Color? = null,
|
backgroundColor: Color? = null,
|
||||||
|
backgroundBrush: Brush? = null,
|
||||||
leading: @Composable (() -> Unit)? = null,
|
leading: @Composable (() -> Unit)? = null,
|
||||||
expandText: Boolean = false,
|
expandText: Boolean = false,
|
||||||
contentPadding: PaddingValues = PaddingValues(vertical = 16.dp),
|
contentPadding: PaddingValues = PaddingValues(vertical = 16.dp),
|
||||||
@@ -65,7 +67,11 @@ fun ActionButton(
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(RoundedCornerShape(roundCorner.dp))
|
.clip(RoundedCornerShape(roundCorner.dp))
|
||||||
.background(animatedBackgroundColor)
|
.background(
|
||||||
|
brush = backgroundBrush ?: Brush.linearGradient(
|
||||||
|
colors = listOf(animatedBackgroundColor, animatedBackgroundColor)
|
||||||
|
)
|
||||||
|
)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
if (enabled && !isLoading) {
|
if (enabled && !isLoading) {
|
||||||
click()
|
click()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.aiosman.ravenow.ui.composables
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.composables.toolbar.CollapsingToolbarScaffoldScopeInstance.align
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AgentCreatedSuccessIndicator() {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
if (AppState.agentCreatedSuccess) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 70.dp),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(150.dp)
|
||||||
|
.height(40.dp)
|
||||||
|
.background(appColors.text.copy(alpha = 0.5f), shape = RoundedCornerShape(15.dp)),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.bars_x_buttons_home_n_copy_2),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(7.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.create_success),
|
||||||
|
color = appColors.background,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(3000)
|
||||||
|
AppState.agentCreatedSuccess = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -56,15 +56,14 @@ fun AnimatedFavouriteIcon(
|
|||||||
}) {
|
}) {
|
||||||
Image(
|
Image(
|
||||||
painter = if (isFavourite) {
|
painter = if (isFavourite) {
|
||||||
painterResource(id = R.drawable.rider_pro_favourited)
|
painterResource(id = R.mipmap.icon_variant_2)
|
||||||
} else {
|
} else {
|
||||||
painterResource(id = R.drawable.rider_pro_favourite)
|
painterResource(id = R.mipmap.icon_collect)
|
||||||
},
|
},
|
||||||
contentDescription = "Favourite",
|
contentDescription = "Favourite",
|
||||||
modifier = modifier.graphicsLayer {
|
modifier = modifier.graphicsLayer {
|
||||||
rotationZ = animatableRotation.value
|
rotationZ = animatableRotation.value
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,8 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|||||||
@Composable
|
@Composable
|
||||||
fun EditCommentBottomModal(
|
fun EditCommentBottomModal(
|
||||||
replyComment: CommentEntity? = null,
|
replyComment: CommentEntity? = null,
|
||||||
onSend: (String) -> Unit = {}
|
autoFocus: Boolean = false,
|
||||||
|
onSend: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
@@ -59,9 +60,11 @@ fun EditCommentBottomModal(
|
|||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(autoFocus) {
|
||||||
|
if (autoFocus) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -69,72 +72,22 @@ fun EditCommentBottomModal(
|
|||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
if (replyComment == null) "Comment" else "Reply",
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
if (replyComment != null) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context,
|
|
||||||
replyComment.avatar,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentDescription = "Avatar",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
replyComment.name,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
replyComment.comment,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 32.dp),
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.Top
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(Color.White)
|
.background(Color.Gray.copy(alpha = 0.1f))
|
||||||
.border(1.dp, Color.Black, RoundedCornerShape(20.dp))
|
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Top
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = text,
|
value = text,
|
||||||
@@ -149,30 +102,39 @@ fun EditCommentBottomModal(
|
|||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
fontWeight = FontWeight.Normal
|
fontWeight = FontWeight.Normal
|
||||||
),
|
),
|
||||||
minLines = 1
|
decorationBox = { innerTextField ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
innerTextField()
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = if (replyComment == null) "快来互动吧..." else "回复@${replyComment.name}",
|
||||||
|
color = AppColors.text.copy(alpha = 0.3f), // 30%透明度
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
}
|
||||||
Crossfade(
|
}
|
||||||
targetState = text.isNotEmpty(), animationSpec = tween(500),
|
}
|
||||||
label = ""
|
)
|
||||||
) { isNotEmpty ->
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.mipmap.rider_pro_moment_post),
|
painter = painterResource(id = R.mipmap.btn),
|
||||||
contentDescription = "Send",
|
contentDescription = "Send",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(25.dp)
|
.size(40.dp)
|
||||||
.align(Alignment.Top)
|
.padding(top = 13.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
onSend(text)
|
onSend(text)
|
||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tint = if (isNotEmpty) AppColors.main else AppColors.nonActive
|
tint = Color.Unspecified
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(navBarHeight))
|
Spacer(modifier = Modifier.height(navBarHeight))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,16 +92,22 @@ fun MomentCard(
|
|||||||
showFollowButton = showFollowButton
|
showFollowButton = showFollowButton
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val lastClickTime = remember { mutableStateOf(0L) }
|
||||||
|
val clickDelay = 500L
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastClickTime.value > clickDelay) {
|
||||||
|
lastClickTime.value = currentTime
|
||||||
navController.navigateToPost(
|
navController.navigateToPost(
|
||||||
momentEntity.id,
|
momentEntity.id,
|
||||||
highlightCommentId = 0,
|
highlightCommentId = 0,
|
||||||
initImagePagerIndex = imageIndex
|
initImagePagerIndex = imageIndex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
MomentContentGroup(
|
MomentContentGroup(
|
||||||
momentEntity = momentEntity,
|
momentEntity = momentEntity,
|
||||||
@@ -213,7 +219,6 @@ fun MomentPostLocation(location: String) {
|
|||||||
text = location,
|
text = location,
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +243,8 @@ fun MomentTopRowGroup(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val lastClickTime = remember { mutableStateOf(0L) }
|
||||||
|
val clickDelay = 500L
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context,
|
context,
|
||||||
momentEntity.avatar,
|
momentEntity.avatar,
|
||||||
@@ -246,12 +253,16 @@ fun MomentTopRowGroup(
|
|||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.clip(RoundedCornerShape(40.dp))
|
.clip(RoundedCornerShape(40.dp))
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastClickTime.value > clickDelay) {
|
||||||
|
lastClickTime.value = currentTime
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.AccountProfile.route.replace(
|
NavigationRoute.AccountProfile.route.replace(
|
||||||
"{id}",
|
"{id}",
|
||||||
momentEntity.authorId.toString()
|
momentEntity.authorId.toString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
@@ -267,7 +278,19 @@ fun MomentTopRowGroup(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
MomentName(
|
MomentName(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f)
|
||||||
|
.noRippleClickable {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastClickTime.value > clickDelay) {
|
||||||
|
lastClickTime.value = currentTime
|
||||||
|
navController.navigate(
|
||||||
|
NavigationRoute.AccountProfile.route.replace(
|
||||||
|
"{id}",
|
||||||
|
momentEntity.authorId.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
name = momentEntity.nickname
|
name = momentEntity.nickname
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
@@ -341,16 +364,6 @@ fun MomentContentGroup(
|
|||||||
onPageChange: (Int) -> Unit = {}
|
onPageChange: (Int) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
if (momentEntity.momentTextContent.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = momentEntity.momentTextContent,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (momentEntity.relMoment != null) {
|
if (momentEntity.relMoment != null) {
|
||||||
RelPostCard(
|
RelPostCard(
|
||||||
momentEntity = momentEntity.relMoment!!,
|
momentEntity = momentEntity.relMoment!!,
|
||||||
@@ -366,6 +379,17 @@ fun MomentContentGroup(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (momentEntity.momentTextContent.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = momentEntity.momentTextContent,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,8 +402,8 @@ fun MomentOperateBtn(@DrawableRes icon: Int, count: String) {
|
|||||||
.size(width = 24.dp, height = 24.dp),
|
.size(width = 24.dp, height = 24.dp),
|
||||||
painter = painterResource(id = icon),
|
painter = painterResource(id = icon),
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
|
||||||
)
|
)
|
||||||
|
if (count.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = count,
|
text = count,
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
modifier = Modifier.padding(start = 7.dp),
|
||||||
@@ -387,6 +411,7 @@ fun MomentOperateBtn(@DrawableRes icon: Int, count: String) {
|
|||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -416,6 +441,8 @@ fun MomentBottomOperateRowGroup(
|
|||||||
momentEntity: MomentEntity,
|
momentEntity: MomentEntity,
|
||||||
imageIndex: Int = 0
|
imageIndex: Int = 0
|
||||||
) {
|
) {
|
||||||
|
val lastClickTime = remember { mutableStateOf(0L) }
|
||||||
|
val clickDelay = 500L
|
||||||
var showCommentModal by remember { mutableStateOf(false) }
|
var showCommentModal by remember { mutableStateOf(false) }
|
||||||
if (showCommentModal) {
|
if (showCommentModal) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
@@ -451,58 +478,14 @@ fun MomentBottomOperateRowGroup(
|
|||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.padding(start = 16.dp, end = 0.dp)
|
.padding(start = 16.dp, end = 0.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxHeight(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
MomentOperateBtn(count = momentEntity.likeCount.toString()) {
|
|
||||||
AnimatedLikeIcon(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
liked = momentEntity.liked
|
|
||||||
) {
|
|
||||||
onLikeClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.noRippleClickable {
|
|
||||||
onCommentClick()
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
MomentOperateBtn(
|
|
||||||
icon = R.drawable.rider_pro_comment,
|
|
||||||
count = momentEntity.commentCount.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
,
|
|
||||||
contentAlignment = Alignment.CenterEnd
|
|
||||||
) {
|
|
||||||
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
|
|
||||||
AnimatedFavouriteIcon(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
isFavourite = momentEntity.isFavorite
|
|
||||||
) {
|
|
||||||
onFavoriteClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (momentEntity.images.size > 1) {
|
if (momentEntity.images.size > 1) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -523,8 +506,68 @@ fun MomentBottomOperateRowGroup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 点赞按钮
|
||||||
|
MomentOperateBtn(count = momentEntity.likeCount.toString()) {
|
||||||
|
AnimatedLikeIcon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
liked = momentEntity.liked
|
||||||
|
) {
|
||||||
|
onLikeClick()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
// 评论按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastClickTime.value > clickDelay) {
|
||||||
|
lastClickTime.value = currentTime
|
||||||
|
onCommentClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
MomentOperateBtn(
|
||||||
|
icon = R.mipmap.icon_comment,
|
||||||
|
count = momentEntity.commentCount.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(28.dp))
|
||||||
|
// 转发按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
// TODO: 实现转发功能
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
MomentOperateBtn(
|
||||||
|
icon = R.mipmap.icon_share,
|
||||||
|
count = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收藏按钮
|
||||||
|
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
|
||||||
|
AnimatedFavouriteIcon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
isFavourite = momentEntity.isFavorite
|
||||||
|
) {
|
||||||
|
onFavoriteClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package com.aiosman.ravenow.ui.composables
|
package com.aiosman.ravenow.ui.composables
|
||||||
|
|
||||||
|
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.Arrangement
|
||||||
|
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
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -12,11 +17,14 @@ import androidx.compose.runtime.Composable
|
|||||||
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.res.painterResource
|
||||||
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.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
/**
|
/**
|
||||||
* 可复用的标签页组件
|
* 可复用的标签页组件
|
||||||
*/
|
*/
|
||||||
@@ -37,12 +45,12 @@ fun TabItem(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = 14.sp,
|
fontSize = 15.sp,
|
||||||
color = if (isSelected) AppColors.tabSelectedText else AppColors.tabUnselectedText,
|
color = if (isSelected) AppColors.tabSelectedText else AppColors.tabUnselectedText,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(if (isSelected) AppColors.tabSelectedBackground else AppColors.tabUnselectedBackground)
|
.background(if (isSelected) AppColors.tabSelectedBackground else AppColors.tabUnselectedBackground)
|
||||||
.padding(horizontal = 11.dp, vertical = 4.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,3 +62,43 @@ fun TabItem(
|
|||||||
fun TabSpacer() {
|
fun TabSpacer() {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UnderlineTabItem(
|
||||||
|
text: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.noRippleClickable { onClick() },
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
color = if (isSelected) AppColors.text else AppColors.text.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 13.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 选中状态下显示图标
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (isSelected) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.underline),
|
||||||
|
contentDescription = "selected indicator",
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,16 +15,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
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.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
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.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.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -32,6 +38,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 水平布局的输入框
|
* 水平布局的输入框
|
||||||
@@ -47,12 +54,15 @@ fun FormTextInput(
|
|||||||
onValueChange: (String) -> Unit
|
onValueChange: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(25.dp))
|
||||||
.background(background ?: AppColors.inputBackground)
|
.background(background ?: AppColors.inputBackground)
|
||||||
.let {
|
.let {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
@@ -61,7 +71,11 @@ fun FormTextInput(
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(17.dp),
|
.padding(17.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
keyboardController?.show()
|
||||||
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
label?.let {
|
label?.let {
|
||||||
@@ -79,13 +93,27 @@ fun FormTextInput(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 16.dp)
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.align(Alignment.TopStart),
|
||||||
|
tint = AppColors.text.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
if (value.isEmpty()) {
|
if (value.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = hint ?: "",
|
text = hint ?: "",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = AppColors.inputHint
|
color = AppColors.inputHint
|
||||||
)
|
)
|
||||||
@@ -98,15 +126,18 @@ fun FormTextInput(
|
|||||||
onValueChange = {
|
onValueChange = {
|
||||||
onValueChange(it)
|
onValueChange(it)
|
||||||
},
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
),
|
),
|
||||||
cursorBrush = SolidColor(AppColors.text),
|
cursorBrush = SolidColor(AppColors.text),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
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.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
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.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.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -32,6 +38,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 垂直布局的输入框
|
* 垂直布局的输入框
|
||||||
@@ -44,15 +51,19 @@ fun FormTextInput2(
|
|||||||
error: String? = null,
|
error: String? = null,
|
||||||
hint: String? = null,
|
hint: String? = null,
|
||||||
background: Color? = null,
|
background: Color? = null,
|
||||||
|
focusRequester: FocusRequester? = null,
|
||||||
onValueChange: (String) -> Unit
|
onValueChange: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
val localFocusRequester = focusRequester ?: remember { FocusRequester() }
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.height(150.dp)
|
modifier = modifier.height(150.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(25.dp))
|
||||||
.background(background ?: AppColors.inputBackground)
|
.background(background ?: AppColors.inputBackground)
|
||||||
.let {
|
.let {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
@@ -61,7 +72,11 @@ fun FormTextInput2(
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(17.dp),
|
.padding(17.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
localFocusRequester.requestFocus()
|
||||||
|
keyboardController?.show()
|
||||||
|
},
|
||||||
|
|
||||||
) {
|
) {
|
||||||
label?.let {
|
label?.let {
|
||||||
@@ -79,13 +94,27 @@ fun FormTextInput2(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(top = 8.dp)
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.align(Alignment.TopStart),
|
||||||
|
tint = AppColors.text.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
if (value.isEmpty()) {
|
if (value.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = hint ?: "",
|
text = hint ?: "",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = AppColors.inputHint
|
color = AppColors.inputHint
|
||||||
)
|
)
|
||||||
@@ -93,20 +122,23 @@ fun FormTextInput2(
|
|||||||
}
|
}
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
maxLines = 5,
|
maxLines = 6,
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
onValueChange(it)
|
onValueChange(it)
|
||||||
},
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(localFocusRequester),
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
lineHeight = 20.sp
|
lineHeight = 20.sp
|
||||||
),
|
),
|
||||||
cursorBrush = SolidColor(AppColors.text),
|
cursorBrush = SolidColor(AppColors.text),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,23 @@ import androidx.compose.foundation.Image
|
|||||||
import androidx.compose.foundation.background
|
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.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.height
|
||||||
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.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
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.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.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
|
||||||
@@ -24,7 +29,9 @@ import androidx.compose.ui.draw.clip
|
|||||||
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
|
||||||
|
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.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
@@ -35,6 +42,8 @@ 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.ReloadButton
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -71,6 +80,75 @@ fun FavouriteListPage() {
|
|||||||
) {
|
) {
|
||||||
NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false)
|
NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false)
|
||||||
}
|
}
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
var moments = dataFlow.collectAsLazyPagingItems()
|
||||||
|
|
||||||
|
if (!isNetworkAvailable) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top=149.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(181.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
model.refreshPager(force = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(moments.itemCount == 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top=189.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (com.aiosman.ravenow.AppState.darkMode) R.mipmap.invalid_dark
|
||||||
|
else R.mipmap.invalid_name_1),
|
||||||
|
contentDescription = "No favourites",
|
||||||
|
modifier = Modifier.size(110.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.favourites_null),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(3),
|
columns = GridCells.Fixed(3),
|
||||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)
|
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)
|
||||||
@@ -111,15 +189,11 @@ fun FavouriteListPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
PullRefreshIndicator(
|
PullRefreshIndicator(
|
||||||
FavouriteListViewModel.isLoading,
|
FavouriteListViewModel.isLoading,
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
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.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
|
||||||
@@ -37,6 +39,8 @@ 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 kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -67,7 +71,47 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
) {
|
) {
|
||||||
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
|
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
|
||||||
}
|
}
|
||||||
if (users.itemCount == 0) {
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
|
if (!isNetworkAvailable) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 149.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(181.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
model.loadData(userId, true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (users.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -81,7 +125,7 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
|
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
|
||||||
else R.mipmap.qst_fs_qs_img),
|
else R.mipmap.invalid_name_8),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(181.dp)
|
modifier = Modifier.size(181.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
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.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
|
||||||
@@ -37,7 +38,9 @@ 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.ReloadButton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关注消息列表
|
* 关注消息列表
|
||||||
@@ -54,19 +57,51 @@ fun FollowerNoticeScreen() {
|
|||||||
val model = FollowerNoticeViewModel
|
val model = FollowerNoticeViewModel
|
||||||
var dataFlow = model.followerItemsFlow
|
var dataFlow = model.followerItemsFlow
|
||||||
var followers = dataFlow.collectAsLazyPagingItems()
|
var followers = dataFlow.collectAsLazyPagingItems()
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp)
|
|
||||||
.background(color = AppColors.background)
|
|
||||||
) {
|
|
||||||
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.reload()
|
model.reload()
|
||||||
model.updateNotice()
|
model.updateNotice()
|
||||||
}
|
}
|
||||||
if (followers.itemCount == 0) {
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
|
if (!isNetworkAvailable) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top=149.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(181.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
model.reload(force = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (followers.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -80,7 +115,7 @@ fun FollowerNoticeScreen() {
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
|
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
|
||||||
else R.mipmap.qst_fs_qs_img),
|
else R.mipmap.invalid_name_8),
|
||||||
contentDescription = "No Followers",
|
contentDescription = "No Followers",
|
||||||
modifier = Modifier.size(181.dp)
|
modifier = Modifier.size(181.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
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.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
|
||||||
@@ -37,7 +38,9 @@ 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.ReloadButton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -69,7 +72,48 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false)
|
NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false)
|
||||||
|
|
||||||
}
|
}
|
||||||
if(users.itemCount == 0) {
|
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
|
if (!isNetworkAvailable) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top=149.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(181.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = appColors.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
model.loadData(userId, true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(users.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -83,7 +127,7 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id =if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img_my
|
id =if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img_my
|
||||||
else R.mipmap.qst_gz_qs_img_my),
|
else R.mipmap.invalid_name_9),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(181.dp)
|
modifier = Modifier.size(181.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import com.aiosman.ravenow.exp.formatChatTime
|
|||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.navigateToChat
|
import com.aiosman.ravenow.ui.navigateToChat
|
||||||
import com.aiosman.ravenow.utils.TrtcHelper
|
import com.aiosman.ravenow.utils.TrtcHelper
|
||||||
// 临时兼容层 - TODO: 完成 OpenIM 迁移后删除
|
|
||||||
import com.aiosman.ravenow.compat.*
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.BottomSheetDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CreateBottomSheet(
|
||||||
|
sheetState: SheetState,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onAiClick: () -> Unit,
|
||||||
|
onGroupChatClick: () -> Unit,
|
||||||
|
onMomentClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
//水平效果呈现镜像排列
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
windowInsets = BottomSheetDefaults.windowInsets,
|
||||||
|
containerColor = appColors.background,
|
||||||
|
dragHandle = null,
|
||||||
|
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 24.dp, bottom = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.h_cj_rw_icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.create_title),
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = appColors.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 3.dp)
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.h_cj_x_img),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 18.dp),
|
||||||
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(30.dp))
|
||||||
|
// 三个创建选项
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
// 动态选项
|
||||||
|
CreateOption(
|
||||||
|
icon = R.drawable.ic_create_monent,
|
||||||
|
label = stringResource(R.string.create_moment),
|
||||||
|
onClick = onMomentClick
|
||||||
|
)
|
||||||
|
// 群聊选项
|
||||||
|
CreateOption(
|
||||||
|
icon = R.mipmap.icons_circle_camera,
|
||||||
|
label = stringResource(R.string.create_group_chat_option),
|
||||||
|
onClick = onGroupChatClick
|
||||||
|
)
|
||||||
|
// AI选项
|
||||||
|
CreateOption(
|
||||||
|
icon = R.mipmap.icons_circle_ai,
|
||||||
|
label = stringResource(R.string.create_ai),
|
||||||
|
onClick = onAiClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
|
// 关闭按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.noRippleClickable { onDismiss() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_create_close),
|
||||||
|
contentDescription = stringResource(R.string.create_close),
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CreateOption(
|
||||||
|
icon: Int,
|
||||||
|
label: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.noRippleClickable { onClick() }
|
||||||
|
) {
|
||||||
|
// 直接使用图标,不要背景
|
||||||
|
Image(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = label,
|
||||||
|
modifier = Modifier.size(72.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// 文字标签
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import androidx.compose.foundation.pager.HorizontalPager
|
|||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.DrawerValue
|
import androidx.compose.material3.DrawerValue
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalNavigationDrawer
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
@@ -34,6 +35,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
@@ -78,9 +80,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|||||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||||
import com.aiosman.ravenow.utils.ResourceCleanupManager
|
import com.aiosman.ravenow.utils.ResourceCleanupManager
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun IndexScreen() {
|
fun IndexScreen() {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
@@ -91,6 +94,7 @@ fun IndexScreen() {
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val item = listOf(
|
val item = listOf(
|
||||||
NavigationItem.Home,
|
NavigationItem.Home,
|
||||||
|
//NavigationItem.Dynamic,
|
||||||
NavigationItem.Ai,
|
NavigationItem.Ai,
|
||||||
NavigationItem.Add,
|
NavigationItem.Add,
|
||||||
NavigationItem.Notification,
|
NavigationItem.Notification,
|
||||||
@@ -100,6 +104,7 @@ fun IndexScreen() {
|
|||||||
val pagerState = rememberPagerState(pageCount = { item.size })
|
val pagerState = rememberPagerState(pageCount = { item.size })
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
val bottomSheetState = rememberModalBottomSheetState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// 注意:不要在离开 Index 路由时全量清理资源,以免返回后列表被重置
|
// 注意:不要在离开 Index 路由时全量清理资源,以免返回后列表被重置
|
||||||
@@ -271,12 +276,15 @@ fun IndexScreen() {
|
|||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
modifier = Modifier.height(58.dp + navigationBarHeight),
|
modifier = Modifier.height(58.dp + navigationBarHeight),
|
||||||
containerColor = AppColors.background
|
containerColor = AppColors.tabUnselectedBackground
|
||||||
) {
|
) {
|
||||||
item.forEachIndexed { idx, it ->
|
item.forEachIndexed { idx, it ->
|
||||||
val isSelected = model.tabIndex == idx
|
val isSelected = model.tabIndex == idx
|
||||||
|
// 定义新的选中颜色
|
||||||
|
val selectedColor = Color(0xFF7C45ED)
|
||||||
|
|
||||||
val iconTint by animateColorAsState(
|
val iconTint by animateColorAsState(
|
||||||
targetValue = if (isSelected) AppColors.brandColorsColor else AppColors.text,
|
targetValue = if (isSelected) selectedColor else AppColors.text,
|
||||||
animationSpec = tween(durationMillis = 250), label = ""
|
animationSpec = tween(durationMillis = 250), label = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -291,8 +299,8 @@ fun IndexScreen() {
|
|||||||
navController.navigate(NavigationRoute.Login.route)
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
return@noRippleClickable
|
return@noRippleClickable
|
||||||
}
|
}
|
||||||
NewPostViewModel.asNewPost()
|
// 显示创建底部弹窗
|
||||||
navController.navigate(NavigationRoute.NewPost.route)
|
model.showCreateBottomSheet = true
|
||||||
return@noRippleClickable
|
return@noRippleClickable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +346,7 @@ fun IndexScreen() {
|
|||||||
.width(48.dp)
|
.width(48.dp)
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
.background(
|
.background(
|
||||||
color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.15f) else Color.Transparent,
|
color = if (isSelected) selectedColor.copy(alpha = 0.15f) else Color.Transparent,
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -357,7 +365,7 @@ fun IndexScreen() {
|
|||||||
Text(
|
Text(
|
||||||
text = it.label(),
|
text = it.label(),
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
color = if (isSelected) AppColors.brandColorsColor else AppColors.text,
|
color = if (isSelected) selectedColor else AppColors.text,
|
||||||
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
|
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -378,8 +386,8 @@ fun IndexScreen() {
|
|||||||
userScrollEnabled = false
|
userScrollEnabled = false
|
||||||
) { page ->
|
) { page ->
|
||||||
when (page) {
|
when (page) {
|
||||||
0 -> Home()
|
0 -> Agent()
|
||||||
1 -> Agent()
|
1 -> Home()
|
||||||
2 -> Add()
|
2 -> Add()
|
||||||
3 -> Notifications()
|
3 -> Notifications()
|
||||||
4 -> Profile()
|
4 -> Profile()
|
||||||
@@ -388,6 +396,56 @@ fun IndexScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建底部弹窗
|
||||||
|
if (model.showCreateBottomSheet) {
|
||||||
|
CreateBottomSheet(
|
||||||
|
sheetState = bottomSheetState,
|
||||||
|
onDismiss = {
|
||||||
|
// 使用协程来优雅地关闭弹窗
|
||||||
|
coroutineScope.launch {
|
||||||
|
bottomSheetState.hide()
|
||||||
|
model.showCreateBottomSheet = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAiClick = {
|
||||||
|
// 使用协程来优雅地关闭弹窗并导航
|
||||||
|
coroutineScope.launch {
|
||||||
|
bottomSheetState.hide() // 触发关闭动画
|
||||||
|
model.showCreateBottomSheet = false
|
||||||
|
// 检查游客模式,如果是游客则跳转登录
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
navController.navigate(NavigationRoute.AddAgent.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onGroupChatClick = {
|
||||||
|
// 使用协程来优雅地关闭弹窗并导航
|
||||||
|
coroutineScope.launch {
|
||||||
|
bottomSheetState.hide() // 触发关闭动画
|
||||||
|
model.showCreateBottomSheet = false
|
||||||
|
// 检查游客模式,如果是游客则跳转登录
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.JOIN_GROUP_CHAT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
navController.navigate(NavigationRoute.CreateGroupChat.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMomentClick = {
|
||||||
|
// 使用协程来优雅地关闭弹窗并导航
|
||||||
|
coroutineScope.launch {
|
||||||
|
bottomSheetState.hide() // 触发关闭动画
|
||||||
|
model.showCreateBottomSheet = false
|
||||||
|
// 导航到动态创建页面
|
||||||
|
NewPostViewModel.asNewPost()
|
||||||
|
navController.navigate(NavigationRoute.NewPost.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ object IndexViewModel:ViewModel() {
|
|||||||
|
|
||||||
var openDrawer by mutableStateOf(false)
|
var openDrawer by mutableStateOf(false)
|
||||||
|
|
||||||
|
var showCreateBottomSheet by mutableStateOf(false)
|
||||||
|
|
||||||
fun ResetModel(){
|
fun ResetModel(){
|
||||||
tabIndex = 0
|
tabIndex = 0
|
||||||
|
showCreateBottomSheet = false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,16 +16,21 @@ sealed class NavigationItem(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
data object Home : NavigationItem("Home",
|
data object Home : NavigationItem("Home",
|
||||||
icon = { R.drawable.rider_pro_nav_home },
|
icon = { R.mipmap.bars_x_buttons_home_n_copy },
|
||||||
selectedIcon = { R.mipmap.rider_pro_nav_home_hl },
|
selectedIcon = { R.mipmap.bars_x_buttons_home_n_copy_2 },
|
||||||
label = { stringResource(R.string.main_home) }
|
label = { stringResource(R.string.main_ai) }
|
||||||
)
|
)
|
||||||
|
|
||||||
data object Ai : NavigationItem("Ai",
|
data object Ai : NavigationItem("Ai",
|
||||||
icon = { R.drawable.rider_pro_nav_ai },
|
icon = { R.mipmap.bars_x_buttons_discover_bold},
|
||||||
selectedIcon = { R.mipmap.rider_pro_nav_ai_hl },
|
selectedIcon = { R.mipmap.bars_x_buttons_discover_fill },
|
||||||
label = { stringResource(R.string.main_ai) }
|
label = { stringResource(R.string.index_dynamic) }
|
||||||
)
|
)
|
||||||
|
// data object Ai : NavigationItem("Ai",
|
||||||
|
// icon = { R.drawable.rider_pro_nav_ai },
|
||||||
|
// selectedIcon = { R.mipmap.rider_pro_nav_ai_hl },
|
||||||
|
// label = { stringResource(R.string.main_ai) }
|
||||||
|
// )
|
||||||
|
|
||||||
data object Add : NavigationItem("Add",
|
data object Add : NavigationItem("Add",
|
||||||
icon = { R.drawable.ic_nav_add },
|
icon = { R.drawable.ic_nav_add },
|
||||||
@@ -35,13 +40,13 @@ sealed class NavigationItem(
|
|||||||
|
|
||||||
data object Notification : NavigationItem("Notification",
|
data object Notification : NavigationItem("Notification",
|
||||||
icon = { R.drawable.rider_pro_nav_notification },
|
icon = { R.drawable.rider_pro_nav_notification },
|
||||||
selectedIcon = { R.mipmap.rider_pro_nav_message_hl },
|
selectedIcon = { R.mipmap.bars_x_buttons_chat_s },
|
||||||
label = { stringResource(R.string.main_message) }
|
label = { stringResource(R.string.main_message) }
|
||||||
)
|
)
|
||||||
|
|
||||||
data object Profile : NavigationItem("Profile",
|
data object Profile : NavigationItem("Profile",
|
||||||
icon = { R.drawable.rider_pro_nav_profile },
|
icon = { R.drawable.rider_pro_nav_profile },
|
||||||
selectedIcon = { R.mipmap.rider_pro_nav_profile_hl },
|
selectedIcon = { R.mipmap.bars_x_buttons_user_s },
|
||||||
label = { stringResource(R.string.main_profile) }
|
label = { stringResource(R.string.main_profile) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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
|
||||||
@@ -27,9 +28,14 @@ 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.Icon
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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
|
||||||
@@ -43,6 +49,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -60,13 +67,36 @@ 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.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.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.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 com.aiosman.ravenow.utils.ResourceCleanupManager
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.compose.foundation.lazy.grid.items as gridItems
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
|
||||||
@OptIn( ExperimentalFoundationApi::class)
|
// 检测是否接近列表底部的扩展函数
|
||||||
|
fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
|
||||||
|
val layoutInfo = this.layoutInfo
|
||||||
|
val totalItemsCount = layoutInfo.totalItemsCount
|
||||||
|
val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||||
|
|
||||||
|
return lastVisibleItemIndex >= (totalItemsCount - buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Agent() {
|
fun Agent() {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
@@ -97,209 +127,272 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
val agentItems = viewModel.agentItems
|
||||||
|
var selectedTabIndex by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
// 无限滚动状态
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
// 创建一个可观察的滚动到底部状态
|
||||||
|
val isScrolledToEnd by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
listState.isScrolledToEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测滚动到底部并加载更多数据
|
||||||
|
LaunchedEffect(isScrolledToEnd) {
|
||||||
|
if (isScrolledToEnd && !viewModel.isLoadingMore && agentItems.isNotEmpty() && viewModel.hasMoreData) {
|
||||||
|
viewModel.loadMoreAgents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.home_logo),
|
||||||
|
contentDescription = "Rave AI Logo",
|
||||||
|
modifier = Modifier
|
||||||
|
.height(44.dp)
|
||||||
|
.padding(top =9.dp,bottom=9.dp)
|
||||||
|
.wrapContentSize(),
|
||||||
|
// colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
|
contentDescription = "search",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(44.dp)
|
||||||
|
.padding(top = 9.dp,bottom=9.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigate(NavigationRoute.Search.route)
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = AppColors.background
|
||||||
|
),
|
||||||
|
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
|
||||||
|
.padding(top = statusBarPaddingValues.calculateTopPadding())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = AppColors.background,
|
||||||
|
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
.padding(
|
.padding(
|
||||||
top = statusBarPaddingValues.calculateTopPadding()+18.dp,
|
|
||||||
bottom = navigationBarPaddings,
|
bottom = navigationBarPaddings,
|
||||||
start = 16.dp,
|
start = 8.dp,
|
||||||
end = 16.dp
|
end = 8.dp
|
||||||
),
|
)
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
) {
|
||||||
Row(
|
|
||||||
|
// 类别标签页 - 吸顶
|
||||||
|
stickyHeader(key = "category_tabs") {
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(top = 4.dp, bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.fillMaxWidth(),
|
.padding(bottom = 8.dp),
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 搜索框
|
item {
|
||||||
Row(
|
CustomTabItem(
|
||||||
modifier = Modifier
|
text = stringResource(R.string.agent_recommend),
|
||||||
.height(36.dp)
|
isSelected = selectedTabIndex == 0,
|
||||||
.weight(1f)
|
onClick = {
|
||||||
.clip(shape = RoundedCornerShape(8.dp))
|
selectedTabIndex = 0
|
||||||
.background(AppColors.inputBackground)
|
viewModel.loadAllAgents()
|
||||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
}
|
||||||
.noRippleClickable {
|
|
||||||
// 搜索框点击事件
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = AppColors.inputHint
|
|
||||||
)
|
|
||||||
Box {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.search),
|
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
|
||||||
color = AppColors.inputHint,
|
|
||||||
fontSize = 17.sp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
TabSpacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态添加分类标签
|
||||||
|
viewModel.categories.forEachIndexed { index, category ->
|
||||||
|
item {
|
||||||
|
CustomTabItem(
|
||||||
|
text = category.name,
|
||||||
|
isSelected = selectedTabIndex == index + 1,
|
||||||
|
onClick = {
|
||||||
|
selectedTabIndex = index + 1
|
||||||
|
viewModel.loadAgentsByCategory(category.id)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
// 新增
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
|
||||||
// 检查游客模式,如果是游客则跳转登录
|
|
||||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
|
|
||||||
navController.navigate(NavigationRoute.Login.route)
|
|
||||||
} else {
|
|
||||||
// 导航到添加智能体页面
|
|
||||||
navController.navigate(
|
|
||||||
NavigationRoute.AddAgent.route
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
lastClickTime = System.currentTimeMillis()
|
item {
|
||||||
|
TabSpacer()
|
||||||
}
|
}
|
||||||
},
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// 推荐Agent
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 推荐内容区域
|
||||||
|
item {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(260.dp)
|
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
|
|
||||||
) {
|
) {
|
||||||
// 标题
|
when {
|
||||||
Row(
|
selectedTabIndex == 0 -> {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
||||||
modifier = Modifier.padding(bottom = 12.dp)
|
}
|
||||||
) {
|
selectedTabIndex in 1..viewModel.categories.size -> {
|
||||||
Image(
|
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
||||||
painter = painterResource(R.mipmap.rider_pro_agent2),
|
}
|
||||||
contentDescription = "agent",
|
else -> {
|
||||||
modifier = Modifier.size(28.dp),
|
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
|
||||||
|
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
|
||||||
)
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
}
|
||||||
androidx.compose.material3.Text(
|
}
|
||||||
text = stringResource(R.string.agent_recommend_agent),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent ViewPager
|
// 热门聊天室
|
||||||
AgentViewPagerSection(agentItems = viewModel.agentItems.take(9),viewModel)
|
stickyHeader(key = "hot_rooms_header") {
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(0.dp))
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight(),
|
.background(AppColors.background)
|
||||||
// center the tabs
|
.padding(top = 8.dp, bottom = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 热门聊天室网格
|
||||||
|
items(viewModel.chatRooms.chunked(2)) { rowRooms ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
rowRooms.forEach { chatRoom ->
|
||||||
|
ChatRoomCard(
|
||||||
|
chatRoom = chatRoom,
|
||||||
|
navController = LocalNavController.current,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (rowRooms.size == 1) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(modifier = Modifier.height(20.dp)) }
|
||||||
|
|
||||||
|
// "发现更多" 标题 - 吸顶
|
||||||
|
stickyHeader(key = "discover_more") {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(top = 8.dp, bottom = 12.dp),
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.Bottom
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.rider_pro_agent2),
|
painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2),
|
||||||
contentDescription = "agent",
|
contentDescription = "agent",
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp)
|
||||||
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
text = stringResource(R.string.agent_find),
|
text = stringResource(R.string.agent_find),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
// 只有非游客用户才显示"我的Agent"tab
|
|
||||||
if (!AppStore.isGuest) {
|
|
||||||
TabItem(
|
|
||||||
text = stringResource(R.string.agent_mine),
|
|
||||||
isSelected = pagerState.currentPage == 0,
|
|
||||||
onClick = {
|
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 300L) {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(0)
|
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
lastClickTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
TabSpacer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TabItem(
|
// Agent网格 - 使用行式布局
|
||||||
text = stringResource(R.string.agent_hot),
|
items(
|
||||||
isSelected = if (AppStore.isGuest) pagerState.currentPage == 0 else pagerState.currentPage == 1,
|
items = agentItems.chunked(2),
|
||||||
onClick = {
|
key = { row -> row.firstOrNull()?.openId ?: "" }
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 300L) {
|
) { rowItems ->
|
||||||
scope.launch {
|
Row(
|
||||||
val targetPage = if (AppStore.isGuest) 0 else 1
|
|
||||||
pagerState.animateScrollToPage(targetPage)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
lastClickTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
/*TabSpacer()
|
|
||||||
TabItem(
|
|
||||||
text = stringResource(R.string.agent_recommend),
|
|
||||||
isSelected = pagerState.currentPage == 2,
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
TabSpacer()
|
|
||||||
TabItem(
|
|
||||||
text = stringResource(R.string.agent_other),
|
|
||||||
isSelected = pagerState.currentPage == 3,
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*/
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
HorizontalPager(
|
|
||||||
state = pagerState,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.padding(vertical = 16.dp),
|
||||||
beyondBoundsPageCount = 1 // 预加载相邻页面,避免切换时重新加载
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
if (AppStore.isGuest) {
|
rowItems.forEach { agentItem ->
|
||||||
// 游客模式下只显示热门Agent
|
Box(
|
||||||
when (it) {
|
modifier = Modifier.weight(1f)
|
||||||
0 -> {
|
) {
|
||||||
HotAgent()
|
AgentCardSquare(
|
||||||
|
agentItem = agentItem,
|
||||||
|
viewModel = viewModel,
|
||||||
|
navController = LocalNavController.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果这一行只有一个item,添加一个空的占位符
|
||||||
|
if (rowItems.size == 1) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 正常用户显示我的Agent和热门Agent
|
|
||||||
when (it) {
|
|
||||||
0 -> {
|
|
||||||
MineAgent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
// 加载更多指示器
|
||||||
HotAgent()
|
if (viewModel.isLoadingMore) {
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 24.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = AppColors.text,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "加载中...",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,13 +400,188 @@ 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SuspiciousIndentation")
|
||||||
|
@Composable
|
||||||
|
fun AgentCardSquare(
|
||||||
|
agentItem: AgentItem,
|
||||||
|
viewModel: AgentViewModel,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val cardHeight = 180.dp
|
||||||
|
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
|
||||||
|
|
||||||
|
// 防抖状态
|
||||||
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(cardHeight)
|
||||||
|
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
||||||
|
.clickable {
|
||||||
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
|
viewModel.goToProfile(agentItem.openId, navController)
|
||||||
|
}) {
|
||||||
|
lastClickTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(y = 4.dp)
|
||||||
|
.size(avatarSize)
|
||||||
|
.background(AppColors.background, RoundedCornerShape(avatarSize / 2))
|
||||||
|
.clip(RoundedCornerShape(avatarSize / 2)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.group_copy),
|
||||||
|
contentDescription = "默认头像",
|
||||||
|
modifier = Modifier.size(avatarSize),
|
||||||
|
)
|
||||||
|
if (agentItem.avatar.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = agentItem.avatar,
|
||||||
|
contentDescription = "Agent头像",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(avatarSize)
|
||||||
|
.clip(RoundedCornerShape(avatarSize / 2)),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域(名称和描述)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp + avatarSize + 8.dp, start = 8.dp, end = 8.dp, bottom = 48.dp), // 为底部聊天按钮留出空间
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
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 = 2,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 聊天按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.width(60.dp)
|
||||||
|
.height(32.dp)
|
||||||
|
.background(
|
||||||
|
color = AppColors.text,
|
||||||
|
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 = 15.sp,
|
||||||
|
color = AppColors.background,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@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
|
||||||
|
|
||||||
// 每页显示3个agent
|
// 每页显示5个agent
|
||||||
val itemsPerPage = 3
|
val itemsPerPage = 5
|
||||||
val totalPages = (agentItems.size + itemsPerPage - 1) / itemsPerPage
|
val totalPages = (agentItems.size + itemsPerPage - 1) / itemsPerPage
|
||||||
|
|
||||||
if (totalPages > 0) {
|
if (totalPages > 0) {
|
||||||
@@ -323,7 +591,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
|
|||||||
// Agent内容
|
// Agent内容
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(180.dp)
|
.height(310.dp)
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
@@ -344,7 +612,8 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
|
|||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
|
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
|
||||||
page = page,
|
page = page,
|
||||||
modifier = Modifier.height(180.dp)
|
modifier = Modifier
|
||||||
|
.height(310.dp)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = scale
|
scaleX = scale
|
||||||
scaleY = scale
|
scaleY = scale
|
||||||
@@ -368,7 +637,9 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
|
|||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.size(3.dp)
|
.size(3.dp)
|
||||||
.background(
|
.background(
|
||||||
color = if (pagerState.currentPage == index) AppColors.text else AppColors.secondaryText.copy(alpha = 0.3f),
|
color = if (pagerState.currentPage == index) AppColors.text else AppColors.secondaryText.copy(
|
||||||
|
alpha = 0.3f
|
||||||
|
),
|
||||||
shape = androidx.compose.foundation.shape.CircleShape
|
shape = androidx.compose.foundation.shape.CircleShape
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -413,7 +684,7 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp))
|
.background(Color(0x00F5F5F5), RoundedCornerShape(24.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
viewModel.goToProfile(agentItem.openId, navController)
|
viewModel.goToProfile(agentItem.openId, navController)
|
||||||
@@ -423,6 +694,12 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.group_copy),
|
||||||
|
contentDescription = "默认头像",
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
)
|
||||||
|
|
||||||
if (agentItem.avatar.isNotEmpty()) {
|
if (agentItem.avatar.isNotEmpty()) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = agentItem.avatar,
|
imageUrl = agentItem.avatar,
|
||||||
@@ -432,13 +709,6 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
.clip(RoundedCornerShape(24.dp)),
|
.clip(RoundedCornerShape(24.dp)),
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.mipmap.rider_pro_agent),
|
|
||||||
contentDescription = "默认头像",
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +748,12 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
.size(width = 60.dp, height = 32.dp)
|
.size(width = 60.dp, height = 32.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color(0X147c7480),
|
color = Color(0X147c7480),
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(
|
||||||
|
topStart = 14.dp,
|
||||||
|
topEnd = 14.dp,
|
||||||
|
bottomStart = 0.dp,
|
||||||
|
bottomEnd = 14.dp
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.clickable {
|
.clickable {
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
@@ -487,7 +762,10 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
navController.navigate(NavigationRoute.Login.route)
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
} else {
|
} else {
|
||||||
viewModel.createSingleChat(agentItem.openId)
|
viewModel.createSingleChat(agentItem.openId)
|
||||||
viewModel.goToChatAi(agentItem.openId, navController = navController)
|
viewModel.goToChatAi(
|
||||||
|
agentItem.openId,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
lastClickTime = System.currentTimeMillis()
|
lastClickTime = System.currentTimeMillis()
|
||||||
@@ -504,3 +782,210 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Composable
|
||||||
|
fun ChatRoomsSection(
|
||||||
|
chatRooms: List<ChatRoom>,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// 标题
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_hot_room),
|
||||||
|
contentDescription = "chat room",
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = stringResource(R.string.hot_rooms),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
chatRooms.chunked(2).forEach { rowRooms ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
rowRooms.forEach { chatRoom ->
|
||||||
|
ChatRoomCard(
|
||||||
|
chatRoom = chatRoom,
|
||||||
|
navController = navController,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatRoomCard(
|
||||||
|
chatRoom: ChatRoom,
|
||||||
|
navController: NavHostController,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val cardSize = 180.dp
|
||||||
|
val viewModel: AgentViewModel = viewModel()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// 防抖状态
|
||||||
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
|
// Loading 对话框
|
||||||
|
if (viewModel.isJoiningRoom) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { /* 阻止用户关闭对话框 */ },
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = false,
|
||||||
|
dismissOnClickOutside = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(120.dp)
|
||||||
|
.background(
|
||||||
|
color = AppColors.background,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
color = AppColors.main
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "加入中...",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正方形卡片,文字重叠在底部
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(cardSize)
|
||||||
|
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
|
||||||
|
.clickable(enabled = !viewModel.isJoiningRoom) {
|
||||||
|
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
|
// 加入群聊房间
|
||||||
|
viewModel.joinRoom(
|
||||||
|
id = chatRoom.id,
|
||||||
|
name = chatRoom.name,
|
||||||
|
avatar = chatRoom.avatar,
|
||||||
|
context = context,
|
||||||
|
navController = navController,
|
||||||
|
onSuccess = {
|
||||||
|
// 成功加入房间
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
// 处理错误,可以显示Toast或其他提示
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
lastClickTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// 优先显示banner,如果没有banner则显示头像
|
||||||
|
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
|
||||||
|
|
||||||
|
if (imageUrl.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
|
||||||
|
modifier = Modifier
|
||||||
|
.width(cardSize)
|
||||||
|
.height(120.dp)
|
||||||
|
.clip(RoundedCornerShape(
|
||||||
|
topStart = 12.dp,
|
||||||
|
topEnd = 12.dp,
|
||||||
|
bottomStart = 0.dp,
|
||||||
|
bottomEnd = 0.dp)),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 默认房间图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_agent),
|
||||||
|
contentDescription = "默认房间图标",
|
||||||
|
modifier = Modifier.size(cardSize * 0.4f),
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 房间名称,重叠在底部
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 32.dp, start = 10.dp, end = 10.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = chatRoom.name,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
|
||||||
|
color = AppColors.text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = androidx.compose.ui.text.style.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 = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.alpha(0.6f),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import com.aiosman.ravenow.data.Agent
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.data.api.CategoryTemplate
|
||||||
import com.aiosman.ravenow.data.api.RaveNowAPI
|
import com.aiosman.ravenow.data.api.RaveNowAPI
|
||||||
import com.aiosman.ravenow.data.api.SingleChatRequestBody
|
import com.aiosman.ravenow.data.api.SingleChatRequestBody
|
||||||
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
|
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
|
||||||
@@ -14,6 +17,37 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
|||||||
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
|
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import android.util.Log
|
||||||
|
import com.aiosman.ravenow.data.Room
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.ConstVars
|
||||||
|
import com.aiosman.ravenow.data.api.CreateGroupChatRequestBody
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
|
||||||
|
import com.aiosman.ravenow.ui.navigateToGroupChat
|
||||||
|
import com.aiosman.ravenow.data.api.ApiErrorResponse
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import com.aiosman.ravenow.data.api.JoinGroupChatRequestBody
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存数据结构,用于存储每个分类的Agent列表
|
||||||
|
*/
|
||||||
|
data class AgentCacheData(
|
||||||
|
val items: List<AgentItem>,
|
||||||
|
val currentPage: Int,
|
||||||
|
val hasMoreData: Boolean
|
||||||
|
)
|
||||||
|
data class ChatRoom(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val avatar: String = "",
|
||||||
|
val banner: String = "",
|
||||||
|
val memberCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
object AgentViewModel: ViewModel() {
|
object AgentViewModel: ViewModel() {
|
||||||
|
|
||||||
@@ -22,10 +56,17 @@ object AgentViewModel: ViewModel() {
|
|||||||
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
|
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var categories by mutableStateOf<List<CategoryItem>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var chatRooms by mutableStateOf<List<ChatRoom>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var rooms by mutableStateOf<List<Room>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
@@ -33,31 +74,222 @@ object AgentViewModel: ViewModel() {
|
|||||||
var isLoading by mutableStateOf(false)
|
var isLoading by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// 分页相关状态
|
||||||
|
var isLoadingMore by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var currentPage by mutableStateOf(1)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var hasMoreData by mutableStateOf(true)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isJoiningRoom by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val pageSize = 20
|
||||||
|
private var currentCategoryId: Int? = null
|
||||||
|
|
||||||
|
// 缓存:使用分类ID作为key,null表示推荐列表
|
||||||
|
private val agentCache = mutableMapOf<Int?, AgentCacheData>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadAgentData()
|
loadAgentData()
|
||||||
|
loadCategories()
|
||||||
|
loadChatRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAgentData() {
|
private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false, forceRefresh: Boolean = false) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
// 如果不是强制刷新且不是加载更多,检查缓存
|
||||||
|
if (!forceRefresh && !isLoadMore) {
|
||||||
|
val cached = agentCache[categoryId]
|
||||||
|
if (cached != null && cached.items.isNotEmpty()) {
|
||||||
|
// 使用缓存数据
|
||||||
|
agentItems = cached.items
|
||||||
|
currentPage = cached.currentPage
|
||||||
|
hasMoreData = cached.hasMoreData
|
||||||
|
currentCategoryId = categoryId
|
||||||
|
println("使用缓存数据,分类ID: $categoryId, 数据数量: ${cached.items.size}")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
isLoadingMore = true
|
||||||
|
} else {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
// 重置分页状态
|
||||||
|
currentPage = 1
|
||||||
|
hasMoreData = true
|
||||||
|
currentCategoryId = categoryId
|
||||||
|
}
|
||||||
|
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val response = if (categoryId != null) {
|
||||||
|
// 根据分类ID获取智能体
|
||||||
|
apiClient.getAgent(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
withWorkflow = 1,
|
||||||
|
categoryIds = listOf(categoryId),
|
||||||
|
random = 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 获取推荐智能体,使用random=1
|
||||||
|
apiClient.getAgent(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
withWorkflow = 1,
|
||||||
|
categoryIds = null,
|
||||||
|
random = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val responseData = response.body()?.data
|
||||||
agentItems = agents.map { agent ->
|
val agents = responseData?.list ?: emptyList<Agent>()
|
||||||
|
val newAgentItems = agents.map { agent ->
|
||||||
AgentItem.fromAgent(agent)
|
AgentItem.fromAgent(agent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
// 加载更多:追加到现有列表
|
||||||
|
agentItems = agentItems + newAgentItems
|
||||||
|
currentPage = page
|
||||||
|
} else {
|
||||||
|
// 首次加载或刷新:替换整个列表
|
||||||
|
agentItems = newAgentItems
|
||||||
|
currentPage = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否还有更多数据
|
||||||
|
hasMoreData = agents.size >= pageSize
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
agentCache[categoryId] = AgentCacheData(
|
||||||
|
items = agentItems,
|
||||||
|
currentPage = currentPage,
|
||||||
|
hasMoreData = hasMoreData
|
||||||
|
)
|
||||||
|
println("更新缓存,分类ID: $categoryId, 数据数量: ${agentItems.size}")
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "获取Agent数据失败: ${response.code()}"
|
errorMessage = "获取Agent数据失败: ${response.code()}"
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errorMessage = "网络请求失败: ${e.message}"
|
errorMessage = "网络请求失败: ${e.message}"
|
||||||
} finally {
|
} finally {
|
||||||
|
if (isLoadMore) {
|
||||||
|
isLoadingMore = false
|
||||||
|
} else {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadChatRooms() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response = apiClient.getRooms(
|
||||||
|
page = 1,
|
||||||
|
pageSize = 20,
|
||||||
|
isRecommended = 1,
|
||||||
|
random = 1
|
||||||
|
)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val allRooms = response.body()?.list ?: emptyList()
|
||||||
|
val targetCount = (allRooms.size / 2) * 2
|
||||||
|
rooms = allRooms.take(targetCount)
|
||||||
|
|
||||||
|
// 转换为ChatRoom格式用于兼容现有UI
|
||||||
|
chatRooms = rooms.map { room ->
|
||||||
|
ChatRoom(
|
||||||
|
id = room.id,
|
||||||
|
name = room.name,
|
||||||
|
avatar = room.avatar,
|
||||||
|
banner = ConstVars.BASE_SERVER + "/api/v1/outside/" + room.recommendBanner + "?token=${AppStore.token}",
|
||||||
|
memberCount = room.userCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 如果网络请求失败,使用默认数据
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCategories() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// 如果分类已经加载,不重复请求
|
||||||
|
if (categories.isNotEmpty()) {
|
||||||
|
println("使用已缓存的分类数据,数量: ${categories.size}")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取完整的语言标记(如 "zh-CN")
|
||||||
|
val sysLang = com.aiosman.ravenow.utils.Utils.getPreferredLanguageTag()
|
||||||
|
val response = apiClient.getCategories(
|
||||||
|
page = 1,
|
||||||
|
pageSize = 100,
|
||||||
|
isActive = true,
|
||||||
|
withChildren = false,
|
||||||
|
withParent = false,
|
||||||
|
withCount = true,
|
||||||
|
hideEmpty = true,
|
||||||
|
lang = sysLang
|
||||||
|
)
|
||||||
|
println("分类数据请求完成,响应成功: ${response.isSuccessful}, 语言标记: $sysLang")
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val categoryList = response.body()?.list ?: emptyList()
|
||||||
|
println("获取到 ${categoryList.size} 个分类")
|
||||||
|
// 使用当前语言获取翻译后的分类名称
|
||||||
|
categories = categoryList.map { category ->
|
||||||
|
CategoryItem.fromCategoryTemplate(category, sysLang)
|
||||||
|
}
|
||||||
|
println("成功处理并映射了 ${categories.size} 个分类")
|
||||||
|
} else {
|
||||||
|
errorMessage = "获取分类数据失败: ${response.code()}"
|
||||||
|
println("获取分类数据失败: ${response.code()}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = "获取分类数据失败: ${e.message}"
|
||||||
|
println("获取分类数据异常: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAgentsByCategory(categoryId: Int) {
|
||||||
|
loadAgentData(categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAllAgents() {
|
||||||
|
loadAgentData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载更多Agent数据
|
||||||
|
*/
|
||||||
|
fun loadMoreAgents() {
|
||||||
|
// 检查是否正在加载或没有更多数据
|
||||||
|
if (isLoadingMore || !hasMoreData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextPage = currentPage + 1
|
||||||
|
loadAgentData(
|
||||||
|
categoryId = currentCategoryId,
|
||||||
|
page = nextPage,
|
||||||
|
isLoadMore = true
|
||||||
|
)
|
||||||
|
}
|
||||||
fun createSingleChat(
|
fun createSingleChat(
|
||||||
openId: String,
|
openId: String,
|
||||||
) {
|
) {
|
||||||
@@ -96,10 +328,12 @@ object AgentViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新推荐Agent数据
|
* 刷新当前分类的Agent数据(强制刷新,清除缓存)
|
||||||
*/
|
*/
|
||||||
fun refreshAgentData() {
|
fun refreshAgentData() {
|
||||||
loadAgentData()
|
// 清除当前分类的缓存
|
||||||
|
agentCache.remove(currentCategoryId)
|
||||||
|
loadAgentData(categoryId = currentCategoryId, forceRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,14 +345,115 @@ object AgentViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加入房间
|
||||||
|
*/
|
||||||
|
fun joinRoom(
|
||||||
|
id: Int,
|
||||||
|
name: String,
|
||||||
|
avatar: String,
|
||||||
|
context: Context,
|
||||||
|
navController: NavHostController,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
onError: (String) -> Unit
|
||||||
|
) {
|
||||||
|
// 防止重复点击
|
||||||
|
if (isJoiningRoom) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isJoiningRoom = true
|
||||||
|
val response = apiClient.joinRoom(JoinGroupChatRequestBody(roomId = id))
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
// 打开房间
|
||||||
|
val openRoomResponse = apiClient.createGroupChatAi(
|
||||||
|
roomId = id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (openRoomResponse.isSuccessful){
|
||||||
|
val respData = openRoomResponse.body()
|
||||||
|
respData?.let {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
// 群聊直接使用群ID进行导航
|
||||||
|
navController.navigateToGroupChat(
|
||||||
|
id = respData.data.trtcRoomId,
|
||||||
|
name = name,
|
||||||
|
avatar = avatar
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onError("加入房间失败")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSuccess()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 处理错误响应
|
||||||
|
try {
|
||||||
|
val errorBody = response.errorBody()?.string()
|
||||||
|
if (errorBody != null) {
|
||||||
|
val gson = Gson()
|
||||||
|
val errorResponse = gson.fromJson(errorBody, ApiErrorResponse::class.java)
|
||||||
|
|
||||||
|
// 在主线程显示 Toast
|
||||||
|
Toast.makeText(context, errorResponse.error, Toast.LENGTH_LONG).show()
|
||||||
|
onError(errorResponse.error)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
|
||||||
|
onError("加入房间失败")
|
||||||
|
}
|
||||||
|
} catch (parseException: Exception) {
|
||||||
|
// 如果解析错误响应失败,显示默认错误信息
|
||||||
|
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
|
||||||
|
onError("加入房间失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(context, "网络请求失败:${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
onError("网络请求失败:${e.message}")
|
||||||
|
} finally {
|
||||||
|
isJoiningRoom = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置ViewModel状态,用于登出或切换账号时清理数据
|
* 重置ViewModel状态,用于登出或切换账号时清理数据
|
||||||
*/
|
*/
|
||||||
fun ResetModel() {
|
fun ResetModel() {
|
||||||
agentItems = emptyList()
|
agentItems = emptyList()
|
||||||
|
categories = emptyList()
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
isLoadingMore = false
|
||||||
|
currentPage = 1
|
||||||
|
hasMoreData = true
|
||||||
|
currentCategoryId = null
|
||||||
|
// 清空缓存
|
||||||
|
agentCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
data class CategoryItem(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val avatar: String,
|
||||||
|
val promptCount: Int?
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromCategoryTemplate(template: CategoryTemplate, lang: String): CategoryItem {
|
||||||
|
return CategoryItem(
|
||||||
|
id = template.id,
|
||||||
|
name = template.getLocalizedName(lang),
|
||||||
|
description = template.getLocalizedDescription(lang),
|
||||||
|
avatar = "${ApiClient.BASE_API_URL}${template.avatar}",
|
||||||
|
promptCount = template.promptCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
|
|||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.message.tab.AllChatListScreen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息列表界面
|
* 消息列表界面
|
||||||
@@ -95,7 +95,7 @@ fun NotificationsScreen() {
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var pagerState = rememberPagerState (pageCount = { 3 })
|
var pagerState = rememberPagerState (pageCount = { 4 })
|
||||||
var scope = rememberCoroutineScope()
|
var scope = rememberCoroutineScope()
|
||||||
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
|
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
|
||||||
MessageListViewModel.viewModelScope.launch {
|
MessageListViewModel.viewModelScope.launch {
|
||||||
@@ -143,140 +143,180 @@ fun NotificationsScreen() {
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 15.dp),
|
.height(44.dp)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.align(Alignment.CenterVertically),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.main_message),
|
text = stringResource(R.string.main_message),
|
||||||
fontSize = 17.sp,
|
fontSize = 20.sp,
|
||||||
fontWeight = FontWeight.W700,
|
fontWeight = FontWeight.W900,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_group),
|
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
contentDescription = "add",
|
contentDescription = "search",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
debouncer {
|
// TODO: 实现搜索功能
|
||||||
navController.navigate(NavigationRoute.CreateGroupChat.route)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
Box {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_notification),
|
||||||
|
contentDescription = "notifications",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigate(NavigationRoute.NotificationScreen.route)
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
|
||||||
}
|
// 通知红点
|
||||||
// 搜索栏//
|
val totalNoticeCount = MessageListViewModel.likeNoticeCount +
|
||||||
|
MessageListViewModel.followNoticeCount +
|
||||||
|
MessageListViewModel.commentNoticeCount +
|
||||||
|
MessageListViewModel.favouriteNoticeCount
|
||||||
|
|
||||||
|
if (totalNoticeCount > 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.size(8.dp)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.background(
|
||||||
.height(40.dp)
|
color = Color(0xFFFF3B30),
|
||||||
|
shape = CircleShape
|
||||||
.noRippleClickable {
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(36.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(shape = RoundedCornerShape(8.dp))
|
|
||||||
.background(AppColors.inputBackground)
|
|
||||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
// 搜索框点击事件
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = AppColors.inputHint
|
|
||||||
)
|
)
|
||||||
Box {
|
.align(Alignment.TopEnd)
|
||||||
androidx.compose.material.Text(
|
.offset(x = 8.dp, y = (-4).dp)
|
||||||
text = stringResource(R.string.search),
|
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
|
||||||
color = AppColors.inputHint,
|
|
||||||
fontSize = 17.sp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
//创建群聊//
|
||||||
modifier = Modifier
|
// Image(
|
||||||
.fillMaxWidth()
|
// painter = painterResource(id = R.drawable.rider_pro_group),
|
||||||
.padding(horizontal = 16.dp),
|
// contentDescription = "add",
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
// modifier = Modifier
|
||||||
) {
|
// .size(24.dp)
|
||||||
val likeDebouncer = rememberDebouncer()
|
// .noRippleClickable {
|
||||||
val followDebouncer = rememberDebouncer()
|
// debouncer {
|
||||||
val commentDebouncer = rememberDebouncer()
|
// navController.navigate(NavigationRoute.CreateGroupChat.route)
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
// )
|
||||||
|
|
||||||
NotificationIndicator(
|
|
||||||
MessageListViewModel.likeNoticeCount,
|
// // 搜索栏//
|
||||||
R.mipmap.rider_pro_like,
|
// Box(
|
||||||
stringResource(R.string.like_upper),
|
// modifier = Modifier
|
||||||
Color(0xFFFAFD5D)
|
// .fillMaxWidth()
|
||||||
) {
|
// .padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
likeDebouncer {
|
// .height(40.dp)
|
||||||
if (MessageListViewModel.likeNoticeCount > 0) {
|
//
|
||||||
// 刷新点赞消息列表
|
// .noRippleClickable {
|
||||||
LikeNoticeViewModel.isFirstLoad = true
|
// },
|
||||||
// 清除点赞消息数量
|
// contentAlignment = Alignment.CenterStart
|
||||||
MessageListViewModel.clearLikeNoticeCount()
|
// )
|
||||||
}
|
// {
|
||||||
navController.navigate(NavigationRoute.Likes.route)
|
// Row(
|
||||||
}
|
// modifier = Modifier
|
||||||
}
|
// .height(36.dp)
|
||||||
NotificationIndicator(
|
// .fillMaxWidth()
|
||||||
MessageListViewModel.followNoticeCount,
|
// .clip(shape = RoundedCornerShape(8.dp))
|
||||||
R.mipmap.rider_pro_followers,
|
// .background(AppColors.inputBackground)
|
||||||
stringResource(R.string.followers_upper),
|
// .padding(horizontal = 8.dp, vertical = 0.dp)
|
||||||
Color(0xFFF470FE)
|
// .noRippleClickable {
|
||||||
) {
|
// // 搜索框点击事件
|
||||||
followDebouncer {
|
// },
|
||||||
if (MessageListViewModel.followNoticeCount > 0) {
|
// verticalAlignment = Alignment.CenterVertically
|
||||||
// 刷新关注消息列表
|
// ) {
|
||||||
FollowerNoticeViewModel.isFirstLoad = true
|
// Icon(
|
||||||
MessageListViewModel.clearFollowNoticeCount()
|
// painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
}
|
// contentDescription = null,
|
||||||
navController.navigate(NavigationRoute.Followers.route)
|
// tint = AppColors.inputHint
|
||||||
}
|
// )
|
||||||
}
|
// Box {
|
||||||
NotificationIndicator(
|
// androidx.compose.material.Text(
|
||||||
MessageListViewModel.commentNoticeCount,
|
// text = stringResource(R.string.search),
|
||||||
R.mipmap.rider_pro_comment,
|
// modifier = Modifier.padding(start = 8.dp),
|
||||||
stringResource(R.string.comment).uppercase(),
|
// color = AppColors.inputHint,
|
||||||
Color(0xFF6246FF)
|
// fontSize = 17.sp
|
||||||
) {
|
// )
|
||||||
commentDebouncer {
|
// }
|
||||||
navController.navigate(NavigationRoute.CommentNoticeScreen.route)
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
}
|
//赞、粉丝、评论//
|
||||||
|
// Row(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxWidth()
|
||||||
|
// .padding(horizontal = 16.dp),
|
||||||
|
// horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
// ) {
|
||||||
|
// val likeDebouncer = rememberDebouncer()
|
||||||
|
// val followDebouncer = rememberDebouncer()
|
||||||
|
// val commentDebouncer = rememberDebouncer()
|
||||||
|
//
|
||||||
|
// NotificationIndicator(
|
||||||
|
// MessageListViewModel.likeNoticeCount,
|
||||||
|
// R.mipmap.rider_pro_like,
|
||||||
|
// stringResource(R.string.like_upper),
|
||||||
|
// Color(0xFFFAFD5D)
|
||||||
|
// ) {
|
||||||
|
// likeDebouncer {
|
||||||
|
// if (MessageListViewModel.likeNoticeCount > 0) {
|
||||||
|
// // 刷新点赞消息列表
|
||||||
|
// LikeNoticeViewModel.isFirstLoad = true
|
||||||
|
// // 清除点赞消息数量
|
||||||
|
// MessageListViewModel.clearLikeNoticeCount()
|
||||||
|
// }
|
||||||
|
// navController.navigate(NavigationRoute.Likes.route)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// NotificationIndicator(
|
||||||
|
// MessageListViewModel.followNoticeCount,
|
||||||
|
// R.mipmap.rider_pro_followers,
|
||||||
|
// stringResource(R.string.followers_upper),
|
||||||
|
// Color(0xFFF470FE)
|
||||||
|
// ) {
|
||||||
|
// followDebouncer {
|
||||||
|
// if (MessageListViewModel.followNoticeCount > 0) {
|
||||||
|
// // 刷新关注消息列表
|
||||||
|
// FollowerNoticeViewModel.isFirstLoad = true
|
||||||
|
// MessageListViewModel.clearFollowNoticeCount()
|
||||||
|
// }
|
||||||
|
// navController.navigate(NavigationRoute.Followers.route)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// NotificationIndicator(
|
||||||
|
// MessageListViewModel.commentNoticeCount,
|
||||||
|
// R.mipmap.rider_pro_comment,
|
||||||
|
// stringResource(R.string.comment).uppercase(),
|
||||||
|
// Color(0xFF6246FF)
|
||||||
|
// ) {
|
||||||
|
// commentDebouncer {
|
||||||
|
// navController.navigate(NavigationRoute.CommentNoticeScreen.route)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
Spacer(modifier = Modifier.height(23.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.padding(start = 16.dp,bottom = 16.dp),
|
.padding(start = 16.dp,bottom = 16.dp),
|
||||||
// center the tabs
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.Bottom
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
@@ -284,7 +324,7 @@ fun NotificationsScreen() {
|
|||||||
|
|
||||||
Box {
|
Box {
|
||||||
TabItem(
|
TabItem(
|
||||||
text = stringResource(R.string.chat_ai),
|
text = stringResource(R.string.chat_all),
|
||||||
isSelected = pagerState.currentPage == 0,
|
isSelected = pagerState.currentPage == 0,
|
||||||
onClick = {
|
onClick = {
|
||||||
tabDebouncer {
|
tabDebouncer {
|
||||||
@@ -295,6 +335,38 @@ fun NotificationsScreen() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 全部未读消息红点
|
||||||
|
val totalUnreadCount = AgentChatListViewModel.totalUnreadCount +
|
||||||
|
GroupChatListViewModel.totalUnreadCount +
|
||||||
|
FriendChatListViewModel.totalUnreadCount
|
||||||
|
if (totalUnreadCount > 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.background(
|
||||||
|
color = Color(0xFFFF3B30),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = 8.dp, y = (-4).dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TabSpacer()
|
||||||
|
|
||||||
|
Box {
|
||||||
|
TabItem(
|
||||||
|
text = stringResource(R.string.chat_ai),
|
||||||
|
isSelected = pagerState.currentPage == 1,
|
||||||
|
onClick = {
|
||||||
|
tabDebouncer {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 智能体未读消息红点
|
// 智能体未读消息红点
|
||||||
if (AgentChatListViewModel.totalUnreadCount > 0) {
|
if (AgentChatListViewModel.totalUnreadCount > 0) {
|
||||||
Box(
|
Box(
|
||||||
@@ -313,11 +385,11 @@ fun NotificationsScreen() {
|
|||||||
Box {
|
Box {
|
||||||
TabItem(
|
TabItem(
|
||||||
text = stringResource(R.string.chat_group),
|
text = stringResource(R.string.chat_group),
|
||||||
isSelected = pagerState.currentPage == 1,
|
isSelected = pagerState.currentPage == 2,
|
||||||
onClick = {
|
onClick = {
|
||||||
tabDebouncer {
|
tabDebouncer {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(1)
|
pagerState.animateScrollToPage(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,14 +410,15 @@ fun NotificationsScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TabSpacer()
|
TabSpacer()
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
TabItem(
|
TabItem(
|
||||||
text = stringResource(R.string.chat_friend),
|
text = stringResource(R.string.chat_friend),
|
||||||
isSelected = pagerState.currentPage == 2,
|
isSelected = pagerState.currentPage == 3,
|
||||||
onClick = {
|
onClick = {
|
||||||
tabDebouncer {
|
tabDebouncer {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(2)
|
pagerState.animateScrollToPage(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,14 +447,17 @@ fun NotificationsScreen() {
|
|||||||
) {
|
) {
|
||||||
when (it) {
|
when (it) {
|
||||||
0 -> {
|
0 -> {
|
||||||
|
AllChatListScreen()
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
AgentChatListScreen()
|
AgentChatListScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
2 -> {
|
||||||
GroupChatListScreen()
|
GroupChatListScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> {
|
3 -> {
|
||||||
FriendChatListScreen()
|
FriendChatListScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,11 +91,14 @@ fun AgentChatListScreen() {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
|
if (isNetworkAvailable) {
|
||||||
Spacer(modifier = Modifier.height(39.dp))
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if(AppState.darkMode) R.mipmap.qs_znt_qs_as_img
|
id = if(AppState.darkMode) R.mipmap.qs_znt_qs_as_img
|
||||||
else R.mipmap.qs_znt_qs_img),
|
else R.mipmap.invalid_name_5),
|
||||||
contentDescription = "null data",
|
contentDescription = "null data",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(181.dp)
|
.size(181.dp)
|
||||||
@@ -114,6 +117,35 @@ fun AgentChatListScreen() {
|
|||||||
fontSize = 14.sp
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
AgentChatListViewModel.refreshPager(context = context)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ import com.aiosman.ravenow.ui.navigateToChatAi
|
|||||||
import io.openim.android.sdk.OpenIMClient
|
import io.openim.android.sdk.OpenIMClient
|
||||||
import io.openim.android.sdk.listener.OnBase
|
import io.openim.android.sdk.listener.OnBase
|
||||||
import io.openim.android.sdk.models.ConversationInfo
|
import io.openim.android.sdk.models.ConversationInfo
|
||||||
|
import io.openim.android.sdk.models.Message
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import com.aiosman.ravenow.utils.MessageParser
|
||||||
|
|
||||||
data class AgentConversation(
|
data class AgentConversation(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -43,39 +45,19 @@ data class AgentConversation(
|
|||||||
val lastMessage = Calendar.getInstance().apply {
|
val lastMessage = Calendar.getInstance().apply {
|
||||||
timeInMillis = conversation.latestMsgSendTime
|
timeInMillis = conversation.latestMsgSendTime
|
||||||
}
|
}
|
||||||
var displayText = conversation.latestMsg?: ""
|
|
||||||
// when (conversation.latestMsg) {
|
// 解析最新消息
|
||||||
// 101 -> { // TEXT
|
val (displayText, isSelf) = MessageParser.parseLatestMessage(conversation.latestMsg)
|
||||||
// displayText = conversation.latestMsg?: ""
|
|
||||||
// }
|
|
||||||
// 102 -> { // IMAGE
|
|
||||||
// displayText = "[图片]"
|
|
||||||
// }
|
|
||||||
// 103 -> { // AUDIO
|
|
||||||
// displayText = "[语音]"
|
|
||||||
// }
|
|
||||||
// 104 -> { // VIDEO
|
|
||||||
// displayText = "[视频]"
|
|
||||||
// }
|
|
||||||
// 105 -> { // FILE
|
|
||||||
// displayText = "[文件]"
|
|
||||||
// }
|
|
||||||
// else -> {
|
|
||||||
// displayText = "[消息]"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
return AgentConversation(
|
return AgentConversation(
|
||||||
id = conversation.conversationID,
|
id = conversation.conversationID,
|
||||||
nickname = conversation.showName ?: "",
|
nickname = conversation.showName ?: "",
|
||||||
lastMessage = conversation.latestMsg ?: "",
|
lastMessage = displayText, // 使用解析后的显示文本
|
||||||
lastMessageTime = lastMessage.time.formatChatTime(context),
|
lastMessageTime = lastMessage.time.formatChatTime(context),
|
||||||
avatar = "${ApiClient.BASE_API_URL+"/"}${conversation.faceURL}"+"?token="+"${AppStore.token}".replace("storage/avatars/", "/avatar/"),
|
avatar = "${ApiClient.BASE_API_URL+"/"}${conversation.faceURL}"+"?token="+"${AppStore.token}".replace("storage/avatars/", "/avatar/"),
|
||||||
unreadCount = conversation.unreadCount,
|
unreadCount = conversation.unreadCount,
|
||||||
trtcUserId = conversation.userID ?: "",
|
trtcUserId = conversation.userID ?: "",
|
||||||
displayText = displayText,
|
displayText = displayText,
|
||||||
// TODO: openim latestMsg
|
isSelf = isSelf // 使用解析后的发送者信息
|
||||||
isSelf = false,
|
|
||||||
// isSelf = conversation.latestMsg?.sendID == AppState.profile?.trtcUserId
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,390 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.message.tab
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
|
||||||
|
data class CombinedConversation(
|
||||||
|
val type: String, // "agent", "group", or "friend"
|
||||||
|
val agentConversation: AgentConversation? = null,
|
||||||
|
val groupConversation: GroupConversation? = null,
|
||||||
|
val friendConversation: FriendConversation? = null
|
||||||
|
) {
|
||||||
|
val id: String
|
||||||
|
get() = when (type) {
|
||||||
|
"agent" -> "agent_${agentConversation?.id ?: 0}"
|
||||||
|
"group" -> "group_${groupConversation?.id ?: 0}"
|
||||||
|
"friend" -> "friend_${friendConversation?.id ?: 0}"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val avatar: String
|
||||||
|
get() = when (type) {
|
||||||
|
"agent" -> agentConversation?.avatar ?: ""
|
||||||
|
"group" -> groupConversation?.avatar ?: ""
|
||||||
|
"friend" -> friendConversation?.avatar ?: ""
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val name: String
|
||||||
|
get() = when (type) {
|
||||||
|
"agent" -> agentConversation?.nickname ?: ""
|
||||||
|
"group" -> groupConversation?.groupName ?: ""
|
||||||
|
"friend" -> friendConversation?.nickname ?: ""
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastMessageTime: String
|
||||||
|
get() = when (type) {
|
||||||
|
"agent" -> agentConversation?.lastMessageTime ?: ""
|
||||||
|
"group" -> groupConversation?.lastMessageTime ?: ""
|
||||||
|
"friend" -> friendConversation?.lastMessageTime ?: ""
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val displayText: String
|
||||||
|
get() = when (type) {
|
||||||
|
"agent" -> agentConversation?.displayText ?: ""
|
||||||
|
"group" -> groupConversation?.displayText ?: ""
|
||||||
|
"friend" -> friendConversation?.displayText ?: ""
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val unreadCount: Int
|
||||||
|
get() = when (type) {
|
||||||
|
"agent" -> agentConversation?.unreadCount ?: 0
|
||||||
|
"group" -> groupConversation?.unreadCount ?: 0
|
||||||
|
"friend" -> friendConversation?.unreadCount ?: 0
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSelf: Boolean
|
||||||
|
get() = when (type) {
|
||||||
|
"agent" -> agentConversation?.isSelf ?: false
|
||||||
|
"group" -> groupConversation?.isSelf ?: false
|
||||||
|
"friend" -> friendConversation?.isSelf ?: false
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AllChatListScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
var allConversations by remember { mutableStateOf<List<CombinedConversation>>(emptyList()) }
|
||||||
|
var refreshing by remember { mutableStateOf(false) }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val state = rememberPullRefreshState(
|
||||||
|
refreshing = refreshing,
|
||||||
|
onRefresh = {
|
||||||
|
refreshing = true
|
||||||
|
refreshAllData(context,
|
||||||
|
onSuccess = { conversations ->
|
||||||
|
allConversations = conversations
|
||||||
|
refreshing = false
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
error = errorMsg
|
||||||
|
refreshing = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
isLoading = true
|
||||||
|
refreshAllData(context,
|
||||||
|
onSuccess = { conversations ->
|
||||||
|
allConversations = conversations
|
||||||
|
isLoading = false
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
error = errorMsg
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pullRefresh(state)
|
||||||
|
) {
|
||||||
|
if (allConversations.isEmpty() && !isLoading) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
|
if (isNetworkAvailable) {
|
||||||
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
|
Image(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img
|
||||||
|
else R.mipmap.invalid_name_2),
|
||||||
|
contentDescription = "null data",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(181.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_empty_title),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_empty_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
} 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
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
isLoading = true
|
||||||
|
refreshAllData(context,
|
||||||
|
onSuccess = { conversations ->
|
||||||
|
allConversations = conversations
|
||||||
|
isLoading = false
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
error = errorMsg
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
items = allConversations,
|
||||||
|
key = { _, item -> item.id }
|
||||||
|
) { index, item ->
|
||||||
|
when (item.type) {
|
||||||
|
"agent" -> {
|
||||||
|
item.agentConversation?.let { agent ->
|
||||||
|
AgentChatItem(
|
||||||
|
conversation = agent,
|
||||||
|
onUserAvatarClick = { conv ->
|
||||||
|
AgentChatListViewModel.goToUserDetail(conv, navController)
|
||||||
|
},
|
||||||
|
onChatClick = { conv ->
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
|
AgentChatListViewModel.createSingleChat(conv.trtcUserId)
|
||||||
|
AgentChatListViewModel.goToChatAi(conv.trtcUserId, navController)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"group" -> {
|
||||||
|
item.groupConversation?.let { group ->
|
||||||
|
GroupChatItem(
|
||||||
|
conversation = group,
|
||||||
|
onGroupAvatarClick = { conv ->
|
||||||
|
GroupChatListViewModel.goToGroupDetail(conv, navController)
|
||||||
|
},
|
||||||
|
onChatClick = { conv ->
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
|
GroupChatListViewModel.goToChat(conv, navController)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"friend" -> {
|
||||||
|
item.friendConversation?.let { friend ->
|
||||||
|
FriendChatItem(
|
||||||
|
conversation = friend,
|
||||||
|
onUserAvatarClick = { conv ->
|
||||||
|
FriendChatListViewModel.goToUserDetail(conv, navController)
|
||||||
|
},
|
||||||
|
onChatClick = { conv ->
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
|
FriendChatListViewModel.goToChat(conv, navController)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分隔线
|
||||||
|
// if (index < allConversations.size - 1) {
|
||||||
|
// HorizontalDivider(
|
||||||
|
// modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
// color = AppColors.divider
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading && allConversations.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = AppColors.main
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = refreshing,
|
||||||
|
state = state,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
error?.let { errorMsg ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = errorMsg,
|
||||||
|
color = AppColors.error,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAllData(
|
||||||
|
context: android.content.Context,
|
||||||
|
onSuccess: (List<CombinedConversation>) -> Unit,
|
||||||
|
onError: (String) -> Unit
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 同时刷新所有类型的数据
|
||||||
|
AgentChatListViewModel.refreshPager(context = context)
|
||||||
|
GroupChatListViewModel.refreshPager(context = context)
|
||||||
|
FriendChatListViewModel.refreshPager(context = context)
|
||||||
|
|
||||||
|
val combinedList = mutableListOf<CombinedConversation>()
|
||||||
|
|
||||||
|
AgentChatListViewModel.agentChatList.forEach { agent ->
|
||||||
|
combinedList.add(CombinedConversation(type = "agent", agentConversation = agent))
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupChatListViewModel.groupChatList.forEach { group ->
|
||||||
|
combinedList.add(CombinedConversation(type = "group", groupConversation = group))
|
||||||
|
}
|
||||||
|
|
||||||
|
FriendChatListViewModel.friendChatList.forEach { friend ->
|
||||||
|
val isDuplicate = combinedList.any {//判断重复
|
||||||
|
it.type == "agent" && it.agentConversation?.trtcUserId == friend.trtcUserId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
combinedList.add(CombinedConversation(type = "friend", friendConversation = friend))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按最后消息时间排序
|
||||||
|
val sortedList = combinedList.sortedByDescending {
|
||||||
|
it.lastMessageTime
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess(sortedList)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onError("刷新数据失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,8 +33,14 @@ 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.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.graphics.Brush
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -73,12 +79,14 @@ fun FriendChatListScreen() {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
//verticalArrangement = Arrangement.Center
|
//verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
|
if (isNetworkAvailable) {
|
||||||
Spacer(modifier = Modifier.height(39.dp))
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img
|
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img
|
||||||
else R.mipmap.qs_py_qs_img),
|
else R.mipmap.invalid_name_2),
|
||||||
contentDescription = "null data",
|
contentDescription = "null data",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(181.dp)
|
.size(181.dp)
|
||||||
@@ -96,6 +104,34 @@ fun FriendChatListScreen() {
|
|||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
|
}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
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -266,4 +302,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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import com.aiosman.ravenow.data.api.ApiClient
|
|||||||
import io.openim.android.sdk.OpenIMClient
|
import io.openim.android.sdk.OpenIMClient
|
||||||
import io.openim.android.sdk.listener.OnBase
|
import io.openim.android.sdk.listener.OnBase
|
||||||
import io.openim.android.sdk.models.ConversationInfo
|
import io.openim.android.sdk.models.ConversationInfo
|
||||||
|
import io.openim.android.sdk.models.Message
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import com.aiosman.ravenow.utils.MessageParser
|
||||||
|
|
||||||
data class FriendConversation(
|
data class FriendConversation(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -40,17 +42,20 @@ data class FriendConversation(
|
|||||||
val lastMessage = Calendar.getInstance().apply {
|
val lastMessage = Calendar.getInstance().apply {
|
||||||
timeInMillis = conversation.latestMsgSendTime
|
timeInMillis = conversation.latestMsgSendTime
|
||||||
}
|
}
|
||||||
var displayText = conversation.latestMsg
|
|
||||||
|
// 解析最新消息
|
||||||
|
val (displayText, isSelf) = MessageParser.parseLatestMessage(conversation.latestMsg)
|
||||||
|
|
||||||
return FriendConversation(
|
return FriendConversation(
|
||||||
id = conversation.conversationID,
|
id = conversation.conversationID,
|
||||||
nickname = conversation.showName ?: "",
|
nickname = conversation.showName ?: "",
|
||||||
lastMessage = conversation.latestMsg ?: "",
|
lastMessage = displayText, // 使用解析后的显示文本
|
||||||
lastMessageTime = lastMessage.time.formatChatTime(context),
|
lastMessageTime = lastMessage.time.formatChatTime(context),
|
||||||
avatar = "${ApiClient.BASE_API_URL+"/"}${conversation.faceURL}"+"?token="+"${AppStore.token}".replace("storage/avatars/", "/avatar/"),
|
avatar = "${ApiClient.BASE_API_URL+"/"}${conversation.faceURL}"+"?token="+"${AppStore.token}".replace("storage/avatars/", "/avatar/"),
|
||||||
unreadCount = conversation.unreadCount,
|
unreadCount = conversation.unreadCount,
|
||||||
trtcUserId = conversation.userID ?: "",
|
trtcUserId = conversation.userID ?: "",
|
||||||
displayText = displayText,
|
displayText = displayText,
|
||||||
isSelf = false
|
isSelf = isSelf // 使用解析后的发送者信息
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,11 +71,14 @@ fun GroupChatListScreen() {
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
|
if (isNetworkAvailable) {
|
||||||
Spacer(modifier = Modifier.height(39.dp))
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if(AppState.darkMode) R.mipmap.qs_ql_qs_as_img
|
id = if(AppState.darkMode) R.mipmap.qs_ql_qs_as_img
|
||||||
else R.mipmap.qs_ql_qs_img),
|
else R.mipmap.invalid_name_12),
|
||||||
contentDescription = "null data",
|
contentDescription = "null data",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(181.dp)
|
.size(181.dp)
|
||||||
@@ -93,6 +96,34 @@ fun GroupChatListScreen() {
|
|||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
|
}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
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
GroupChatListViewModel.refreshPager(context = context)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.index.tabs.message.tab
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.icu.util.Calendar
|
import android.icu.util.Calendar
|
||||||
|
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
|
||||||
@@ -26,8 +27,10 @@ import io.openim.android.sdk.listener.OnAdvanceMsgListener
|
|||||||
import io.openim.android.sdk.listener.OnBase
|
import io.openim.android.sdk.listener.OnBase
|
||||||
import io.openim.android.sdk.listener.OnConversationListener
|
import io.openim.android.sdk.listener.OnConversationListener
|
||||||
import io.openim.android.sdk.models.ConversationInfo
|
import io.openim.android.sdk.models.ConversationInfo
|
||||||
|
import io.openim.android.sdk.models.Message
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import com.aiosman.ravenow.utils.MessageParser
|
||||||
|
|
||||||
data class GroupConversation(
|
data class GroupConversation(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -47,11 +50,14 @@ data class GroupConversation(
|
|||||||
timeInMillis = conversation.latestMsgSendTime
|
timeInMillis = conversation.latestMsgSendTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析最新消息
|
||||||
|
val (displayText, isSelf) = MessageParser.parseLatestMessage(conversation.latestMsg)
|
||||||
|
|
||||||
return GroupConversation(
|
return GroupConversation(
|
||||||
id = conversation.conversationID,
|
id = conversation.conversationID,
|
||||||
groupId = conversation.groupID ?: "",
|
groupId = conversation.groupID ?: "",
|
||||||
groupName = conversation.showName ?: "",
|
groupName = conversation.showName ?: "",
|
||||||
lastMessage = conversation.latestMsg?: "",
|
lastMessage = displayText, // 使用解析后的显示文本
|
||||||
lastMessageTime = lastMessage.time.formatChatTime(context),
|
lastMessageTime = lastMessage.time.formatChatTime(context),
|
||||||
avatar = if (conversation.faceURL.isNullOrEmpty()) {
|
avatar = if (conversation.faceURL.isNullOrEmpty()) {
|
||||||
// 将 groupId 转换为 Base64
|
// 将 groupId 转换为 Base64
|
||||||
@@ -64,10 +70,9 @@ data class GroupConversation(
|
|||||||
"${ApiClient.BASE_API_URL+"/outside/rooms/avatar/"}${conversation.faceURL}"+"?token="+"${AppStore.token}"
|
"${ApiClient.BASE_API_URL+"/outside/rooms/avatar/"}${conversation.faceURL}"+"?token="+"${AppStore.token}"
|
||||||
},
|
},
|
||||||
unreadCount = conversation.unreadCount,
|
unreadCount = conversation.unreadCount,
|
||||||
displayText = conversation.latestMsg?: "",
|
displayText = displayText,
|
||||||
isSelf = false,
|
isSelf = isSelf, // 使用解析后的发送者信息
|
||||||
// TODO openim get grouplist
|
memberCount = 0 // TODO: 获取群组成员数量
|
||||||
memberCount = 0
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,10 +168,12 @@ object GroupChatListViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createGroupChat(
|
fun createGroupChat(
|
||||||
trtcGroupId: String,
|
trtcGroupId: String? = null,
|
||||||
|
roomId: Int? = null
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId)
|
val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId,roomId = roomId)
|
||||||
|
Log.d("debug",response.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,12 @@ open class BaseMomentModel :ViewModel(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun onAddComment(id: Int) {
|
fun onAddComment(id: Int) {
|
||||||
// val currentPagingData = _momentsFlow.value
|
momentLoader.updateCommentCount(id, +1)
|
||||||
// updateCommentCount(id)
|
}
|
||||||
|
|
||||||
|
fun onDeleteComment(id: Int) {
|
||||||
|
momentLoader.updateCommentCount(id, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -83,6 +86,7 @@ open class BaseMomentModel :ViewModel(){
|
|||||||
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
|
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
|
||||||
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
|
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun favoriteMoment(id: Int) {
|
suspend fun favoriteMoment(id: Int) {
|
||||||
momentService.favoriteMoment(id)
|
momentService.favoriteMoment(id)
|
||||||
momentLoader.updateFavoriteCount(id, true)
|
momentLoader.updateFavoriteCount(id, true)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.aiosman.ravenow.ui.index.tabs.moment
|
|||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
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
|
||||||
@@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.systemBars
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
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.Icon
|
import androidx.compose.material.Icon
|
||||||
@@ -30,9 +32,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.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
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.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
@@ -48,9 +47,18 @@ import com.aiosman.ravenow.ui.index.tabs.moment.tabs.dynamic.Dynamic
|
|||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.Explore
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.Explore
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentsList
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentsList
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.NewsScreen
|
||||||
import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel
|
import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import com.aiosman.ravenow.ui.composables.TabItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.UnderlineTabItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态列表
|
* 动态列表
|
||||||
@@ -63,8 +71,8 @@ fun MomentsList() {
|
|||||||
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()
|
||||||
// 游客模式下不显示timeline,只显示3个tab:Explore、Dynamic、Hot
|
// 现在有6个tab:推荐、短视频、新闻、探索、关注、热门
|
||||||
val tabCount = if (AppStore.isGuest) 3 else 4
|
val tabCount = 6
|
||||||
var pagerState = rememberPagerState { tabCount }
|
var pagerState = rememberPagerState { tabCount }
|
||||||
var scope = rememberCoroutineScope()
|
var scope = rememberCoroutineScope()
|
||||||
Column(
|
Column(
|
||||||
@@ -76,159 +84,142 @@ fun MomentsList() {
|
|||||||
),
|
),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
// 顶部区域:可滚动的标签页 + 搜索按钮
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(44.dp),
|
.height(44.dp)
|
||||||
// center the tabs
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 可滚动的标签页行
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(
|
val tabDebouncer = rememberDebouncer()
|
||||||
modifier = Modifier
|
|
||||||
.noRippleClickable {
|
// 推荐标签
|
||||||
|
UnderlineTabItem(
|
||||||
|
text = stringResource(R.string.tab_recommend),
|
||||||
|
isSelected = pagerState.currentPage == 0,
|
||||||
|
onClick = {
|
||||||
|
tabDebouncer {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(0)
|
pagerState.animateScrollToPage(0)
|
||||||
}
|
}
|
||||||
}.padding(start = 16.dp),
|
}
|
||||||
verticalArrangement = Arrangement.Center,
|
}
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.index_worldwide),
|
|
||||||
fontSize = if (pagerState.currentPage == 0)18.sp else 16.sp,
|
|
||||||
color = if (pagerState.currentPage == 0) AppColors.text else AppColors.nonActiveText,
|
|
||||||
fontWeight = FontWeight.W600)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(
|
|
||||||
if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
|
|
||||||
else R.drawable.tab_indicator_unselected
|
|
||||||
),
|
|
||||||
contentDescription = "tab indicator",
|
|
||||||
modifier = Modifier
|
|
||||||
.width(34.dp)
|
|
||||||
.height(4.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
// 短视频标签
|
||||||
Column(
|
UnderlineTabItem(
|
||||||
modifier = Modifier
|
text = stringResource(R.string.tab_short_video),
|
||||||
.noRippleClickable {
|
isSelected = pagerState.currentPage == 1,
|
||||||
|
onClick = {
|
||||||
|
tabDebouncer {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(1)
|
pagerState.animateScrollToPage(1)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
verticalArrangement = Arrangement.Center,
|
}
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.index_dynamic),
|
|
||||||
fontSize = if (pagerState.currentPage == 1)18.sp else 16.sp,
|
|
||||||
color = if (pagerState.currentPage == 1) AppColors.text else AppColors.nonActiveText,
|
|
||||||
fontWeight = FontWeight.W600)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(
|
|
||||||
if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected
|
|
||||||
else R.drawable.tab_indicator_unselected
|
|
||||||
),
|
|
||||||
contentDescription = "tab indicator",
|
|
||||||
modifier = Modifier
|
|
||||||
.width(34.dp)
|
|
||||||
.height(4.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有非游客用户才显示"关注"tab
|
// 动态标签
|
||||||
if (!AppStore.isGuest) {
|
UnderlineTabItem(
|
||||||
//关注tab
|
text = stringResource(R.string.moment),
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
isSelected = pagerState.currentPage == 2,
|
||||||
Column(
|
onClick = {
|
||||||
modifier = Modifier
|
tabDebouncer {
|
||||||
.noRippleClickable {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(2)
|
pagerState.animateScrollToPage(2)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
verticalArrangement = Arrangement.Center,
|
}
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
)
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
|
// 只有非游客用户才显示"关注"tab
|
||||||
|
if (!AppStore.isGuest) {
|
||||||
|
UnderlineTabItem(
|
||||||
text = stringResource(R.string.index_following),
|
text = stringResource(R.string.index_following),
|
||||||
fontSize = if (pagerState.currentPage == 2)18.sp else 16.sp,
|
isSelected = pagerState.currentPage == 3,
|
||||||
color = if (pagerState.currentPage == 2) AppColors.text else AppColors.nonActiveText,
|
onClick = {
|
||||||
fontWeight = FontWeight.W600)
|
tabDebouncer {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(
|
|
||||||
if (pagerState.currentPage == 2) R.mipmap.tab_indicator_selected
|
|
||||||
else R.drawable.tab_indicator_unselected
|
|
||||||
),
|
|
||||||
contentDescription = "tab indicator",
|
|
||||||
modifier = Modifier
|
|
||||||
.width(34.dp)
|
|
||||||
.height(4.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//热门tab
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.noRippleClickable {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val targetPage = if (AppStore.isGuest) 2 else 3
|
pagerState.animateScrollToPage(3)
|
||||||
pagerState.animateScrollToPage(targetPage)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.index_hot),
|
|
||||||
fontSize = if ((AppStore.isGuest && pagerState.currentPage == 2) || (!AppStore.isGuest && pagerState.currentPage == 3)) 18.sp else 16.sp,
|
|
||||||
color = if ((AppStore.isGuest && pagerState.currentPage == 2) || (!AppStore.isGuest && pagerState.currentPage == 3)) AppColors.text else AppColors.nonActiveText,
|
|
||||||
fontWeight = FontWeight.W600)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(
|
|
||||||
if ((AppStore.isGuest && pagerState.currentPage == 2) || (!AppStore.isGuest && pagerState.currentPage == 3)) R.mipmap.tab_indicator_selected
|
|
||||||
else R.drawable.tab_indicator_unselected
|
|
||||||
),
|
|
||||||
contentDescription = "tab indicator",
|
|
||||||
modifier = Modifier
|
|
||||||
.width(34.dp)
|
|
||||||
.height(4.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// 热门标签
|
||||||
|
UnderlineTabItem(
|
||||||
|
text = stringResource(R.string.index_hot),
|
||||||
|
isSelected = pagerState.currentPage == 4,
|
||||||
|
onClick = {
|
||||||
|
tabDebouncer {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(4)
|
||||||
}
|
}
|
||||||
//搜索按钮
|
}
|
||||||
Column(
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 热门标签 (游客模式)
|
||||||
|
UnderlineTabItem(
|
||||||
|
text = stringResource(R.string.index_hot),
|
||||||
|
isSelected = pagerState.currentPage == 4,
|
||||||
|
onClick = {
|
||||||
|
tabDebouncer {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 新闻标签
|
||||||
|
UnderlineTabItem(
|
||||||
|
text = stringResource(R.string.tab_news),
|
||||||
|
isSelected = pagerState.currentPage == 5,
|
||||||
|
onClick = {
|
||||||
|
tabDebouncer {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索按钮
|
||||||
|
val lastClickTime = remember { mutableStateOf(0L) }
|
||||||
|
val clickDelay = 500L
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
|
contentDescription = "search",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 8.dp, end = 16.dp)
|
.size(24.dp)
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.End
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
ImageVector.vectorResource(R.drawable.rider_pro_nav_search),
|
|
||||||
contentDescription = "Clickable Icon",
|
|
||||||
tint = AppColors.text,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastClickTime.value > clickDelay) {
|
||||||
|
lastClickTime.value = currentTime
|
||||||
navController.navigate(NavigationRoute.Search.route)
|
navController.navigate(NavigationRoute.Search.route)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
@@ -236,38 +227,62 @@ fun MomentsList() {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
if (AppStore.isGuest) {
|
|
||||||
// 游客模式:Explore(0), Dynamic(1), Hot(2)
|
|
||||||
when (it) {
|
when (it) {
|
||||||
0 -> {
|
0 -> {
|
||||||
Explore()
|
// 推荐页面
|
||||||
|
NewsScreen()
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
Dynamic()
|
// 短视频页面
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
HotMomentsList()
|
// 动态页面 - 暂时显示时间线内容
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 正常用户:Explore(0), Dynamic(1), Timeline(2), Hot(3)
|
|
||||||
when (it) {
|
|
||||||
0 -> {
|
|
||||||
Explore()
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
Dynamic()
|
Dynamic()
|
||||||
}
|
}
|
||||||
2 -> {
|
|
||||||
TimelineMomentsList()
|
|
||||||
}
|
|
||||||
3 -> {
|
3 -> {
|
||||||
|
// 关注页面 (仅非游客用户) 或 热门页面 (游客用户)
|
||||||
|
if (AppStore.isGuest) {
|
||||||
|
HotMomentsList()
|
||||||
|
} else {
|
||||||
|
TimelineMomentsList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
// 热门页面 (仅非游客用户)
|
||||||
HotMomentsList()
|
HotMomentsList()
|
||||||
}
|
}
|
||||||
|
5 -> {
|
||||||
|
// 新闻页面
|
||||||
|
NewsScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Composable
|
||||||
|
fun CustomTabItem(
|
||||||
|
text: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.noRippleClickable { onClick() },
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = if (isSelected) AppColors.tabSelectedText else AppColors.tabUnselectedText,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(if (isSelected) AppColors.tabSelectedBackground else AppColors.tabUnselectedBackground)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ fun Dynamic() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ fun Explore() {
|
|||||||
trtcId = roomItem.trtcId.toString(),
|
trtcId = roomItem.trtcId.toString(),
|
||||||
name = roomItem.title,
|
name = roomItem.title,
|
||||||
avatar = roomItem.avatar,
|
avatar = roomItem.avatar,
|
||||||
|
context = context,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()
|
||||||
@@ -523,7 +524,7 @@ fun Explore() {
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
/* Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.rider_pro_nav_profile),
|
painter = painterResource(R.drawable.rider_pro_nav_profile),
|
||||||
contentDescription = "chat",
|
contentDescription = "chat",
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
@@ -535,7 +536,7 @@ fun Explore() {
|
|||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
||||||
)*/
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部:标题和描述
|
// 底部:标题和描述
|
||||||
@@ -636,6 +637,7 @@ fun Explore() {
|
|||||||
trtcId = bannerItem.trtcId.toString(),
|
trtcId = bannerItem.trtcId.toString(),
|
||||||
name = bannerItem.title,
|
name = bannerItem.title,
|
||||||
avatar = bannerItem.avatar,
|
avatar = bannerItem.avatar,
|
||||||
|
context = context,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userServic
|
|||||||
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel
|
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel
|
||||||
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
|
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
|
||||||
import com.aiosman.ravenow.ui.navigateToGroupChat
|
import com.aiosman.ravenow.ui.navigateToGroupChat
|
||||||
|
import com.aiosman.ravenow.data.api.ApiErrorResponse
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ExploreViewModel : ViewModel() {
|
class ExploreViewModel : ViewModel() {
|
||||||
@@ -130,21 +134,24 @@ class ExploreViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createSingleChat(
|
fun createSingleChat(
|
||||||
openId: String,
|
openId: String,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val response = ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
|
val response =
|
||||||
|
ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun goToChatAi(
|
fun goToChatAi(
|
||||||
openId: String,
|
openId: String,
|
||||||
navController: NavHostController
|
navController: NavHostController
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val profile = userService.getUserProfileByOpenId(openId)
|
val profile = userService.getUserProfileByOpenId(openId)
|
||||||
createGroup2ChatAi(profile.trtcUserId,"ai_group",navController,profile.id)
|
createGroup2ChatAi(profile.trtcUserId, "ai_group", navController, profile.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +159,7 @@ class ExploreViewModel : ViewModel() {
|
|||||||
trtcId: String,
|
trtcId: String,
|
||||||
name: String,
|
name: String,
|
||||||
avatar: String,
|
avatar: String,
|
||||||
|
context: Context,
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
onSuccess: () -> Unit,
|
onSuccess: () -> Unit,
|
||||||
onError: (String) -> Unit
|
onError: (String) -> Unit
|
||||||
@@ -164,9 +172,11 @@ class ExploreViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
createGroupChat(trtcGroupId = trtcId)
|
createGroupChat(trtcGroupId = trtcId)
|
||||||
// 群聊直接使用群ID进行导航
|
// 群聊直接使用群ID进行导航
|
||||||
navController.navigateToGroupChat( id = trtcId,
|
navController.navigateToGroupChat(
|
||||||
|
id = trtcId,
|
||||||
name = name,
|
name = name,
|
||||||
avatar = avatar)
|
avatar = avatar
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
onError("加入房间失败")
|
onError("加入房间失败")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -175,9 +185,28 @@ class ExploreViewModel : ViewModel() {
|
|||||||
onSuccess()
|
onSuccess()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// 处理错误响应
|
||||||
|
try {
|
||||||
|
val errorBody = response.errorBody()?.string()
|
||||||
|
if (errorBody != null) {
|
||||||
|
val gson = Gson()
|
||||||
|
val errorResponse = gson.fromJson(errorBody, ApiErrorResponse::class.java)
|
||||||
|
|
||||||
|
// 在主线程显示 Toast
|
||||||
|
Toast.makeText(context, errorResponse.error, Toast.LENGTH_LONG).show()
|
||||||
|
onError(errorResponse.error)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
|
||||||
onError("加入房间失败")
|
onError("加入房间失败")
|
||||||
}
|
}
|
||||||
|
} catch (parseException: Exception) {
|
||||||
|
// 如果解析错误响应失败,显示默认错误信息
|
||||||
|
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
|
||||||
|
onError("加入房间失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(context, "网络请求失败:${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
onError("网络请求失败:${e.message}")
|
onError("网络请求失败:${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.Image
|
|||||||
import androidx.compose.foundation.background
|
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.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
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
|
||||||
@@ -13,11 +14,12 @@ import androidx.compose.foundation.layout.navigationBars
|
|||||||
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.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
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
|
||||||
@@ -39,6 +41,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
@@ -104,8 +107,8 @@ fun DiscoverView() {
|
|||||||
val isLoading by model.isLoading.collectAsState()
|
val isLoading by model.isLoading.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyStaggeredGridState()
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
// 监听滚动到底部,自动加载更多
|
// 监听滚动到底部,自动加载更多
|
||||||
LaunchedEffect(gridState, moments.size) {
|
LaunchedEffect(gridState, moments.size) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
@@ -124,18 +127,40 @@ fun DiscoverView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalStaggeredGrid(
|
||||||
columns = GridCells.Fixed(3),
|
columns = StaggeredGridCells.Fixed(2),
|
||||||
state = gridState,
|
state = gridState,
|
||||||
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
|
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
|
||||||
// contentPadding = PaddingValues(8.dp)
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
||||||
) {
|
) {
|
||||||
items(moments) { momentItem ->
|
items(moments) { momentItem ->
|
||||||
val debouncer = rememberDebouncer()
|
val debouncer = rememberDebouncer()
|
||||||
|
|
||||||
|
val textContent = momentItem.momentTextContent
|
||||||
|
val textLines = if (textContent.isNotEmpty()) {
|
||||||
|
val estimatedCharsPerLine = 20
|
||||||
|
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1
|
||||||
|
minOf(estimatedLines, 2) // 最多2行
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseHeight = 200.dp
|
||||||
|
val singleLineTextHeight = 20.dp
|
||||||
|
val doubleLineTextHeight = 40.dp
|
||||||
|
val authorInfoHeight = 25.dp
|
||||||
|
val paddingHeight = 10.dp
|
||||||
|
val paddingHeight2 =3.dp
|
||||||
|
val totalHeight = baseHeight + when (textLines) {
|
||||||
|
0 -> authorInfoHeight + paddingHeight
|
||||||
|
1 -> singleLineTextHeight + authorInfoHeight + paddingHeight
|
||||||
|
else -> doubleLineTextHeight + authorInfoHeight +paddingHeight2
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f)
|
.height(totalHeight)
|
||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
debouncer {
|
debouncer {
|
||||||
@@ -146,15 +171,69 @@ fun DiscoverView() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
||||||
) {
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = momentItem.images[0].thumbnail,
|
imageUrl = momentItem.images[0].thumbnail,
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxWidth()
|
||||||
|
.height(baseHeight)
|
||||||
|
.clip(RoundedCornerShape(
|
||||||
|
topStart = 12.dp,
|
||||||
|
topEnd = 12.dp,
|
||||||
|
bottomStart = 0.dp,
|
||||||
|
bottomEnd = 0.dp)),
|
||||||
context = context,
|
context = context,
|
||||||
showShimmer = true
|
showShimmer = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(totalHeight - baseHeight)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
if (momentItem.momentTextContent.isNotEmpty()) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = momentItem.momentTextContent,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = momentItem.avatar,
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
context = context,
|
||||||
|
showShimmer = true
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = momentItem.nickname,
|
||||||
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = AppColors.text.copy(alpha = 0.6f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (momentItem.images.size > 1) {
|
if (momentItem.images.size > 1) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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.R
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.exp.formatPostTime2
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FullArticleModal(
|
||||||
|
moment: MomentEntity,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
|
val sheetHeight = screenHeight * 0.9f // 90% 高度
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(sheetHeight),
|
||||||
|
containerColor = appColors.background,
|
||||||
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
|
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 滚动内容
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
// 新闻图片区域 - 固定高度和宽度
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(250.dp)
|
||||||
|
|
||||||
|
.background(color = appColors.secondaryBackground)
|
||||||
|
) {
|
||||||
|
if (moment.images.isNotEmpty()) {
|
||||||
|
val firstImage = moment.images[0]
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = context,
|
||||||
|
imageUrl = firstImage.url,
|
||||||
|
contentDescription = "新闻图片",
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
blurHash = firstImage.blurHash,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = androidx.compose.ui.res.painterResource(id = R.drawable.default_moment_img),
|
||||||
|
contentDescription = "默认图片",
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 新闻标题
|
||||||
|
Text(
|
||||||
|
text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = appColors.text,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// 新闻来源和发布时间
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 来源按钮
|
||||||
|
Button(
|
||||||
|
onClick = { },
|
||||||
|
modifier = Modifier.height(28.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color(0xFF7c68ef)
|
||||||
|
),
|
||||||
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (moment.newsSource.isNotEmpty()) moment.newsSource else moment.nickname,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发布时间
|
||||||
|
Text(
|
||||||
|
text = moment.time.formatPostTime2(),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = appColors.secondaryText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 帖子内容
|
||||||
|
NewsContent(
|
||||||
|
content = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
|
||||||
|
images = moment.images,
|
||||||
|
context = context
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(200.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NewsContent(
|
||||||
|
content: String,
|
||||||
|
images: List<com.aiosman.ravenow.entity.MomentImageEntity>,
|
||||||
|
context: android.content.Context
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = content,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
lineHeight = 24.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
// 图片内容
|
||||||
|
if (images.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
images.forEach { image ->
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = context,
|
||||||
|
imageUrl = image.url,
|
||||||
|
contentDescription = "内容图片",
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
blurHash = image.blurHash,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.data.CommentService
|
||||||
|
import com.aiosman.ravenow.data.CommentServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
||||||
|
import com.aiosman.ravenow.ui.composables.debouncedClickable
|
||||||
|
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.ui.post.CommentContent
|
||||||
|
import com.aiosman.ravenow.ui.post.CommentMenuModal
|
||||||
|
import com.aiosman.ravenow.ui.post.CommentsViewModel
|
||||||
|
import com.aiosman.ravenow.ui.post.OrderSelectionComponent
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
class NewsCommentModalViewModel(
|
||||||
|
val postId: Int?
|
||||||
|
) : ViewModel() {
|
||||||
|
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId.toString())
|
||||||
|
var commentService: CommentService = CommentServiceImpl()
|
||||||
|
|
||||||
|
init {
|
||||||
|
commentsViewModel.preTransit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun likeComment(commentId: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
commentsViewModel.likeComment(commentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unlikeComment(commentId: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
commentsViewModel.unlikeComment(commentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createComment(
|
||||||
|
content: String,
|
||||||
|
parentCommentId: Int? = null,
|
||||||
|
replyUserId: Int? = null,
|
||||||
|
replyCommentId: Int? = null
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
commentsViewModel.createComment(
|
||||||
|
content = content,
|
||||||
|
parentCommentId = parentCommentId,
|
||||||
|
replyUserId = replyUserId,
|
||||||
|
replyCommentId = replyCommentId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteComment(commentId: Int) {
|
||||||
|
commentsViewModel.deleteComment(commentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新闻评论弹窗
|
||||||
|
// @param postId 新闻帖子ID
|
||||||
|
// @param commentCount 评论数量
|
||||||
|
// @param onDismiss 关闭回调
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun NewsCommentModal(
|
||||||
|
postId: Int? = null,
|
||||||
|
commentCount: Int = 0,
|
||||||
|
onDismiss: () -> Unit = {},
|
||||||
|
onCommentAdded: () -> Unit = {},
|
||||||
|
onCommentDeleted: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val debouncedNavigation = rememberDebouncedNavigation()
|
||||||
|
|
||||||
|
// 实时评论数状态
|
||||||
|
var currentCommentCount by remember { mutableStateOf(commentCount) }
|
||||||
|
|
||||||
|
val model = viewModel<NewsCommentModalViewModel>(
|
||||||
|
key = "NewsCommentModalViewModel_$postId",
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return NewsCommentModalViewModel(postId) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val commentViewModel = model.commentsViewModel
|
||||||
|
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
|
|
||||||
|
var showCommentMenu by remember { mutableStateOf(false) }
|
||||||
|
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
|
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
|
|
||||||
|
// 菜单弹窗
|
||||||
|
if (showCommentMenu) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = {
|
||||||
|
showCommentMenu = false
|
||||||
|
},
|
||||||
|
containerColor = AppColors.background,
|
||||||
|
sheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true
|
||||||
|
),
|
||||||
|
dragHandle = {},
|
||||||
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
|
windowInsets = WindowInsets(0)
|
||||||
|
) {
|
||||||
|
CommentMenuModal(
|
||||||
|
onDeleteClick = {
|
||||||
|
showCommentMenu = false
|
||||||
|
contextComment?.let {
|
||||||
|
model.deleteComment(it.id)
|
||||||
|
onCommentDeleted()
|
||||||
|
currentCommentCount = (currentCommentCount - 1).coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
commentEntity = contextComment,
|
||||||
|
onCloseClick = {
|
||||||
|
showCommentMenu = false
|
||||||
|
},
|
||||||
|
isSelf = AppState.UserId?.toLong() == contextComment?.author,
|
||||||
|
onLikeClick = {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCommentMenu = false
|
||||||
|
contextComment?.let {
|
||||||
|
if (it.liked) {
|
||||||
|
model.unlikeComment(it.id)
|
||||||
|
} else {
|
||||||
|
model.likeComment(it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onReplyClick = {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCommentMenu = false
|
||||||
|
replyComment = contextComment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${currentCommentCount}条评论",
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
|
||||||
|
// 排序选择
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
OrderSelectionComponent {
|
||||||
|
commentViewModel.order = it
|
||||||
|
commentViewModel.reloadComment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评论列表
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
CommentContent(
|
||||||
|
viewModel = commentViewModel,
|
||||||
|
onLongClick = { comment ->
|
||||||
|
showCommentMenu = true
|
||||||
|
contextComment = comment
|
||||||
|
},
|
||||||
|
onReply = { parentComment, _, _, _ ->
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
replyComment = parentComment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部输入栏
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
HorizontalDivider(color = AppColors.inputBackground)
|
||||||
|
|
||||||
|
EditCommentBottomModal(
|
||||||
|
replyComment = replyComment,
|
||||||
|
autoFocus = false
|
||||||
|
) {
|
||||||
|
if (replyComment != null) {
|
||||||
|
if (replyComment?.parentCommentId != null) {
|
||||||
|
// 第三级评论
|
||||||
|
model.createComment(
|
||||||
|
content = it,
|
||||||
|
parentCommentId = replyComment?.parentCommentId,
|
||||||
|
replyUserId = replyComment?.author?.toInt(),
|
||||||
|
replyCommentId = replyComment?.id
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 子级评论
|
||||||
|
model.createComment(
|
||||||
|
content = it,
|
||||||
|
parentCommentId = replyComment?.id,
|
||||||
|
replyCommentId = replyComment?.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 顶级评论
|
||||||
|
model.createComment(content = it)
|
||||||
|
}
|
||||||
|
replyComment = null
|
||||||
|
onCommentAdded()
|
||||||
|
currentCommentCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(navBarHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.statusBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.pager.VerticalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||||
|
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.exp.timeAgo
|
||||||
|
import com.aiosman.ravenow.exp.formatPostTime2
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.NewsViewModel
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun NewsScreen() {
|
||||||
|
val model = NewsViewModel
|
||||||
|
val moments = model.moments
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 评论弹窗状态
|
||||||
|
var showCommentModal by remember { mutableStateOf(false) }
|
||||||
|
var selectedMoment by remember { mutableStateOf<MomentEntity?>(null) }
|
||||||
|
|
||||||
|
// 查看全文弹窗状态
|
||||||
|
var showFullArticleModal by remember { mutableStateOf(false) }
|
||||||
|
var selectedArticleMoment by remember { mutableStateOf<MomentEntity?>(null) }
|
||||||
|
// 垂直翻页状态
|
||||||
|
val pagerState = rememberPagerState(pageCount = { moments.size })
|
||||||
|
|
||||||
|
// 防抖器
|
||||||
|
val likeDebouncer = rememberDebouncer()
|
||||||
|
val favoriteDebouncer = rememberDebouncer()
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
model.refreshPager()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听数据变化,重置加载状态
|
||||||
|
LaunchedEffect(moments.size) {
|
||||||
|
// 当数据增加时,如果接近列表末尾,Pager会自动更新页数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当翻页接近末尾时加载更多
|
||||||
|
LaunchedEffect(pagerState.currentPage, moments.size) {
|
||||||
|
if (moments.isNotEmpty() && pagerState.currentPage >= moments.size - 2) {
|
||||||
|
model.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
if (moments.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text = "暂无新闻内容", color = AppColors.text, fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VerticalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { page ->
|
||||||
|
val momentItem = moments.getOrNull(page) ?: return@VerticalPager
|
||||||
|
NewsItem(
|
||||||
|
moment = momentItem,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
onCommentClick = {
|
||||||
|
selectedMoment = momentItem
|
||||||
|
showCommentModal = true
|
||||||
|
},
|
||||||
|
onReadFullClick = {
|
||||||
|
selectedArticleMoment = momentItem
|
||||||
|
showFullArticleModal = true
|
||||||
|
},
|
||||||
|
onLikeClick = {
|
||||||
|
likeDebouncer {
|
||||||
|
// 检查游客模式
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
if (momentItem.liked) {
|
||||||
|
model.dislikeMoment(momentItem.id)
|
||||||
|
} else {
|
||||||
|
model.likeMoment(momentItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFavoriteClick = {
|
||||||
|
favoriteDebouncer {
|
||||||
|
// 检查游客模式
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
if (momentItem.isFavorite) {
|
||||||
|
model.unfavoriteMoment(momentItem.id)
|
||||||
|
} else {
|
||||||
|
model.favoriteMoment(momentItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看全文弹窗
|
||||||
|
if (showFullArticleModal && selectedArticleMoment != null) {
|
||||||
|
FullArticleModal(
|
||||||
|
moment = selectedArticleMoment!!,
|
||||||
|
onDismiss = {
|
||||||
|
showFullArticleModal = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评论弹窗
|
||||||
|
if (showCommentModal && selectedMoment != null) {
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
|
val sheetHeight = screenHeight * 0.67f // 三分之二高度
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = {
|
||||||
|
showCommentModal = false
|
||||||
|
},
|
||||||
|
sheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(sheetHeight),
|
||||||
|
containerColor = AppColors.background,
|
||||||
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
|
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
|
||||||
|
) {
|
||||||
|
NewsCommentModal(
|
||||||
|
postId = selectedMoment?.id,
|
||||||
|
commentCount = selectedMoment?.commentCount ?: 0,
|
||||||
|
onDismiss = {
|
||||||
|
showCommentModal = false
|
||||||
|
},
|
||||||
|
onCommentAdded = {
|
||||||
|
selectedMoment?.id?.let { model.onAddComment(it) }
|
||||||
|
},
|
||||||
|
onCommentDeleted = {
|
||||||
|
selectedMoment?.id?.let { model.onDeleteComment(it) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//单个新闻项
|
||||||
|
@Composable
|
||||||
|
fun NewsItem(
|
||||||
|
moment: MomentEntity,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onCommentClick: () -> Unit = {},
|
||||||
|
onReadFullClick: () -> Unit = {},
|
||||||
|
onLikeClick: () -> Unit = {},
|
||||||
|
onFavoriteClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(bottom = 30.dp)
|
||||||
|
) {
|
||||||
|
// 新闻图片
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(300.dp)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
if (moment.images.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = context,
|
||||||
|
imageUrl = moment.images[0].thumbnail,
|
||||||
|
contentDescription = "新闻图片",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
blurHash = moment.images[0].blurHash,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = androidx.compose.ui.res.painterResource(id = R.drawable.default_moment_img),
|
||||||
|
contentDescription = "默认图片",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 新闻标题
|
||||||
|
Text(
|
||||||
|
text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AppColors.text,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 新闻内容(超出使用省略号)
|
||||||
|
Text(
|
||||||
|
text = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
maxLines = 6,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 新闻信息
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 来源和时间(显示月份与具体时间)
|
||||||
|
Text(
|
||||||
|
text = if (moment.newsSource.isNotEmpty()) "${moment.newsSource} • ${moment.time.formatPostTime2()}" else "${moment.nickname} • ${moment.time.formatPostTime2()}",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
// 查看全文
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.noRippleClickable { onReadFullClick() }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.read_full_article),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color(0xFF7c45ed)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Image(
|
||||||
|
painter = androidx.compose.ui.res.painterResource(id = R.mipmap.arrow),
|
||||||
|
contentDescription = "箭头",
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color(0xFF7c45ed))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 互动栏
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 25.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
// 点赞
|
||||||
|
NewsActionButton(
|
||||||
|
icon = if (moment.liked) R.drawable.rider_pro_moment_liked else R.drawable.rider_pro_moment_like,
|
||||||
|
count = moment.likeCount.toString(),
|
||||||
|
isActive = moment.liked,
|
||||||
|
modifier = Modifier.noRippleClickable { onLikeClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 评论
|
||||||
|
NewsActionButton(
|
||||||
|
icon = R.mipmap.icon_comment,
|
||||||
|
count = moment.commentCount.toString(),
|
||||||
|
isActive = false,
|
||||||
|
modifier = Modifier.noRippleClickable { onCommentClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 收藏
|
||||||
|
NewsActionButton(
|
||||||
|
icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect,
|
||||||
|
count = moment.favoriteCount.toString(),
|
||||||
|
isActive = moment.isFavorite,
|
||||||
|
modifier = Modifier.noRippleClickable { onFavoriteClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分享
|
||||||
|
NewsActionButton(
|
||||||
|
icon = R.mipmap.icon_share,
|
||||||
|
count = "",
|
||||||
|
isActive = false,
|
||||||
|
text = stringResource(R.string.share),
|
||||||
|
textSize = 8.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 互动栏按钮
|
||||||
|
@Composable
|
||||||
|
fun NewsActionButton(
|
||||||
|
icon: Int,
|
||||||
|
count: String,
|
||||||
|
isActive: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String? = null,
|
||||||
|
textSize: androidx.compose.ui.unit.TextUnit = 12.sp
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.width(60.dp)
|
||||||
|
.background(
|
||||||
|
color = AppColors.secondaryBackground,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = androidx.compose.ui.res.painterResource(id = icon),
|
||||||
|
contentDescription = "操作图标",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (count.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = count,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (text != null) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = textSize,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
|
||||||
|
|
||||||
|
object NewsViewModel : BaseMomentModel() {
|
||||||
|
override fun extraArgs(): MomentLoaderExtraArgs {
|
||||||
|
// 只拉取新闻
|
||||||
|
return MomentLoaderExtraArgs(
|
||||||
|
explore = false,
|
||||||
|
timelineId = null,
|
||||||
|
authorId = null,
|
||||||
|
newsOnly = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -43,6 +43,8 @@ import com.aiosman.ravenow.R
|
|||||||
import com.aiosman.ravenow.ui.composables.MomentCard
|
import com.aiosman.ravenow.ui.composables.MomentCard
|
||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态列表
|
* 动态列表
|
||||||
@@ -76,7 +78,49 @@ fun TimelineMomentsList() {
|
|||||||
model.loadMore()
|
model.loadMore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (moments.isEmpty()) {
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
|
if (!isNetworkAvailable) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 188.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
val exploreDebouncer = rememberDebouncer()
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(140.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
ExploreButton(
|
||||||
|
onClick = {
|
||||||
|
exploreDebouncer {
|
||||||
|
/* TODO: 添加点击事件处理 */
|
||||||
|
} }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (moments.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -91,7 +135,7 @@ fun TimelineMomentsList() {
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img
|
id = if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img
|
||||||
else R.mipmap.qst_gz_qs_img),
|
else R.mipmap.invalid_name_4),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(140.dp)
|
modifier = Modifier.size(140.dp)
|
||||||
)
|
)
|
||||||
@@ -191,9 +235,9 @@ fun ExploreButton(
|
|||||||
) {
|
) {
|
||||||
val gradientBrush = Brush.linearGradient(
|
val gradientBrush = Brush.linearGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color(0xFFee2a33),
|
Color(0xFF7c45ed),
|
||||||
Color(0xFFd80264),
|
Color(0xFF7c68ef),
|
||||||
Color(0xFF664c92)
|
Color(0xFF7bd8f8)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.profile
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
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.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉黑确认弹窗
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BlockConfirmDialog(
|
||||||
|
userProfile: AccountProfileEntity?,
|
||||||
|
onConfirmBlock: () -> Unit = {},
|
||||||
|
onDismiss: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = AppColors.background,
|
||||||
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||||
|
)
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
// 用户头像
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
LocalContext.current,
|
||||||
|
userProfile?.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(60.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
color = AppColors.background,
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentDescription = "用户头像",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认文本
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.confirm_block_user, userProfile?.nickName ?: "该用户"),
|
||||||
|
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AppColors.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(bottom = 24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 说明信息
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(bottom = 32.dp)
|
||||||
|
) {
|
||||||
|
// 第一条说明
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_infor_off_eye),
|
||||||
|
contentDescription = "",
|
||||||
|
tint = AppColors.text,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.block_description_1),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二条说明
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_notice_mute),
|
||||||
|
contentDescription = "",
|
||||||
|
tint = AppColors.text,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.block_description_2),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三条说明
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.icons_infor_off_bell),
|
||||||
|
contentDescription = "",
|
||||||
|
tint = AppColors.text,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.block_description_3),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认拉黑按钮
|
||||||
|
androidx.compose.material3.Button(
|
||||||
|
onClick = {
|
||||||
|
onConfirmBlock()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp),
|
||||||
|
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
|
||||||
|
containerColor = AppColors.text
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.block),
|
||||||
|
color = AppColors.background,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,10 +50,12 @@ 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.drawWithContent
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
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.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -96,6 +98,8 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -117,7 +121,6 @@ fun ProfileV3(
|
|||||||
postCount: Int? = null, // 新增参数用于传递帖子总数
|
postCount: Int? = null, // 新增参数用于传递帖子总数
|
||||||
) {
|
) {
|
||||||
val model = MyProfileViewModel
|
val model = MyProfileViewModel
|
||||||
val state = rememberCollapsingToolbarScaffoldState()
|
|
||||||
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
|
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
|
||||||
val enabled by remember { mutableStateOf(true) }
|
val enabled by remember { mutableStateOf(true) }
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
@@ -126,6 +129,8 @@ fun ProfileV3(
|
|||||||
var showAgentMenu by remember { mutableStateOf(false) }
|
var showAgentMenu by remember { mutableStateOf(false) }
|
||||||
var contextAgent by remember { mutableStateOf<AgentEntity?>(null) }
|
var contextAgent by remember { mutableStateOf<AgentEntity?>(null) }
|
||||||
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
|
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
var showOtherUserMenu by remember { mutableStateOf(false) }
|
||||||
|
var showBlockConfirmDialog by remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
@@ -149,6 +154,19 @@ fun ProfileV3(
|
|||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
val toolbarAlpha by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
if (!isSelf) {
|
||||||
|
1f
|
||||||
|
} else {
|
||||||
|
val maxScroll = 500f // 最大滚动距离,可调整
|
||||||
|
val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f)
|
||||||
|
progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// observe list scrolling
|
// observe list scrolling
|
||||||
val reachedListBottom by remember {
|
val reachedListBottom by remember {
|
||||||
@@ -199,8 +217,6 @@ fun ProfileV3(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun switchTheme() {
|
fun switchTheme() {
|
||||||
// delay
|
// delay
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -279,43 +295,15 @@ fun ProfileV3(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.pullRefresh(refreshState)
|
modifier = Modifier.pullRefresh(refreshState)
|
||||||
) {
|
) {
|
||||||
CollapsingToolbarScaffold(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(AppColors.profileBackground),
|
.verticalScroll(scrollState)
|
||||||
state = state,
|
|
||||||
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
|
|
||||||
toolbarScrollable = true,
|
|
||||||
enabled = enabled,
|
|
||||||
toolbar = { toolbarScrollState ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(miniToolbarHeight.dp)
|
|
||||||
// 保持在最低高度和当前高度之间
|
|
||||||
.background(AppColors.profileBackground)
|
.background(AppColors.profileBackground)
|
||||||
) {
|
) {
|
||||||
}
|
// Banner
|
||||||
// header
|
val banner = profile?.banner
|
||||||
Box(
|
if (banner != null) {
|
||||||
modifier = Modifier
|
|
||||||
.parallax(0.5f)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(if (isAiAccount) 600.dp else 700.dp)
|
|
||||||
.background(AppColors.profileBackground)
|
|
||||||
.verticalScroll(toolbarScrollState)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.graphicsLayer {
|
|
||||||
alpha = state.toolbarState.progress
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// banner
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -327,13 +315,11 @@ fun ProfileV3(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(bannerHeight.dp - 24.dp)
|
.height(bannerHeight.dp - 24.dp)
|
||||||
.let {
|
.let {
|
||||||
if (isSelf&&isMain) {
|
if (isSelf && isMain) {
|
||||||
it.noRippleClickable {
|
it.noRippleClickable {
|
||||||
Intent(Intent.ACTION_PICK).apply {
|
Intent(Intent.ACTION_PICK).apply {
|
||||||
type = "image/*"
|
type = "image/*"
|
||||||
pickBannerImageLauncher.launch(
|
pickBannerImageLauncher.launch(this)
|
||||||
this
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -348,77 +334,25 @@ fun ProfileV3(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val banner = profile?.banner
|
|
||||||
|
|
||||||
if (banner != null) {
|
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
LocalContext.current,
|
LocalContext.current,
|
||||||
banner,
|
banner,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize(),
|
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Image(
|
Spacer(modifier = Modifier.height(100.dp))
|
||||||
painter = painterResource(id = R.drawable.rave_now_profile_backgrount_demo_1),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize(),
|
|
||||||
contentDescription = "",
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isSelf&&isMain) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(
|
|
||||||
top = statusBarPaddingValues.calculateTopPadding(),
|
|
||||||
start = 8.dp,
|
|
||||||
end = 8.dp
|
|
||||||
)
|
|
||||||
.noRippleClickable {
|
|
||||||
IndexViewModel.openDrawer = true
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(
|
|
||||||
AppColors.background.copy(
|
|
||||||
alpha = 0.7f
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
|
||||||
contentDescription = "",
|
|
||||||
|
|
||||||
tint = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// 用户信息
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(AppColors.profileBackground)
|
.background(AppColors.profileBackground)
|
||||||
) {
|
.padding(horizontal = 16.dp)
|
||||||
// user info
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
|
|
||||||
) {
|
|
||||||
// Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
// 个人信息
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
|
||||||
) {
|
) {
|
||||||
profile?.let {
|
profile?.let {
|
||||||
UserItem(
|
UserItem(
|
||||||
@@ -427,17 +361,20 @@ fun ProfileV3(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
// 操作按钮
|
||||||
profile?.let {
|
profile?.let {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
SelfProfileAction(
|
SelfProfileAction(
|
||||||
onEditProfile = {
|
onEditProfile = {
|
||||||
navController.navigate(
|
navController.navigate(NavigationRoute.AccountEdit.route)
|
||||||
NavigationRoute.AccountEdit.route
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onPremiumClick = {
|
onPremiumClick = {
|
||||||
navController.navigate(NavigationRoute.VipSelPage.route)
|
navController.navigate(NavigationRoute.VipSelPage.route)
|
||||||
@@ -455,24 +392,20 @@ fun ProfileV3(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加用户智能体行(智能体用户不显示)
|
// 用户智能体行
|
||||||
if (!isAiAccount) {
|
if (!isAiAccount) {
|
||||||
UserAgentsRow(
|
UserAgentsRow(
|
||||||
userId = if (isSelf) null else profile?.id,
|
userId = if (isSelf) null else profile?.id,
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
onMoreClick = {
|
onMoreClick = {
|
||||||
// 导航到智能体列表页面
|
// 导航到智能体列表页面
|
||||||
// TODO: 实现导航逻辑
|
|
||||||
},
|
},
|
||||||
onAgentClick = { agent ->
|
onAgentClick = { agent ->
|
||||||
// 导航到智能体详情页面
|
// 导航到智能体详情页面
|
||||||
// TODO: 实现导航逻辑
|
|
||||||
},
|
},
|
||||||
onAvatarClick = { agent ->
|
onAvatarClick = { agent ->
|
||||||
// 导航到智能体个人主页
|
// 导航到智能体个人主页
|
||||||
@@ -500,100 +433,13 @@ fun ProfileV3(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
// 内容
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.graphicsLayer {
|
|
||||||
alpha = 1 - state.toolbarState.progress
|
|
||||||
}
|
|
||||||
.background(AppColors.profileBackground)
|
|
||||||
.onGloballyPositioned {
|
|
||||||
miniToolbarHeight = with(density) {
|
|
||||||
it.size.height.toDp().value.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
StatusBarSpacer()
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
horizontal = 16.dp,
|
|
||||||
vertical = 8.dp,
|
|
||||||
).noRippleClickable {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
if (!isMain) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon), // Replace with your image resource
|
|
||||||
contentDescription = "Back",
|
|
||||||
modifier = Modifier
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.navigateUp()
|
|
||||||
}
|
|
||||||
.size(24.dp),
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
CustomAsyncImage(
|
|
||||||
LocalContext.current,
|
|
||||||
profile?.avatar,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
contentDescription = "",
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Text(
|
|
||||||
text = profile?.nickName ?: "",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
if (isSelf&&isMain) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.noRippleClickable {
|
|
||||||
IndexViewModel.openDrawer = true
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
|
||||||
contentDescription = "",
|
|
||||||
tint = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(AppColors.profileBackground)
|
.background(AppColors.profileBackground)
|
||||||
|
.padding(top = 8.dp)
|
||||||
) {
|
) {
|
||||||
UserContentPageIndicator(
|
UserContentPageIndicator(
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
@@ -602,6 +448,7 @@ fun ProfileV3(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
|
modifier = Modifier.height(500.dp) // 固定滚动高度
|
||||||
) { idx ->
|
) { idx ->
|
||||||
when (idx) {
|
when (idx) {
|
||||||
0 ->
|
0 ->
|
||||||
@@ -631,15 +478,196 @@ fun ProfileV3(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 顶部导航栏
|
||||||
|
TopNavigationBar(
|
||||||
|
isMain = isMain,
|
||||||
|
isSelf = isSelf,
|
||||||
|
profile = profile,
|
||||||
|
navController = navController,
|
||||||
|
alpha = toolbarAlpha,
|
||||||
|
onMenuClick = {
|
||||||
|
showOtherUserMenu = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
PullRefreshIndicator(
|
PullRefreshIndicator(
|
||||||
model.refreshing,
|
model.refreshing,
|
||||||
refreshState,
|
refreshState,
|
||||||
Modifier.align(Alignment.TopCenter)
|
Modifier.align(Alignment.TopCenter)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 其他用户菜单弹窗
|
||||||
|
if (showOtherUserMenu) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { showOtherUserMenu = false },
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||||
|
containerColor = Color.Transparent, // 设置容器背景透明
|
||||||
|
contentColor = Color.Transparent, // 设置内容背景透明
|
||||||
|
dragHandle = null, // 移除拖拽手柄
|
||||||
|
windowInsets = androidx.compose.foundation.layout.WindowInsets(0) // 移除窗口边距
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
) {
|
||||||
|
OtherUserMenuModal(
|
||||||
|
onBlockClick = {
|
||||||
|
showBlockConfirmDialog = true
|
||||||
|
},
|
||||||
|
onReportClick = {
|
||||||
|
// 实现举报逻辑
|
||||||
|
},
|
||||||
|
onCancelClick = {
|
||||||
|
showOtherUserMenu = false
|
||||||
|
},
|
||||||
|
onDismiss = { showOtherUserMenu = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 拉黑确认弹窗
|
||||||
|
if (showBlockConfirmDialog) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = {
|
||||||
|
showBlockConfirmDialog = false
|
||||||
|
},
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = Color.Transparent,
|
||||||
|
dragHandle = null,
|
||||||
|
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
) {
|
||||||
|
BlockConfirmDialog(
|
||||||
|
userProfile = profile,
|
||||||
|
onConfirmBlock = {
|
||||||
|
// 实现拉黑逻辑
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showBlockConfirmDialog = false
|
||||||
|
showOtherUserMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//顶部导航栏组件
|
||||||
|
@Composable
|
||||||
|
fun TopNavigationBar(
|
||||||
|
isMain: Boolean,
|
||||||
|
isSelf: Boolean,
|
||||||
|
profile: AccountProfileEntity?,
|
||||||
|
navController: androidx.navigation.NavController,
|
||||||
|
alpha: Float,
|
||||||
|
onMenuClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.graphicsLayer { this.alpha = alpha }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(appColors.profileBackground)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (!isMain) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
CustomAsyncImage(
|
||||||
|
LocalContext.current,
|
||||||
|
profile?.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text = profile?.nickName ?: "",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
if (isSelf && isMain) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.padding(16.dp)
|
||||||
|
)
|
||||||
|
} else if (!isSelf) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.noRippleClickable {
|
||||||
|
onMenuClick()
|
||||||
|
}
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||||
|
contentDescription = "菜单",
|
||||||
|
tint = appColors.text,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
if (isSelf && isMain) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(top = 32.dp, end = 16.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
IndexViewModel.openDrawer = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||||
|
contentDescription = "",
|
||||||
|
tint = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -721,7 +749,7 @@ fun AgentMenuModal(
|
|||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
MenuActionItem(
|
MenuActionItem(
|
||||||
icon = R.drawable.rider_pro_moment_delete,
|
icon = R.drawable.rider_pro_moment_delete,
|
||||||
text = "删除"
|
text = stringResource(R.string.delete)
|
||||||
) {
|
) {
|
||||||
onDeleteClick()
|
onDeleteClick()
|
||||||
}
|
}
|
||||||
@@ -797,3 +825,96 @@ fun DeleteConfirmDialog(
|
|||||||
containerColor = AppColors.background
|
containerColor = AppColors.background
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 其他用户主页菜单弹窗
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun OtherUserMenuModal(
|
||||||
|
onBlockClick: () -> Unit = {},
|
||||||
|
onReportClick: () -> Unit = {},
|
||||||
|
onCancelClick: () -> Unit = {},
|
||||||
|
onDismiss: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.padding(bottom = 11.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = AppColors.background,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 拉黑选项
|
||||||
|
androidx.compose.material3.TextButton(
|
||||||
|
onClick = {
|
||||||
|
onBlockClick()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.block),
|
||||||
|
color = AppColors.error,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割线
|
||||||
|
androidx.compose.material3.HorizontalDivider(
|
||||||
|
color = AppColors.divider,
|
||||||
|
thickness = 0.5.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
// 举报选项
|
||||||
|
androidx.compose.material3.TextButton(
|
||||||
|
onClick = {
|
||||||
|
onReportClick()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.report),
|
||||||
|
color = AppColors.error,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 取消按钮
|
||||||
|
androidx.compose.material3.TextButton(
|
||||||
|
onClick = {
|
||||||
|
onCancelClick()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = AppColors.background,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.cancel),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,9 +37,16 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
|
|||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
@Composable
|
@Composable
|
||||||
fun GalleryItem(
|
fun GalleryItem(
|
||||||
moment: MomentEntity,
|
moment: MomentEntity,
|
||||||
@@ -93,7 +100,7 @@ fun GalleryItem(
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "故事还没开始",
|
text = "故事还没开始",
|
||||||
fontSize = 12.sp,
|
fontSize = 16.sp,
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600
|
||||||
)
|
)
|
||||||
@@ -129,8 +136,50 @@ fun GalleryGrid(
|
|||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
val debouncer = rememberDebouncer()
|
val debouncer = rememberDebouncer()
|
||||||
|
var refreshKey by remember { mutableStateOf(0) }
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (moments.isEmpty()) {
|
if (!isNetworkAvailable) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 60.dp),
|
||||||
|
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),
|
||||||
|
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 = {
|
||||||
|
refreshKey++
|
||||||
|
MyProfileViewModel.ResetModel()
|
||||||
|
MyProfileViewModel.loadProfile(pullRefresh = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (moments.isEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -141,7 +190,7 @@ fun GalleryGrid(
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if(AppState.darkMode) R.mipmap.qs_dt_qs_as_img
|
id = if(AppState.darkMode) R.mipmap.qs_dt_qs_as_img
|
||||||
else R.mipmap.qs_dt_qs_img),
|
else R.mipmap.invalid_name_7),
|
||||||
contentDescription = "暂无图片",
|
contentDescription = "暂无图片",
|
||||||
modifier = Modifier.size(181.dp),
|
modifier = Modifier.size(181.dp),
|
||||||
)
|
)
|
||||||
@@ -150,7 +199,7 @@ fun GalleryGrid(
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "故事还没开始",
|
text = "故事还没开始",
|
||||||
fontSize = 12.sp,
|
fontSize = 16.sp,
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ fun OtherProfileAction(
|
|||||||
// 定义渐变色
|
// 定义渐变色
|
||||||
val followGradient = Brush.horizontalGradient(
|
val followGradient = Brush.horizontalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color(0xFFE53E3E), // 红色
|
Color(0xFF7c45ed),
|
||||||
Color(0xFF9F7AEA) // 紫色
|
Color(0x777c68ef)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ fun OtherProfileAction(
|
|||||||
Text(
|
Text(
|
||||||
text = if (profile.isFollowing) "已关注" else stringResource(R.string.follow_upper),
|
text = if (profile.isFollowing) "已关注" else stringResource(R.string.follow_upper),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.W900,
|
||||||
color = if (profile.isFollowing) {
|
color = if (profile.isFollowing) {
|
||||||
// 已关注状态 - 灰色文字
|
// 已关注状态 - 灰色文字
|
||||||
AppColors.text.copy(alpha = 0.6f)
|
AppColors.text.copy(alpha = 0.6f)
|
||||||
@@ -133,11 +133,37 @@ fun OtherProfileAction(
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.chat_upper),
|
text = stringResource(R.string.chat_upper),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.W900,
|
||||||
color = AppColors.text, // 使用主题文字颜色
|
color = AppColors.text, // 使用主题文字颜色
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分享按钮 - 灰色背景样式
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.nonActive)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
// 检查游客模式,如果是游客则跳转登录
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
// TODO: 添加分享逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.share),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W900,
|
||||||
|
color = AppColors.text, // 使用主题文字颜色
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
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.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -29,11 +30,13 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|||||||
@Composable
|
@Composable
|
||||||
fun SelfProfileAction(
|
fun SelfProfileAction(
|
||||||
onEditProfile: () -> Unit,
|
onEditProfile: () -> Unit,
|
||||||
onPremiumClick: (() -> Unit)? = null
|
onPremiumClick: (() -> Unit),
|
||||||
|
onShare: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val editProfileDebouncer = rememberDebouncer()
|
val editProfileDebouncer = rememberDebouncer()
|
||||||
val premiumClickDebouncer = rememberDebouncer()
|
val premiumClickDebouncer = rememberDebouncer()
|
||||||
|
val shareDebouncer = rememberDebouncer()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -45,52 +48,101 @@ fun SelfProfileAction(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.width(60.dp).height(25.dp)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(AppColors.nonActive)
|
.background(androidx.compose.ui.graphics.Color(0x229284BD))
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
editProfileDebouncer {
|
editProfileDebouncer {
|
||||||
onEditProfile()
|
onEditProfile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.fill_and_sign),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(12.dp),
|
||||||
|
colorFilter = ColorFilter.tint(androidx.compose.ui.graphics.Color(0xFF9284BD))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.edit_profile),
|
text = stringResource(R.string.edit_profile),
|
||||||
fontSize = 14.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.W600,
|
||||||
color = AppColors.text,
|
color = androidx.compose.ui.graphics.Color(0xFF9284BD),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rave Premium 按钮(右侧)
|
// // 预留按钮位置
|
||||||
Row(
|
// Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
// verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
// horizontalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.weight(1f)
|
// .weight(1f)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
// .clip(RoundedCornerShape(10.dp))
|
||||||
.background(AppColors.premiumBackground)
|
// .padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
// .noRippleClickable {
|
||||||
.noRippleClickable {
|
//
|
||||||
premiumClickDebouncer {
|
// }
|
||||||
onPremiumClick?.invoke()
|
// ) {
|
||||||
}
|
// Text(
|
||||||
}
|
// text = "",
|
||||||
) {
|
// fontSize = 14.sp,
|
||||||
Image(
|
// fontWeight = FontWeight.W900,
|
||||||
painter = painterResource(id = R.drawable.ic_member),
|
// color = AppColors.text,
|
||||||
contentDescription = "",
|
// )
|
||||||
modifier = Modifier.size(18.dp),
|
// }
|
||||||
colorFilter = ColorFilter.tint(AppColors.premiumText)
|
//
|
||||||
)
|
// // 分享按钮
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
// Row(
|
||||||
Text(
|
// verticalAlignment = Alignment.CenterVertically,
|
||||||
text = "Rave Premium",
|
// horizontalArrangement = Arrangement.Center,
|
||||||
fontSize = 14.sp,
|
// modifier = Modifier
|
||||||
fontWeight = FontWeight.W600,
|
// .weight(1f)
|
||||||
color = AppColors.premiumText,
|
// .clip(RoundedCornerShape(10.dp))
|
||||||
)
|
// .background(AppColors.nonActive)
|
||||||
}
|
// .padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
// .noRippleClickable {
|
||||||
|
// shareDebouncer {
|
||||||
|
// // TODO: 添加分享逻辑
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ) {
|
||||||
|
// Text(
|
||||||
|
// text = stringResource(R.string.share),
|
||||||
|
// fontSize = 14.sp,
|
||||||
|
// fontWeight = FontWeight.W900,
|
||||||
|
// color = AppColors.text,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Rave Premium 按钮(右侧)
|
||||||
|
// Row(
|
||||||
|
// verticalAlignment = Alignment.CenterVertically,
|
||||||
|
// horizontalArrangement = Arrangement.Center,
|
||||||
|
// modifier = Modifier
|
||||||
|
// .weight(1f)
|
||||||
|
// .clip(RoundedCornerShape(8.dp))
|
||||||
|
// .background(AppColors.premiumBackground)
|
||||||
|
// .padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
// .noRippleClickable {
|
||||||
|
// premiumClickDebouncer {
|
||||||
|
// onPremiumClick?.invoke()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ) {
|
||||||
|
// Image(
|
||||||
|
// painter = painterResource(id = R.drawable.ic_member),
|
||||||
|
// contentDescription = "",
|
||||||
|
// modifier = Modifier.size(18.dp),
|
||||||
|
// colorFilter = ColorFilter.tint(AppColors.premiumText)
|
||||||
|
// )
|
||||||
|
// Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
// Text(
|
||||||
|
// text = "Rave Premium",
|
||||||
|
// fontSize = 14.sp,
|
||||||
|
// fontWeight = FontWeight.W600,
|
||||||
|
// color = AppColors.premiumText,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
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.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
|
||||||
@@ -29,6 +30,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
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.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
|
||||||
@@ -44,7 +46,10 @@ import com.aiosman.ravenow.R
|
|||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
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.network.ReloadButton
|
||||||
import com.aiosman.ravenow.utils.DebounceUtils
|
import com.aiosman.ravenow.utils.DebounceUtils
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserAgentsList(
|
fun UserAgentsList(
|
||||||
@@ -196,6 +201,7 @@ fun UserAgentCard(
|
|||||||
@Composable
|
@Composable
|
||||||
fun EmptyAgentsView() {
|
fun EmptyAgentsView() {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -203,12 +209,15 @@ fun EmptyAgentsView() {
|
|||||||
.padding(vertical = 60.dp),
|
.padding(vertical = 60.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
if (isNetworkAvailable) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id =if(AppState.darkMode) R.mipmap.qs_ai_qs_as_img
|
id =if(AppState.darkMode) R.mipmap.ai_dark
|
||||||
else R.mipmap.qs_ai_qs_img),
|
else R.mipmap.ai),
|
||||||
contentDescription = "暂无Agent",
|
contentDescription = "暂无Agent",
|
||||||
modifier = Modifier.size(181.dp),
|
modifier = Modifier
|
||||||
|
.size(width = 181.dp, height = 153.dp)
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
@@ -228,5 +237,37 @@ fun EmptyAgentsView() {
|
|||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontWeight = FontWeight.W400
|
fontWeight = FontWeight.W400
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(181.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
MyProfileViewModel.ResetModel()
|
||||||
|
MyProfileViewModel.loadProfile(pullRefresh = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,94 +65,94 @@ fun UserAgentsRow(
|
|||||||
viewModel.loadUserAgents(userId)
|
viewModel.loadUserAgents(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 总是显示智能体区域,即使没有数据也显示标题和状态
|
// // 总是显示智能体区域,即使没有数据也显示标题和状态
|
||||||
Column(
|
// Column(
|
||||||
modifier = modifier
|
// modifier = modifier
|
||||||
.fillMaxWidth()
|
// .fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
// .padding(horizontal = 16.dp)
|
||||||
) {
|
// ) {
|
||||||
Text(
|
// Text(
|
||||||
text = if (isSelf) "我的智能体" else "TA的智能体",
|
// text = if (isSelf) "我的智能体" else "TA的智能体",
|
||||||
fontSize = 16.sp,
|
// fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600,
|
// fontWeight = FontWeight.W600,
|
||||||
color = AppColors.text,
|
// color = AppColors.text,
|
||||||
modifier = Modifier.padding(bottom = 12.dp)
|
// modifier = Modifier.padding(bottom = 12.dp)
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
when {
|
// when {
|
||||||
viewModel.isLoading -> {
|
// viewModel.isLoading -> {
|
||||||
// 显示加载状态
|
// // 显示加载状态
|
||||||
Box(
|
// Box(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.fillMaxWidth()
|
// .fillMaxWidth()
|
||||||
.height(60.dp),
|
// .height(60.dp),
|
||||||
contentAlignment = Alignment.Center
|
// contentAlignment = Alignment.Center
|
||||||
) {
|
// ) {
|
||||||
Text(
|
// Text(
|
||||||
text = "加载中...",
|
// text = "加载中...",
|
||||||
fontSize = 14.sp,
|
// fontSize = 14.sp,
|
||||||
color = AppColors.text.copy(alpha = 0.6f)
|
// color = AppColors.text.copy(alpha = 0.6f)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
viewModel.error != null -> {
|
// viewModel.error != null -> {
|
||||||
// 显示错误状态
|
// // 显示错误状态
|
||||||
Box(
|
// Box(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.fillMaxWidth()
|
// .fillMaxWidth()
|
||||||
.height(60.dp),
|
// .height(60.dp),
|
||||||
contentAlignment = Alignment.Center
|
// contentAlignment = Alignment.Center
|
||||||
) {
|
// ) {
|
||||||
Text(
|
// Text(
|
||||||
text = "加载失败: ${viewModel.error}",
|
// text = "加载失败: ${viewModel.error}",
|
||||||
fontSize = 14.sp,
|
// fontSize = 14.sp,
|
||||||
color = AppColors.text.copy(alpha = 0.6f)
|
// color = AppColors.text.copy(alpha = 0.6f)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
viewModel.agents.isEmpty() -> {
|
// viewModel.agents.isEmpty() -> {
|
||||||
// 显示空状态
|
// // 显示空状态
|
||||||
Box(
|
// Box(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.fillMaxWidth()
|
// .fillMaxWidth()
|
||||||
.height(60.dp),
|
// .height(60.dp),
|
||||||
contentAlignment = Alignment.Center
|
// contentAlignment = Alignment.Center
|
||||||
) {
|
// ) {
|
||||||
Text(
|
// Text(
|
||||||
text = if (isSelf) "您还没有创建智能体" else "TA还没有创建智能体",
|
// text = if (isSelf) "您还没有创建智能体" else "TA还没有创建智能体",
|
||||||
fontSize = 14.sp,
|
// fontSize = 14.sp,
|
||||||
color = AppColors.text.copy(alpha = 0.6f)
|
// color = AppColors.text.copy(alpha = 0.6f)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
else -> {
|
// else -> {
|
||||||
// 显示智能体列表
|
// // 显示智能体列表
|
||||||
LazyRow(
|
// LazyRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
// horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
modifier = Modifier.fillMaxWidth()
|
// modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
// ) {
|
||||||
// 显示智能体项目
|
// // 显示智能体项目
|
||||||
items(viewModel.agents) { agent ->
|
// items(viewModel.agents) { agent ->
|
||||||
AgentItem(
|
// AgentItem(
|
||||||
agent = agent,
|
// agent = agent,
|
||||||
onClick = { onAgentClick(agent) },
|
// onClick = { onAgentClick(agent) },
|
||||||
onAvatarClick = { onAvatarClick(agent) },
|
// onAvatarClick = { onAvatarClick(agent) },
|
||||||
onLongClick = { onAgentLongClick(agent) }
|
// onLongClick = { onAgentLongClick(agent) }
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 添加"更多"按钮
|
// // 添加"更多"按钮
|
||||||
item {
|
// item {
|
||||||
MoreAgentItem(
|
// MoreAgentItem(
|
||||||
onClick = onMoreClick
|
// onClick = onMoreClick
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.background
|
|||||||
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
|
||||||
|
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.WindowInsets
|
||||||
@@ -28,6 +29,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Tab
|
import androidx.compose.material.Tab
|
||||||
import androidx.compose.material.TabRow
|
import androidx.compose.material.TabRow
|
||||||
@@ -44,6 +47,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -53,6 +57,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
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
|
||||||
@@ -70,6 +75,7 @@ import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListViewModel
|
|||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@@ -304,6 +310,7 @@ fun MomentResultTab() {
|
|||||||
var dataFlow = model.momentsFlow
|
var dataFlow = model.momentsFlow
|
||||||
var moments = dataFlow.collectAsLazyPagingItems()
|
var moments = dataFlow.collectAsLazyPagingItems()
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -317,10 +324,13 @@ fun MomentResultTab() {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
|
if (isNetworkAvailable) {
|
||||||
androidx.compose.foundation.Image(
|
androidx.compose.foundation.Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||||
else R.mipmap.syss_yh_qs_img),
|
else R.mipmap.invalid_name_1),
|
||||||
contentDescription = "No Comment",
|
contentDescription = "No Comment",
|
||||||
modifier = Modifier.size(140.dp)
|
modifier = Modifier.size(140.dp)
|
||||||
)
|
)
|
||||||
@@ -337,6 +347,33 @@ fun MomentResultTab() {
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W400
|
fontWeight = FontWeight.W400
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(140.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = LocalAppTheme.current.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = LocalAppTheme.current.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
SearchViewModel.ResetModel()
|
||||||
|
SearchViewModel.search()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -369,6 +406,7 @@ fun UserResultTab() {
|
|||||||
val model = SearchViewModel
|
val model = SearchViewModel
|
||||||
val users = model.usersFlow.collectAsLazyPagingItems()
|
val users = model.usersFlow.collectAsLazyPagingItems()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
@@ -380,10 +418,13 @@ fun UserResultTab() {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
|
if (isNetworkAvailable) {
|
||||||
androidx.compose.foundation.Image(
|
androidx.compose.foundation.Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||||
else R.mipmap.syss_yh_qs_img),
|
else R.mipmap.invalid_name_1),
|
||||||
contentDescription = "No Comment",
|
contentDescription = "No Comment",
|
||||||
modifier = Modifier.size(140.dp)
|
modifier = Modifier.size(140.dp)
|
||||||
)
|
)
|
||||||
@@ -400,6 +441,33 @@ fun UserResultTab() {
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W400
|
fontWeight = FontWeight.W400
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(140.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = LocalAppTheme.current.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = LocalAppTheme.current.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
SearchViewModel.ResetModel()
|
||||||
|
SearchViewModel.search()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -497,3 +565,42 @@ fun UserItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,15 +39,14 @@ import com.aiosman.ravenow.R
|
|||||||
import com.aiosman.ravenow.entity.AccountLikeEntity
|
import com.aiosman.ravenow.entity.AccountLikeEntity
|
||||||
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.BottomNavigationPlaceholder
|
import com.aiosman.ravenow.ui.composables.BottomNavigationPlaceholder
|
||||||
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.StatusBarMaskLayout
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
|
||||||
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 java.util.Date
|
import java.util.Date
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun LikeNoticeScreen() {
|
fun LikeNoticeScreen() {
|
||||||
@@ -72,18 +71,47 @@ fun LikeNoticeScreen() {
|
|||||||
.background(color = AppColors.background)
|
.background(color = AppColors.background)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
|
if (!isNetworkAvailable) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxWidth()
|
.padding(top=149.dp),
|
||||||
.padding(vertical = 16.dp)
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(
|
Column(
|
||||||
stringResource(R.string.like_upper),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
moreIcon = false
|
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)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (likes.itemCount == 0) {
|
} else if (likes.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
.padding(top=149.dp),
|
.padding(top=149.dp),
|
||||||
@@ -96,7 +124,7 @@ fun LikeNoticeScreen() {
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id =if(AppState.darkMode) R.mipmap.qst_z_qs_as_img
|
id =if(AppState.darkMode) R.mipmap.qst_z_qs_as_img
|
||||||
else R.mipmap.qst_z_qs_img),
|
else R.mipmap.invalid_name_6),
|
||||||
contentDescription = "No Notice",
|
contentDescription = "No Notice",
|
||||||
modifier = Modifier.size(181.dp)
|
modifier = Modifier.size(181.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import android.util.Log
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
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.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||||
import androidx.compose.foundation.gestures.scrollable
|
import androidx.compose.foundation.gestures.scrollable
|
||||||
|
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.PaddingValues
|
||||||
@@ -40,12 +42,14 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
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.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
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
|
||||||
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.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
|
||||||
@@ -70,6 +74,11 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.airbnb.lottie.compose.LottieAnimation
|
||||||
|
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||||
|
import com.airbnb.lottie.compose.LottieConstants
|
||||||
|
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginPage() {
|
fun LoginPage() {
|
||||||
@@ -209,14 +218,14 @@ fun LoginPage() {
|
|||||||
saveData()
|
saveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示成功提示
|
// // 显示成功提示
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
// coroutineScope.launch(Dispatchers.Main) {
|
||||||
Toast.makeText(
|
// Toast.makeText(
|
||||||
context,
|
// context,
|
||||||
"游客登录成功",
|
// "游客登录成功",
|
||||||
Toast.LENGTH_SHORT
|
// Toast.LENGTH_SHORT
|
||||||
).show()
|
// ).show()
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 初始化应用状态(游客模式会自动跳过推送和TRTC初始化)
|
// 初始化应用状态(游客模式会自动跳过推送和TRTC初始化)
|
||||||
try {
|
try {
|
||||||
@@ -259,14 +268,38 @@ fun LoginPage() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.padding(top = 60.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.size(30.dp)
|
||||||
|
.background(
|
||||||
|
color = AppColors.text.copy(alpha = 0.1f),
|
||||||
|
shape = androidx.compose.foundation.shape.CircleShape
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
guestLogin()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
val localContext = LocalContext.current // 获取 Context
|
Image(
|
||||||
MovingImageWall(localContext.resources) // 将 resources 传递给 MovingImageWall
|
painter = painterResource(id = R.drawable.rider_pro_close),
|
||||||
|
contentDescription = "Close",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -274,77 +307,109 @@ fun LoginPage() {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Image(
|
Box(
|
||||||
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
|
|
||||||
contentDescription = "Rave Now",
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(52.dp)
|
.fillMaxWidth()
|
||||||
|
.height(400.dp)
|
||||||
|
|
||||||
|
) {
|
||||||
|
val lottieFile = if (AppState.darkMode) "login.lottie" else "login_light.lottie"
|
||||||
|
LottieAnimation(
|
||||||
|
composition = rememberLottieComposition(LottieCompositionSpec.Asset(lottieFile)).value,
|
||||||
|
iterations = LottieConstants.IterateForever,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
}
|
||||||
Text(
|
Text(
|
||||||
"Rave Now",
|
text = stringResource(R.string.join_party_carnival),
|
||||||
fontSize = 28.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.W900,
|
fontWeight = FontWeight.W600,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
// Image(
|
||||||
Text(
|
// painter = painterResource(id = R.mipmap.invalid_name),
|
||||||
"Your Night Starts Here",
|
// contentDescription = "Rave Now",
|
||||||
fontSize = 20.sp,
|
// modifier = Modifier
|
||||||
fontWeight = FontWeight.W700,
|
// .size(52.dp)
|
||||||
color = AppColors.text
|
// .clip(RoundedCornerShape(10.dp))
|
||||||
)
|
// )
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
// Text(
|
||||||
|
// "Rave Now",
|
||||||
|
// fontSize = 28.sp,
|
||||||
|
// fontWeight = FontWeight.W900,
|
||||||
|
// color = AppColors.text
|
||||||
|
// )
|
||||||
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
// Text(
|
||||||
|
// "Your Night Starts Here",
|
||||||
|
// fontSize = 20.sp,
|
||||||
|
// fontWeight = FontWeight.W700,
|
||||||
|
// color = AppColors.text
|
||||||
|
// )
|
||||||
|
//注册tab
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(R.string.sign_up_upper),
|
text = stringResource(R.string.sign_up_upper),
|
||||||
color = AppColors.mainText,
|
color = if (AppState.darkMode) Color.Black else Color.White,
|
||||||
backgroundColor = AppColors.main
|
backgroundColor = if (AppState.darkMode) Color.White else Color.Black
|
||||||
) {
|
) {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.EmailSignUp.route,
|
NavigationRoute.EmailSignUp.route,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showGoogleLogin) {
|
//谷歌登录tab
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.5.dp,
|
||||||
|
color = if (AppState.darkMode) Color.White else Color.Black,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
),
|
||||||
text = stringResource(R.string.sign_in_with_google),
|
text = stringResource(R.string.sign_in_with_google),
|
||||||
color = AppColors.text,
|
color = if (AppState.darkMode) Color.White else Color.Black,
|
||||||
|
backgroundColor = if (AppState.darkMode) Color.Black else Color.White,
|
||||||
leading = {
|
leading = {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_google),
|
painter = painterResource(id = R.mipmap.rider_pro_signup_google),
|
||||||
contentDescription = "Google",
|
contentDescription = "Google",
|
||||||
modifier = Modifier.size(36.dp)
|
modifier = Modifier.size(18.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
expandText = true,
|
expandText = true,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
|
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 10.dp)
|
||||||
) {
|
) {
|
||||||
googleLogin()
|
googleLogin()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
//登录tab
|
//登录tab
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
ActionButton(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(R.string.login_upper),
|
text = stringResource(R.string.login_upper),
|
||||||
color = AppColors.text,
|
color = AppColors.text.copy(alpha = 0.5f),
|
||||||
) {
|
fontSize = 16.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.noRippleClickable {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.UserAuth.route,
|
NavigationRoute.UserAuth.route,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
// 游客登录按钮
|
// // 游客登录按钮
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
ActionButton(
|
// ActionButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
// modifier = Modifier.fillMaxWidth(),
|
||||||
text = "游客模式",
|
// text = "游客模式",
|
||||||
color = AppColors.text.copy(alpha = 0.7f),
|
// color = AppColors.text.copy(alpha = 0.7f),
|
||||||
) {
|
// ) {
|
||||||
guestLogin()
|
// guestLogin()
|
||||||
}
|
// }
|
||||||
Spacer(modifier = Modifier.height(70.dp))
|
Spacer(modifier = Modifier.height(70.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ 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.Brush
|
||||||
|
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
|
||||||
@@ -60,7 +62,7 @@ fun UserAuthScreen() {
|
|||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
var email by remember { mutableStateOf("") }
|
var email by remember { mutableStateOf("") }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var rememberMe by remember { mutableStateOf(false) }
|
var rememberMe by remember { mutableStateOf(true) }
|
||||||
val accountService: AccountService = AccountServiceImpl()
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
val captchaService: CaptchaService = CaptchaServiceImpl()
|
val captchaService: CaptchaService = CaptchaServiceImpl()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -299,32 +301,38 @@ fun UserAuthScreen() {
|
|||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(R.string.lets_ride_upper),
|
text = stringResource(R.string.lets_ride_upper),
|
||||||
backgroundColor = AppColors.main,
|
backgroundBrush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7c45ed),
|
||||||
|
Color(0x777c68ef),
|
||||||
|
Color(0x777bd8f8)
|
||||||
|
)
|
||||||
|
),
|
||||||
color = AppColors.mainText,
|
color = AppColors.mainText,
|
||||||
) {
|
) {
|
||||||
onLogin()
|
onLogin()
|
||||||
}
|
}
|
||||||
if (AppState.enableGoogleLogin) {
|
// if (AppState.enableGoogleLogin) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(stringResource(R.string.or_login_with), color = AppColors.secondaryText)
|
// Text(stringResource(R.string.or_login_with), color = AppColors.secondaryText)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
ActionButton(
|
// ActionButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
// modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(R.string.sign_in_with_google),
|
// text = stringResource(R.string.sign_in_with_google),
|
||||||
color = AppColors.text,
|
// color = AppColors.text,
|
||||||
leading = {
|
// leading = {
|
||||||
Image(
|
// Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_google),
|
// painter = painterResource(id = R.drawable.rider_pro_google),
|
||||||
contentDescription = "Google",
|
// contentDescription = "Google",
|
||||||
modifier = Modifier.size(36.dp)
|
// modifier = Modifier.size(36.dp)
|
||||||
)
|
// )
|
||||||
},
|
// },
|
||||||
expandText = true,
|
// expandText = true,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
|
// contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
|
||||||
) {
|
// ) {
|
||||||
googleLogin()
|
// googleLogin()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
64
app/src/main/java/com/aiosman/ravenow/ui/network/Reload.kt
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package com.aiosman.ravenow.ui.network
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
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.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.aiosman.ravenow.ui.notification
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.composables.TabItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.TabSpacer
|
||||||
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
import com.aiosman.ravenow.ui.follower.FollowerNoticeScreen
|
||||||
|
import com.aiosman.ravenow.ui.like.LikeNoticeScreen
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun NotificationScreen() {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val pagerState = rememberPagerState(pageCount = { 3 })
|
||||||
|
val Debouncer = rememberDebouncer()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color = AppColors.background)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
Debouncer {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_info_notice_setting),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.W900,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 16.dp, top = 8.dp, bottom = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
TabItem(
|
||||||
|
text = stringResource(R.string.like),
|
||||||
|
isSelected = pagerState.currentPage == 0,
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
TabSpacer()
|
||||||
|
|
||||||
|
TabItem(
|
||||||
|
text = stringResource(R.string.followers_upper),
|
||||||
|
isSelected = pagerState.currentPage == 1,
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
TabSpacer()
|
||||||
|
|
||||||
|
TabItem(
|
||||||
|
text = stringResource(R.string.comment).uppercase(),
|
||||||
|
isSelected = pagerState.currentPage == 2,
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) { page ->
|
||||||
|
when (page) {
|
||||||
|
0 -> LikeNoticeScreen()
|
||||||
|
1 -> FollowerNoticeScreen()
|
||||||
|
2 -> CommentNoticeScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,7 +96,7 @@ fun NewPostScreen() {
|
|||||||
var isAiEnabled by remember { mutableStateOf(false) }
|
var isAiEnabled by remember { mutableStateOf(false) }
|
||||||
var isRotating by remember { mutableStateOf(false) }
|
var isRotating by remember { mutableStateOf(false) }
|
||||||
var isRequesting by remember { mutableStateOf(false) }
|
var isRequesting by remember { mutableStateOf(false) }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current // 添加这行
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
val model = NewPostViewModel
|
val model = NewPostViewModel
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
|
|||||||
@@ -799,7 +799,7 @@ fun CommentContent(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.qs_plq_qs_img),
|
painter = painterResource(id = R.mipmap.invalid_name_3),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(181.dp)
|
modifier = Modifier.size(181.dp)
|
||||||
)
|
)
|
||||||
@@ -917,7 +917,20 @@ fun Header(
|
|||||||
Text(
|
Text(
|
||||||
text = nickname ?: "",
|
text = nickname ?: "",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.debouncedClickable(debounceTime = 1000L) {
|
||||||
|
userId?.let {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(
|
||||||
|
NavigationRoute.AccountProfile.route.replace(
|
||||||
|
"{id}",
|
||||||
|
userId.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 17.sp
|
fontSize = 17.sp
|
||||||
)
|
)
|
||||||
@@ -1196,7 +1209,7 @@ fun PostImageView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation and Indicator container
|
// 图片导航控件
|
||||||
if (images.size > 1) {
|
if (images.size > 1) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -1347,91 +1360,19 @@ fun CommentItem(
|
|||||||
}
|
}
|
||||||
) {}
|
) {}
|
||||||
) {
|
) {
|
||||||
Row {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = commentEntity.name,
|
text = commentEntity.name,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Column(
|
||||||
Text(
|
horizontalAlignment = Alignment.End
|
||||||
text = commentEntity.date.timeAgo(context),
|
|
||||||
fontSize = 11.sp,
|
|
||||||
color = Color.Gray
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row (modifier = Modifier.padding(top = 4.dp)){
|
|
||||||
if (isChild) {
|
|
||||||
val annotatedText = buildAnnotatedString {
|
|
||||||
if (commentEntity.replyUserId != null) {
|
|
||||||
pushStringAnnotation(
|
|
||||||
tag = "replyUser",
|
|
||||||
annotation = commentEntity.replyUserId.toString()
|
|
||||||
)
|
|
||||||
withStyle(
|
|
||||||
style = SpanStyle(
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = Color(0xFF6F94AE)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
append("@${commentEntity.replyUserNickname}")
|
|
||||||
}
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
append(" ${commentEntity.comment}")
|
|
||||||
|
|
||||||
}
|
|
||||||
Box {
|
|
||||||
CustomClickableText(
|
|
||||||
text = annotatedText,
|
|
||||||
onClick = { offset ->
|
|
||||||
annotatedText.getStringAnnotations(
|
|
||||||
tag = "replyUser",
|
|
||||||
start = offset,
|
|
||||||
end = offset
|
|
||||||
).firstOrNull()?.let {
|
|
||||||
debouncedNavigation {
|
|
||||||
navController.navigate(
|
|
||||||
NavigationRoute.AccountProfile.route.replace(
|
|
||||||
"{id}",
|
|
||||||
it.item
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = TextStyle(fontSize = 14.sp, color = AppColors.text),
|
|
||||||
onLongPress = {
|
|
||||||
onLongClick(commentEntity)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = commentEntity.comment,
|
|
||||||
fontSize = 13.sp,
|
|
||||||
maxLines = Int.MAX_VALUE,
|
|
||||||
softWrap = true,
|
|
||||||
lineHeight = 20.sp,
|
|
||||||
color = AppColors.text,
|
|
||||||
modifier = Modifier.combinedClickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onLongClick = {
|
|
||||||
onLongClick(
|
|
||||||
commentEntity
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row (modifier = Modifier.padding(top = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,){
|
|
||||||
AnimatedLikeIcon(
|
AnimatedLikeIcon(
|
||||||
liked = commentEntity.liked,
|
liked = commentEntity.liked,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -1446,29 +1387,54 @@ fun CommentItem(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = commentEntity.likes.toString(),
|
text = commentEntity.likes.toString(),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = AppColors.text
|
color = AppColors.text,
|
||||||
|
modifier = Modifier.padding(top = 4.dp,end = 4.dp)
|
||||||
)
|
)
|
||||||
Text(
|
}
|
||||||
text = stringResource(R.string.like),
|
}
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = AppColors.nonActiveText,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(27.dp))
|
|
||||||
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_comment),
|
Text(
|
||||||
contentDescription = "",
|
text = commentEntity.comment,
|
||||||
modifier = Modifier.size(16.dp),
|
fontSize = 13.sp,
|
||||||
tint = AppColors.nonActiveText
|
maxLines = Int.MAX_VALUE,
|
||||||
|
softWrap = true,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(end = 50.dp)
|
||||||
|
.padding(top = 0.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick(
|
||||||
|
commentEntity
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
},
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Row (
|
||||||
|
modifier = Modifier.padding(top = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = commentEntity.date.timeAgo(context),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.reply),
|
text = stringResource(R.string.reply),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
@@ -1541,6 +1507,7 @@ fun CommentItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PostBottomBar(
|
fun PostBottomBar(
|
||||||
onCreateCommentClick: () -> Unit = {},
|
onCreateCommentClick: () -> Unit = {},
|
||||||
@@ -1607,6 +1574,24 @@ fun PostBottomBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
AnimatedFavouriteIcon(
|
||||||
|
isFavourite = momentEntity?.isFavorite == true,
|
||||||
|
onClick = {
|
||||||
|
// 检查游客模式,如果是游客则跳转登录
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onFavoriteClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(text = momentEntity?.favoriteCount.toString(), color = AppColors.text)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
AnimatedLikeIcon(
|
AnimatedLikeIcon(
|
||||||
liked = momentEntity?.liked == true,
|
liked = momentEntity?.liked == true,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -1623,24 +1608,6 @@ fun PostBottomBar(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(text = momentEntity?.likeCount.toString(), color = AppColors.text)
|
Text(text = momentEntity?.likeCount.toString(), color = AppColors.text)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
AnimatedFavouriteIcon(
|
|
||||||
isFavourite = momentEntity?.isFavorite == true,
|
|
||||||
onClick = {
|
|
||||||
// 检查游客模式,如果是游客则跳转登录
|
|
||||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
|
||||||
debouncedNavigation {
|
|
||||||
navController.navigate(NavigationRoute.Login.route)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onFavoriteClick()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(text = momentEntity?.favoriteCount.toString(), color = AppColors.text)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
BottomNavigationPlaceholder(
|
BottomNavigationPlaceholder(
|
||||||
color = AppColors.background
|
color = AppColors.background
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
package com.aiosman.ravenow.ui.splash
|
package com.aiosman.ravenow.ui.splash
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
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
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
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
|
||||||
@@ -22,38 +21,36 @@ import com.aiosman.ravenow.R
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SplashScreen() {
|
fun SplashScreen() {
|
||||||
Scaffold {
|
|
||||||
it
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
// to bottom
|
// 居中的图标
|
||||||
Box(
|
Image(
|
||||||
contentAlignment = Alignment.TopCenter,
|
painter = painterResource(id = R.mipmap.invalid_name),
|
||||||
modifier = Modifier.padding(top = 211.dp)
|
contentDescription = "App Logo",
|
||||||
) {
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(120.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 底部文字
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.fillMaxWidth()
|
verticalArrangement = Arrangement.Bottom,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 80.dp)
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.rider_pro_logo),
|
painterResource(id = R.mipmap.kp_p_img),
|
||||||
contentDescription = "Rave Now",
|
contentDescription = "",
|
||||||
modifier = Modifier
|
modifier = Modifier.size(85.dp, 25.dp)
|
||||||
.width(108.dp)
|
|
||||||
.height(45.dp)
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.padding(top = 16.dp))
|
||||||
Text(
|
Text(
|
||||||
"Rave Now".uppercase(),
|
stringResource(R.string.splash_title),
|
||||||
fontSize = 28.sp,
|
fontSize = 13.sp
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
)
|
||||||
Text("Your Night Starts Here".uppercase(), fontSize = 20.sp, fontWeight = FontWeight.W700)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
69
app/src/main/java/com/aiosman/ravenow/utils/MessageParser.kt
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package com.aiosman.ravenow.utils
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonSyntaxException
|
||||||
|
import io.openim.android.sdk.models.Message
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM 消息解析工具类
|
||||||
|
* 用于解析 ConversationInfo 中的 latestMsg JSON 字符串
|
||||||
|
*/
|
||||||
|
object MessageParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析最新消息的显示文本和发送者信息
|
||||||
|
* @param latestMsgJson 最新消息的JSON字符串
|
||||||
|
* @return Pair<displayText, isSelf> 显示文本和是否是自己发送的消息
|
||||||
|
*/
|
||||||
|
fun parseLatestMessage(latestMsgJson: String?): Pair<String, Boolean> {
|
||||||
|
var displayText = ""
|
||||||
|
var isSelf = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!latestMsgJson.isNullOrEmpty()) {
|
||||||
|
val gson = Gson()
|
||||||
|
val message = gson.fromJson(latestMsgJson, Message::class.java)
|
||||||
|
|
||||||
|
// 判断是否是自己发送的消息
|
||||||
|
isSelf = message.sendID == AppState.profile?.trtcUserId
|
||||||
|
|
||||||
|
// 根据消息类型生成显示文本
|
||||||
|
displayText = getMessageDisplayText(message)
|
||||||
|
} else {
|
||||||
|
displayText = "[暂无消息]"
|
||||||
|
}
|
||||||
|
} catch (e: JsonSyntaxException) {
|
||||||
|
// JSON 解析失败,使用原始文本
|
||||||
|
displayText = latestMsgJson ?: "[消息解析失败]"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 其他异常
|
||||||
|
displayText = "[消息]"
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(displayText, isSelf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据消息类型获取显示文本
|
||||||
|
* @param message OpenIM Message 对象
|
||||||
|
* @return 消息的显示文本
|
||||||
|
*/
|
||||||
|
private fun getMessageDisplayText(message: Message): String {
|
||||||
|
return when (message.contentType) {
|
||||||
|
101 -> { // TEXT
|
||||||
|
message.textElem?.content ?: "[文本消息]"
|
||||||
|
}
|
||||||
|
102 -> "[图片]" // IMAGE
|
||||||
|
103 -> "[语音]" // AUDIO
|
||||||
|
104 -> "[视频]" // VIDEO
|
||||||
|
105 -> "[文件]" // FILE
|
||||||
|
106 -> "[位置]" // LOCATION
|
||||||
|
107 -> "[自定义消息]" // CUSTOM
|
||||||
|
108 -> "[合并消息]" // MERGE
|
||||||
|
109 -> "[名片]" // CARD
|
||||||
|
110 -> "[引用消息]" // QUOTE
|
||||||
|
else -> "[消息]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,23 @@ object Utils {
|
|||||||
return Locale.getDefault().language
|
return Locale.getDefault().language
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整的语言标记,如 "zh-CN", "en-US"
|
||||||
|
* 优先使用完整的 BCP-47 语言标记,提升与后端 translations 键的匹配率
|
||||||
|
*/
|
||||||
|
fun getPreferredLanguageTag(): String {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
val language = locale.language
|
||||||
|
val country = locale.country
|
||||||
|
|
||||||
|
// 如果有国家/地区代码,返回完整的语言标记
|
||||||
|
return if (country.isNotEmpty()) {
|
||||||
|
"$language-$country"
|
||||||
|
} else {
|
||||||
|
language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
|||||||
22
app/src/main/res/drawable/home_logo.xml
Normal file
19
app/src/main/res/drawable/ic_create_ai.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="72dp"
|
||||||
|
android:height="72dp"
|
||||||
|
android:viewportWidth="72"
|
||||||
|
android:viewportHeight="72">
|
||||||
|
<path
|
||||||
|
android:pathData="M36,36m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0"
|
||||||
|
android:fillColor="#8FBFFA"
|
||||||
|
android:fillAlpha="0.08"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M36.804,31.654h-0.108c-2.5,0 -5.222,0 -7.559,0.409a4.115,4.115 0,0 0,-3.3 3.202c-0.337,1.56 -0.337,2.86 -0.337,5.224v0.175c0,2.365 0,3.665 0.338,5.225a4.115,4.115 0,0 0,3.3 3.2c2.336,0.411 5.058,0.411 7.558,0.411h0.108c2.5,0 5.222,0 7.559,-0.41a4.115,4.115 0,0 0,3.3 -3.2C48,44.328 48,43.03 48,40.663v-0.175c0,-2.364 0,-3.663 -0.338,-5.224a4.115,4.115 0,0 0,-3.3 -3.202c-2.336,-0.409 -5.058,-0.409 -7.558,-0.409z"
|
||||||
|
android:fillColor="#8FBFFA"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M36.75,22.5c-1.06,0 -2.031,0.302 -2.738,1.006 -0.704,0.704 -1.004,1.673 -1.004,2.735 0,1.058 0.302,2.028 1.006,2.732 0.35,0.35 0.765,0.601 1.228,0.764v1.921c0.49,-0.004 0.977,-0.004 1.454,-0.004h0.108c0.48,0 0.967,0 1.456,0.004v-1.92c0.461,-0.16 0.881,-0.421 1.228,-0.765 0.704,-0.704 1.006,-1.674 1.006,-2.734s-0.302,-2.03 -1.006,-2.733c-0.705,-0.704 -1.677,-1.006 -2.738,-1.006zM32.682,35.919c0.694,0 1.258,0.563 1.258,1.257v1.213a1.258,1.258 0,0 1,-2.516 0v-1.213c0,-0.694 0.564,-1.257 1.258,-1.257zM40.818,35.919c0.694,0 1.258,0.563 1.258,1.257v1.213a1.258,1.258 0,0 1,-2.516 0v-1.213c0,-0.694 0.564,-1.257 1.258,-1.257zM32.193,41.933a1.259,1.259 0,0 1,1.7 0.48l0.013,0.019c0.16,0.227 0.354,0.43 0.573,0.601 0.435,0.342 1.154,0.718 2.271,0.718 1.115,0 1.836,-0.378 2.273,-0.718a2.87,2.87 0,0 0,0.571 -0.603l0.012,-0.016A1.259,1.259 0,0 1,41.8 43.64l-1.1,-0.606 1.1,0.608v0.004l-0.004,0.006 -0.008,0.014 -0.022,0.036 -0.066,0.105a5.392,5.392 0,0 1,-1.128 1.202c-0.82,0.646 -2.077,1.256 -3.824,1.256 -1.743,0 -3,-0.61 -3.822,-1.256a5.392,5.392 0,0 1,-1.128 -1.202,2.99 2.99,0 0,1 -0.064,-0.105l-0.02,-0.036 -0.01,-0.014 -0.002,-0.006 -0.002,-0.002c0,-0.002 0,-0.004 1.1,-0.61l-1.102,0.608a1.256,1.256 0,0 1,0.495 -1.71z"
|
||||||
|
android:fillColor="#2859C5"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
15
app/src/main/res/drawable/ic_create_close.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="32dp"
|
||||||
|
android:height="33dp"
|
||||||
|
android:viewportWidth="32"
|
||||||
|
android:viewportHeight="33">
|
||||||
|
<path
|
||||||
|
android:pathData="M15.478,0.75L16.522,0.75A15.478,15.478 0,0 1,32 16.228L32,17.522A15.478,15.478 0,0 1,16.522 33L15.478,33A15.478,15.478 0,0 1,0 17.522L0,16.228A15.478,15.478 0,0 1,15.478 0.75z"
|
||||||
|
android:fillColor="#7C7480"
|
||||||
|
android:fillAlpha="0.08"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M20.782,12.155a1.29,1.29 0,0 1,0 1.824l-2.889,2.889 2.86,2.859a1.29,1.29 0,0 1,0 1.824l-0.077,0.076a1.29,1.29 0,0 1,-1.824 0l-2.859,-2.86 -2.889,2.89a1.29,1.29 0,0 1,-1.824 0l-0.062,-0.062a1.29,1.29 0,0 1,0 -1.824l2.89,-2.89 -2.86,-2.858a1.29,1.29 0,0 1,0 -1.824l0.076,-0.076a1.29,1.29 0,0 1,1.824 0l2.86,2.859 2.888,-2.889a1.29,1.29 0,0 1,1.824 0l0.062,0.062z"
|
||||||
|
android:fillColor="#918E93"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
28
app/src/main/res/drawable/ic_create_group_chat.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="72dp"
|
||||||
|
android:height="72dp"
|
||||||
|
android:viewportWidth="72"
|
||||||
|
android:viewportHeight="72">
|
||||||
|
<path
|
||||||
|
android:pathData="M36,36m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0"
|
||||||
|
android:fillColor="#FC0"
|
||||||
|
android:fillAlpha="0.08"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M31.206,26.187c1.179,-1.192 2.856,-1.826 4.61,-1.57l8.182,1.196c3.012,0.44 5.11,3.336 4.687,6.468l-1.533,11.34 -1.75,-0.255"
|
||||||
|
android:strokeWidth="2.147"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#FECE51"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m28.156,30.17 l7.756,-1.134a5.726,5.726 0,0 1,6.502 4.899l0.766,5.662a5.726,5.726 0,0 1,-4.847 6.433L24.85,48l-1.54,-11.398a5.726,5.726 0,0 1,4.846 -6.433z"
|
||||||
|
android:strokeWidth="2.147"
|
||||||
|
android:fillColor="#FECE51"
|
||||||
|
android:strokeColor="#FECE51"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m27.74,39.562 l11.024,-0.06 -0.015,2.861 -11.024,0.06z"
|
||||||
|
android:fillColor="#D86002"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
21
app/src/main/res/drawable/ic_create_monent.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="72dp"
|
||||||
|
android:height="72dp"
|
||||||
|
android:viewportWidth="72"
|
||||||
|
android:viewportHeight="72">
|
||||||
|
<path
|
||||||
|
android:pathData="M36,36m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0"
|
||||||
|
android:fillColor="#3DC779"
|
||||||
|
android:fillAlpha="0.08"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M31.555,24.228a0.964,0.964 0,0 0,-0.845 0.498l-1.958,3.548 -0.636,0.054 -0.154,0.012a5,5 0,0 0,-4.504 4.233c-0.246,1.664 -0.476,3.433 -0.476,5.253 0,1.82 0.232,3.59 0.476,5.256a5,5 0,0 0,4.504 4.231c2.64,0.228 5.33,0.459 8.038,0.459s5.398,-0.231 8.038,-0.459a5,5 0,0 0,4.503 -4.231c0.247,-1.667 0.477,-3.433 0.477,-5.256 0,-1.82 -0.232,-3.587 -0.477,-5.253a5,5 0,0 0,-4.503 -4.233l-0.127,-0.01 -0.663,-0.058 -1.958,-3.546a0.964,0.964 0,0 0,-0.845 -0.498h-8.89z"
|
||||||
|
android:fillColor="#61CD8C"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M36,33.345c1.253,0 2.26,0.306 2.934,0.98 0.674,0.674 0.98,1.68 0.98,2.932 0,1.253 -0.306,2.26 -0.98,2.934 -0.674,0.674 -1.681,0.98 -2.935,0.98 -1.252,0.002 -2.26,-0.305 -2.933,-0.978 -0.674,-0.674 -0.98,-1.681 -0.98,-2.934 0,-1.253 0.306,-2.26 0.98,-2.934 0.674,-0.674 1.68,-0.98 2.934,-0.98z"
|
||||||
|
android:strokeWidth="2.25"
|
||||||
|
android:fillColor="#61CD8C"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:strokeColor="#FFF"/>
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ai.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/apple_logo_medium.png
Normal file
|
After Width: | Height: | Size: 356 B |
BIN
app/src/main/res/mipmap-hdpi/arrow.png
Normal file
|
After Width: | Height: | Size: 247 B |
BIN
app/src/main/res/mipmap-hdpi/bars_x_buttons_chat_s.png
Normal file
|
After Width: | Height: | Size: 773 B |
BIN
app/src/main/res/mipmap-hdpi/bars_x_buttons_discover_bold.png
Normal file
|
After Width: | Height: | Size: 551 B |
BIN
app/src/main/res/mipmap-hdpi/bars_x_buttons_discover_fill.png
Normal file
|
After Width: | Height: | Size: 927 B |
BIN
app/src/main/res/mipmap-hdpi/bars_x_buttons_home_n_copy.png
Normal file
|
After Width: | Height: | Size: 535 B |
BIN
app/src/main/res/mipmap-hdpi/bars_x_buttons_home_n_copy_2.png
Normal file
|
After Width: | Height: | Size: 772 B |
BIN
app/src/main/res/mipmap-hdpi/bars_x_buttons_home_s.png
Normal file
|
After Width: | Height: | Size: 689 B |