Compare commits
20 Commits
open_im
...
update_tik
| Author | SHA1 | Date | |
|---|---|---|---|
| b06f2d6c4a | |||
| a215d79ce8 | |||
| 0e0d622864 | |||
| a1196715d0 | |||
| 68273ae166 | |||
| 6c7be4ba47 | |||
| cf25540417 | |||
| eca85c8377 | |||
| 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-09T09:51:06.656104400Z">
|
||||||
<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.227:45035;connection=094cb92e" />
|
||||||
</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"
|
||||||
|
|||||||
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"
|
||||||
|
|||||||
@@ -67,8 +67,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 +106,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环境
|
||||||
|
// } else {
|
||||||
|
// "https://rider-pro.aiosman.com/beta_api" // Release环境
|
||||||
|
// }
|
||||||
|
val BASE_SERVER = "https://rider-pro.aiosman.com/beta_api"
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
@@ -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>?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类翻译数据模型
|
||||||
|
*/
|
||||||
|
data class CategoryTranslation(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类翻译集合
|
||||||
|
*/
|
||||||
|
data class CategoryTranslations(
|
||||||
|
@SerializedName("ja")
|
||||||
|
val ja: CategoryTranslation? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据模型
|
||||||
|
*/
|
||||||
|
data class Category(
|
||||||
|
@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? = null,
|
||||||
|
@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: CategoryTranslations? = null
|
||||||
|
)
|
||||||
@@ -32,6 +32,28 @@ data class Moment(
|
|||||||
val time: String,
|
val time: String,
|
||||||
@SerializedName("isFollowed")
|
@SerializedName("isFollowed")
|
||||||
val isFollowed: Boolean,
|
val isFollowed: Boolean,
|
||||||
|
@SerializedName("isNews")
|
||||||
|
val isNews: Boolean? = null,
|
||||||
|
@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,
|
||||||
|
@SerializedName("hasFullText")
|
||||||
|
val hasFullText: Boolean? = null,
|
||||||
|
@SerializedName("summary")
|
||||||
|
val summary: String? = null,
|
||||||
|
@SerializedName("publishedAt")
|
||||||
|
val publishedAt: String? = null,
|
||||||
|
@SerializedName("imageCached")
|
||||||
|
val imageCached: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
fun toMomentItem(): MomentEntity {
|
fun toMomentItem(): MomentEntity {
|
||||||
return MomentEntity(
|
return MomentEntity(
|
||||||
@@ -60,6 +82,17 @@ 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,
|
||||||
|
hasFullText = hasFullText,
|
||||||
|
summary = summary,
|
||||||
|
publishedAt = publishedAt,
|
||||||
|
imageCached = imageCached,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
fun getSafeOkHttpClient(
|
fun getSafeOkHttpClient(
|
||||||
authInterceptor: AuthInterceptor? = null
|
authInterceptor: AuthInterceptor? = null,
|
||||||
|
timeoutSeconds: Long = 30
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
return OkHttpClient.Builder()
|
return OkHttpClient.Builder()
|
||||||
.apply {
|
.apply {
|
||||||
@@ -23,6 +25,9 @@ fun getSafeOkHttpClient(
|
|||||||
addInterceptor(it)
|
addInterceptor(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +51,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
|
||||||
@@ -55,7 +61,7 @@ class AuthInterceptor() : Interceptor {
|
|||||||
val client = Retrofit.Builder()
|
val client = Retrofit.Builder()
|
||||||
.baseUrl(ApiClient.RETROFIT_URL)
|
.baseUrl(ApiClient.RETROFIT_URL)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.client(getSafeOkHttpClient())
|
.client(getSafeOkHttpClient(timeoutSeconds = 30))
|
||||||
.build()
|
.build()
|
||||||
.create(RaveNowAPI::class.java)
|
.create(RaveNowAPI::class.java)
|
||||||
|
|
||||||
@@ -69,12 +75,15 @@ 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(), timeoutSeconds = 30)
|
||||||
|
}
|
||||||
|
private val longTimeoutOkHttpClient: OkHttpClient by lazy {
|
||||||
|
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 120)
|
||||||
}
|
}
|
||||||
private val retrofit: Retrofit by lazy {
|
private val retrofit: Retrofit by lazy {
|
||||||
Retrofit.Builder()
|
Retrofit.Builder()
|
||||||
@@ -83,9 +92,19 @@ object ApiClient {
|
|||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
private val longTimeoutRetrofit: Retrofit by lazy {
|
||||||
|
Retrofit.Builder()
|
||||||
|
.baseUrl(RETROFIT_URL)
|
||||||
|
.client(longTimeoutOkHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
val api: RaveNowAPI by lazy {
|
val api: RaveNowAPI by lazy {
|
||||||
retrofit.create(RaveNowAPI::class.java)
|
retrofit.create(RaveNowAPI::class.java)
|
||||||
}
|
}
|
||||||
|
val longTimeoutApi: RaveNowAPI by lazy {
|
||||||
|
longTimeoutRetrofit.create(RaveNowAPI::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
fun formatTime(date: Date): String {
|
fun formatTime(date: Date): String {
|
||||||
val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault())
|
val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.aiosman.ravenow.data.AccountLike
|
|||||||
import com.aiosman.ravenow.data.AccountNotice
|
import com.aiosman.ravenow.data.AccountNotice
|
||||||
import com.aiosman.ravenow.data.AccountProfile
|
import com.aiosman.ravenow.data.AccountProfile
|
||||||
import com.aiosman.ravenow.data.Agent
|
import com.aiosman.ravenow.data.Agent
|
||||||
|
import com.aiosman.ravenow.data.Category
|
||||||
import com.aiosman.ravenow.data.Comment
|
import com.aiosman.ravenow.data.Comment
|
||||||
import com.aiosman.ravenow.data.DataContainer
|
import com.aiosman.ravenow.data.DataContainer
|
||||||
import com.aiosman.ravenow.data.ListContainer
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
@@ -42,6 +43,20 @@ data class AgentMomentRequestBody(
|
|||||||
val sessionId: String
|
val sessionId: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class GenerateAgentInfoRequestBody(
|
||||||
|
@SerializedName("descriptionText")
|
||||||
|
val descriptionText: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GenerateAgentInfoResponseBody(
|
||||||
|
@SerializedName("title")
|
||||||
|
val title: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String,
|
||||||
|
@SerializedName("content")
|
||||||
|
val content: String
|
||||||
|
)
|
||||||
|
|
||||||
data class SingleChatRequestBody(
|
data class SingleChatRequestBody(
|
||||||
@SerializedName("agentOpenId")
|
@SerializedName("agentOpenId")
|
||||||
val agentOpenId: String? = null,
|
val agentOpenId: String? = null,
|
||||||
@@ -302,6 +317,7 @@ interface RaveNowAPI {
|
|||||||
suspend fun getPosts(
|
suspend fun getPosts(
|
||||||
@Query("page") page: Int = 1,
|
@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("id") postId: Int? = null,
|
||||||
@Query("timelineId") timelineId: Int? = null,
|
@Query("timelineId") timelineId: Int? = null,
|
||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
@Query("contentSearch") contentSearch: String? = null,
|
@Query("contentSearch") contentSearch: String? = null,
|
||||||
@@ -309,6 +325,15 @@ 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("imageTag") imageTag: String? = null,
|
||||||
|
@Query("search") search: String? = null,
|
||||||
|
@Query("advancedSearch") advancedSearch: String? = null,
|
||||||
|
@Query("newsFilter") newsFilter: String? = null,
|
||||||
|
@Query("onlyNews") onlyNews: Boolean? = null,
|
||||||
|
@Query("newsSource") newsSource: String? = null,
|
||||||
|
@Query("newsLanguage") newsLanguage: String? = null,
|
||||||
|
@Query("newsCategory") newsCategory: String? = null,
|
||||||
|
@Query("requireImageCache") requireImageCache: Boolean? = null,
|
||||||
): Response<ListContainer<Moment>>
|
): Response<ListContainer<Moment>>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@@ -546,19 +571,39 @@ interface RaveNowAPI {
|
|||||||
@Body body: RemoveAccountRequestBody
|
@Body body: RemoveAccountRequestBody
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GET("outside/prompts")
|
@GET("outside/prompts")
|
||||||
suspend fun getAgent(
|
suspend fun getAgent(
|
||||||
@Query("page") page: Int = 1,
|
@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("withWorkflow") withWorkflow: Int = 1,
|
@Query("order") order: String? = null,
|
||||||
|
@Query("orderKey") orderKey: String? = null,
|
||||||
|
@Query("createdAt") createdAt: String? = null,
|
||||||
|
@Query("updatedAt") updatedAt: String? = null,
|
||||||
|
@Query("createdStart") createdStart: String? = null,
|
||||||
|
@Query("createdEnd") createdEnd: String? = null,
|
||||||
|
@Query("updatedStart") updatedStart: String? = null,
|
||||||
|
@Query("updatedEnd") updatedEnd: String? = null,
|
||||||
|
@Query("title") title: String? = null,
|
||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
|
@Query("authorOpenId") authorOpenId: String? = null,
|
||||||
|
@Query("showPrivate") showPrivate: String? = null,
|
||||||
|
@Query("explore") explore: String? = null,
|
||||||
|
@Query("desc") desc: String? = null,
|
||||||
|
@Query("withWorkflow") withWorkflow: String? = null,
|
||||||
|
@Query("hasAvatar") hasAvatar: String? = null,
|
||||||
|
@Query("random") random: String? = null,
|
||||||
|
@Query("categoryName") categoryName: String? = null,
|
||||||
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
|
@Query("uncategorized") uncategorized: String? = null,
|
||||||
): Response<DataContainer<ListContainer<Agent>>>
|
): Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
@GET("outside/my/prompts")
|
@GET("outside/my/prompts")
|
||||||
suspend fun getMyAgent(
|
suspend fun getMyAgent(
|
||||||
@Query("page") page: Int = 1,
|
@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("withWorkflow") withWorkflow: Int = 1,
|
@Query("withWorkflow") withWorkflow: String = "1",
|
||||||
): Response<ListContainer<Agent>>
|
): Response<ListContainer<Agent>>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@@ -595,6 +640,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 +651,15 @@ interface RaveNowAPI {
|
|||||||
suspend fun joinRoom(@Body body: JoinGroupChatRequestBody,
|
suspend fun joinRoom(@Body body: JoinGroupChatRequestBody,
|
||||||
): Response<DataContainer<Room>>
|
): Response<DataContainer<Room>>
|
||||||
|
|
||||||
|
@POST("outside/generate/agent-info")
|
||||||
|
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
|
||||||
|
|
||||||
|
@GET("outside/categories")
|
||||||
|
suspend fun getCategories(
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("lang") lang: String? = null
|
||||||
|
): Response<ListContainer<Category>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
160
app/src/main/java/com/aiosman/ravenow/entity/Category.kt
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.data.Category
|
||||||
|
import com.aiosman.ravenow.data.CategoryTranslation
|
||||||
|
import com.aiosman.ravenow.data.CategoryTranslations
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.utils.Utils
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类实体类
|
||||||
|
*/
|
||||||
|
data class CategoryEntity(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val avatar: String,
|
||||||
|
val parentId: Int? = null,
|
||||||
|
val sort: Int,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val promptCount: Int,
|
||||||
|
val createdAt: String,
|
||||||
|
val updatedAt: String,
|
||||||
|
val translations: CategoryTranslations? = null
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* 获取本地化名称
|
||||||
|
*/
|
||||||
|
fun getLocalizedName(): String {
|
||||||
|
return when (Utils.getCurrentLanguage()) {
|
||||||
|
"ja" -> translations?.ja?.name ?: name
|
||||||
|
else -> name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本地化描述
|
||||||
|
*/
|
||||||
|
fun getLocalizedDescription(): String {
|
||||||
|
return when (Utils.getCurrentLanguage()) {
|
||||||
|
"ja" -> translations?.ja?.description ?: description
|
||||||
|
else -> description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category 数据模型扩展函数
|
||||||
|
*/
|
||||||
|
fun Category.toCategoryEntity(): CategoryEntity {
|
||||||
|
return CategoryEntity(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
avatar = if (avatar.isNotEmpty()) "${ApiClient.BASE_API_URL}/outside$avatar?token=${AppStore.token}" else "",
|
||||||
|
parentId = parentId,
|
||||||
|
sort = sort,
|
||||||
|
isActive = isActive,
|
||||||
|
promptCount = promptCount,
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
translations = translations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据后端服务
|
||||||
|
*/
|
||||||
|
class CategoryBackend {
|
||||||
|
private val DataBatchSize = 20
|
||||||
|
|
||||||
|
suspend fun getCategories(pageNumber: Int): ListContainer<CategoryEntity>? {
|
||||||
|
try {
|
||||||
|
val resp = ApiClient.api.getCategories(
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
lang = Utils.getCurrentLanguageTag()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!resp.isSuccessful) {
|
||||||
|
throw ServiceException("获取分类失败: ${resp.code()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = resp.body() ?: return null
|
||||||
|
|
||||||
|
return ListContainer(
|
||||||
|
total = body.total,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
list = body.list.map { it.toCategoryEntity() }
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ServiceException("网络请求失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据加载器参数
|
||||||
|
*/
|
||||||
|
class CategoryLoaderExtraArgs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据加载器
|
||||||
|
*/
|
||||||
|
class CategoryLoader : DataLoader<CategoryEntity, CategoryLoaderExtraArgs>() {
|
||||||
|
override suspend fun fetchData(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
extra: CategoryLoaderExtraArgs
|
||||||
|
): ListContainer<CategoryEntity> {
|
||||||
|
val backend = CategoryBackend()
|
||||||
|
return backend.getCategories(page) ?: ListContainer(
|
||||||
|
total = 0,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
list = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类分页数据源
|
||||||
|
*/
|
||||||
|
class CategoryPagingSource(
|
||||||
|
private val backend: CategoryBackend
|
||||||
|
) : PagingSource<Int, CategoryEntity>() {
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, CategoryEntity>): Int? {
|
||||||
|
return state.anchorPosition?.let { anchorPosition ->
|
||||||
|
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
||||||
|
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CategoryEntity> {
|
||||||
|
return try {
|
||||||
|
val page = params.key ?: 1
|
||||||
|
val response = backend.getCategories(page)
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
LoadResult.Error(IOException("获取分类数据失败"))
|
||||||
|
} else {
|
||||||
|
val hasMore = response.list.size == response.pageSize
|
||||||
|
LoadResult.Page(
|
||||||
|
data = response.list,
|
||||||
|
prevKey = if (page == 1) null else page - 1,
|
||||||
|
nextKey = if (hasMore) page + 1 else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -299,12 +299,36 @@ data class MomentEntity(
|
|||||||
// 关联动态
|
// 关联动态
|
||||||
var relMoment: MomentEntity? = null,
|
var relMoment: MomentEntity? = null,
|
||||||
// 是否收藏
|
// 是否收藏
|
||||||
var isFavorite: Boolean = false
|
var isFavorite: Boolean = false,
|
||||||
|
// 是否为新闻
|
||||||
|
val isNews: Boolean? = null,
|
||||||
|
// 新闻标题
|
||||||
|
val newsTitle: String? = null,
|
||||||
|
// 新闻链接
|
||||||
|
val newsUrl: String? = null,
|
||||||
|
// 新闻来源
|
||||||
|
val newsSource: String? = null,
|
||||||
|
// 新闻分类
|
||||||
|
val newsCategory: String? = null,
|
||||||
|
// 新闻语言
|
||||||
|
val newsLanguage: String? = null,
|
||||||
|
// 新闻内容
|
||||||
|
val newsContent: String? = null,
|
||||||
|
// 是否有完整文本
|
||||||
|
val hasFullText: Boolean? = null,
|
||||||
|
// 摘要
|
||||||
|
val summary: String? = null,
|
||||||
|
// 发布时间
|
||||||
|
val publishedAt: String? = null,
|
||||||
|
// 图片是否已缓存
|
||||||
|
val imageCached: Boolean? = null
|
||||||
)
|
)
|
||||||
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? = false,
|
||||||
|
val trend: Boolean? = false
|
||||||
)
|
)
|
||||||
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||||
override suspend fun fetchData(
|
override suspend fun fetchData(
|
||||||
@@ -317,7 +341,10 @@ 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 "",
|
||||||
|
trend = if (extra.trend == true) "1" else ""
|
||||||
|
|
||||||
)
|
)
|
||||||
val data = result.body()?.let {
|
val data = result.body()?.let {
|
||||||
ListContainer(
|
ListContainer(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.aiosman.ravenow.ui.account.RemoveAccountScreen
|
|||||||
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
|
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
|
||||||
import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
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.agent.CreateAgentV2Screen
|
||||||
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.ChatScreen
|
import com.aiosman.ravenow.ui.chat.ChatScreen
|
||||||
@@ -544,7 +545,7 @@ fun NavigationController(
|
|||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.AddAgent.route,
|
route = NavigationRoute.AddAgent.route,
|
||||||
) {
|
) {
|
||||||
AddAgentScreen()
|
CreateAgentV2Screen()
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(
|
composable(
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ 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 hasExitedPage by mutableStateOf(false) // 标记是否已经完全退出页面
|
||||||
|
|
||||||
|
// 保存AI生成的输入文本,避免页面重建时丢失
|
||||||
|
var generateInputText by mutableStateOf("")
|
||||||
|
|
||||||
suspend fun updateAgentAvatar(context: Context) {
|
suspend fun updateAgentAvatar(context: Context) {
|
||||||
croppedBitmap?.let {
|
croppedBitmap?.let {
|
||||||
@@ -84,5 +88,7 @@ object AddAgentViewModel : ViewModel() {
|
|||||||
croppedBitmap = null
|
croppedBitmap = null
|
||||||
isUpdating = false
|
isUpdating = false
|
||||||
isSelectingAvatar = false
|
isSelectingAvatar = false
|
||||||
|
hasExitedPage = false
|
||||||
|
generateInputText = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
648
app/src/main/java/com/aiosman/ravenow/ui/agent/CreateAgentV2.kt
Normal file
@@ -0,0 +1,648 @@
|
|||||||
|
package com.aiosman.ravenow.ui.agent
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
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.AppState
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingDots(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
dotColor: Color = Color.Gray
|
||||||
|
) {
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "loading_dots")
|
||||||
|
|
||||||
|
val animationValues = (0..2).map { index ->
|
||||||
|
infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 600,
|
||||||
|
easing = EaseInOut,
|
||||||
|
delayMillis = index * 200
|
||||||
|
),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "dot_$index"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
animationValues.forEach { animValue ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(6.dp)
|
||||||
|
.offset(y = (-8 * animValue.value).dp)
|
||||||
|
.background(
|
||||||
|
color = dotColor.copy(alpha = 0.5f + 0.5f * animValue.value),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CreateAgentV2Screen(
|
||||||
|
viewModel: CreateAgentV2ViewModel = remember { CreateAgentV2ViewModel() }
|
||||||
|
) {
|
||||||
|
// 页面进入时的状态管理
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// 总是先同步状态
|
||||||
|
viewModel.syncStateOnResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面退出时的处理
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
// 页面退出时,标记为已退出(除非是在选择头像)
|
||||||
|
if (!viewModel.isSelectingAvatar) {
|
||||||
|
viewModel.markPageExited()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// 获取当前用户名,如果没有则使用默认值
|
||||||
|
val userName = AppState.profile?.nickName ?: "用户"
|
||||||
|
|
||||||
|
// 渐变边框旋转动画
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "gradient_rotation")
|
||||||
|
val rotationAngle by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 360f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 16000,
|
||||||
|
easing = LinearEasing
|
||||||
|
),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
),
|
||||||
|
label = "rotation_angle"
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(appColors.background)
|
||||||
|
) {
|
||||||
|
// 状态栏占位
|
||||||
|
StatusBarSpacer()
|
||||||
|
|
||||||
|
// 顶部标题栏
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 返回按钮
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "返回",
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// 标题 - 左对齐
|
||||||
|
Text(
|
||||||
|
text = "创建AI",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.W700,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主要内容区域 - 可滚动
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.Start
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
|
// AI头像图标
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(
|
||||||
|
color = appColors.inputBackground,
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_create_head_logo),
|
||||||
|
contentDescription = "AI头像",
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 问候语
|
||||||
|
Text(
|
||||||
|
text = "$userName 你好呀!今天想创建什么?",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = appColors.text,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 描述性文字
|
||||||
|
Text(
|
||||||
|
text = "只需要一句话,你的专属AI在这里诞生",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = appColors.secondaryText,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
|
// 根据模式显示不同的UI
|
||||||
|
if (!viewModel.isManualMode) {
|
||||||
|
// AI生成模式 - 渐变边框输入框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.shadow(
|
||||||
|
elevation = 8.dp,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
ambientColor = Color(0xFF6246ff).copy(alpha = 0.4f),
|
||||||
|
spotColor = Color(0xFFd80264).copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 渐变边框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF6246ff), // 紫色
|
||||||
|
Color(0xFFd80264), // 红色
|
||||||
|
Color(0xFF6246ff), // 紫色
|
||||||
|
Color(0xFFd80264) // 红色
|
||||||
|
),
|
||||||
|
start = Offset(
|
||||||
|
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
|
||||||
|
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
|
||||||
|
),
|
||||||
|
end = Offset(
|
||||||
|
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
|
||||||
|
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
.padding(1.5.dp) // 边框宽度
|
||||||
|
) {
|
||||||
|
// 内部输入框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = appColors.background,
|
||||||
|
shape = RoundedCornerShape(15.dp)
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
TextField(
|
||||||
|
value = viewModel.inputText,
|
||||||
|
onValueChange = {
|
||||||
|
if (!viewModel.isGenerating) {
|
||||||
|
viewModel.updateInputText(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "一个会写诗的AI,一个会懂你笑点的AI",
|
||||||
|
color = appColors.inputHint,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
focusedTextColor = appColors.text,
|
||||||
|
unfocusedTextColor = appColors.text,
|
||||||
|
disabledTextColor = appColors.inputHint,
|
||||||
|
cursorColor = if (viewModel.isGenerating) Color.Transparent else appColors.main,
|
||||||
|
focusedPlaceholderColor = appColors.inputHint,
|
||||||
|
unfocusedPlaceholderColor = appColors.inputHint,
|
||||||
|
disabledPlaceholderColor = appColors.inputHint.copy(alpha = 0.5f)
|
||||||
|
),
|
||||||
|
enabled = !viewModel.isGenerating,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 100.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// AI美化按钮
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (!viewModel.isGenerating) {
|
||||||
|
viewModel.generateAgentInfo()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = viewModel.canGenerate() && !viewModel.isGenerating,
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = if (viewModel.isGenerating) appColors.inputHint else Color(0xFF7c46ed),
|
||||||
|
disabledContentColor = appColors.inputHint
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_create_agent_generate),
|
||||||
|
contentDescription = "AI美化图标",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color(0xFF7c46ed))
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (viewModel.isGenerating) "生成中..." else "AI美化",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// AI生成中的loading状态
|
||||||
|
if (viewModel.isGenerating) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
LoadingDots(
|
||||||
|
dotColor = appColors.main
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "正在为你构思",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = appColors.secondaryText,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动创造AI按钮 - 只在非生成状态下显示
|
||||||
|
if (!viewModel.isGenerating) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
if (!viewModel.isGenerating) {
|
||||||
|
viewModel.enableManualMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !viewModel.isGenerating,
|
||||||
|
modifier = Modifier.height(40.dp),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
border = BorderStroke(1.dp, appColors.inputHint.copy(alpha = 0.3f)),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = appColors.secondaryText,
|
||||||
|
disabledContentColor = appColors.inputHint
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "编辑图标",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = appColors.secondaryText
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "手动创造AI",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 手动模式 - "一句话创造AI"按钮
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
ambientColor = Color(0xFF6246ff).copy(alpha = 0.3f),
|
||||||
|
spotColor = Color(0xFFd80264).copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 渐变边框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF6246ff), // 紫色
|
||||||
|
Color(0xFFd80264), // 红色
|
||||||
|
Color(0xFF6246ff), // 紫色
|
||||||
|
Color(0xFFd80264) // 红色
|
||||||
|
),
|
||||||
|
start = Offset(
|
||||||
|
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
|
||||||
|
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
|
||||||
|
),
|
||||||
|
end = Offset(
|
||||||
|
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
|
||||||
|
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.padding(1.dp) // 边框宽度
|
||||||
|
) {
|
||||||
|
// 内部按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = appColors.background,
|
||||||
|
shape = RoundedCornerShape(11.dp)
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
viewModel.disableManualMode()
|
||||||
|
}
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "编辑图标",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = appColors.secondaryText
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "一句话创造AI",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = appColors.secondaryText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成结果显示区域
|
||||||
|
if (viewModel.hasGeneratedResult()) {
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 头像选择组件
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
viewModel.setSelectingAvatar(true)
|
||||||
|
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (viewModel.croppedBitmap != null) {
|
||||||
|
// 有头像时显示头像
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
viewModel.croppedBitmap,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentDescription = "AI头像",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
placeholderRes = R.mipmap.rider_pro_agent_avatar,
|
||||||
|
showShimmer = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 没有头像时显示渐变背景和编辑图标
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
0f to Color(0xFF7c45ed),
|
||||||
|
0.24f to Color(0xFF7c68ef),
|
||||||
|
1f to Color(0xFF7bd8f8)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "选择头像",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 标题输入框
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "名称",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = appColors.text,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = viewModel.agentTitle,
|
||||||
|
onValueChange = { viewModel.updateAgentTitle(it) },
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = appColors.inputBackground,
|
||||||
|
unfocusedContainerColor = appColors.inputBackground,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
focusedTextColor = appColors.text,
|
||||||
|
unfocusedTextColor = appColors.text
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 描述输入框
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "描述",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = appColors.text,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = viewModel.agentDescription,
|
||||||
|
onValueChange = { viewModel.updateAgentDescription(it) },
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = appColors.inputBackground,
|
||||||
|
unfocusedContainerColor = appColors.inputBackground,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
focusedTextColor = appColors.text,
|
||||||
|
unfocusedTextColor = appColors.text
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 120.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 错误信息显示
|
||||||
|
viewModel.errorMessage?.let { error ->
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = Color.Red,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建AI按钮
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7bd8f8),
|
||||||
|
Color(0xFF7c68ef),
|
||||||
|
Color(0xFF7c45ed)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
),
|
||||||
|
color = Color.White,
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
text = "好的,就它了",
|
||||||
|
isLoading = viewModel.isCreating,
|
||||||
|
loadingText = "创建中...",
|
||||||
|
enabled = viewModel.canCreate()
|
||||||
|
) {
|
||||||
|
viewModel.createAgent(context) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package com.aiosman.ravenow.ui.agent
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.data.api.GenerateAgentInfoRequestBody
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class CreateAgentV2ViewModel : ViewModel() {
|
||||||
|
// UI状态
|
||||||
|
var inputText by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
var agentTitle by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
var agentDescription by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isGenerating by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isCreating by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isManualMode by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
// 临时保存的生成结果,用于在生成过程中暂时隐藏当前结果
|
||||||
|
private var tempAgentTitle by mutableStateOf("")
|
||||||
|
private var tempAgentDescription by mutableStateOf("")
|
||||||
|
|
||||||
|
// AddAgentViewModel实例,用于头像和创建逻辑
|
||||||
|
private val addAgentViewModel = AddAgentViewModel
|
||||||
|
|
||||||
|
// 获取头像相关状态
|
||||||
|
val croppedBitmap get() = addAgentViewModel.croppedBitmap
|
||||||
|
val isSelectingAvatar get() = addAgentViewModel.isSelectingAvatar
|
||||||
|
|
||||||
|
init {
|
||||||
|
// 初始化时检查是否需要恢复状态
|
||||||
|
if (addAgentViewModel.hasExitedPage) {
|
||||||
|
// 如果之前已经完全退出页面,清空所有数据
|
||||||
|
addAgentViewModel.clearData()
|
||||||
|
} else {
|
||||||
|
// 否则恢复已有状态(包括从头像选择回来的情况)
|
||||||
|
if (addAgentViewModel.name.isNotEmpty()) {
|
||||||
|
agentTitle = addAgentViewModel.name
|
||||||
|
}
|
||||||
|
if (addAgentViewModel.desc.isNotEmpty()) {
|
||||||
|
agentDescription = addAgentViewModel.desc
|
||||||
|
}
|
||||||
|
// 恢复输入文本
|
||||||
|
if (addAgentViewModel.generateInputText.isNotEmpty()) {
|
||||||
|
inputText = addAgentViewModel.generateInputText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateInputText(text: String) {
|
||||||
|
inputText = text
|
||||||
|
addAgentViewModel.generateInputText = text // 同时保存到AddAgentViewModel
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAgentTitle(title: String) {
|
||||||
|
agentTitle = title
|
||||||
|
syncToAddAgentViewModel()
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAgentDescription(description: String) {
|
||||||
|
agentDescription = description
|
||||||
|
syncToAddAgentViewModel()
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearError() {
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncToAddAgentViewModel() {
|
||||||
|
addAgentViewModel.name = agentTitle
|
||||||
|
addAgentViewModel.desc = agentDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectingAvatar(isSelecting: Boolean) {
|
||||||
|
addAgentViewModel.isSelectingAvatar = isSelecting
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markPageExited() {
|
||||||
|
addAgentViewModel.hasExitedPage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncStateOnResume() {
|
||||||
|
// 如果之前在选择头像,现在回来了,重置选择状态
|
||||||
|
if (addAgentViewModel.isSelectingAvatar) {
|
||||||
|
addAgentViewModel.isSelectingAvatar = false
|
||||||
|
// 从头像选择页面回来,恢复文本状态
|
||||||
|
if (addAgentViewModel.name.isNotEmpty()) {
|
||||||
|
agentTitle = addAgentViewModel.name
|
||||||
|
}
|
||||||
|
if (addAgentViewModel.desc.isNotEmpty()) {
|
||||||
|
agentDescription = addAgentViewModel.desc
|
||||||
|
}
|
||||||
|
if (addAgentViewModel.generateInputText.isNotEmpty()) {
|
||||||
|
inputText = addAgentViewModel.generateInputText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableManualMode() {
|
||||||
|
isManualMode = true
|
||||||
|
// 手动模式下,如果没有现有内容,初始化为空
|
||||||
|
if (agentTitle.isEmpty() && agentDescription.isEmpty()) {
|
||||||
|
agentTitle = ""
|
||||||
|
agentDescription = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableManualMode() {
|
||||||
|
isManualMode = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateAgentInfo() {
|
||||||
|
if (inputText.isBlank() || isGenerating) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isGenerating = true
|
||||||
|
clearError()
|
||||||
|
|
||||||
|
// 开始生成时,暂存当前结果并清空显示
|
||||||
|
tempAgentTitle = agentTitle
|
||||||
|
tempAgentDescription = agentDescription
|
||||||
|
agentTitle = ""
|
||||||
|
agentDescription = ""
|
||||||
|
|
||||||
|
val response = ApiClient.longTimeoutApi.generateAgentInfo(
|
||||||
|
GenerateAgentInfoRequestBody(inputText)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val data = response.body()?.data
|
||||||
|
data?.let {
|
||||||
|
// 成功时,使用新结果
|
||||||
|
agentTitle = it.title
|
||||||
|
agentDescription = it.description
|
||||||
|
syncToAddAgentViewModel()
|
||||||
|
// 清空临时保存
|
||||||
|
tempAgentTitle = ""
|
||||||
|
tempAgentDescription = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 失败时,恢复之前的结果
|
||||||
|
agentTitle = tempAgentTitle
|
||||||
|
agentDescription = tempAgentDescription
|
||||||
|
tempAgentTitle = ""
|
||||||
|
tempAgentDescription = ""
|
||||||
|
errorMessage = "生成失败,请重试"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 异常时,恢复之前的结果
|
||||||
|
agentTitle = tempAgentTitle
|
||||||
|
agentDescription = tempAgentDescription
|
||||||
|
tempAgentTitle = ""
|
||||||
|
tempAgentDescription = ""
|
||||||
|
errorMessage = "网络错误: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isGenerating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createAgent(context: Context, onSuccess: () -> Unit) {
|
||||||
|
if (isCreating) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isCreating = true
|
||||||
|
clearError()
|
||||||
|
|
||||||
|
// 验证输入
|
||||||
|
val validationError = addAgentViewModel.validate()
|
||||||
|
if (validationError != null) {
|
||||||
|
errorMessage = validationError
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用创建智能体API
|
||||||
|
val result = addAgentViewModel.createAgent(context)
|
||||||
|
if (result != null) {
|
||||||
|
// 创建成功,清空数据
|
||||||
|
clearData()
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
errorMessage = "创建失败,请重试"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = "创建智能体失败: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isCreating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearData() {
|
||||||
|
inputText = ""
|
||||||
|
agentTitle = ""
|
||||||
|
agentDescription = ""
|
||||||
|
errorMessage = null
|
||||||
|
isGenerating = false
|
||||||
|
isCreating = false
|
||||||
|
addAgentViewModel.clearData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以创建
|
||||||
|
fun canCreate(): Boolean {
|
||||||
|
return !isCreating && agentTitle.isNotBlank() && agentDescription.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以生成
|
||||||
|
fun canGenerate(): Boolean {
|
||||||
|
return !isGenerating && inputText.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有生成结果或处于手动模式
|
||||||
|
fun hasGeneratedResult(): Boolean {
|
||||||
|
return agentTitle.isNotEmpty() || agentDescription.isNotEmpty() || isManualMode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
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.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.entity.CommentEntity
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
||||||
@@ -79,6 +80,7 @@ fun CommentModalContent(
|
|||||||
onCommentAdded: () -> Unit = {},
|
onCommentAdded: () -> Unit = {},
|
||||||
onDismiss: () -> Unit = {}
|
onDismiss: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
val model = viewModel<CommentModalViewModel>(
|
val model = viewModel<CommentModalViewModel>(
|
||||||
key = "CommentModalViewModel_$postId",
|
key = "CommentModalViewModel_$postId",
|
||||||
factory = object : ViewModelProvider.Factory {
|
factory = object : ViewModelProvider.Factory {
|
||||||
@@ -113,7 +115,7 @@ fun CommentModalContent(
|
|||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showCommentMenu = false
|
showCommentMenu = false
|
||||||
},
|
},
|
||||||
containerColor = Color.White,
|
containerColor = AppColors.background,
|
||||||
sheetState = rememberModalBottomSheetState(
|
sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
),
|
),
|
||||||
@@ -142,24 +144,8 @@ fun CommentModalContent(
|
|||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
|
|
||||||
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.comment),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
color = Color(0xFFF7F7F7)
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -170,7 +156,8 @@ fun CommentModalContent(
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.comment_count, commentCount),
|
text = stringResource(id = R.string.comment_count, commentCount),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = Color(0xff666666)
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AppColors.nonActiveText
|
||||||
)
|
)
|
||||||
OrderSelectionComponent {
|
OrderSelectionComponent {
|
||||||
commentViewModel.order = it
|
commentViewModel.order = it
|
||||||
@@ -205,7 +192,7 @@ fun CommentModalContent(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0xfff7f7f7))
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
EditCommentBottomModal(replyComment) {
|
EditCommentBottomModal(replyComment) {
|
||||||
commentViewModel.viewModelScope.launch {
|
commentViewModel.viewModelScope.launch {
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ fun EditCommentBottomModal(
|
|||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
// 移除自动聚焦,避免自动弹出键盘
|
||||||
focusRequester.requestFocus()
|
// LaunchedEffect(Unit) {
|
||||||
}
|
// focusRequester.requestFocus()
|
||||||
|
// }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -69,20 +70,6 @@ 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) {
|
if (replyComment != null) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -129,9 +116,9 @@ fun EditCommentBottomModal(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(Color.White)
|
.background(AppColors.inputBackground)
|
||||||
.border(1.dp, Color.Black, RoundedCornerShape(20.dp))
|
.border(1.dp, AppColors.text.copy(alpha = 0.2f), RoundedCornerShape(20.dp))
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Top
|
verticalAlignment = Alignment.Top
|
||||||
@@ -146,7 +133,7 @@ fun EditCommentBottomModal(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
color = Color.Black,
|
color = AppColors.text,
|
||||||
fontWeight = FontWeight.Normal
|
fontWeight = FontWeight.Normal
|
||||||
),
|
),
|
||||||
minLines = 1
|
minLines = 1
|
||||||
@@ -160,7 +147,7 @@ fun EditCommentBottomModal(
|
|||||||
painter = painterResource(id = R.mipmap.rider_pro_moment_post),
|
painter = painterResource(id = R.mipmap.rider_pro_moment_post),
|
||||||
contentDescription = "Send",
|
contentDescription = "Send",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(25.dp)
|
.size(20.dp)
|
||||||
.align(Alignment.Top)
|
.align(Alignment.Top)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
@@ -168,7 +155,7 @@ fun EditCommentBottomModal(
|
|||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tint = if (isNotEmpty) AppColors.main else AppColors.nonActive
|
tint = if (isNotEmpty) Color.Unspecified else AppColors.nonActive
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,150 @@
|
|||||||
|
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
|
||||||
|
) {
|
||||||
|
// 标题
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.create_title),
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = appColors.text,
|
||||||
|
modifier = Modifier.padding(bottom = 32.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 三个创建选项
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
// 群聊选项
|
||||||
|
CreateOption(
|
||||||
|
icon = R.drawable.ic_create_group_chat,
|
||||||
|
label = stringResource(R.string.create_group_chat_option),
|
||||||
|
onClick = onGroupChatClick
|
||||||
|
)
|
||||||
|
// 动态选项
|
||||||
|
CreateOption(
|
||||||
|
icon = R.drawable.ic_create_monent,
|
||||||
|
label = stringResource(R.string.create_moment),
|
||||||
|
onClick = onMomentClick
|
||||||
|
)
|
||||||
|
// AI选项
|
||||||
|
CreateOption(
|
||||||
|
icon = R.drawable.ic_create_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 路由时全量清理资源,以免返回后列表被重置
|
||||||
@@ -291,8 +296,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,8 +383,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 +393,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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -22,10 +22,15 @@ sealed class NavigationItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
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.dynamic_hl },
|
||||||
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 },
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ 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
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
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
|
||||||
@@ -35,6 +37,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
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
|
||||||
@@ -43,6 +46,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 +64,18 @@ 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.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
|
||||||
@OptIn( ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Agent() {
|
fun Agent() {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
@@ -80,12 +89,25 @@ fun Agent() {
|
|||||||
var scope = rememberCoroutineScope()
|
var scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val viewModel: AgentViewModel = viewModel()
|
val viewModel: AgentViewModel = viewModel()
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
// 确保推荐Agent数据已加载
|
// 确保推荐Agent数据已加载
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.ensureDataLoaded()
|
viewModel.ensureDataLoaded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听滚动状态,实现自动加载更多
|
||||||
|
LaunchedEffect(scrollState) {
|
||||||
|
snapshotFlow { scrollState.value }
|
||||||
|
.collect { scrollValue ->
|
||||||
|
val maxScroll = scrollState.maxValue
|
||||||
|
if (scrollValue >= maxScroll - 100 && !viewModel.isLoading) {
|
||||||
|
// 滚动到接近底部时加载更多
|
||||||
|
viewModel.loadMoreAgentData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 防抖状态
|
// 防抖状态
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
@@ -97,109 +119,182 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 固定顶部搜索条
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
.zIndex(999.0f)
|
||||||
|
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight().padding(top = 32.dp, start = 16.dp, end = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "Rave AI",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.W900,
|
||||||
|
color = AppColors.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
|
contentDescription = "search",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigate(NavigationRoute.Search.route)
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可滚动的内容区域
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
.padding(
|
.padding(
|
||||||
top = statusBarPaddingValues.calculateTopPadding()+18.dp,
|
top = 44.dp + statusBarPaddingValues.calculateTopPadding() + 15.dp,
|
||||||
bottom = navigationBarPaddings,
|
bottom = navigationBarPaddings,
|
||||||
start = 16.dp,
|
start = 16.dp,
|
||||||
end = 16.dp
|
end = 16.dp
|
||||||
),
|
),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Row(
|
// // 搜索框
|
||||||
modifier = Modifier
|
// Row(
|
||||||
.wrapContentHeight()
|
// modifier = Modifier
|
||||||
.fillMaxWidth(),
|
// .height(36.dp)
|
||||||
horizontalArrangement = Arrangement.Start,
|
// .weight(1f)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
// .clip(shape = RoundedCornerShape(8.dp))
|
||||||
) {
|
// .background(AppColors.inputBackground)
|
||||||
// 搜索框
|
// .padding(horizontal = 8.dp, vertical = 0.dp)
|
||||||
Row(
|
// .noRippleClickable {
|
||||||
modifier = Modifier
|
// // 搜索框点击事件
|
||||||
.height(36.dp)
|
// },
|
||||||
.weight(1f)
|
// verticalAlignment = Alignment.CenterVertically
|
||||||
.clip(shape = RoundedCornerShape(8.dp))
|
// ) {
|
||||||
.background(AppColors.inputBackground)
|
// Icon(
|
||||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
// painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
.noRippleClickable {
|
// contentDescription = null,
|
||||||
// 搜索框点击事件
|
// tint = AppColors.inputHint
|
||||||
},
|
// )
|
||||||
verticalAlignment = Alignment.CenterVertically
|
// Box {
|
||||||
) {
|
// Text(
|
||||||
Icon(
|
// text = stringResource(R.string.search),
|
||||||
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
// modifier = Modifier.padding(start = 8.dp),
|
||||||
contentDescription = null,
|
// color = AppColors.inputHint,
|
||||||
tint = AppColors.inputHint
|
// fontSize = 17.sp
|
||||||
)
|
// )
|
||||||
Box {
|
// }
|
||||||
Text(
|
// }
|
||||||
text = stringResource(R.string.search),
|
// Spacer(modifier = Modifier.width(16.dp))
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
// // 创建智能体
|
||||||
color = AppColors.inputHint,
|
// Icon(
|
||||||
fontSize = 17.sp
|
// modifier = Modifier
|
||||||
)
|
// .size(36.dp)
|
||||||
}
|
// .noRippleClickable {
|
||||||
}
|
// if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
// // 检查游客模式,如果是游客则跳转登录
|
||||||
// 新增
|
// if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
|
||||||
Icon(
|
// navController.navigate(NavigationRoute.Login.route)
|
||||||
modifier = Modifier
|
// } else {
|
||||||
.size(36.dp)
|
// // 导航到添加智能体页面
|
||||||
.noRippleClickable {
|
// navController.navigate(
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
// NavigationRoute.AddAgent.route
|
||||||
// 检查游客模式,如果是游客则跳转登录
|
// )
|
||||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
|
// }
|
||||||
navController.navigate(NavigationRoute.Login.route)
|
// }) {
|
||||||
} else {
|
// lastClickTime = System.currentTimeMillis()
|
||||||
// 导航到添加智能体页面
|
// }
|
||||||
navController.navigate(
|
// },
|
||||||
NavigationRoute.AddAgent.route
|
// painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
|
||||||
)
|
// contentDescription = null,
|
||||||
}
|
// tint = AppColors.text
|
||||||
}) {
|
// )
|
||||||
lastClickTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 推荐Agent
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(260.dp)
|
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
|
|
||||||
) {
|
) {
|
||||||
// 标题
|
|
||||||
Row(
|
// // 标题
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
// Row(
|
||||||
modifier = Modifier.padding(bottom = 12.dp)
|
// verticalAlignment = Alignment.CenterVertically,
|
||||||
|
// modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
// ) {
|
||||||
|
// Image(
|
||||||
|
// painter = painterResource(R.mipmap.rider_pro_agent2),
|
||||||
|
// contentDescription = "agent",
|
||||||
|
// modifier = Modifier.size(28.dp),
|
||||||
|
//
|
||||||
|
// )
|
||||||
|
// 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
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 使用 ViewModel 中的选中状态
|
||||||
|
val selectedTabIndex = viewModel.selectedCategoryIndex
|
||||||
|
|
||||||
|
// 动态标签页
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
viewModel.categories.forEachIndexed { index, category ->
|
||||||
painter = painterResource(R.mipmap.rider_pro_agent2),
|
item {
|
||||||
contentDescription = "agent",
|
CustomTabItem(
|
||||||
modifier = Modifier.size(28.dp),
|
text = category.getLocalizedName(),
|
||||||
|
isSelected = selectedTabIndex == index,
|
||||||
)
|
onClick = {
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
viewModel.selectCategory(index)
|
||||||
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
|
if (index < viewModel.categories.size - 1) {
|
||||||
AgentViewPagerSection(agentItems = viewModel.agentItems.take(9),viewModel)
|
item {
|
||||||
|
TabSpacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(0.dp))
|
// 显示当前选中分类的 Agent 数据
|
||||||
|
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推荐聊天房间
|
||||||
|
ChatRoomsSection(
|
||||||
|
chatRooms = viewModel.chatRooms,
|
||||||
|
navController = LocalNavController.current
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -221,99 +316,201 @@ fun Agent() {
|
|||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
TabItem(
|
// Agent两列网格布局
|
||||||
text = stringResource(R.string.agent_hot),
|
AgentGridLayout(
|
||||||
isSelected = if (AppStore.isGuest) pagerState.currentPage == 0 else pagerState.currentPage == 1,
|
agentItems = viewModel.agentItems,
|
||||||
onClick = {
|
viewModel = viewModel,
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 300L) {
|
navController = LocalNavController.current
|
||||||
scope.launch {
|
|
||||||
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(
|
@Composable
|
||||||
text = stringResource(R.string.agent_other),
|
fun AgentGridLayout(
|
||||||
isSelected = pagerState.currentPage == 3,
|
agentItems: List<AgentItem>,
|
||||||
onClick = {
|
viewModel: AgentViewModel,
|
||||||
scope.launch {
|
navController: NavHostController
|
||||||
pagerState.animateScrollToPage(3)
|
) {
|
||||||
}
|
Column(
|
||||||
}
|
modifier = Modifier.fillMaxWidth()
|
||||||
)*/
|
) {
|
||||||
}
|
// 将agentItems按两列分组
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
|
||||||
HorizontalPager(
|
Row(
|
||||||
state = pagerState,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.padding(
|
||||||
beyondBoundsPageCount = 1 // 预加载相邻页面,避免切换时重新加载
|
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
|
||||||
|
bottom = 20.dp
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
if (AppStore.isGuest) {
|
// 第一列
|
||||||
// 游客模式下只显示热门Agent
|
Box(
|
||||||
when (it) {
|
modifier = Modifier.weight(1f)
|
||||||
0 -> {
|
) {
|
||||||
HotAgent()
|
AgentCardSquare(
|
||||||
}
|
agentItem = rowItems[0],
|
||||||
}
|
viewModel = viewModel,
|
||||||
} else {
|
navController = navController
|
||||||
// 正常用户显示我的Agent和热门Agent
|
)
|
||||||
when (it) {
|
|
||||||
0 -> {
|
|
||||||
MineAgent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
// 第二列(如果存在)
|
||||||
HotAgent()
|
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 = -avatarSize / 2)
|
||||||
|
.size(avatarSize)
|
||||||
|
.background(AppColors.background, RoundedCornerShape(avatarSize / 2))
|
||||||
|
.clip(RoundedCornerShape(avatarSize / 2)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_agent),
|
||||||
|
contentDescription = "默认头像",
|
||||||
|
modifier = Modifier.size(avatarSize / 2),
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域(名称和描述)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 48.dp), // 为底部聊天按钮留出空间
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = agentItem.title,
|
||||||
|
fontSize = 16.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 = 14.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(80.dp)
|
||||||
|
.height(32.dp)
|
||||||
|
.background(
|
||||||
|
color = AppColors.inputBackground,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
|
// 检查游客模式,如果是游客则跳转登录
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
viewModel.createSingleChat(agentItem.openId)
|
||||||
|
viewModel.goToChatAi(
|
||||||
|
agentItem.openId,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
lastClickTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = stringResource(R.string.chat),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@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 +520,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
|
|||||||
// Agent内容
|
// Agent内容
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(180.dp)
|
.height(300.dp)
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
@@ -344,7 +541,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(300.dp)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = scale
|
scaleX = scale
|
||||||
scaleY = scale
|
scaleY = scale
|
||||||
@@ -368,7 +566,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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -379,7 +579,13 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier,navController: NavHostController) {
|
fun AgentPage(
|
||||||
|
viewModel: AgentViewModel,
|
||||||
|
agentItems: List<AgentItem>,
|
||||||
|
page: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -387,7 +593,11 @@ fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int,
|
|||||||
) {
|
) {
|
||||||
// 显示3个agent
|
// 显示3个agent
|
||||||
agentItems.forEachIndexed { index, agentItem ->
|
agentItems.forEachIndexed { index, agentItem ->
|
||||||
AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current)
|
AgentCard2(
|
||||||
|
agentItem = agentItem,
|
||||||
|
viewModel = viewModel,
|
||||||
|
navController = LocalNavController.current
|
||||||
|
)
|
||||||
if (index < agentItems.size - 1) {
|
if (index < agentItems.size - 1) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
@@ -397,7 +607,7 @@ fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int,
|
|||||||
|
|
||||||
@SuppressLint("SuspiciousIndentation")
|
@SuppressLint("SuspiciousIndentation")
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) {
|
fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: NavHostController) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
// 防抖状态
|
// 防抖状态
|
||||||
@@ -413,7 +623,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(AppColors.secondaryBackground, 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)
|
||||||
@@ -477,7 +687,7 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 60.dp, height = 32.dp)
|
.size(width = 60.dp, height = 32.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color(0X147c7480),
|
color = AppColors.inputBackground,
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
)
|
)
|
||||||
.clickable {
|
.clickable {
|
||||||
@@ -487,7 +697,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 +717,135 @@ 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_agent2),
|
||||||
|
contentDescription = "chat room",
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = stringResource(R.string.agent_chat_room),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3行宫格布局
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// 将聊天房间按3个一组分组
|
||||||
|
chatRooms.chunked(3).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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 如果这一行不足3个,添加空白占位
|
||||||
|
repeat(3 - rowRooms.size) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatRoomCard(
|
||||||
|
chatRoom: ChatRoom,
|
||||||
|
navController: NavHostController,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val cardSize = 100.dp
|
||||||
|
|
||||||
|
// 防抖状态
|
||||||
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
|
// 正方形卡片,文字重叠在底部
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(cardSize)
|
||||||
|
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
||||||
|
.clickable {
|
||||||
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
|
// 这里可以添加进入聊天房间的逻辑
|
||||||
|
// navController.navigate(NavigationRoute.ChatRoom.route.replace("{id}", chatRoom.id))
|
||||||
|
}) {
|
||||||
|
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
|
||||||
|
.size(cardSize)
|
||||||
|
.clip(RoundedCornerShape(12.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()
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.6f),
|
||||||
|
shape = RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = chatRoom.name,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +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.AppState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.ConstVars
|
||||||
|
import com.aiosman.ravenow.data.Room
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
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
|
||||||
@@ -13,8 +17,18 @@ import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createG
|
|||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
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 com.aiosman.ravenow.entity.CategoryEntity
|
||||||
|
import com.aiosman.ravenow.entity.CategoryBackend
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class ChatRoom(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val avatar: String = "",
|
||||||
|
val banner: String = "",
|
||||||
|
val memberCount: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
object AgentViewModel: ViewModel() {
|
object AgentViewModel: ViewModel() {
|
||||||
|
|
||||||
private val apiClient: RaveNowAPI = ApiClient.api
|
private val apiClient: RaveNowAPI = ApiClient.api
|
||||||
@@ -22,32 +36,65 @@ object AgentViewModel: ViewModel() {
|
|||||||
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
|
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var chatRooms by mutableStateOf<List<ChatRoom>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var rooms by mutableStateOf<List<Room>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var categories by mutableStateOf<List<CategoryEntity>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var selectedCategoryIndex by mutableStateOf(0)
|
||||||
|
private set
|
||||||
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var isLoading by mutableStateOf(false)
|
var isLoading by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
private var currentPage = 1
|
||||||
|
private var hasMoreData = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadAgentData()
|
loadAgentData()
|
||||||
|
loadChatRooms()
|
||||||
|
loadCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAgentData() {
|
private fun loadAgentData(categoryIndex: Int = 0) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
|
currentPage = 1
|
||||||
|
hasMoreData = true
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val selectedCategory = if (categoryIndex < categories.size) categories[categoryIndex] else null
|
||||||
|
|
||||||
|
val response = if (categoryIndex == 0 || selectedCategory == null) {
|
||||||
|
// 推荐分类或无效分类,加载所有 Agent
|
||||||
|
apiClient.getAgent(page = currentPage, pageSize = 20, withWorkflow = "1", random = "true")
|
||||||
|
} else {
|
||||||
|
// 特定分类,使用 categoryName 参数
|
||||||
|
apiClient.getAgent(
|
||||||
|
page = currentPage,
|
||||||
|
pageSize = 20,
|
||||||
|
withWorkflow = "1",
|
||||||
|
categoryName = selectedCategory.name,
|
||||||
|
random = "true"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
AgentItem.fromAgent(agent)
|
AgentItem.fromAgent(agent)
|
||||||
}
|
}
|
||||||
|
hasMoreData = agents.size >= 20
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "获取Agent数据失败: ${response.code()}"
|
errorMessage = "获取Agent数据失败: ${response.code()}"
|
||||||
}
|
}
|
||||||
@@ -58,6 +105,129 @@ object AgentViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadChatRooms() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response = apiClient.getRooms(page = 1, pageSize = 21, isRecommended = 1, random = 1) // 请求21个,确保是3的倍数
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val allRooms = response.body()?.list ?: emptyList()
|
||||||
|
// 确保房间数量是3的倍数,如果不足则截取,如果超出则取前几个
|
||||||
|
val targetCount = (allRooms.size / 3) * 3 // 向下取整到最近的3的倍数
|
||||||
|
rooms = allRooms.take(targetCount)
|
||||||
|
|
||||||
|
// 转换为ChatRoom格式用于兼容现有UI
|
||||||
|
chatRooms = rooms.map { room ->
|
||||||
|
ChatRoom(
|
||||||
|
id = room.trtcRoomId,
|
||||||
|
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 {
|
||||||
|
try {
|
||||||
|
val categoryBackend = CategoryBackend()
|
||||||
|
val response = categoryBackend.getCategories(1)
|
||||||
|
if (response != null) {
|
||||||
|
// 添加一个默认的"推荐"分类在第一位
|
||||||
|
val recommendCategory = CategoryEntity(
|
||||||
|
id = 0,
|
||||||
|
name = "推荐",
|
||||||
|
description = "推荐内容",
|
||||||
|
avatar = "",
|
||||||
|
parentId = null,
|
||||||
|
sort = 0,
|
||||||
|
isActive = true,
|
||||||
|
promptCount = 0,
|
||||||
|
createdAt = "",
|
||||||
|
updatedAt = "",
|
||||||
|
translations = null
|
||||||
|
)
|
||||||
|
categories = listOf(recommendCategory) + response.list
|
||||||
|
|
||||||
|
// 分类加载完成后,重新加载当前选中分类的 Agent 数据
|
||||||
|
if (agentItems.isEmpty()) {
|
||||||
|
loadAgentData(selectedCategoryIndex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果请求失败,使用默认分类
|
||||||
|
categories = listOf()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 如果网络请求失败,使用默认分类
|
||||||
|
categories = listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载更多Agent数据
|
||||||
|
*/
|
||||||
|
fun loadMoreAgentData() {
|
||||||
|
if (!hasMoreData || isLoading) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
isLoading = true
|
||||||
|
try {
|
||||||
|
val nextPage = currentPage + 1
|
||||||
|
val selectedCategory = if (selectedCategoryIndex < categories.size) categories[selectedCategoryIndex] else null
|
||||||
|
|
||||||
|
val response = if (selectedCategoryIndex == 0 || selectedCategory == null) {
|
||||||
|
// 推荐分类或无效分类,加载所有 Agent
|
||||||
|
apiClient.getAgent(page = nextPage, pageSize = 20, withWorkflow = "1")
|
||||||
|
} else {
|
||||||
|
// 特定分类,使用 categoryName 参数
|
||||||
|
apiClient.getAgent(
|
||||||
|
page = nextPage,
|
||||||
|
pageSize = 20,
|
||||||
|
withWorkflow = "1",
|
||||||
|
categoryName = selectedCategory.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
|
val newAgentItems = agents.map { agent ->
|
||||||
|
AgentItem.fromAgent(agent)
|
||||||
|
}
|
||||||
|
agentItems = agentItems + newAgentItems
|
||||||
|
currentPage = nextPage
|
||||||
|
hasMoreData = agents.size >= 20
|
||||||
|
} else {
|
||||||
|
errorMessage = "加载更多Agent数据失败: ${response.code()}"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = "网络请求失败: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择分类并加载对应的 Agent 数据
|
||||||
|
*/
|
||||||
|
fun selectCategory(categoryIndex: Int) {
|
||||||
|
if (categoryIndex != selectedCategoryIndex) {
|
||||||
|
selectedCategoryIndex = categoryIndex
|
||||||
|
loadAgentData(categoryIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createSingleChat(
|
fun createSingleChat(
|
||||||
openId: String,
|
openId: String,
|
||||||
) {
|
) {
|
||||||
@@ -116,9 +286,13 @@ object AgentViewModel: ViewModel() {
|
|||||||
*/
|
*/
|
||||||
fun ResetModel() {
|
fun ResetModel() {
|
||||||
agentItems = emptyList()
|
agentItems = emptyList()
|
||||||
|
categories = emptyList()
|
||||||
|
selectedCategoryIndex = 0
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
currentPage = 1
|
||||||
|
hasMoreData = true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
// TODO: 实现通知功能
|
||||||
|
},
|
||||||
|
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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 // 使用解析后的发送者信息
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,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 +49,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 +69,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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ 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
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
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.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -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
|
||||||
@@ -47,10 +46,24 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
|||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.dynamic.Dynamic
|
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.news.News
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend.Recommend
|
||||||
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.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.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import com.aiosman.ravenow.ui.composables.TabItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.TabSpacer
|
||||||
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
|
||||||
|
data class TabData(
|
||||||
|
val text: String,
|
||||||
|
val index: Int
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态列表
|
* 动态列表
|
||||||
@@ -63,8 +76,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
|
// 游客模式下显示4个tab:Worldwide、Hot、News、Recommend,非游客模式显示5个tab:Worldwide、Following、Hot、News、Recommend
|
||||||
val tabCount = if (AppStore.isGuest) 3 else 4
|
val tabCount = if (AppStore.isGuest) 4 else 5
|
||||||
var pagerState = rememberPagerState { tabCount }
|
var pagerState = rememberPagerState { tabCount }
|
||||||
var scope = rememberCoroutineScope()
|
var scope = rememberCoroutineScope()
|
||||||
Column(
|
Column(
|
||||||
@@ -79,152 +92,107 @@ fun MomentsList() {
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(44.dp),
|
.height(44.dp)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
// center the tabs
|
// center the tabs
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(
|
//原探索//
|
||||||
modifier = Modifier
|
// Column(
|
||||||
.noRippleClickable {
|
// modifier = Modifier
|
||||||
scope.launch {
|
// .noRippleClickable {
|
||||||
pagerState.animateScrollToPage(0)
|
// scope.launch {
|
||||||
}
|
// pagerState.animateScrollToPage(0)
|
||||||
}.padding(start = 16.dp),
|
// }
|
||||||
verticalArrangement = Arrangement.Center,
|
// }.padding(start = 16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
// 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))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.index_worldwide),
|
text = stringResource(R.string.moment),
|
||||||
fontSize = if (pagerState.currentPage == 0)18.sp else 16.sp,
|
fontSize = 20.sp,
|
||||||
color = if (pagerState.currentPage == 0) AppColors.text else AppColors.nonActiveText,
|
fontWeight = FontWeight.W900,
|
||||||
fontWeight = FontWeight.W600)
|
color = AppColors.text,
|
||||||
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
|
modifier = Modifier
|
||||||
.width(34.dp)
|
.align(Alignment.CenterVertically)
|
||||||
.height(4.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.noRippleClickable {
|
|
||||||
scope.launch {
|
|
||||||
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(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
||||||
if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected
|
contentDescription = "search",
|
||||||
else R.drawable.tab_indicator_unselected
|
|
||||||
),
|
|
||||||
contentDescription = "tab indicator",
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(34.dp)
|
.size(24.dp)
|
||||||
.height(4.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有非游客用户才显示"关注"tab
|
|
||||||
if (!AppStore.isGuest) {
|
|
||||||
//关注tab
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.noRippleClickable {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(2)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.index_following),
|
|
||||||
fontSize = if (pagerState.currentPage == 2)18.sp else 16.sp,
|
|
||||||
color = if (pagerState.currentPage == 2) AppColors.text else AppColors.nonActiveText,
|
|
||||||
fontWeight = FontWeight.W600)
|
|
||||||
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 {
|
|
||||||
val targetPage = if (AppStore.isGuest) 2 else 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
//搜索按钮
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(bottom = 8.dp, end = 16.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 {
|
||||||
navController.navigate(NavigationRoute.Search.route)
|
navController.navigate(NavigationRoute.Search.route)
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer(modifier = Modifier.height(23.dp))
|
||||||
|
|
||||||
|
val tabDebouncer = rememberDebouncer()
|
||||||
|
|
||||||
|
// 创建tab数据列表
|
||||||
|
val tabs = if (AppStore.isGuest) {
|
||||||
|
listOf(
|
||||||
|
TabData(stringResource(R.string.index_worldwide), 0),
|
||||||
|
TabData(stringResource(R.string.index_hot), 1),
|
||||||
|
TabData(stringResource(R.string.index_news), 2),
|
||||||
|
TabData(stringResource(R.string.index_recommend), 3)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
TabData(stringResource(R.string.index_worldwide), 0),
|
||||||
|
TabData(stringResource(R.string.index_following), 1),
|
||||||
|
TabData(stringResource(R.string.index_hot), 2),
|
||||||
|
TabData(stringResource(R.string.index_news), 3),
|
||||||
|
TabData(stringResource(R.string.index_recommend), 4)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 16.dp, bottom = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
items(tabs) { tab ->
|
||||||
|
UnderlineTabItem(
|
||||||
|
text = tab.text,
|
||||||
|
isSelected = pagerState.currentPage == tab.index,
|
||||||
|
onClick = {
|
||||||
|
tabDebouncer {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(tab.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -237,37 +205,103 @@ fun MomentsList() {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
if (AppStore.isGuest) {
|
if (AppStore.isGuest) {
|
||||||
// 游客模式:Explore(0), Dynamic(1), Hot(2)
|
// 游客模式:Worldwide(0), Hot(1), News(2), Recommend(3)
|
||||||
when (it) {
|
when (it) {
|
||||||
0 -> {
|
0 -> {
|
||||||
Explore()
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
Dynamic()
|
Dynamic()
|
||||||
}
|
}
|
||||||
2 -> {
|
1 -> {
|
||||||
HotMomentsList()
|
HotMomentsList()
|
||||||
}
|
}
|
||||||
|
2 -> {
|
||||||
|
News()
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
Recommend()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 正常用户:Explore(0), Dynamic(1), Timeline(2), Hot(3)
|
// 正常用户:Worldwide(0), Following(1), Hot(2), News(3), Recommend(4)
|
||||||
when (it) {
|
when (it) {
|
||||||
0 -> {
|
0 -> {
|
||||||
Explore()
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
Dynamic()
|
Dynamic()
|
||||||
}
|
}
|
||||||
2 -> {
|
1 -> {
|
||||||
TimelineMomentsList()
|
TimelineMomentsList()
|
||||||
}
|
}
|
||||||
3 -> {
|
2 -> {
|
||||||
HotMomentsList()
|
HotMomentsList()
|
||||||
}
|
}
|
||||||
|
3 -> {
|
||||||
|
News()
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
Recommend()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@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 = if (isSelected) 18.sp else 16.sp,
|
||||||
|
color = if (isSelected) AppColors.text else AppColors.nonActiveText,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(34.dp)
|
||||||
|
.height(4.dp)
|
||||||
|
.background(
|
||||||
|
color = if (isSelected) AppColors.text else Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(2.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class ExploreViewModel : ViewModel() {
|
|||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
@@ -114,7 +114,7 @@ class ExploreViewModel : ViewModel() {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
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.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.composables.MomentCard
|
||||||
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新闻动态列表
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun News() {
|
||||||
|
val model = NewsViewModel
|
||||||
|
val moments = model.moments
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||||
|
model.refreshPager(
|
||||||
|
pullRefresh = true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
// 用于跟踪是否已经触发过加载更多
|
||||||
|
var hasTriggeredLoadMore by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// observe list scrolling with simplified logic
|
||||||
|
val reachedBottom by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val layoutInfo = listState.layoutInfo
|
||||||
|
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
|
||||||
|
val totalItems = layoutInfo.totalItemsCount
|
||||||
|
|
||||||
|
if (lastVisibleItem == null || totalItems == 0) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val isLastItemVisible = lastVisibleItem.index >= totalItems - 2
|
||||||
|
|
||||||
|
// 简化逻辑:只要滑动到底部且还没有触发过,就触发加载
|
||||||
|
isLastItemVisible && !hasTriggeredLoadMore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load more if scrolled to bottom
|
||||||
|
LaunchedEffect(reachedBottom) {
|
||||||
|
if (reachedBottom) {
|
||||||
|
hasTriggeredLoadMore = true
|
||||||
|
model.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
model.refreshPager()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听数据变化,重置加载状态
|
||||||
|
LaunchedEffect(moments.size) {
|
||||||
|
if (moments.size > 0) {
|
||||||
|
// 延迟重置,确保数据已经稳定
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
hasTriggeredLoadMore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
|
||||||
|
) {
|
||||||
|
Box(Modifier.pullRefresh(state)) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = listState
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
moments.size,
|
||||||
|
key = { idx -> idx }
|
||||||
|
) { idx ->
|
||||||
|
//处理下标越界
|
||||||
|
if (idx < 0 || idx >= moments.size) return@items
|
||||||
|
val momentItem = moments[idx] ?: return@items
|
||||||
|
|
||||||
|
val commentDebouncer = rememberDebouncer()
|
||||||
|
val likeDebouncer = rememberDebouncer()
|
||||||
|
val favoriteDebouncer = rememberDebouncer()
|
||||||
|
val followDebouncer = rememberDebouncer()
|
||||||
|
|
||||||
|
MomentCard(
|
||||||
|
momentEntity = momentItem,
|
||||||
|
onAddComment = {
|
||||||
|
commentDebouncer {
|
||||||
|
// 检查游客模式,如果是游客则跳转登录
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
model.onAddComment(momentItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFollowClick = {
|
||||||
|
followDebouncer {
|
||||||
|
// 检查游客模式,如果是游客则跳转登录
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.FOLLOW_USER)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
model.followAction(momentItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showFollowButton = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = model.refreshing,
|
||||||
|
state = state,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
|
|
||||||
|
object NewsViewModel : BaseMomentModel() {
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extraArgs(): MomentLoaderExtraArgs {
|
||||||
|
return MomentLoaderExtraArgs(explore = true, newsOnly = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImagePainter
|
||||||
|
import coil.compose.SubcomposeAsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.request.CachePolicy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载图片组件 - 专门用于推荐页面的图片预加载
|
||||||
|
* 支持预加载周围页面的图片,提升滑动体验
|
||||||
|
* 增加了loading状态指示器
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AsyncImage(
|
||||||
|
imageUrl: String,
|
||||||
|
contentDescription: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentScale: ContentScale = ContentScale.Crop,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
model = ImageRequest.Builder(context)
|
||||||
|
.data(imageUrl)
|
||||||
|
.crossfade(true)
|
||||||
|
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||||
|
.diskCachePolicy(CachePolicy.ENABLED)
|
||||||
|
.build(),
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = contentScale,
|
||||||
|
loading = {
|
||||||
|
// Loading 状态
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.8f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
color = Color.White,
|
||||||
|
strokeWidth = 3.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error = {
|
||||||
|
// 错误状态
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "图片加载失败",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
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.navigation.NavHostController
|
||||||
|
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.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.comment.CommentModalContent
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐动态列表 - 垂直滑动样式
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Recommend() {
|
||||||
|
val model = RecommendViewModel
|
||||||
|
val moments = model.moments
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
model.refreshPager()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moments.isEmpty()) {
|
||||||
|
// 空状态
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "暂无推荐内容",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用垂直滑动的Pager
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
RecommendPager(
|
||||||
|
moments = moments,
|
||||||
|
model = model,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
|
||||||
|
// 加载更多状态指示器
|
||||||
|
if (model.refreshing) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.3f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "加载更多中...",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RecommendPager(
|
||||||
|
moments: List<MomentEntity>,
|
||||||
|
model: RecommendViewModel,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
val pagerState = remember {
|
||||||
|
RecommendPagerState(
|
||||||
|
currentPage = 0,
|
||||||
|
minPage = 0,
|
||||||
|
maxPage = moments.size - 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var showCommentModal by remember { mutableStateOf(false) }
|
||||||
|
var currentMoment by remember { mutableStateOf<MomentEntity?>(null) }
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val appColor = LocalAppTheme.current
|
||||||
|
|
||||||
|
// 当moments列表变化时,更新pagerState的maxPage
|
||||||
|
LaunchedEffect(moments.size) {
|
||||||
|
pagerState.maxPage = moments.size - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
RecommendPager(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = pagerState,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
offscreenLimit = 1,
|
||||||
|
onLoadMore = {
|
||||||
|
if (!model.refreshing) {
|
||||||
|
model.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val momentItem = moments[page]
|
||||||
|
|
||||||
|
SingleRecommendItemContent(
|
||||||
|
momentEntity = momentItem,
|
||||||
|
pagerState = pagerState,
|
||||||
|
page = page,
|
||||||
|
onCommentClick = {
|
||||||
|
currentMoment = momentItem
|
||||||
|
showCommentModal = true
|
||||||
|
},
|
||||||
|
onLikeClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (momentItem.liked) {
|
||||||
|
model.dislikeMoment(momentItem.id)
|
||||||
|
} else {
|
||||||
|
model.likeMoment(momentItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFavoriteClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (momentItem.isFavorite) {
|
||||||
|
model.unfavoriteMoment(momentItem.id)
|
||||||
|
} else {
|
||||||
|
model.favoriteMoment(momentItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFollowClick = {
|
||||||
|
model.followAction(momentItem)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCommentModal && currentMoment != null) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { showCommentModal = false },
|
||||||
|
containerColor = appColor.background,
|
||||||
|
sheetState = sheetState
|
||||||
|
) {
|
||||||
|
CommentModalContent(
|
||||||
|
postId = currentMoment!!.id,
|
||||||
|
|
||||||
|
) {
|
||||||
|
// 评论回调
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SingleRecommendItemContent(
|
||||||
|
momentEntity: MomentEntity,
|
||||||
|
pagerState: RecommendPagerState,
|
||||||
|
page: Int,
|
||||||
|
onCommentClick: () -> Unit,
|
||||||
|
onLikeClick: () -> Unit,
|
||||||
|
onFavoriteClick: () -> Unit,
|
||||||
|
onFollowClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize().background(AppColors.background)) {
|
||||||
|
// 主图片内容
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (momentEntity.images.isNotEmpty()) {
|
||||||
|
AsyncImage(
|
||||||
|
imageUrl = momentEntity.images[0].thumbnail,
|
||||||
|
contentDescription = "推荐内容",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 默认背景
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧操作按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomEnd
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(bottom = 72.dp, end = 12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// 用户头像
|
||||||
|
UserAvatar(momentEntity = momentEntity)
|
||||||
|
|
||||||
|
// 点赞按钮
|
||||||
|
RecommendActionButton(
|
||||||
|
icon = R.drawable.rider_pro_video_like,
|
||||||
|
text = formatCount(momentEntity.likeCount),
|
||||||
|
isActive = momentEntity.liked,
|
||||||
|
onClick = {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onLikeClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 评论按钮
|
||||||
|
RecommendActionButton(
|
||||||
|
icon = R.drawable.rider_pro_video_comment,
|
||||||
|
text = formatCount(momentEntity.commentCount),
|
||||||
|
onClick = {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onCommentClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 收藏按钮
|
||||||
|
RecommendActionButton(
|
||||||
|
icon = R.drawable.rider_pro_video_favor,
|
||||||
|
text = formatCount(momentEntity.favoriteCount),
|
||||||
|
isActive = momentEntity.isFavorite,
|
||||||
|
onClick = {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onFavoriteClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分享按钮
|
||||||
|
RecommendActionButton(
|
||||||
|
icon = R.drawable.rider_pro_video_share,
|
||||||
|
text = "分享",
|
||||||
|
onClick = {
|
||||||
|
// TODO: 实现分享功能
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部信息
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomStart
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
// 用户信息
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "@${momentEntity.nickname}",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容描述
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp),
|
||||||
|
text = momentEntity.momentTextContent,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserAvatar(momentEntity: MomentEntity) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.size(40.dp)
|
||||||
|
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
|
||||||
|
.clip(RoundedCornerShape(40.dp))
|
||||||
|
) {
|
||||||
|
if (momentEntity.avatar.isNotEmpty()) {
|
||||||
|
AsyncImage(
|
||||||
|
imageUrl = momentEntity.avatar,
|
||||||
|
contentDescription = "用户头像",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.default_avatar),
|
||||||
|
contentDescription = "默认头像",
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RecommendActionButton(
|
||||||
|
icon: Int,
|
||||||
|
text: String,
|
||||||
|
isActive: Boolean = false,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.clickable { onClick() },
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
painter = painterResource(id = icon),
|
||||||
|
contentDescription = "",
|
||||||
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
|
||||||
|
if (isActive) Color.Red else Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.White,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatCount(count: Int): String {
|
||||||
|
return when {
|
||||||
|
count >= 1000000 -> "${(count / 1000000.0).let { "%.1f".format(it) }}M"
|
||||||
|
count >= 1000 -> "${(count / 1000.0).let { "%.1f".format(it) }}k"
|
||||||
|
else -> count.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.ParentDataModifier
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class RecommendPagerState(
|
||||||
|
currentPage: Int = 0,
|
||||||
|
minPage: Int = 0,
|
||||||
|
maxPage: Int = 0
|
||||||
|
) {
|
||||||
|
private var _minPage by mutableStateOf(minPage)
|
||||||
|
var minPage: Int
|
||||||
|
get() = _minPage
|
||||||
|
set(value) {
|
||||||
|
_minPage = value.coerceAtMost(_maxPage)
|
||||||
|
_currentPage = _currentPage.coerceIn(_minPage, _maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy())
|
||||||
|
var maxPage: Int
|
||||||
|
get() = _maxPage
|
||||||
|
set(value) {
|
||||||
|
_maxPage = value.coerceAtLeast(_minPage)
|
||||||
|
_currentPage = _currentPage.coerceIn(_minPage, maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage))
|
||||||
|
var currentPage: Int
|
||||||
|
get() = _currentPage
|
||||||
|
set(value) {
|
||||||
|
_currentPage = value.coerceIn(minPage, maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SelectionState { Selected, Undecided }
|
||||||
|
|
||||||
|
var selectionState by mutableStateOf(SelectionState.Selected)
|
||||||
|
|
||||||
|
suspend inline fun <R> selectPage(block: RecommendPagerState.() -> R): R = try {
|
||||||
|
selectionState = SelectionState.Undecided
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
selectPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun selectPage() {
|
||||||
|
currentPage -= currentPageOffset.roundToInt()
|
||||||
|
snapToOffset(0f)
|
||||||
|
selectionState = SelectionState.Selected
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _currentPageOffset = Animatable(0f).apply {
|
||||||
|
updateBounds(-1f, 1f)
|
||||||
|
}
|
||||||
|
val currentPageOffset: Float
|
||||||
|
get() = _currentPageOffset.value
|
||||||
|
|
||||||
|
suspend fun snapToOffset(offset: Float) {
|
||||||
|
val max = if (currentPage == minPage) 0f else 1f
|
||||||
|
val min = if (currentPage == maxPage) 0f else -1f
|
||||||
|
_currentPageOffset.snapTo(offset.coerceIn(min, max))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fling(velocity: Float) {
|
||||||
|
if (velocity < 0 && currentPage == maxPage) return
|
||||||
|
if (velocity > 0 && currentPage == minPage) return
|
||||||
|
|
||||||
|
// 根据 fling 的方向滑动到下一页或上一页
|
||||||
|
_currentPageOffset.animateTo(velocity)
|
||||||
|
selectPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "RecommendPagerState{minPage=$minPage, maxPage=$maxPage, " +
|
||||||
|
"currentPage=$currentPage, currentPageOffset=$currentPageOffset}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
private data class PageData(val page: Int) : ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any? = this@PageData
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Measurable.page: Int
|
||||||
|
get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RecommendPager(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: RecommendPagerState,
|
||||||
|
orientation: Orientation = Orientation.Horizontal,
|
||||||
|
offscreenLimit: Int = 2,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||||
|
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||||
|
onLoadMore: (() -> Unit)? = null,
|
||||||
|
content: @Composable RecommendPagerScope.() -> Unit
|
||||||
|
) {
|
||||||
|
var pageSize by remember { mutableStateOf(0) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 监听当前页面变化,当到达倒数第一个时触发加载更多
|
||||||
|
LaunchedEffect(state.currentPage, state.maxPage) {
|
||||||
|
if (state.currentPage >= state.maxPage - 1 && onLoadMore != null) {
|
||||||
|
onLoadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
content = {
|
||||||
|
// 根据 offscreenLimit 计算页面范围
|
||||||
|
val minPage = maxOf(state.currentPage - offscreenLimit, state.minPage)
|
||||||
|
val maxPage = minOf(state.currentPage + offscreenLimit, state.maxPage)
|
||||||
|
|
||||||
|
for (page in minPage..maxPage) {
|
||||||
|
val pageData = PageData(page)
|
||||||
|
val scope = RecommendPagerScope(state, page)
|
||||||
|
key(pageData) {
|
||||||
|
Column(
|
||||||
|
modifier = pageData
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
scope.content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.draggable(
|
||||||
|
orientation = orientation,
|
||||||
|
onDragStarted = {
|
||||||
|
state.selectionState = RecommendPagerState.SelectionState.Undecided
|
||||||
|
},
|
||||||
|
onDragStopped = { velocity ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
// 根据速度判断是否滑动到下一页
|
||||||
|
val threshold = 1000f // 速度阈值,可调整
|
||||||
|
if (velocity > threshold) {
|
||||||
|
state.fling(1f) // 向下滑动
|
||||||
|
} else if (velocity < -threshold) {
|
||||||
|
state.fling(-1f) // 向上滑动
|
||||||
|
} else {
|
||||||
|
state.fling(0f) // 保持当前页
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = rememberDraggableState { dy ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
with(state) {
|
||||||
|
val pos = pageSize * currentPageOffset
|
||||||
|
val max = if (currentPage == minPage) 0 else pageSize
|
||||||
|
val min = if (currentPage == maxPage) 0 else -pageSize
|
||||||
|
|
||||||
|
// 直接将手指的位移应用到 currentPageOffset
|
||||||
|
val newPos = (pos + dy).coerceIn(min.toFloat(), max.toFloat())
|
||||||
|
snapToOffset(newPos / pageSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
) { measurables, constraints ->
|
||||||
|
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||||
|
val currentPage = state.currentPage
|
||||||
|
val offset = state.currentPageOffset
|
||||||
|
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
|
|
||||||
|
measurables.forEach { measurable ->
|
||||||
|
val placeable = measurable.measure(childConstraints)
|
||||||
|
val page = measurable.page
|
||||||
|
|
||||||
|
// 根据对齐参数计算 x 和 y 位置
|
||||||
|
val xPosition = when (horizontalAlignment) {
|
||||||
|
Alignment.Start -> 0
|
||||||
|
Alignment.CenterHorizontally -> (constraints.maxWidth - placeable.width) / 2
|
||||||
|
Alignment.End -> constraints.maxWidth - placeable.width
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val yPosition = when (verticalAlignment) {
|
||||||
|
Alignment.Top -> 0
|
||||||
|
Alignment.CenterVertically -> (constraints.maxHeight - placeable.height) / 2
|
||||||
|
Alignment.Bottom -> constraints.maxHeight - placeable.height
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage == page) { // 只在当前页面设置 pageSize,避免不必要的设置
|
||||||
|
pageSize = if (orientation == Orientation.Horizontal) {
|
||||||
|
placeable.width
|
||||||
|
} else {
|
||||||
|
placeable.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isVisible = abs(page - (currentPage - offset)) <= 1
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
// 修正 y 的计算(垂直滑动)
|
||||||
|
val yOffset = if (orientation == Orientation.Vertical) {
|
||||||
|
((page - currentPage) * pageSize + offset * pageSize).roundToInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保内容不会溢出到容器顶部
|
||||||
|
val finalYPosition = (yPosition + yOffset).coerceAtLeast(0)
|
||||||
|
|
||||||
|
// 使用 placeRelative 进行放置
|
||||||
|
placeable.placeRelative(
|
||||||
|
x = xPosition + if (orientation == Orientation.Horizontal) ((page - (currentPage - offset)) * placeable.width).roundToInt() else 0,
|
||||||
|
y = finalYPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecommendPagerScope(
|
||||||
|
private val state: RecommendPagerState,
|
||||||
|
val page: Int
|
||||||
|
) {
|
||||||
|
val currentPage: Int
|
||||||
|
get() = state.currentPage
|
||||||
|
|
||||||
|
val currentPageOffset: Float
|
||||||
|
get() = state.currentPageOffset
|
||||||
|
|
||||||
|
val selectionState: RecommendPagerState.SelectionState
|
||||||
|
get() = state.selectionState
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
|
object RecommendViewModel : BaseMomentModel() {
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extraArgs(): MomentLoaderExtraArgs {
|
||||||
|
return MomentLoaderExtraArgs(trend = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,7 +93,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
|
||||||
)
|
)
|
||||||
@@ -150,7 +150,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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ fun LoginPage() {
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
|
painter = painterResource(id = R.mipmap.invalid_name),
|
||||||
contentDescription = "Rave Now",
|
contentDescription = "Rave Now",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(52.dp)
|
.size(52.dp)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前系统语言的完整标签,包含国家/地区代码
|
||||||
|
* 返回格式:en, ja, zh-CN, zh-TW 等
|
||||||
|
*/
|
||||||
|
fun getCurrentLanguageTag(): String {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
val language = locale.language
|
||||||
|
val country = locale.country
|
||||||
|
|
||||||
|
return when {
|
||||||
|
// 中文需要区分简体和繁体
|
||||||
|
language == "zh" && 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)
|
||||||
|
|||||||
18
app/src/main/res/drawable/ic_create_agent_generate.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="m9.098,4.33 l1.475,-1.475a0.643,0.643 0,0 1,0.909 0l1.663,1.663a0.643,0.643 0,0 1,0 0.91L11.67,6.901M9.098,4.33l-6.243,6.242a0.643,0.643 0,0 0,-0.188 0.455v1.663c0,0.355 0.288,0.643 0.643,0.643h1.663c0.17,0 0.334,-0.067 0.455,-0.188l6.242,-6.243M9.098,4.33l2.572,2.572"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.333"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#7C45ED"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M3.608,6.333c0.13,0 0.202,-0.07 0.215,-0.208 0.056,-0.434 0.114,-0.775 0.172,-1.022 0.06,-0.247 0.152,-0.434 0.28,-0.56 0.128,-0.126 0.318,-0.221 0.57,-0.286 0.252,-0.066 0.599,-0.133 1.042,-0.202 0.152,-0.026 0.228,-0.1 0.228,-0.222 0,-0.06 -0.02,-0.11 -0.059,-0.146a0.304,0.304 0,0 0,-0.143 -0.075,15.533 15.533,0 0,1 -1.048,-0.225c-0.256,-0.067 -0.45,-0.161 -0.583,-0.283 -0.132,-0.121 -0.228,-0.304 -0.287,-0.547a10.18,10.18 0,0 1,-0.172 -1.015c-0.013,-0.14 -0.085,-0.209 -0.215,-0.209a0.221,0.221 0,0 0,-0.153 0.056,0.208 0.208,0 0,0 -0.068,0.146 9.948,9.948 0,0 1,-0.176 1.039c-0.06,0.25 -0.155,0.438 -0.283,0.566 -0.128,0.128 -0.32,0.224 -0.576,0.286 -0.256,0.063 -0.608,0.125 -1.055,0.186a0.251,0.251 0,0 0,-0.143 0.072,0.203 0.203,0 0,0 -0.059,0.15c0,0.06 0.02,0.109 0.059,0.146 0.039,0.037 0.086,0.062 0.143,0.075 0.447,0.082 0.799,0.158 1.055,0.228 0.256,0.069 0.448,0.166 0.576,0.29 0.128,0.123 0.221,0.306 0.28,0.55 0.058,0.242 0.118,0.581 0.179,1.015a0.202,0.202 0,0 0,0.068 0.14,0.221 0.221,0 0,0 0.153,0.055zM12.516,14.702c0.086,0 0.139,-0.05 0.156,-0.15 0.056,-0.308 0.108,-0.552 0.156,-0.732a0.963,0.963 0,0 1,0.208 -0.417,0.879 0.879,0 0,1 0.41,-0.225c0.183,-0.052 0.439,-0.106 0.769,-0.162 0.1,-0.018 0.15,-0.072 0.15,-0.163 0,-0.091 -0.05,-0.146 -0.15,-0.163a6.959,6.959 0,0 1,-0.768 -0.166,0.919 0.919,0 0,1 -0.41,-0.224 0.937,0.937 0,0 1,-0.209 -0.414c-0.048,-0.18 -0.1,-0.426 -0.156,-0.739 -0.017,-0.095 -0.07,-0.143 -0.156,-0.143 -0.091,0 -0.146,0.048 -0.163,0.143a9.897,9.897 0,0 1,-0.156 0.74,0.937 0.937,0 0,1 -0.209,0.413 0.919,0.919 0,0 1,-0.41 0.224c-0.182,0.054 -0.436,0.11 -0.762,0.166 -0.1,0.017 -0.15,0.072 -0.15,0.163 0,0.091 0.05,0.145 0.15,0.163 0.326,0.056 0.58,0.11 0.762,0.162a0.879,0.879 0,0 1,0.41 0.225c0.091,0.098 0.16,0.237 0.209,0.417 0.047,0.18 0.1,0.424 0.156,0.732 0.009,0.044 0.026,0.08 0.052,0.108a0.143,0.143 0,0 0,0.11 0.042z"
|
||||||
|
android:fillColor="#7C45ED"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
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>
|
||||||
36
app/src/main/res/drawable/ic_create_head_logo.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:pathData="M24,24m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"
|
||||||
|
android:fillColor="#5E5CE6"
|
||||||
|
android:fillAlpha="0.1"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M29.132,11.3c0.861,0.075 1.609,0.484 2.124,1.082a3.019,3.019 50,0 1,-0.378 4.325c-0.448,0.365 -0.987,0.6 -1.56,0.68l-0.02,0.002 0.057,0.035c1.649,0.997 2.914,2.401 3.789,4.009l0.041,0.076c1.115,2.087 1.576,4.508 1.374,6.807 -0.237,2.703 -1.392,4.636 -3.188,5.873a3.07,3.07 50,0 1,0.025 1.749c-0.168,0.58 -0.476,0.756 -0.578,0.791 -0.176,0.059 -1.855,-0.393 -2.957,-1.133 -1.289,0.249 -2.726,0.304 -4.271,0.168 -3.173,-0.276 -6.14,-1.296 -8.154,-3.002 -1.84,-1.559 -2.906,-3.682 -2.672,-6.345 0.261,-2.968 1.606,-5.945 3.885,-7.972 1.978,-1.761 4.657,-2.815 7.951,-2.529 0.608,0.053 1.187,0.148 1.738,0.28l0.039,0.009 -0.01,-0.013a3.013,3.013 50,0 1,-0.627 -2.061l0.004,-0.051c0.074,-0.845 0.492,-1.579 1.109,-2.083a3.163,3.163 50,0 1,2.281 -0.698z"
|
||||||
|
android:fillType="nonZero">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="23.669"
|
||||||
|
android:startY="11.286"
|
||||||
|
android:endX="23.669"
|
||||||
|
android:endY="36.734"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF7C45ED"/>
|
||||||
|
<item android:offset="0.236" android:color="#FF7C68EF"/>
|
||||||
|
<item android:offset="1" android:color="#FF7BD8F8"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M17.51,22.904L17.736,22.924A0.912,0.912 50,0 1,18.565 23.912L18.419,25.584A0.912,0.912 50,0 1,17.431 26.413L17.205,26.393A0.912,0.912 50,0 1,16.376 25.405L16.522,23.733A0.912,0.912 50,0 1,17.51 22.904z"
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M22.163,23.341L22.389,23.361A0.912,0.912 50,0 1,23.218 24.349L23.072,26.021A0.912,0.912 50,0 1,22.083 26.85L21.857,26.83A0.912,0.912 50,0 1,21.028 25.842L21.175,24.17A0.912,0.912 50,0 1,22.163 23.341z"
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
</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>
|
||||||
14
app/src/main/res/drawable/ic_manual_create_agent.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="m9.185,2 l2.37,2.341 -6.518,6.44h-2.37V8.438zM2.667,13.707h10.667"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.185"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
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_home_s.png
Normal file
|
After Width: | Height: | Size: 689 B |
BIN
app/src/main/res/mipmap-hdpi/invalid_name.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
app/src/main/res/mipmap-mdpi/bars_x_buttons_discover_bold.png
Normal file
|
After Width: | Height: | Size: 418 B |
BIN
app/src/main/res/mipmap-mdpi/bars_x_buttons_home_s.png
Normal file
|
After Width: | Height: | Size: 417 B |
BIN
app/src/main/res/mipmap-mdpi/invalid_name.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-xhdpi/bars_x_buttons_discover_bold.png
Normal file
|
After Width: | Height: | Size: 695 B |
BIN
app/src/main/res/mipmap-xhdpi/bars_x_buttons_home_s.png
Normal file
|
After Width: | Height: | Size: 707 B |
BIN
app/src/main/res/mipmap-xhdpi/invalid_name.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/bars_x_buttons_discover_bold.png
Normal file
|
After Width: | Height: | Size: 935 B |
BIN
app/src/main/res/mipmap-xxhdpi/bars_x_buttons_home_s.png
Normal file
|
After Width: | Height: | Size: 984 B |
BIN
app/src/main/res/mipmap-xxhdpi/invalid_name.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/bars_x_buttons_discover_bold.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/bars_x_buttons_home_s.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/invalid_name.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
60
app/src/main/res/values-ja/strings.xml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Create Bottom Sheet -->
|
||||||
|
<string name="create_title">作成</string>
|
||||||
|
<string name="create_ai">AI</string>
|
||||||
|
<string name="create_group_chat_option">グループチャット</string>
|
||||||
|
<string name="create_moment">モーメント</string>
|
||||||
|
<string name="create_close">閉じる</string>
|
||||||
|
|
||||||
|
<!-- Create Agent V2 -->
|
||||||
|
<string name="create_agent_v2_back">戻る</string>
|
||||||
|
<string name="create_agent_v2_title">AI作成</string>
|
||||||
|
<string name="create_agent_v2_greeting">%s さん、こんにちは!今日は何を作りたいですか?</string>
|
||||||
|
<string name="create_agent_v2_description">たった一言で、あなた専用のAIがここに誕生します</string>
|
||||||
|
<string name="create_agent_v2_input_hint">詩を書けるAI、あなたの笑いのツボがわかるAI</string>
|
||||||
|
<string name="create_agent_v2_ai_enhance">AI美化</string>
|
||||||
|
<string name="create_agent_v2_generating">生成中...</string>
|
||||||
|
<string name="create_agent_v2_manual_create">手動でAI作成</string>
|
||||||
|
<string name="create_agent_v2_one_sentence_create">一言でAI作成</string>
|
||||||
|
<string name="create_agent_v2_thinking">あなたのために構想中</string>
|
||||||
|
<string name="create_agent_v2_name_label">名前</string>
|
||||||
|
<string name="create_agent_v2_description_label">説明</string>
|
||||||
|
<string name="create_agent_v2_create_button">よし、これで決まり</string>
|
||||||
|
<string name="create_agent_v2_creating">作成中...</string>
|
||||||
|
<string name="create_agent_v2_ai_avatar_desc">AIアバター</string>
|
||||||
|
<string name="create_agent_v2_ai_enhance_icon_desc">AI美化アイコン</string>
|
||||||
|
<string name="create_agent_v2_edit_icon_desc">編集アイコン</string>
|
||||||
|
<string name="create_agent_v2_select_avatar_desc">アバター選択</string>
|
||||||
|
|
||||||
|
<!-- Index tabs -->
|
||||||
|
<string name="index_worldwide">ワールドワイド</string>
|
||||||
|
<string name="index_dynamic">ダイナミック</string>
|
||||||
|
<string name="index_following">フォロー中</string>
|
||||||
|
<string name="index_hot">ホット</string>
|
||||||
|
<string name="index_news">ニュース</string>
|
||||||
|
|
||||||
|
<!-- Main navigation -->
|
||||||
|
<string name="main_home">ホーム</string>
|
||||||
|
<string name="main_ai">エージェント</string>
|
||||||
|
<string name="main_message">メッセージ</string>
|
||||||
|
<string name="main_profile">プロフィール</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_mine">マイ</string>
|
||||||
|
<string name="agent_hot">ホット</string>
|
||||||
|
<string name="agent_recommend">おすすめ</string>
|
||||||
|
<string name="agent_other">その他</string>
|
||||||
|
<string name="agent_find">AI発見</string>
|
||||||
|
<string name="chat">チャット</string>
|
||||||
|
<string name="agent_chat_room">チャット</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">おすすめチャットルーム</string>
|
||||||
|
<string name="search">検索</string>
|
||||||
|
<string name="agent_recommend_agent">おすすめエージェント</string>
|
||||||
|
|
||||||
|
<!-- Chat related -->
|
||||||
|
<string name="chat_ai">AI</string>
|
||||||
|
<string name="chat_group">グループ</string>
|
||||||
|
<string name="chat_friend">友達</string>
|
||||||
|
<string name="hot_rooms">人気チャットルーム</string>
|
||||||
|
</resources>
|
||||||
@@ -188,7 +188,38 @@
|
|||||||
<string name="group_room_enter_success">成功加入房间</string>
|
<string name="group_room_enter_success">成功加入房间</string>
|
||||||
<string name="group_room_enter_fail">加入房间失败</string>
|
<string name="group_room_enter_fail">加入房间失败</string>
|
||||||
<string name="agent_createing">创建中...</string>
|
<string name="agent_createing">创建中...</string>
|
||||||
<string name="agent_find">发现</string>
|
<string name="agent_find">发现AI</string>
|
||||||
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
|
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
|
||||||
|
|
||||||
|
<!-- Create Bottom Sheet -->
|
||||||
|
<string name="create_title">创建</string>
|
||||||
|
<string name="create_ai">AI</string>
|
||||||
|
<string name="create_group_chat_option">群聊</string>
|
||||||
|
<string name="create_moment">动态</string>
|
||||||
|
<string name="create_close">关闭</string>
|
||||||
|
|
||||||
|
<!-- Create Agent V2 -->
|
||||||
|
<string name="create_agent_v2_back">返回</string>
|
||||||
|
<string name="create_agent_v2_title">创建AI</string>
|
||||||
|
<string name="create_agent_v2_greeting">%s 你好呀!今天想创建什么?</string>
|
||||||
|
<string name="create_agent_v2_description">只需要一句话,你的专属AI在这里诞生</string>
|
||||||
|
<string name="create_agent_v2_input_hint">一个会写诗的AI,一个会懂你笑点的AI</string>
|
||||||
|
<string name="create_agent_v2_ai_enhance">AI美化</string>
|
||||||
|
<string name="create_agent_v2_generating">生成中...</string>
|
||||||
|
<string name="create_agent_v2_manual_create">手动创造AI</string>
|
||||||
|
<string name="create_agent_v2_one_sentence_create">一句话创造AI</string>
|
||||||
|
<string name="create_agent_v2_thinking">正在为你构思</string>
|
||||||
|
<string name="create_agent_v2_name_label">名称</string>
|
||||||
|
<string name="create_agent_v2_description_label">描述</string>
|
||||||
|
<string name="create_agent_v2_create_button">好的,就它了</string>
|
||||||
|
<string name="create_agent_v2_creating">创建中...</string>
|
||||||
|
<string name="create_agent_v2_ai_avatar_desc">AI头像</string>
|
||||||
|
<string name="create_agent_v2_ai_enhance_icon_desc">AI美化图标</string>
|
||||||
|
<string name="create_agent_v2_edit_icon_desc">编辑图标</string>
|
||||||
|
<string name="create_agent_v2_select_avatar_desc">选择头像</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_chat_room">聊天</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">推荐聊天房间</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -122,6 +122,8 @@
|
|||||||
<string name="index_dynamic">Dynamic</string>
|
<string name="index_dynamic">Dynamic</string>
|
||||||
<string name="index_following">Following</string>
|
<string name="index_following">Following</string>
|
||||||
<string name="index_hot">Hot</string>
|
<string name="index_hot">Hot</string>
|
||||||
|
<string name="index_news">News</string>
|
||||||
|
<string name="index_recommend">Recommend</string>
|
||||||
<string name="main_home">Home</string>
|
<string name="main_home">Home</string>
|
||||||
<string name="main_ai">Agent</string>
|
<string name="main_ai">Agent</string>
|
||||||
<string name="main_message">Message</string>
|
<string name="main_message">Message</string>
|
||||||
@@ -184,7 +186,38 @@
|
|||||||
<string name="group_room_enter_success">成功加入房间</string>
|
<string name="group_room_enter_success">成功加入房间</string>
|
||||||
<string name="group_room_enter_fail">加入房间失败</string>
|
<string name="group_room_enter_fail">加入房间失败</string>
|
||||||
<string name="agent_createing">创建中...</string>
|
<string name="agent_createing">创建中...</string>
|
||||||
<string name="agent_find">发现</string>
|
<string name="agent_find">发现AI</string>
|
||||||
<string name="text_error_password_too_long">Password cannot exceed %1$d characters</string>
|
<string name="text_error_password_too_long">Password cannot exceed %1$d characters</string>
|
||||||
|
|
||||||
|
<!-- Create Bottom Sheet -->
|
||||||
|
<string name="create_title">Create</string>
|
||||||
|
<string name="create_ai">AI</string>
|
||||||
|
<string name="create_group_chat_option">Group Chat</string>
|
||||||
|
<string name="create_moment">Moment</string>
|
||||||
|
<string name="create_close">Close</string>
|
||||||
|
|
||||||
|
<!-- Create Agent V2 -->
|
||||||
|
<string name="create_agent_v2_back">Back</string>
|
||||||
|
<string name="create_agent_v2_title">Create AI</string>
|
||||||
|
<string name="create_agent_v2_greeting">Hello %s! What would you like to create today?</string>
|
||||||
|
<string name="create_agent_v2_description">Just one sentence, and your exclusive AI will be born here</string>
|
||||||
|
<string name="create_agent_v2_input_hint">An AI that writes poetry, an AI that understands your humor</string>
|
||||||
|
<string name="create_agent_v2_ai_enhance">AI Enhancement</string>
|
||||||
|
<string name="create_agent_v2_generating">Generating...</string>
|
||||||
|
<string name="create_agent_v2_manual_create">Manually Create AI</string>
|
||||||
|
<string name="create_agent_v2_one_sentence_create">Create AI with One Sentence</string>
|
||||||
|
<string name="create_agent_v2_thinking">Thinking for you</string>
|
||||||
|
<string name="create_agent_v2_name_label">Name</string>
|
||||||
|
<string name="create_agent_v2_description_label">Description</string>
|
||||||
|
<string name="create_agent_v2_create_button">Alright, that\'s the one</string>
|
||||||
|
<string name="create_agent_v2_creating">Creating...</string>
|
||||||
|
<string name="create_agent_v2_ai_avatar_desc">AI Avatar</string>
|
||||||
|
<string name="create_agent_v2_ai_enhance_icon_desc">AI Enhancement Icon</string>
|
||||||
|
<string name="create_agent_v2_edit_icon_desc">Edit Icon</string>
|
||||||
|
<string name="create_agent_v2_select_avatar_desc">Select Avatar</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_chat_room">Chat</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">Recommended Chat Rooms</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -5,7 +5,8 @@ animation = "1.7.0-beta05"
|
|||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
composeImageBlurhash = "3.0.2"
|
composeImageBlurhash = "3.0.2"
|
||||||
converterGson = "2.11.0"
|
converterGson = "2.11.0"
|
||||||
coreSdk = "3.8.3"
|
imSdk = "3.8.3.2"
|
||||||
|
imcoreSdk = "3.8.3-patch10"
|
||||||
coreSplashscreen = "1.0.1"
|
coreSplashscreen = "1.0.1"
|
||||||
credentialsPlayServicesAuth = "1.2.2"
|
credentialsPlayServicesAuth = "1.2.2"
|
||||||
eventbus = "3.3.1"
|
eventbus = "3.3.1"
|
||||||
@@ -42,7 +43,6 @@ zoomable = "1.6.1"
|
|||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
|
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
|
||||||
im-sdk = { module = "io.openim:android-sdk", version.ref = "coreSdk" }
|
|
||||||
androidx-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" }
|
androidx-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" }
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
||||||
@@ -60,7 +60,6 @@ coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
|||||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||||
compose-image-blurhash = { module = "com.github.orlando-dev-code:compose-image-blurhash", version.ref = "composeImageBlurhash" }
|
compose-image-blurhash = { module = "com.github.orlando-dev-code:compose-image-blurhash", version.ref = "composeImageBlurhash" }
|
||||||
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
|
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
|
||||||
im-core-sdk = { module = "io.openim:core-sdk", version.ref = "coreSdk" }
|
|
||||||
eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" }
|
eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" }
|
||||||
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
||||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
|
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
|
||||||
@@ -84,6 +83,8 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
|
|||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
|
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
|
||||||
|
im-sdk = { module = "io.openim:android-sdk", version.ref = "imSdk" }
|
||||||
|
im-core-sdk = { module = "io.openim:core-sdk", version.ref = "imcoreSdk" }
|
||||||
jwtdecode = { module = "com.auth0.android:jwtdecode", version.ref = "jwtdecode" }
|
jwtdecode = { module = "com.auth0.android:jwtdecode", version.ref = "jwtdecode" }
|
||||||
kotlin-faker = { module = "io.github.serpro69:kotlin-faker", version.ref = "kotlinFaker" }
|
kotlin-faker = { module = "io.github.serpro69:kotlin-faker", version.ref = "kotlinFaker" }
|
||||||
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
|
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
|
||||||
|
|||||||