20 Commits

Author SHA1 Message Date
b06f2d6c4a 动态模块新增推荐Tab,UI优化及调整
- 新增推荐Tab,采用垂直滑动样式,展示推荐动态内容。
- 推荐Tab支持预加载周围图片,提升滑动体验,并增加loading和错误状态指示。
- 优化评论弹窗UI,移除自动聚焦,调整背景色和输入框样式。
- 动态Tab样式调整,使用下划线指示当前选中Tab。
- 调整MomentLoaderExtraArgs,增加trend参数用于推荐动态加载。
- 新增字符串资源 `index_recommend`。
2025-09-16 16:43:42 +08:00
a215d79ce8 Feat: Add News tab and related functionality
- Added a "News" tab to the main index screen.
- Implemented API parameters for fetching news-specific posts: `imageTag`, `search`, `advancedSearch`, `newsFilter`, `onlyNews`, `newsSource`, `newsLanguage`, `newsCategory`, `requireImageCache`.
- Updated `Moment` data class and `MomentEntity` to include news-related fields like `isNews`, `newsTitle`, `newsUrl`, etc.
- Created `News.kt` composable and `NewsViewModel.kt` to display and manage news items.
- Updated `MomentLoader` to include a `newsOnly` parameter for fetching only news items.
- Added Japanese translations for new index tab strings: "Worldwide", "Dynamic", "Following", "Hot", and "News".
- Adjusted tab count and layout based on guest/logged-in user status to accommodate the new "News" tab.
2025-09-16 14:08:50 +08:00
0e0d622864 Refactor: Optimize Agent tab UI and add chat room recommendations
- Implemented lazy loading for Agent list with pagination.
- Added a section for recommended chat rooms.
- Restructured Agent tab UI:
    - Fixed the top search bar.
    - Organized content into scrollable sections for "Today's Picks", "Recommended Chat Rooms", and "Find Agents".
    - Improved Agent card design with theme-aware backgrounds.
- Introduced `ChatRoom` data class and integrated chat room loading logic in `AgentViewModel`.
- Updated `RiderProAPI` to include `random` parameter for fetching random rooms.
- Enhanced Agent card and chat room card click handling with debounce.
2025-09-15 23:30:10 +08:00
a1196715d0 Feat: Add News tab and related functionality
- Added a "News" tab to the main index screen.
- Implemented API parameters for fetching news-specific posts: `imageTag`, `search`, `advancedSearch`, `newsFilter`, `onlyNews`, `newsSource`, `newsLanguage`, `newsCategory`, `requireImageCache`.
- Updated `Moment` data class and `MomentEntity` to include news-related fields like `isNews`, `newsTitle`, `newsUrl`, etc.
- Created `News.kt` composable and `NewsViewModel.kt` to display and manage news items.
- Updated `MomentLoader` to include a `newsOnly` parameter for fetching only news items.
- Added Japanese translations for new index tab strings: "Worldwide", "Dynamic", "Following", "Hot", and "News".
- Adjusted tab count and layout based on guest/logged-in user status to accommodate the new "News" tab.
2025-09-15 18:31:24 +08:00
68273ae166 Fix: Correct BASE_SERVER URL for debug and release builds
The BASE_SERVER constant was incorrectly assigning the release URL to debug builds and vice-versa. This commit fixes the logic to ensure the correct API endpoint is used for each build type.
2025-09-15 13:46:01 +08:00
6c7be4ba47 feat: Add string resources for Create Agent V2
This commit introduces new string resources for the "Create Agent V2" feature.
These strings are provided in English, Chinese, and Japanese to support localization.

The added strings cover various UI elements and messages within the Create Agent V2 flow, including:
- Titles and labels (e.g., "Create AI", "Name", "Description")
- User guidance and prompts (e.g., "Hello %s! What would you like to create today?", "An AI that writes poetry...")
- Action button texts (e.g., "AI Enhancement", "Manually Create AI", "Alright, that's the one")
- Status messages (e.g., "Generating...", "Creating...", "Thinking for you")
- Accessibility descriptions for icons (e.g., "AI Avatar", "Edit Icon")
2025-09-15 13:42:58 +08:00
cf25540417 Refactor: Implement V2 of Create Agent UI and logic
- Introduced `CreateAgentV2Screen` and `CreateAgentV2ViewModel` for a new agent creation experience.
- Implemented AI-powered agent info generation based on user input, including title and description.
- Added a "manual mode" for users who prefer to input agent details directly.
- Enhanced UI with gradient borders, loading animations, and improved layout.
- Integrated avatar selection and cropping using `AgentImageCropScreen`.
- Refactored `AddAgentViewModel` to support state persistence across page navigation and to store generated input text.
- Updated API client to include a longer timeout for agent info generation requests.
- Added new drawable resources for UI elements.
- Switched `Const.BASE_SERVER` to use the release URL for debug builds.
- Replaced the old `AddAgentScreen` with the new `CreateAgentV2Screen` in navigation.
2025-09-15 12:03:39 +08:00
eca85c8377 Feat: Add Create Bottom Sheet and icons
- Implemented a new `CreateBottomSheet` Composable to provide users with options to create AI, Group Chat, or Moment.
- Added new drawable resources for the create options: `ic_create_ai.xml`, `ic_create_group_chat.xml`, `ic_create_monent.xml`, and `ic_create_close.xml`.
- Integrated the `CreateBottomSheet` into the `IndexScreen`. Clicking the "+" button now opens this bottom sheet instead of directly navigating to new post creation.
- Updated `IndexViewModel` to manage the visibility state of the `CreateBottomSheet`.
- Added string resources for the Create Bottom Sheet in English, Chinese, and Japanese.
- Ensured proper navigation and tourist mode checks for each create option.
- Implemented graceful dismissal of the bottom sheet with animations.
2025-09-12 17:21:29 +08:00
f8be622ba6 Merge pull request #17 from Zhong202501/main
首页Agent卡片组件
2025-09-10 19:22:52 +08:00
f3c841779b Merge pull request #18 from Kevinlinpr/ll
修复一些未处理异常,切换到测试服务器
2025-09-10 19:19:38 +08:00
57e4614ce8 修复一些未处理异常,切换到测试服务器 2025-09-10 18:34:36 +08:00
922d6e72d6 首页Agent卡片组件 2025-09-10 18:02:58 +08:00
c41c097d41 处理最新消息显示 2025-09-10 14:03:27 +08:00
5218ca7046 重构IM viewmodel代码 2025-09-10 11:57:05 +08:00
ce6ee7bf82 imsdk 调通 2025-09-09 19:05:07 +08:00
e00deb5661 merge conflict 2025-09-09 17:57:28 +08:00
95d6522a54 fix im connect error 2025-09-09 17:53:52 +08:00
d231f3678c 首页UI 2025-09-09 16:18:35 +08:00
cd35562244 标签页调整 2025-09-09 14:41:37 +08:00
21cb512237 启动图标;动态界面调整 2025-09-08 18:06:39 +08:00
76 changed files with 4428 additions and 1398 deletions

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 *;
}

View File

@@ -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"

View File

@@ -67,8 +67,13 @@ object AppState {
) )
// 设置当前登录用户 ID // 设置当前登录用户 ID
UserId = resp.id UserId = resp.id
var profileResult = accountService.getMyAccountProfile() try {
profile = profileResult var profileResult = accountService.getMyAccountProfile()
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)

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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>?
} }

View File

@@ -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
)

View File

@@ -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,
) )
} }
} }

View File

@@ -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())

View File

@@ -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>>
} }

View File

@@ -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>? {
// 如果是游客模式且获取我的AgentauthorId为null返回空列表 // 如果是游客模式且获取我的AgentauthorId为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) {

View 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)
}
}
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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 = ""
} }
} }

View 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))
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)
fetchHistoryMessage(context)
// 获取会话信息,然后加载历史消息
getOneConversation {
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 onRecvNewMessage(msg: Message?) {
msg?.let { message ->
// 只处理当前聊天对象的消息
val currentChatUserId = userProfile?.trtcUserId
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
if (currentChatUserId != null && currentUserId != null) {
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
if ((message.sendID == currentChatUserId || 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聊天列表")
}
}
}
}
}
}
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
} }
fun UnRegistListener() { override fun getLogTag(): String {
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null return "ChatAiViewModel"
textMessageListener = null
} }
fun clearUnRead() { override fun handleNewMessage(message: Message, context: Context): Boolean {
val conversationID = "single_${userProfile?.trtcUserId}" // 只处理当前聊天对象的消息
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead( val currentChatUserId = userProfile?.trtcUserId
conversationID, val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
object : OnBase<String> {
override fun onSuccess(data: String?) {
Log.i("openim", "clear unread success")
}
override fun onError(code: Int, error: String?) { if (currentChatUserId != null && currentUserId != null) {
Log.i("openim", "clear unread failure, code:$code, error:$error") // 检查消息是否来自当前聊天对象,且不是自己发送的消息
} return (message.sendID == currentChatUserId || message.sendID == currentUserId) &&
} message.sendID != currentUserId
)
}
fun onLoadMore(context: Context) {
if (!hasMore || isLoading) {
return
} }
isLoading = true return false
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) { override fun getReceiverInfo(): Pair<String?, String?> {
hasMore = false return Pair(userProfile?.trtcUserId, null) // (recvID, groupID)
} }
lastMessage = messages.lastOrNull()
isLoading = false
Log.d("ChatAiViewModel", "fetch history message success")
}
override fun onError(code: Int, error: String?) { override fun getMessageAvatar(message: Message): String? {
Log.e("ChatAiViewModel", "fetch history message error: $error") return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
isLoading = false myProfile?.avatar
} } else {
}, userProfile?.avatar
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) { sendChatAiMessage(myProfile?.trtcUserId!!, userProfile?.trtcUserId!!, message)
android.util.Log.w("ChatAiViewModel", "OpenIM 未登录,无法发送消息") createGroup2ChatAi(userProfile?.trtcUserId!!, "ai_group")
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)
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) {

View File

@@ -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,253 +26,51 @@ class ChatViewModel(
myProfile = accountService.getMyAccountProfile() myProfile = accountService.getMyAccountProfile()
RegistListener(context) RegistListener(context)
fetchHistoryMessage(context)
// 获取会话信息,然后加载历史消息
getOneConversation {
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 onRecvNewMessage(msg: Message?) {
msg?.let { message ->
// 只处理当前聊天对象的消息
val currentChatUserId = userProfile?.trtcUserId
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
if (currentChatUserId != null && currentUserId != null) {
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
if ((message.sendID == currentChatUserId || 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} 的消息,更新聊天列表")
}
}
}
}
}
}
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
} }
fun UnRegistListener() { override fun getLogTag(): String {
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null return "ChatViewModel"
textMessageListener = null
} }
fun clearUnRead() { override fun handleNewMessage(message: Message, context: Context): Boolean {
val conversationID = "single_${userProfile?.trtcUserId}" // 只处理当前聊天对象的消息
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead( val currentChatUserId = userProfile?.trtcUserId
conversationID, val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
object : OnBase<String> {
override fun onSuccess(data: String?) {
Log.i("openim", "clear unread success")
}
override fun onError(code: Int, error: String?) { if (currentChatUserId != null && currentUserId != null) {
Log.i("openim", "clear unread failure, code:$code, error:$error") // 检查消息是否来自当前聊天对象,且不是自己发送的消息
} return (message.sendID == currentChatUserId || message.sendID == currentUserId) &&
} message.sendID != currentUserId
)
}
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
)
} }
return false
} }
fun sendMessage(message: String, context: Context) { override fun getReceiverInfo(): Pair<String?, String?> {
// 检查 OpenIM 是否已登录 return Pair(userProfile?.trtcUserId, null) // (recvID, groupID)
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) { override fun getMessageAvatar(message: Message): String? {
val tempFile = createTempFile(context, imageUri) return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
val imagePath = tempFile?.path myProfile?.avatar
if (imagePath != null) { } else {
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath) userProfile?.avatar
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 {
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("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 {
val result = ChatState.updateChatNotification(it.id, strategy) val result = ChatState.updateChatNotification(it.id, strategy)

View File

@@ -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)
fetchHistoryMessage(context)
// 获取会话信息,然后加载历史消息
getOneConversation {
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 onRecvNewMessage(msg: Message?) {
msg?.let {
// 检查是否是当前群聊的消息
if (it.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 getLogTag(): String {
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null return "GroupChatViewModel"
textMessageListener = null
} }
fun clearUnRead() { override fun handleNewMessage(message: Message, context: Context): Boolean {
val conversationID = "group_${groupId}" // 检查是否是当前群聊的消息
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead( return message.groupID == groupId
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) { override fun getReceiverInfo(): Pair<String?, String?> {
if (!hasMore || isLoading) return return Pair(null, groupId) // (recvID, groupID)
isLoading = true }
viewModelScope.launch {
val conversationID = "group_${groupId}"
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList( override fun getMessageAvatar(message: Message): String? {
object : OnBase<AdvancedMessage> { // 群聊中如果是自己发送的消息显示自己的头像否则为null由ChatItem处理
override fun onSuccess(data: AdvancedMessage?) { return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
val messages = data?.messageList ?: emptyList() myProfile?.avatar
chatData = chatData + messages.map { } else {
ChatItem.convertToChatItem(it, context, avatar = null) 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) { sendChatAiMessage(message = message, trtcGroupId = groupId)
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)
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
} }
} }

View File

@@ -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 {

View File

@@ -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
) )
} }
} }

View File

@@ -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)
) )
} }
} }

View File

@@ -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

View File

@@ -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
)
}
}

View File

@@ -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)
}
}
)
}
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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 },

View File

@@ -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,223 +119,398 @@ fun Agent() {
} }
} }
Column( Box(
modifier = Modifier modifier = Modifier.fillMaxSize()
.fillMaxSize()
.padding(
top = statusBarPaddingValues.calculateTopPadding()+18.dp,
bottom = navigationBarPaddings,
start = 16.dp,
end = 16.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Row( // 固定顶部搜索条
Box(
modifier = Modifier modifier = Modifier
.wrapContentHeight() .fillMaxWidth()
.fillMaxWidth(), .background(AppColors.background)
horizontalArrangement = Arrangement.Start, .zIndex(999.0f)
verticalAlignment = Alignment.CenterVertically .height(44.dp + statusBarPaddingValues.calculateTopPadding())
) { ) {
// 搜索框
Row( Row(
modifier = Modifier modifier = Modifier
.height(36.dp) .fillMaxWidth()
.weight(1f) .fillMaxHeight().padding(top = 32.dp, start = 16.dp, end = 16.dp),
.clip(shape = RoundedCornerShape(8.dp)) horizontalArrangement = Arrangement.Start,
.background(AppColors.inputBackground)
.padding(horizontal = 8.dp, vertical = 0.dp)
.noRippleClickable {
// 搜索框点击事件
},
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( androidx.compose.material3.Text(
painter = painterResource(id = R.drawable.rider_pro_nav_search), text = "Rave AI",
contentDescription = null, fontSize = 20.sp,
tint = AppColors.inputHint 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)
) )
Box {
Text(
text = stringResource(R.string.search),
modifier = Modifier.padding(start = 8.dp),
color = AppColors.inputHint,
fontSize = 17.sp
)
}
} }
Spacer(modifier = Modifier.width(16.dp))
// 新增
Icon(
modifier = Modifier
.size(36.dp)
.noRippleClickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
// 导航到添加智能体页面
navController.navigate(
NavigationRoute.AddAgent.route
)
}
}) {
lastClickTime = System.currentTimeMillis()
}
},
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
contentDescription = null,
tint = AppColors.text
)
} }
// 推荐Agent
// 可滚动的内容区域
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(
top = 44.dp + statusBarPaddingValues.calculateTopPadding() + 15.dp,
bottom = navigationBarPaddings,
start = 16.dp,
end = 16.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// // 搜索框
// Row(
// modifier = Modifier
// .height(36.dp)
// .weight(1f)
// .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 {
// Text(
// text = stringResource(R.string.search),
// modifier = Modifier.padding(start = 8.dp),
// color = AppColors.inputHint,
// fontSize = 17.sp
// )
// }
// }
// Spacer(modifier = Modifier.width(16.dp))
// // 创建智能体
// Icon(
// modifier = Modifier
// .size(36.dp)
// .noRippleClickable {
// if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// // 检查游客模式,如果是游客则跳转登录
// if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
// navController.navigate(NavigationRoute.Login.route)
// } else {
// // 导航到添加智能体页面
// navController.navigate(
// NavigationRoute.AddAgent.route
// )
// }
// }) {
// lastClickTime = System.currentTimeMillis()
// }
// },
// painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
// contentDescription = null,
// tint = AppColors.text
// )
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(260.dp)
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
) { ) {
// 标题
Row(
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)) // Row(
androidx.compose.material3.Text( // verticalAlignment = Alignment.CenterVertically,
text = stringResource(R.string.agent_recommend_agent), // modifier = Modifier.padding(bottom = 12.dp)
fontSize = 16.sp, // ) {
fontWeight = androidx.compose.ui.text.font.FontWeight.W600, // Image(
color = AppColors.text // 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
) {
viewModel.categories.forEachIndexed { index, category ->
item {
CustomTabItem(
text = category.getLocalizedName(),
isSelected = selectedTabIndex == index,
onClick = {
viewModel.selectCategory(index)
}
)
}
if (index < viewModel.categories.size - 1) {
item {
TabSpacer()
}
}
}
}
// 显示当前选中分类的 Agent 数据
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
}
// 推荐聊天房间
ChatRoomsSection(
chatRooms = viewModel.chatRooms,
navController = LocalNavController.current
)
Spacer(modifier = Modifier.height(20.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
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_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
)
}
Spacer(modifier = Modifier.height(20.dp))
// Agent两列网格布局
AgentGridLayout(
agentItems = viewModel.agentItems,
viewModel = viewModel,
navController = LocalNavController.current
)
}
}
}
@Composable
fun AgentGridLayout(
agentItems: List<AgentItem>,
viewModel: AgentViewModel,
navController: NavHostController
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
// 将agentItems按两列分组
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
bottom = 20.dp
),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一列
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[0],
viewModel = viewModel,
navController = navController
) )
} }
// Agent ViewPager // 第二列(如果存在)
AgentViewPagerSection(agentItems = viewModel.agentItems.take(9),viewModel) if (rowItems.size > 1) {
} Box(
modifier = Modifier.weight(1f)
Spacer(modifier = Modifier.height(0.dp)) ) {
Row( AgentCardSquare(
modifier = Modifier agentItem = rowItems[1],
.fillMaxWidth() viewModel = viewModel,
.wrapContentHeight(), navController = navController
// center the tabs )
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
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_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
)
Spacer(modifier = Modifier.weight(1f))
// 只有非游客用户才显示"我的Agent"tab
if (!AppStore.isGuest) {
TabItem(
text = stringResource(R.string.agent_mine),
isSelected = pagerState.currentPage == 0,
onClick = {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 300L) {
scope.launch {
pagerState.animateScrollToPage(0)
}
}) {
lastClickTime = System.currentTimeMillis()
}
}
)
TabSpacer()
}
TabItem(
text = stringResource(R.string.agent_hot),
isSelected = if (AppStore.isGuest) pagerState.currentPage == 0 else pagerState.currentPage == 1,
onClick = {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 300L) {
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(
text = stringResource(R.string.agent_other),
isSelected = pagerState.currentPage == 3,
onClick = {
scope.launch {
pagerState.animateScrollToPage(3)
}
}
)*/
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
beyondBoundsPageCount = 1 // 预加载相邻页面,避免切换时重新加载
) {
if (AppStore.isGuest) {
// 游客模式下只显示热门Agent
when (it) {
0 -> {
HotAgent()
}
}
} else {
// 正常用户显示我的Agent和热门Agent
when (it) {
0 -> {
MineAgent()
}
1 -> {
HotAgent()
} }
} 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,11 +623,11 @@ 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)
}) { }) {
lastClickTime = System.currentTimeMillis() lastClickTime = System.currentTimeMillis()
} }
}, },
@@ -477,19 +687,22 @@ 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 {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 检查游客模式,如果是游客则跳转登录 // 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
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
)
}
}
}

View File

@@ -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
} }
} }

View File

@@ -140,143 +140,183 @@ fun NotificationsScreen() {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
Row( Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier modifier = Modifier
.size(24.dp) .fillMaxWidth()
) .height(44.dp)
Column( .padding(horizontal = 16.dp),
modifier = Modifier horizontalArrangement = Arrangement.Start,
.weight(1f) verticalAlignment = Alignment.CenterVertically
.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
) )
}
Image( Spacer(modifier = Modifier.weight(1f))
painter = painterResource(id = R.drawable.rider_pro_group),
contentDescription = "add",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
debouncer {
navController.navigate(NavigationRoute.CreateGroupChat.route)
}
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Image(
}
// 搜索栏//
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.height(40.dp)
.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), painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = null, contentDescription = "search",
tint = AppColors.inputHint modifier = Modifier
.size(24.dp)
.noRippleClickable {
// TODO: 实现搜索功能
},
colorFilter = ColorFilter.tint(AppColors.text)
) )
Spacer(modifier = Modifier.width(16.dp))
Box { Box {
androidx.compose.material.Text( Image(
text = stringResource(R.string.search), painter = painterResource(id = R.drawable.rider_pro_notification),
modifier = Modifier.padding(start = 8.dp), contentDescription = "notifications",
color = AppColors.inputHint, modifier = Modifier
fontSize = 17.sp .size(24.dp)
.noRippleClickable {
// TODO: 实现通知功能
},
colorFilter = ColorFilter.tint(AppColors.text)
) )
}
}
}
Row( // 通知红点
modifier = Modifier val totalNoticeCount = MessageListViewModel.likeNoticeCount +
.fillMaxWidth() MessageListViewModel.followNoticeCount +
.padding(horizontal = 16.dp), MessageListViewModel.commentNoticeCount +
horizontalArrangement = Arrangement.SpaceBetween, MessageListViewModel.favouriteNoticeCount
) {
val likeDebouncer = rememberDebouncer()
val followDebouncer = rememberDebouncer()
val commentDebouncer = rememberDebouncer()
NotificationIndicator( if (totalNoticeCount > 0) {
MessageListViewModel.likeNoticeCount, Box(
R.mipmap.rider_pro_like, modifier = Modifier
stringResource(R.string.like_upper), .size(8.dp)
Color(0xFFFAFD5D) .background(
) { color = Color(0xFFFF3B30),
likeDebouncer { shape = CircleShape
if (MessageListViewModel.likeNoticeCount > 0) { )
// 刷新点赞消息列表 .align(Alignment.TopEnd)
LikeNoticeViewModel.isFirstLoad = true .offset(x = 8.dp, y = (-4).dp)
// 清除点赞消息数量 )
MessageListViewModel.clearLikeNoticeCount()
} }
navController.navigate(NavigationRoute.Likes.route)
} }
} }
NotificationIndicator(
MessageListViewModel.followNoticeCount, //创建群聊//
R.mipmap.rider_pro_followers, // Image(
stringResource(R.string.followers_upper), // painter = painterResource(id = R.drawable.rider_pro_group),
Color(0xFFF470FE) // contentDescription = "add",
) { // modifier = Modifier
followDebouncer { // .size(24.dp)
if (MessageListViewModel.followNoticeCount > 0) { // .noRippleClickable {
// 刷新关注消息列表 // debouncer {
FollowerNoticeViewModel.isFirstLoad = true // navController.navigate(NavigationRoute.CreateGroupChat.route)
MessageListViewModel.clearFollowNoticeCount() // }
} // },
navController.navigate(NavigationRoute.Followers.route) // colorFilter = ColorFilter.tint(AppColors.text)
} // )
}
NotificationIndicator(
MessageListViewModel.commentNoticeCount, // // 搜索栏//
R.mipmap.rider_pro_comment, // Box(
stringResource(R.string.comment).uppercase(), // modifier = Modifier
Color(0xFF6246FF) // .fillMaxWidth()
) { // .padding(horizontal = 16.dp, vertical = 8.dp)
commentDebouncer { // .height(40.dp)
navController.navigate(NavigationRoute.CommentNoticeScreen.route) //
} // .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 {
// androidx.compose.material.Text(
// text = stringResource(R.string.search),
// modifier = Modifier.padding(start = 8.dp),
// color = AppColors.inputHint,
// fontSize = 17.sp
// )
// }
// }
// }
//赞、粉丝、评论//
// 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
) { ) {

View File

@@ -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
) )
} }
} }

View File

@@ -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 // 使用解析后的发送者信息
) )
} }
} }

View File

@@ -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
) )
} }
} }

View File

@@ -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个tabExplore、Dynamic、Hot // 游客模式下显示4个tabWorldwide、Hot、News、Recommend非游客模式显示5个tabWorldwide、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,153 +92,108 @@ 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( //原探索//
// Column(
// modifier = Modifier
// .noRippleClickable {
// scope.launch {
// pagerState.animateScrollToPage(0)
// }
// }.padding(start = 16.dp),
// verticalArrangement = Arrangement.Center,
// horizontalAlignment = Alignment.CenterHorizontally
//
// ) {
// Text(
// text = stringResource(R.string.index_worldwide),
// fontSize = if (pagerState.currentPage == 0)18.sp else 16.sp,
// color = if (pagerState.currentPage == 0) AppColors.text else AppColors.nonActiveText,
// fontWeight = FontWeight.W600)
// Spacer(modifier = Modifier.height(4.dp))
//
// Image(
// painter = painterResource(
// if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
// else R.drawable.tab_indicator_unselected
// ),
// contentDescription = "tab indicator",
// modifier = Modifier
// .width(34.dp)
// .height(4.dp)
// )
//
// }
// Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(R.string.moment),
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
modifier = Modifier modifier = Modifier
.noRippleClickable { .align(Alignment.CenterVertically)
scope.launch { )
pagerState.animateScrollToPage(0)
}
}.padding(start = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) { Spacer(modifier = Modifier.weight(1f))
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( Image(
painter = painterResource( painter = painterResource(id = R.drawable.rider_pro_nav_search),
if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected contentDescription = "search",
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier modifier = Modifier
.size(24.dp)
.noRippleClickable { .noRippleClickable {
scope.launch { navController.navigate(NavigationRoute.Search.route)
pagerState.animateScrollToPage(1)
}
}, },
verticalArrangement = Arrangement.Center, colorFilter = ColorFilter.tint(AppColors.text)
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( // Spacer(modifier = Modifier.height(23.dp))
painter = painterResource(
if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
)
} val tabDebouncer = rememberDebouncer()
// 只有非游客用户才显示"关注"tab // 创建tab数据列表
if (!AppStore.isGuest) { val tabs = if (AppStore.isGuest) {
//关注tab listOf(
Spacer(modifier = Modifier.width(16.dp)) TabData(stringResource(R.string.index_worldwide), 0),
Column( TabData(stringResource(R.string.index_hot), 1),
modifier = Modifier TabData(stringResource(R.string.index_news), 2),
.noRippleClickable { 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 { scope.launch {
pagerState.animateScrollToPage(2) pagerState.animateScrollToPage(tab.index)
} }
},
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 {
navController.navigate(NavigationRoute.Search.route)
} }
}
) )
} }
} }
@@ -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)
)
}
}

View File

@@ -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 ->

View File

@@ -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)
)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
)
}
}
)
}
}

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
) )

View File

@@ -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)

View File

@@ -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()

View 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 -> "[消息]"
}
}
}

View File

@@ -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)

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" }