42 Commits

Author SHA1 Message Date
2f2da0a159 lottie 依赖 2025-10-17 15:27:12 +08:00
4ffaf3c3a8 Merge pull request #38 from Kevinlinpr/zhong
编辑资料页面UI调整:添加横幅图片区域
2025-10-16 18:10:11 +08:00
29490d288b “我的”页面UI调整 2025-10-16 18:06:24 +08:00
a99ab30c4e 编辑资料页面UI调整:添加横幅图片区域 2025-10-15 18:54:04 +08:00
0442925ae9 Merge pull request #37 from Kevinlinpr/zhong
解决问题:首页智能体头像为默认头像时显示为空
2025-10-14 18:20:22 +08:00
9d3d13a22d 新增消息界面“全部”标签页 2025-10-14 17:40:25 +08:00
f0a9704e2d 个人信息页和用户信息页UI调整 2025-10-13 18:49:47 +08:00
7f2c103ada 修改首页智能体头像显示逻辑,先显示默认头像(所有情况都显示)如果有网络头像则覆盖显示。 2025-10-11 18:55:39 +08:00
bd01ae39d0 Merge pull request #36 from Kevinlinpr/zhong
UI调整
2025-10-10 21:42:24 +08:00
d94e3b5c20 消息界面通知按钮点击事件;新增通知界面 2025-10-10 18:41:57 +08:00
44cc76d2e3 日文资源文件
实现重新加载功能
收藏界面UI调整
2025-10-09 17:36:06 +08:00
fac6f23356 新建文件夹 app/src/main/assets 将.lottie文件放入
UI调整
2025-09-30 18:54:35 +08:00
39928abc46 Merge pull request #35 from Zhong202501/main
缺省图
2025-09-30 15:53:37 +08:00
4d0d7004b0 缺省图 2025-09-29 18:29:59 +08:00
28a6e3fef3 Merge pull request #34 from Zhong202501/main
添加启动界面
2025-09-28 18:50:51 +08:00
b275d88ef7 添加启动界面 2025-09-28 18:24:54 +08:00
595ef7f942 Merge pull request #33 from Zhong202501/main
Agent创建成功全局显示; 适配暗黑模式
2025-09-27 20:11:44 +08:00
1202b55c74 text颜色 2025-09-26 18:50:51 +08:00
074009d256 添加界面状态恢复逻辑 2025-09-26 18:31:27 +08:00
ad1de9e3f7 Agent创建成功全局显示;
适配暗黑模式
2025-09-26 17:01:46 +08:00
359bcfdfd7 Agent创建成功全局显示;
适配暗黑模式
2025-09-26 17:00:12 +08:00
1901fddb2e Merge pull request #32 from Zhong202501/main
创建弹窗UI调整
2025-09-26 11:29:03 +08:00
b96ae94bdb 界面逻辑优化 2025-09-25 18:32:34 +08:00
dedd356896 创建弹窗UI调整 2025-09-24 18:51:20 +08:00
bb9fda75ae Merge pull request #31 from Zhong202501/main
AI美化功能; 输入框逻辑优化; 文本资源文件;
2025-09-24 14:35:08 +08:00
ea911f113b AI美化功能 2025-09-23 18:32:01 +08:00
88f379fe5b Merge pull request #30 from Kevinlinpr/agent_scroll
优化AI界面,添加分页加载功能,支持动态加载更多智能体数据;重构UI布局
2025-09-23 13:43:55 +08:00
1a24136c35 优化AI界面,添加分页加载功能,支持动态加载更多智能体数据;重构UI布局 2025-09-23 11:57:11 +08:00
742410223c Merge pull request #29 from Zhong202501/main
手动创建AI界面调整
2025-09-23 10:37:15 +08:00
bd5aff7564 手动创建AI界面调整 2025-09-22 17:57:39 +08:00
b43c1585c4 Merge pull request #28 from Zhong202501/main
创建AI界面UI兼容;动态页面调整
2025-09-22 10:48:57 +08:00
cb582393f1 手动创造AI界面;调整输入框点击区域;创建AI时的三点彩色动画 2025-09-19 18:45:10 +08:00
a200d00587 UI调整 2025-09-18 18:19:19 +08:00
6d2133545f 动态详情页面评论调整 2025-09-18 18:16:54 +08:00
2aad126010 创建AI界面UI兼容;动态页面调整 2025-09-17 18:41:15 +08:00
b7b777d2d0 Merge pull request #26 from Zhong202501/new
首页底部导航栏图标;创建AI界面
2025-09-17 11:03:17 +08:00
e804c8be0c Merge pull request #20 from Kevinlinpr/new-bottom-create-button
Feat: Add Create Bottom Sheet and icons
2025-09-17 10:44:34 +08:00
228a74695e Merge pull request #22 from Zhong202501/main
添加Category接口
2025-09-17 10:39:22 +08:00
41a51b85da 首页底部导航栏图标;创建AI界面 2025-09-16 18:18:36 +08:00
e74e8615a5 Agent卡片组件UI;Agent聊天界面输入框显示问题 2025-09-15 14:06:05 +08:00
349d39daf2 修复BUG:我的界面右上角图标会跟随背景图一起向上滑走 2025-09-12 18:24:49 +08:00
8154a0ddc4 Category接口;Agent卡片组件背景颜色 2025-09-11 18:14:54 +08:00
215 changed files with 3957 additions and 4103 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-09T09:51:06.656104400Z"> <DropdownSelection timestamp="2025-09-17T06:25:35.585100400Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=192.168.0.227:45035;connection=094cb92e" /> <DeviceId pluginId="Default" identifier="serial=192.168.0.216:5555;connection=698a7727" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@@ -125,7 +125,7 @@ dependencies {
// 添加 lifecycle-runtime-ktx 依赖 // 添加 lifecycle-runtime-ktx 依赖
implementation(libs.androidx.lifecycle.runtime.ktx.v262) implementation(libs.androidx.lifecycle.runtime.ktx.v262)
implementation (libs.eventbus) implementation (libs.eventbus)
implementation(libs.lottie)
} }

Binary file not shown.

View File

@@ -45,6 +45,7 @@ object AppState {
var googleClientId: String? = null var googleClientId: String? = null
var enableGoogleLogin: Boolean = false var enableGoogleLogin: Boolean = false
var enableChat = false var enableChat = false
var agentCreatedSuccess by mutableStateOf(false)
suspend fun initWithAccount(scope: CoroutineScope, context: Context) { suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
// 如果是游客模式,使用简化的初始化流程 // 如果是游客模式,使用简化的初始化流程
if (AppStore.isGuest) { if (AppStore.isGuest) {

View File

@@ -4,12 +4,11 @@ object ConstVars {
// api 地址 - 根据构建类型自动选择 // api 地址 - 根据构建类型自动选择
// Debug: http://192.168.0.201:8088 // Debug: http://192.168.0.201:8088
// Release: https://rider-pro.aiosman.com/beta_api // Release: https://rider-pro.aiosman.com/beta_api
// val BASE_SERVER = if (!BuildConfig.DEBUG) { val BASE_SERVER = if (BuildConfig.DEBUG) {
// "http://47.109.137.67:6363" // Debug环境 "http://47.109.137.67:6363" // Debug环境
// } else { } else {
// "https://rider-pro.aiosman.com/beta_api" // Release环境 "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

@@ -43,7 +43,12 @@ import com.google.firebase.analytics.analytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.aiosman.ravenow.ui.splash.SplashScreen
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
// Firebase Analytics // Firebase Analytics
@@ -122,11 +127,22 @@ class MainActivity : ComponentActivity() {
} }
setContent { setContent {
var showSplash by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
kotlinx.coroutines.delay(2000)
showSplash = false
}
if (showSplash) {
SplashScreen()
} else {
CompositionLocalProvider( CompositionLocalProvider(
LocalAppTheme provides AppState.appTheme LocalAppTheme provides AppState.appTheme
) { ) {
CheckUpdateDialog() CheckUpdateDialog()
Navigation(startDestination) { navController -> Navigation(startDestination) { navController ->
// 处理带有 postId 的通知点击 // 处理带有 postId 的通知点击
val postId = intent.getStringExtra("POST_ID") val postId = intent.getStringExtra("POST_ID")
var commentId = intent.getStringExtra("COMMENT_ID") var commentId = intent.getStringExtra("COMMENT_ID")
@@ -179,17 +195,15 @@ class MainActivity : ComponentActivity() {
} }
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() }) NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
navController.navigate(NavigationRoute.NewPost.route) navController.navigate(NavigationRoute.NewPost.route)
}
}
}
}
} }
} }
} }
}
}
}
/** /**
* 请求通知权限 * 请求通知权限

View File

@@ -1,49 +0,0 @@
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,28 +32,6 @@ 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(
@@ -82,17 +60,6 @@ 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,11 +13,9 @@ 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 {
@@ -25,9 +23,6 @@ fun getSafeOkHttpClient(
addInterceptor(it) addInterceptor(it)
} }
} }
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
.build() .build()
} }
@@ -61,7 +56,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(timeoutSeconds = 30)) .client(getSafeOkHttpClient())
.build() .build()
.create(RaveNowAPI::class.java) .create(RaveNowAPI::class.java)
@@ -80,10 +75,7 @@ object ApiClient {
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(), timeoutSeconds = 30) getSafeOkHttpClient(authInterceptor = AuthInterceptor())
}
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()
@@ -92,19 +84,9 @@ 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,7 +6,6 @@ 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
@@ -43,20 +42,6 @@ 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,
@@ -286,6 +271,44 @@ data class RemoveAccountRequestBody(
val password: String, val password: String,
) )
data class CategoryTemplate(
@SerializedName("id")
val id: Int,
@SerializedName("name")
val name: String,
@SerializedName("description")
val description: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("parentId")
val parentId: Int?,
@SerializedName("parent")
val parent: CategoryTemplate?,
@SerializedName("children")
val children: List<CategoryTemplate>?,
@SerializedName("sort")
val sort: Int,
@SerializedName("isActive")
val isActive: Boolean,
@SerializedName("promptCount")
val promptCount: Int?,
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String
)
data class CategoryListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Int,
@SerializedName("list")
val list: List<CategoryTemplate>
)
interface RaveNowAPI { interface RaveNowAPI {
@GET("membership/config") @GET("membership/config")
@retrofit2.http.Headers("X-Requires-Auth: true") @retrofit2.http.Headers("X-Requires-Auth: true")
@@ -317,7 +340,6 @@ 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,
@@ -325,15 +347,6 @@ 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
@@ -571,39 +584,20 @@ 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("order") order: String? = null, @Query("withWorkflow") withWorkflow: Int = 1,
@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("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: String = "1", @Query("withWorkflow") withWorkflow: Int = 1,
): Response<ListContainer<Agent>> ): Response<ListContainer<Agent>>
@Multipart @Multipart
@@ -640,7 +634,6 @@ 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")
@@ -651,15 +644,38 @@ 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") @GET("outside/categories")
suspend fun getCategories( suspend fun getCategories(
@Query("page") page: Int = 1, @Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int = 20, @Query("pageSize") pageSize: Int? = null,
@Query("lang") lang: String? = null @Query("parentId") parentId: Int? = null,
): Response<ListContainer<Category>> @Query("isActive") isActive: Boolean? = null,
@Query("name") name: String? = null,
@Query("withChildren") withChildren: Boolean? = null,
@Query("withParent") withParent: Boolean? = null,
@Query("withCount") withCount: Boolean? = null,
@Query("hideEmpty") hideEmpty: Boolean? = null
): Response<DataContainer<CategoryListResponse>>
@GET("outside/categories/tree")
suspend fun getCategoryTree(
@Query("withCount") withCount: Boolean? = null,
@Query("hideEmpty") hideEmpty: Boolean? = null
): Response<DataContainer<List<CategoryTemplate>>>
@GET("outside/categories/{id}")
suspend fun getCategoryById(
@Path("id") id: Int
): Response<DataContainer<CategoryTemplate>>
@GET("outside/prompts")
suspend fun getPromptsByCategory(
@Query("categoryIds") categoryIds: List<Int>? = null,
@Query("categoryName") categoryName: String? = null,
@Query("uncategorized") uncategorized: String? = null,
@Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null
): Response<ListContainer<Agent>>
} }

View File

@@ -1,160 +0,0 @@
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,36 +299,12 @@ 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(
@@ -341,10 +317,7 @@ 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,13 +39,13 @@ 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
import com.aiosman.ravenow.ui.chat.GroupChatScreen import com.aiosman.ravenow.ui.chat.GroupChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen import com.aiosman.ravenow.ui.comment.CommentsScreen
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
import com.aiosman.ravenow.ui.composables.AgentCreatedSuccessIndicator
import com.aiosman.ravenow.ui.crop.ImageCropScreen import com.aiosman.ravenow.ui.crop.ImageCropScreen
import com.aiosman.ravenow.ui.favourite.FavouriteListPage import com.aiosman.ravenow.ui.favourite.FavouriteListPage
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen
@@ -71,6 +71,7 @@ import com.aiosman.ravenow.ui.post.NewPostScreen
import com.aiosman.ravenow.ui.post.PostScreen import com.aiosman.ravenow.ui.post.PostScreen
import com.aiosman.ravenow.ui.profile.AccountProfileV2 import com.aiosman.ravenow.ui.profile.AccountProfileV2
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
import com.aiosman.ravenow.ui.notification.NotificationScreen
sealed class NavigationRoute( sealed class NavigationRoute(
val route: String, val route: String,
@@ -116,6 +117,7 @@ sealed class NavigationRoute(
data object GroupInfo : NavigationRoute("GroupInfo/{id}") data object GroupInfo : NavigationRoute("GroupInfo/{id}")
data object VipSelPage : NavigationRoute("VipSelPage") data object VipSelPage : NavigationRoute("VipSelPage")
data object RemoveAccountScreen: NavigationRoute("RemoveAccount") data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
data object NotificationScreen : NavigationRoute("NotificationScreen")
} }
@@ -545,7 +547,7 @@ fun NavigationController(
composable( composable(
route = NavigationRoute.AddAgent.route, route = NavigationRoute.AddAgent.route,
) { ) {
CreateAgentV2Screen() AddAgentScreen()
} }
composable( composable(
@@ -591,6 +593,13 @@ fun NavigationController(
} }
} }
composable(route = NavigationRoute.NotificationScreen.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
NotificationScreen()
}
}
} }
@@ -616,6 +625,7 @@ fun Navigation(
navController = navController, navController = navController,
startDestination = startDestination startDestination = startDestination
) )
AgentCreatedSuccessIndicator()
} }
} }
} }

View File

@@ -1,5 +1,8 @@
package com.aiosman.ravenow.ui.account package com.aiosman.ravenow.ui.account
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -47,12 +50,24 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.util.Log import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import java.io.File
/** /**
* 编辑用户资料界面 * 编辑用户资料界面
*/ */
@Composable @Composable
fun AccountEditScreen2() { fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,) {
val model = AccountEditViewModel val model = AccountEditViewModel
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
@@ -61,6 +76,19 @@ fun AccountEditScreen2() {
// 防抖导航器 // 防抖导航器
val debouncedNavigation = rememberDebouncedNavigation() val debouncedNavigation = rememberDebouncedNavigation()
// 添加图片选择启动器
val scope = rememberCoroutineScope()
val pickBannerImageLauncher = pickupAndCompressLauncher(
context,
scope,
maxSize = ConstVars.BANNER_IMAGE_MAX_SIZE,
quality = 100
) { uri, file ->
// 处理选中的图片
onUpdateBanner?.invoke(uri, file, context)
}
fun onNicknameChange(value: String) { fun onNicknameChange(value: String) {
// 去除换行符,确保昵称不包含换行 // 去除换行符,确保昵称不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "") val cleanValue = value.replace("\n", "").replace("\r", "")
@@ -152,7 +180,60 @@ fun AccountEditScreen2() {
) )
} }
} }
Spacer(modifier = Modifier.height(44.dp))
// 添加横幅图片区域
val banner = model.profile?.banner
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.clip(RoundedCornerShape(12.dp))
) {
if (banner != null) {
CustomAsyncImage(
context = LocalContext.current,
imageUrl = banner,
modifier = Modifier.fillMaxSize(),
contentDescription = "Banner",
contentScale = ContentScale.Crop
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.1f))
)
}
Box(
modifier = Modifier
.width(120.dp)
.height(42.dp)
.align(Alignment.BottomEnd)
.padding(end = 12.dp, bottom = 12.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(9.dp)
)
.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
){
Text(
text = "change",
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// 显示内容或加载状态 // 显示内容或加载状态
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}") Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
@@ -180,7 +261,15 @@ fun AccountEditScreen2() {
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
.clip(CircleShape) .clip(CircleShape)
.background(appColors.main) .background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0x997c68ef),
Color(0xFF7bd8f8)
)
),
)
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.debouncedClickable( .debouncedClickable(
debounceTime = 800L debounceTime = 800L
@@ -198,7 +287,7 @@ fun AccountEditScreen2() {
) )
} }
} }
Spacer(modifier = Modifier.height(58.dp)) Spacer(modifier = Modifier.height(18.dp))
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)

View File

@@ -29,10 +29,12 @@ import androidx.activity.compose.BackHandler
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -61,7 +63,28 @@ import com.aiosman.ravenow.ui.composables.form.FormTextInput2
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.foundation.border
import androidx.compose.ui.draw.shadow
import com.aiosman.ravenow.AppState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.StartOffset
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.text.TextStyle
import com.aiosman.ravenow.ui.agent.AddAgentViewModel.showManualCreation
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
/** /**
* 添加智能体界面 * 添加智能体界面
*/ */
@@ -73,8 +96,12 @@ fun AddAgentScreen() {
var agnetNameError by remember { mutableStateOf<String?>(null) } var agnetNameError by remember { mutableStateOf<String?>(null) }
var agnetDescError by remember { mutableStateOf<String?>(null) } var agnetDescError by remember { mutableStateOf<String?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
var isProcessing by remember { mutableStateOf(false) }
var showWaveAnimation by remember { mutableStateOf(false) }
var isCreatingAgent by remember { mutableStateOf(false) } // 控制是否处于创建状态
var showManualCreationForm by remember { mutableStateOf(false) } // 控制是否显示手动创建表单
var tempDesc by remember { mutableStateOf("") } // 独立的临时描述变量
val keyboardController = LocalSoftwareKeyboardController.current
fun onNameChange(value: String) { fun onNameChange(value: String) {
model.name = value.trim() model.name = value.trim()
@@ -88,15 +115,37 @@ fun AddAgentScreen() {
fun onDescChange(value: String) { fun onDescChange(value: String) {
model.desc = value.trim() model.desc = value.trim()
agnetDescError = when { agnetDescError = when {
value.length > 100 -> "简介长度不能大于100" value.length > 512 -> "简介长度不能大于512"
else -> null
}
}
fun onTempDescChange(value: String) {
tempDesc = value.trim()
agnetDescError = when {
value.length > 512 -> "简介长度不能大于512"
else -> null else -> null
} }
} }
fun validate(): Boolean { fun validate(): Boolean {
return agnetNameError == null && agnetDescError == null return agnetNameError == null && agnetDescError == null
} }
// AI文案优化
suspend fun optimizeTextWithAI(content: String): String? {
return try {
val sessionId = ""
val response = com.aiosman.ravenow.data.api.ApiClient.api.agentMoment(
com.aiosman.ravenow.data.api.AgentMomentRequestBody(
generateText = content,
sessionId = sessionId
)
)
response.body()?.data
} catch (e: Exception) {
e.printStackTrace()
null
}
}
// 处理系统返回键 // 处理系统返回键
BackHandler { BackHandler {
@@ -110,7 +159,22 @@ fun AddAgentScreen() {
// 页面进入时重置头像选择状态 // 页面进入时重置头像选择状态
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.isSelectingAvatar = false model.isSelectingAvatar = false
// 根据标记恢复相应的状态
if (model.isAutoModeManualForm) {
// 恢复自动模式下的手动表单状态
showManualCreationForm = model.showManualCreationForm
isCreatingAgent = model.isCreatingAgent
showWaveAnimation = model.showWaveAnimation
showManualCreation = model.showManualCreation
} else {
// 恢复手动模式下的状态
showManualCreation = model.showManualCreation
showManualCreationForm = model.showManualCreationForm
isCreatingAgent = model.isCreatingAgent
showWaveAnimation = model.showWaveAnimation
} }
}
Column( Column(
modifier = Modifier modifier = Modifier
@@ -118,9 +182,12 @@ fun AddAgentScreen() {
.background(color = appColors.decentBackground), .background(color = appColors.decentBackground),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
var showManualCreation by remember {
mutableStateOf(model.showManualCreation)
}
StatusBarSpacer() StatusBarSpacer()
Box( Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 14.dp, vertical = 16.dp)
.background(color = appColors.decentBackground) .background(color = appColors.decentBackground)
) { ) {
// 自定义header控制返回按钮行为 // 自定义header控制返回按钮行为
@@ -148,80 +215,298 @@ fun AddAgentScreen() {
stringResource(R.string.agent_add), stringResource(R.string.agent_add),
fontWeight = FontWeight.W600, fontWeight = FontWeight.W600,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
fontSize = 17.sp, fontSize = 17.sp,
color = appColors.text color = appColors.text
) )
Spacer(modifier = Modifier.size(12.dp)) }
Icon( }
Spacer(modifier = Modifier.height(1.dp))
if (!isCreatingAgent) {
Column(
modifier = Modifier modifier = Modifier
.size(24.dp) .fillMaxWidth()
.noRippleClickable { .height(50.dp)
// 提交创建智能体的逻辑可以在这里实现 .padding(horizontal = 20.dp),
},
imageVector = Icons.Default.Check,
contentDescription = "Add",
tint = appColors.text
)
}
}
Spacer(modifier = Modifier.height(44.dp))
Box(
modifier = Modifier.size(88.dp),
contentAlignment = Alignment.Center
) { ) {
CustomAsyncImage( Image(
context, painter = painterResource(id = R.mipmap.group_copy),
model.croppedBitmap,
modifier = Modifier
.size(88.dp)
.clip(
RoundedCornerShape(88.dp)
),
contentDescription = "", contentDescription = "",
contentScale = ContentScale.Crop, modifier = Modifier
placeholderRes = R.mipmap.rider_pro_agent_avatar .size(48.dp)
.clip(
RoundedCornerShape(48.dp)
),
contentScale = ContentScale.Crop
) )
}
}
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier.fillMaxWidth()
.padding(start = 20.dp)
) {
Text(
text = "${AppState.profile?.nickName ?: "User"} ${stringResource(R.string.welcome_1)}",
fontSize = 16.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
}
if (!isCreatingAgent) {
Spacer(modifier = Modifier.height(8.dp))
Column(
modifier = Modifier.fillMaxWidth()
.padding(start = 20.dp)
) {
if (!showManualCreation) {
Text(
text = stringResource(R.string.welcome_2),
fontSize = 14.sp,
color = appColors.text.copy(alpha = 0.6f),
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
if (!showManualCreation) {
//自动创造AI界面
Column(
modifier = Modifier
.padding(horizontal = 20.dp)
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(32.dp) .fillMaxWidth()
.clip(CircleShape) .height(95.dp)
.background(appColors.main) .shadow(
.align(Alignment.BottomEnd) elevation = 10.dp,
shape = RoundedCornerShape(10.dp),
spotColor = Color(0x33F563FF),
ambientColor = Color(0x99F563FF),
clip = false
)
.background(
brush = Brush.linearGradient(
listOf(
Color(0xFF6246FF),
Color(0xFF7C45ED)
)
),
shape = RoundedCornerShape(10.dp)
)
.padding(0.5.dp)
.background(
color = appColors.inputBackground2,
shape = RoundedCornerShape(10.dp)
)
) {
val focusRequester = remember { FocusRequester() }
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable { .noRippleClickable {
model.viewModelScope.launch {
focusRequester.requestFocus()
keyboardController?.show()
}
}
)
TextField(
value = tempDesc,
onValueChange = { value -> onTempDescChange(value) },
modifier = Modifier
.fillMaxWidth()
.height(95.dp)
.focusRequester(focusRequester),
placeholder = {
Text(
text = stringResource(R.string.agent_desc_hint_auto),
color = Color.Gray
)
},
textStyle = TextStyle(
color = LocalAppTheme.current.text,
fontSize = 16.sp
),
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
focusedContainerColor = appColors.inputBackground2,
unfocusedContainerColor = appColors.inputBackground2,
),
shape = RoundedCornerShape(10.dp),
supportingText = null,
trailingIcon = null,
leadingIcon = null
)
Row(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 12.dp, bottom = 12.dp)
.noRippleClickable {
// 只有在有内容且未处理中且未创建中时才可点击
if (tempDesc.isNotEmpty() && !isProcessing && !isCreatingAgent) {
isProcessing = true
showWaveAnimation = true // 显示构思动画
keyboardController?.hide()
model.viewModelScope.launch {
try {
val optimizedText = optimizeTextWithAI(tempDesc)
if (optimizedText != null) {
onTempDescChange(optimizedText)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
isProcessing = false
showWaveAnimation = false
isCreatingAgent = true
showManualCreationForm = true
onDescChange(tempDesc)
}
}
}
}
.then(
if (tempDesc.isEmpty() || isProcessing || isCreatingAgent) {
Modifier.alpha(0.5f)
} else {
Modifier
}
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_info_magic),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = Color.Unspecified
)
Spacer(modifier = Modifier.width(5.dp))
Text(
text = stringResource(R.string.agent_text_beautify),
color = Color(0xFF6246FF),
fontSize = 14.sp
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
if ((isCreatingAgent && showWaveAnimation) || (isProcessing && showWaveAnimation)) {
// 显示构思动画
Row(
modifier = Modifier
.align(Alignment.Start)
.padding(start = 20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.size(32.dp)
) {
LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset("loading.lottie")).value,
iterations = LottieConstants.IterateForever,
modifier = Modifier.matchParentSize()
)
}
Text(
text = stringResource(R.string.ideaing),
color = appColors.text.copy(alpha = 0.6f),
fontSize = 14.sp
)
}
} else if (isCreatingAgent && !showWaveAnimation && showManualCreationForm) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.align(Alignment.Start)
) {
Text(
text = stringResource(R.string.avatar),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x777c45ed),
Color(0x777c68ef),
Color(0x557bd8f8)
)
)
)
.align(Alignment.Start)
.noRippleClickable {
// 保存当前状态
model.showManualCreationForm = showManualCreationForm
model.isCreatingAgent = isCreatingAgent
model.showWaveAnimation = showWaveAnimation
model.showManualCreation = showManualCreation
model.isAutoModeManualForm = true // 标记为自动模式下的手动表单
// 设置正在选择头像的标志 // 设置正在选择头像的标志
model.isSelectingAvatar = true model.isSelectingAvatar = true
navController.navigate(NavigationRoute.AgentImageCrop.route) navController.navigate(NavigationRoute.AgentImageCrop.route)
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ){
// 如果已有裁剪后的头像,则显示头像,否则显示编辑图标
if (model.croppedBitmap != null) {
Image(
bitmap = model.croppedBitmap!!.asImageBitmap(),
contentDescription = "Avatar",
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} else {
Icon( Icon(
Icons.Default.Add, painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = "Add", contentDescription = "Edit",
tint = Color.White, tint = Color.White,
modifier = Modifier.size(20.dp),
) )
} }
} }
Spacer(modifier = Modifier.height(58.dp)) Spacer(modifier = Modifier.height(18.dp))
Column( // 原版两个输入框
modifier = Modifier Text(
.padding(horizontal = 16.dp) text = stringResource(R.string.agent_name),
) { fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput( FormTextInput(
value = model.name, value = model.name,
label = stringResource(R.string.agent_name), hint = stringResource(R.string.agent_name_hint_1),
hint = stringResource(R.string.agent_name_hint),
background = appColors.inputBackground2, background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { value -> ) { value ->
onNameChange(value) onNameChange(value)
} }
// Spacer(modifier = Modifier.height(16.dp)) Text(
text = stringResource(R.string.agent_desc),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput2( FormTextInput2(
value = model.desc, value = model.desc,
label = stringResource(R.string.agent_desc),
hint = stringResource(R.string.agent_desc_hint), hint = stringResource(R.string.agent_desc_hint),
background = appColors.inputBackground2, background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -229,38 +514,243 @@ fun AddAgentScreen() {
onDescChange(value) onDescChange(value)
} }
} }
Spacer(modifier = Modifier.height(58.dp)) } else if (!isCreatingAgent && !showWaveAnimation) {
Box(
modifier = Modifier
.align(Alignment.Start)
.padding(start = 20.dp)
.width(136.dp)
.height(40.dp)
.border(
width = 1.dp,
color = Color(0x33858B98),
shape = RoundedCornerShape(12.dp)
)
.background(
color = appColors.background,
shape = RoundedCornerShape(12.dp),
)
.noRippleClickable {
showManualCreation = true
tempDesc = ""
agnetDescError = null
}
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 18.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = null,
tint = appColors.text,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.create_agent_hand),
color = appColors.text,
fontWeight = FontWeight.W600,
fontSize = 14.sp
)
}
}
}
}else {
//手动创造AI界面
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.align(Alignment.Start)
) {
Box(
modifier = Modifier
.align(Alignment.Start)
.width(140.dp)
.height(40.dp)
.shadow(
elevation = 10.dp,
shape = RoundedCornerShape(10.dp),
spotColor = Color(0x33F563FF),
ambientColor = Color(0x99F563FF),
clip = false
)
.background(
brush = Brush.linearGradient(
listOf(
Color(0xFF6246FF),
Color(0xFF7C45ED)
)
),
shape = RoundedCornerShape(10.dp)
)
.padding(0.5.dp)
.background(
color = appColors.background,
shape = RoundedCornerShape(10.dp),
)
.noRippleClickable {
showManualCreation = false
model.name = ""
model.desc = ""
model.croppedBitmap = null
isCreatingAgent = false
showManualCreationForm = false
showWaveAnimation = false
isProcessing = false
}
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_info_magic),
contentDescription = null,
tint = appColors.text,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.create_agent_auto),
color = appColors.text,
fontWeight = FontWeight.W600,
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.avatar),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x777c45ed),
Color(0x777c68ef),
Color(0x557bd8f8)
)
)
)
.align(Alignment.Start)
.noRippleClickable {
// 保存当前状态
model.showManualCreation = showManualCreation
model.showManualCreationForm = showManualCreationForm
model.isCreatingAgent = isCreatingAgent
model.showWaveAnimation = showWaveAnimation
model.isAutoModeManualForm = false // 标记为手动模式下的手动表单
// 设置正在选择头像的标志
model.isSelectingAvatar = true
navController.navigate(NavigationRoute.AgentImageCrop.route)
},
contentAlignment = Alignment.Center
) {
// 如果已有裁剪后的头像,则显示头像,否则显示编辑图标
if (model.croppedBitmap != null) {
Image(
bitmap = model.croppedBitmap!!.asImageBitmap(),
contentDescription = "Avatar",
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} else {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = "Edit",
tint = Color.White,
modifier = Modifier.size(20.dp),
)
}
}
}
Spacer(modifier = Modifier.height(18.dp))
// 原版两个输入框
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
) {
Text(
text = stringResource(R.string.agent_name),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput(
value = model.name,
hint = stringResource(R.string.agent_name_hint_1),
background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(),
) { value ->
onNameChange(value)
}
Text(
text = stringResource(R.string.agent_desc),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput2(
value = model.desc,
hint = stringResource(R.string.agent_desc_hint),
background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(),
) { value ->
onDescChange(value)
}
}
//手动创造AI界面
}
// 错误信息显示 // 错误信息显示
Spacer(modifier = Modifier.weight(1f))
Box(modifier = Modifier.fillMaxWidth()) {
errorMessage?.let { error -> errorMessage?.let { error ->
Text( Text(
text = error, text = error,
color = Color.Red, color = Color.Red,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier
.padding(bottom = 20.dp)
.align(Alignment.Center),
fontSize = 14.sp fontSize = 14.sp
) )
Spacer(modifier = Modifier.height(16.dp))
} }
}
ActionButton( ActionButton(
modifier = Modifier modifier = Modifier
.width(345.dp) .width(345.dp)
.padding(horizontal = 16.dp) .padding(bottom = 40.dp)
.background( .background(
brush = Brush.linearGradient( brush = Brush.linearGradient(
colors = listOf( colors = listOf(
Color(0xFFEE2A33), Color(0x777c45ed),
Color(0xFFD80264), Color(0x777c68ef),
Color(0xFF8468BC) Color(0x557bd8f8)
) )
), ),
shape = RoundedCornerShape(24.dp) shape = RoundedCornerShape(24.dp)
), ),
color = Color.White, color = Color.White,
backgroundColor = Color.Transparent, backgroundColor = Color.Transparent,
text = stringResource(R.string.agent_create), text = stringResource(R.string.create_confirm),
isLoading = model.isUpdating, isLoading = model.isUpdating,
loadingText = stringResource(R.string.agent_createing),
enabled = !model.isUpdating && validate() enabled = !model.isUpdating && validate()
) { ) {
// 验证输入 // 验证输入
@@ -268,6 +758,10 @@ fun AddAgentScreen() {
if (validationError != null) { if (validationError != null) {
// 显示验证错误 // 显示验证错误
errorMessage = validationError errorMessage = validationError
model.viewModelScope.launch {
kotlinx.coroutines.delay(3000)
errorMessage = null
}
return@ActionButton return@ActionButton
} }
@@ -282,11 +776,15 @@ fun AddAgentScreen() {
// 创建成功,清空数据并关闭页面 // 创建成功,清空数据并关闭页面
model.clearData() model.clearData()
navController.popBackStack() navController.popBackStack()
AppState.agentCreatedSuccess = true
} }
} catch (e: Exception) { } catch (e: Exception) {
// 显示错误信息 // 显示错误信息
errorMessage = "创建智能体失败: ${e.message}" errorMessage = "创建智能体失败: ${e.message}"
e.printStackTrace() e.printStackTrace()
// 3秒后清除错误信息
kotlinx.coroutines.delay(3000)
errorMessage = null
} }
} }
} }

View File

@@ -24,11 +24,12 @@ object AddAgentViewModel : ViewModel() {
var croppedBitmap by mutableStateOf<Bitmap?>(null) var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false) var isUpdating by mutableStateOf(false)
var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像 var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像
var hasExitedPage by mutableStateOf(false) // 标记是否已经完全退出页面 var showManualCreationForm by mutableStateOf(false)
var isCreatingAgent by mutableStateOf(false)
// 保存AI生成的输入文本避免页面重建时丢失 var showWaveAnimation by mutableStateOf(false)
var generateInputText by mutableStateOf("") var showManualCreation by mutableStateOf(false)
// 添加一个标志来区分两种手动表单状态
var isAutoModeManualForm by mutableStateOf(false)
suspend fun updateAgentAvatar(context: Context) { suspend fun updateAgentAvatar(context: Context) {
croppedBitmap?.let { croppedBitmap?.let {
val file = File(context.cacheDir, "agent_avatar.jpg") val file = File(context.cacheDir, "agent_avatar.jpg")
@@ -74,7 +75,7 @@ object AddAgentViewModel : ViewModel() {
name.length < 2 -> "智能体名称长度不能少于2个字符" name.length < 2 -> "智能体名称长度不能少于2个字符"
name.length > 20 -> "智能体名称长度不能超过20个字符" name.length > 20 -> "智能体名称长度不能超过20个字符"
desc.isEmpty() -> "智能体描述不能为空" desc.isEmpty() -> "智能体描述不能为空"
desc.length > 100 -> "智能体描述长度不能超过100个字符" desc.length > 512 -> "智能体描述长度不能超过512个字符"
else -> null else -> null
} }
} }
@@ -88,7 +89,10 @@ object AddAgentViewModel : ViewModel() {
croppedBitmap = null croppedBitmap = null
isUpdating = false isUpdating = false
isSelectingAvatar = false isSelectingAvatar = false
hasExitedPage = false showManualCreationForm = false
generateInputText = "" isCreatingAgent = false
showWaveAnimation = false
showManualCreation = false
isAutoModeManualForm = false
} }
} }

View File

@@ -1,648 +0,0 @@
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

@@ -1,238 +0,0 @@
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

@@ -283,7 +283,7 @@ fun ChatAiScreen(userId: String) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(AppColors.decentBackground) .background(Color.White)
.padding(paddingValues) .padding(paddingValues)
) { ) {
LazyColumn( LazyColumn(

View File

@@ -40,7 +40,6 @@ 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
@@ -80,7 +79,6 @@ 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 {
@@ -115,7 +113,7 @@ fun CommentModalContent(
onDismissRequest = { onDismissRequest = {
showCommentMenu = false showCommentMenu = false
}, },
containerColor = AppColors.background, containerColor = Color.White,
sheetState = rememberModalBottomSheetState( sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true skipPartiallyExpanded = true
), ),
@@ -144,8 +142,24 @@ 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()
@@ -156,8 +170,7 @@ 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,
fontWeight = FontWeight.Bold, color = Color(0xff666666)
color = AppColors.nonActiveText
) )
OrderSelectionComponent { OrderSelectionComponent {
commentViewModel.order = it commentViewModel.order = it
@@ -192,7 +205,7 @@ fun CommentModalContent(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.background) .background(Color(0xfff7f7f7))
) { ) {
EditCommentBottomModal(replyComment) { EditCommentBottomModal(replyComment) {
commentViewModel.viewModelScope.launch { commentViewModel.viewModelScope.launch {

View File

@@ -47,7 +47,8 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
@Composable @Composable
fun CommentNoticeScreen() { fun CommentNoticeScreen() {
val viewModel = viewModel<CommentNoticeListViewModel>( val viewModel = viewModel<CommentNoticeListViewModel>(
@@ -71,14 +72,47 @@ fun CommentNoticeScreen() {
modifier = Modifier.fillMaxSize().background(color = AppColors.background) modifier = Modifier.fillMaxSize().background(color = AppColors.background)
) { ) {
StatusBarSpacer() StatusBarSpacer()
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.padding(16.dp) .padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) { ) {
NoticeScreenHeader(stringResource(R.string.comment), moreIcon = false) Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
viewModel.initData(context, force = true)
} }
if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) { )
}
}
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -92,7 +126,7 @@ fun CommentNoticeScreen() {
androidx.compose.foundation.Image( androidx.compose.foundation.Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_pl_qs_as_img id =if(AppState.darkMode) R.mipmap.qst_pl_qs_as_img
else R.mipmap.qst_pl_qs_img), else R.mipmap.invalid_name_11),
contentDescription = "No Comment", contentDescription = "No Comment",
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp)
) )

View File

@@ -0,0 +1,75 @@
package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.toolbar.CollapsingToolbarScaffoldScopeInstance.align
import kotlinx.coroutines.delay
@Composable
fun AgentCreatedSuccessIndicator() {
val appColors = LocalAppTheme.current
if (AppState.agentCreatedSuccess) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 70.dp),
contentAlignment = Alignment.BottomCenter
) {
Box(
modifier = Modifier
.width(150.dp)
.height(40.dp)
.background(appColors.text.copy(alpha = 0.5f), shape = RoundedCornerShape(15.dp)),
contentAlignment = Alignment.CenterStart
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
) {
Icon(
painter = painterResource(id = R.mipmap.bars_x_buttons_home_n_copy_2),
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
Spacer(modifier = Modifier.width(7.dp))
Text(
text = stringResource(R.string.create_success),
color = appColors.background,
fontSize = 13.sp
)
}
}
}
LaunchedEffect(Unit) {
delay(3000)
AppState.agentCreatedSuccess = false
}
}
}

View File

@@ -59,10 +59,9 @@ fun EditCommentBottomModal(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val context = LocalContext.current val context = LocalContext.current
// 移除自动聚焦,避免自动弹出键盘 LaunchedEffect(Unit) {
// LaunchedEffect(Unit) { focusRequester.requestFocus()
// focusRequester.requestFocus() }
// }
Column( Column(
modifier = Modifier modifier = Modifier
@@ -70,58 +69,22 @@ fun EditCommentBottomModal(
.background(AppColors.background) .background(AppColors.background)
.padding(horizontal = 16.dp, vertical = 16.dp) .padding(horizontal = 16.dp, vertical = 16.dp)
) { ) {
if (replyComment != null) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(24.dp)
.clip(CircleShape)
) {
CustomAsyncImage(
context,
replyComment.avatar,
modifier = Modifier.fillMaxSize(),
contentDescription = "Avatar",
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
replyComment.name,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = AppColors.text
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
replyComment.comment,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(start = 32.dp),
overflow = TextOverflow.Ellipsis,
color = AppColors.text
)
Spacer(modifier = Modifier.height(16.dp))
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
Box( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
.background(AppColors.inputBackground) .background(Color.Gray.copy(alpha = 0.1f))
.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 modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) { ) {
BasicTextField( BasicTextField(
value = text, value = text,
@@ -133,33 +96,42 @@ fun EditCommentBottomModal(
.weight(1f) .weight(1f)
.focusRequester(focusRequester), .focusRequester(focusRequester),
textStyle = TextStyle( textStyle = TextStyle(
color = AppColors.text, color = Color.Black,
fontWeight = FontWeight.Normal fontWeight = FontWeight.Normal
), ),
minLines = 1 decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
innerTextField()
if (text.isEmpty()) {
Text(
text = if (replyComment == null) "快来互动吧..." else "回复@${replyComment.name}",
color = AppColors.text.copy(alpha = 0.3f), // 30%透明度
) )
Spacer(modifier = Modifier.width(8.dp)) }
Crossfade( }
targetState = text.isNotEmpty(), animationSpec = tween(500), }
label = "" )
) { isNotEmpty -> }
}
Spacer(modifier = Modifier.width(12.dp))
Icon( Icon(
painter = painterResource(id = R.mipmap.rider_pro_moment_post), painter = painterResource(id = R.mipmap.btn),
contentDescription = "Send", contentDescription = "Send",
modifier = Modifier modifier = Modifier
.size(20.dp) .size(40.dp)
.align(Alignment.Top) .padding(top = 13.dp)
.noRippleClickable { .noRippleClickable {
if (text.isNotEmpty()) { if (text.isNotEmpty()) {
onSend(text) onSend(text)
text = "" text = ""
} }
}, },
tint = if (isNotEmpty) Color.Unspecified else AppColors.nonActive tint = Color.Unspecified
) )
}
}
}
} }
Spacer(modifier = Modifier.height(navBarHeight)) Spacer(modifier = Modifier.height(navBarHeight))
} }

View File

@@ -92,16 +92,22 @@ fun MomentCard(
showFollowButton = showFollowButton showFollowButton = showFollowButton
) )
} }
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.noRippleClickable { .noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
navController.navigateToPost( navController.navigateToPost(
momentEntity.id, momentEntity.id,
highlightCommentId = 0, highlightCommentId = 0,
initImagePagerIndex = imageIndex initImagePagerIndex = imageIndex
) )
} }
}
) { ) {
MomentContentGroup( MomentContentGroup(
momentEntity = momentEntity, momentEntity = momentEntity,
@@ -213,7 +219,6 @@ fun MomentPostLocation(location: String) {
text = location, text = location,
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontSize = 12.sp, fontSize = 12.sp,
) )
} }
@@ -238,6 +243,8 @@ fun MomentTopRowGroup(
Row( Row(
modifier = Modifier modifier = Modifier
) { ) {
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
CustomAsyncImage( CustomAsyncImage(
context, context,
momentEntity.avatar, momentEntity.avatar,
@@ -246,12 +253,16 @@ fun MomentTopRowGroup(
.size(40.dp) .size(40.dp)
.clip(RoundedCornerShape(40.dp)) .clip(RoundedCornerShape(40.dp))
.noRippleClickable { .noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
navController.navigate( navController.navigate(
NavigationRoute.AccountProfile.route.replace( NavigationRoute.AccountProfile.route.replace(
"{id}", "{id}",
momentEntity.authorId.toString() momentEntity.authorId.toString()
) )
) )
}
}, },
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
@@ -267,7 +278,19 @@ fun MomentTopRowGroup(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MomentName( MomentName(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f)
.noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
momentEntity.authorId.toString()
)
)
}
},
name = momentEntity.nickname name = momentEntity.nickname
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -416,6 +439,8 @@ fun MomentBottomOperateRowGroup(
momentEntity: MomentEntity, momentEntity: MomentEntity,
imageIndex: Int = 0 imageIndex: Int = 0
) { ) {
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
var showCommentModal by remember { mutableStateOf(false) } var showCommentModal by remember { mutableStateOf(false) }
if (showCommentModal) { if (showCommentModal) {
ModalBottomSheet( ModalBottomSheet(
@@ -451,58 +476,14 @@ fun MomentBottomOperateRowGroup(
.height(56.dp) .height(56.dp)
.padding(start = 16.dp, end = 0.dp) .padding(start = 16.dp, end = 0.dp)
) { ) {
Row( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
MomentOperateBtn(count = momentEntity.likeCount.toString()) {
AnimatedLikeIcon(
modifier = Modifier.size(24.dp),
liked = momentEntity.liked
) {
onLikeClick()
}
}
}
Spacer(modifier = Modifier.width(4.dp))
Box(
modifier = Modifier
.fillMaxHeight()
.noRippleClickable {
onCommentClick()
},
contentAlignment = Alignment.Center
) {
MomentOperateBtn(
icon = R.drawable.rider_pro_comment,
count = momentEntity.commentCount.toString()
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
,
contentAlignment = Alignment.CenterEnd
) {
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
AnimatedFavouriteIcon(
modifier = Modifier.size(24.dp),
isFavourite = momentEntity.isFavorite
) {
onFavoriteClick()
}
}
}
}
if (momentEntity.images.size > 1) { if (momentEntity.images.size > 1) {
Row( Row(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxWidth()
.weight(1f),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -523,8 +504,68 @@ fun MomentBottomOperateRowGroup(
} }
} }
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
contentAlignment = Alignment.CenterStart
) {
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
AnimatedFavouriteIcon(
modifier = Modifier.size(24.dp),
isFavourite = momentEntity.isFavorite
) {
onFavoriteClick()
} }
}
}
Box(
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight()
.noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
onCommentClick()
}
},
contentAlignment = Alignment.CenterEnd
) {
MomentOperateBtn(
icon = R.drawable.rider_pro_comment,
count = momentEntity.commentCount.toString()
)
}
Spacer(modifier = Modifier.width(24.dp))
Box(
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight(),
contentAlignment = Alignment.CenterEnd
) {
MomentOperateBtn(count = momentEntity.likeCount.toString()) {
AnimatedLikeIcon(
modifier = Modifier.size(24.dp),
liked = momentEntity.liked
) {
onLikeClick()
}
}
}
}
}
}
} }

View File

@@ -15,16 +15,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -32,6 +38,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
/** /**
* 水平布局的输入框 * 水平布局的输入框
@@ -47,12 +54,15 @@ fun FormTextInput(
onValueChange: (String) -> Unit onValueChange: (String) -> Unit
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
Column( Column(
modifier = modifier modifier = modifier
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(25.dp))
.background(background ?: AppColors.inputBackground) .background(background ?: AppColors.inputBackground)
.let { .let {
if (error != null) { if (error != null) {
@@ -61,7 +71,11 @@ fun FormTextInput(
it it
} }
} }
.padding(17.dp), .padding(17.dp)
.noRippleClickable {
focusRequester.requestFocus()
keyboardController?.show()
},
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
label?.let { label?.let {
@@ -79,13 +93,27 @@ fun FormTextInput(
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 16.dp) ) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.align(Alignment.TopStart),
tint = AppColors.text.copy(alpha = 0.4f)
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(24.dp))
Box(
modifier = Modifier.weight(1f)
) { ) {
if (value.isEmpty()) { if (value.isEmpty()) {
Text( Text(
text = hint ?: "", text = hint ?: "",
style = TextStyle( style = TextStyle(
fontSize = 16.sp, fontSize = 14.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = AppColors.inputHint color = AppColors.inputHint
) )
@@ -98,15 +126,18 @@ fun FormTextInput(
onValueChange = { onValueChange = {
onValueChange(it) onValueChange(it)
}, },
modifier = Modifier
.focusRequester(focusRequester),
singleLine = true, singleLine = true,
textStyle = TextStyle( textStyle = TextStyle(
fontSize = 16.sp, fontSize = 14.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = AppColors.text color = AppColors.text
), ),
cursorBrush = SolidColor(AppColors.text), cursorBrush = SolidColor(AppColors.text),
) )
}
}
} }

View File

@@ -15,16 +15,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -32,6 +38,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
/** /**
* 垂直布局的输入框 * 垂直布局的输入框
@@ -44,15 +51,19 @@ fun FormTextInput2(
error: String? = null, error: String? = null,
hint: String? = null, hint: String? = null,
background: Color? = null, background: Color? = null,
focusRequester: FocusRequester? = null,
onValueChange: (String) -> Unit onValueChange: (String) -> Unit
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val localFocusRequester = focusRequester ?: remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
Column( Column(
modifier = modifier.height(150.dp) modifier = modifier.height(150.dp)
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(25.dp))
.background(background ?: AppColors.inputBackground) .background(background ?: AppColors.inputBackground)
.let { .let {
if (error != null) { if (error != null) {
@@ -61,7 +72,11 @@ fun FormTextInput2(
it it
} }
} }
.padding(17.dp), .padding(17.dp)
.noRippleClickable {
localFocusRequester.requestFocus()
keyboardController?.show()
},
) { ) {
label?.let { label?.let {
@@ -79,13 +94,27 @@ fun FormTextInput2(
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(top = 8.dp) ) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.align(Alignment.TopStart),
tint = AppColors.text.copy(alpha = 0.4f)
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(24.dp))
Box(
modifier = Modifier.weight(1f)
) { ) {
if (value.isEmpty()) { if (value.isEmpty()) {
Text( Text(
text = hint ?: "", text = hint ?: "",
style = TextStyle( style = TextStyle(
fontSize = 16.sp, fontSize = 14.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = AppColors.inputHint color = AppColors.inputHint
) )
@@ -93,20 +122,23 @@ fun FormTextInput2(
} }
BasicTextField( BasicTextField(
maxLines = 5, maxLines = 6,
value = value, value = value,
onValueChange = { onValueChange = {
onValueChange(it) onValueChange(it)
}, },
modifier = Modifier
.focusRequester(localFocusRequester),
textStyle = TextStyle( textStyle = TextStyle(
fontSize = 16.sp, fontSize = 14.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = AppColors.text, color = AppColors.text,
lineHeight = 20.sp lineHeight = 20.sp
), ),
cursorBrush = SolidColor(AppColors.text), cursorBrush = SolidColor(AppColors.text),
) )
}
}
} }

View File

@@ -4,18 +4,23 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -24,7 +29,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
@@ -35,6 +42,8 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -71,6 +80,75 @@ fun FavouriteListPage() {
) { ) {
NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false) NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false)
} }
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
var moments = dataFlow.collectAsLazyPagingItems()
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.refreshPager(force = true)
}
)
}
}
} else if(moments.itemCount == 0) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=189.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(
id = if (com.aiosman.ravenow.AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1),
contentDescription = "No favourites",
modifier = Modifier.size(110.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.favourites_null),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
}
}else{
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(3), columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp) modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)
@@ -111,15 +189,11 @@ fun FavouriteListPage() {
) )
} }
} }
}
} }
} }
} }
}
}
PullRefreshIndicator( PullRefreshIndicator(
FavouriteListViewModel.isLoading, FavouriteListViewModel.isLoading,
state, state,

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -19,6 +20,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -37,6 +39,8 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -67,7 +71,47 @@ fun FollowerListScreen(userId: Int) {
) { ) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false) NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
} }
if (users.itemCount == 0) { val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
}
}
} else if (users.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -81,7 +125,7 @@ fun FollowerListScreen(userId: Int) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
else R.mipmap.qst_fs_qs_img), else R.mipmap.invalid_name_8),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp)
) )

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -37,7 +38,9 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.FollowButton import com.aiosman.ravenow.ui.composables.FollowButton
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.network.ReloadButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
/** /**
* 关注消息列表 * 关注消息列表
@@ -54,19 +57,51 @@ fun FollowerNoticeScreen() {
val model = FollowerNoticeViewModel val model = FollowerNoticeViewModel
var dataFlow = model.followerItemsFlow var dataFlow = model.followerItemsFlow
var followers = dataFlow.collectAsLazyPagingItems() var followers = dataFlow.collectAsLazyPagingItems()
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
.background(color = AppColors.background)
) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.reload() model.reload()
model.updateNotice() model.updateNotice()
} }
if (followers.itemCount == 0) { val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.reload(force = true)
}
)
}
}
} else if (followers.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -80,7 +115,7 @@ fun FollowerNoticeScreen() {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
else R.mipmap.qst_fs_qs_img), else R.mipmap.invalid_name_8),
contentDescription = "No Followers", contentDescription = "No Followers",
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp)
) )

View File

@@ -20,6 +20,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -37,7 +38,9 @@ import com.aiosman.ravenow.exp.viewModelFactory
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.network.ReloadButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -69,7 +72,48 @@ fun FollowingListScreen(userId: Int) {
NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false) NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false)
} }
if(users.itemCount == 0) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
}
}
} else if(users.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -83,7 +127,7 @@ fun FollowingListScreen(userId: Int) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img_my id =if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img_my
else R.mipmap.qst_gz_qs_img_my), else R.mipmap.invalid_name_9),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp)
) )

View File

@@ -43,7 +43,7 @@ fun CreateBottomSheet(
onMomentClick: () -> Unit onMomentClick: () -> Unit
) { ) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
//水平效果呈现镜像排列
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
@@ -58,42 +58,59 @@ fun CreateBottomSheet(
.padding(top = 24.dp, bottom = 24.dp), .padding(top = 24.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 标题 Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.h_cj_rw_icon),
contentDescription = null,
modifier = Modifier
.padding(start = 16.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(appColors.text)
)
Spacer(modifier = Modifier.weight(1f))
Text( Text(
text = stringResource(R.string.create_title), text = stringResource(R.string.create_title),
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = appColors.text, color = appColors.text,
modifier = Modifier.padding(bottom = 32.dp) modifier = Modifier
.padding(end = 3.dp)
) )
Image(
painter = painterResource(R.mipmap.h_cj_x_img),
contentDescription = null,
modifier = Modifier
.padding(end = 18.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(appColors.text)
)
}
Spacer(modifier = Modifier.height(30.dp))
// 三个创建选项 // 三个创建选项
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
// 群聊选项
CreateOption(
icon = R.drawable.ic_create_group_chat,
label = stringResource(R.string.create_group_chat_option),
onClick = onGroupChatClick
)
// 动态选项 // 动态选项
CreateOption( CreateOption(
icon = R.drawable.ic_create_monent, icon = R.drawable.ic_create_monent,
label = stringResource(R.string.create_moment), label = stringResource(R.string.create_moment),
onClick = onMomentClick onClick = onMomentClick
) )
// 群聊选项
CreateOption(
icon = R.mipmap.icons_circle_camera,
label = stringResource(R.string.create_group_chat_option),
onClick = onGroupChatClick
)
// AI选项 // AI选项
CreateOption( CreateOption(
icon = R.drawable.ic_create_ai, icon = R.mipmap.icons_circle_ai,
label = stringResource(R.string.create_ai), label = stringResource(R.string.create_ai),
onClick = onAiClick onClick = onAiClick
) )
} }
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))

View File

@@ -362,7 +362,7 @@ fun IndexScreen() {
Text( Text(
text = it.label(), text = it.label(),
fontSize = 10.sp, fontSize = 10.sp,
color = if (isSelected) AppColors.brandColorsColor else AppColors.text, color = if (isSelected) Color.Blue else AppColors.text,
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
) )
} }

View File

@@ -17,13 +17,13 @@ sealed class NavigationItem(
data object Home : NavigationItem("Home", data object Home : NavigationItem("Home",
icon = { R.drawable.rider_pro_nav_home }, icon = { R.drawable.rider_pro_nav_home },
selectedIcon = { R.mipmap.rider_pro_nav_home_hl }, selectedIcon = { R.mipmap.bars_x_buttons_home_s },
label = { stringResource(R.string.main_home) } label = { stringResource(R.string.main_home) }
) )
data object Ai : NavigationItem("Ai", data object Ai : NavigationItem("Ai",
icon = { R.mipmap.bars_x_buttons_discover_bold}, icon = { R.mipmap.bars_x_buttons_discover_bold},
selectedIcon = { R.mipmap.dynamic_hl }, selectedIcon = { R.mipmap.bars_x_buttons_discover_fill },
label = { stringResource(R.string.index_dynamic) } label = { stringResource(R.string.index_dynamic) }
) )
// data object Ai : NavigationItem("Ai", // data object Ai : NavigationItem("Ai",
@@ -40,13 +40,13 @@ sealed class NavigationItem(
data object Notification : NavigationItem("Notification", data object Notification : NavigationItem("Notification",
icon = { R.drawable.rider_pro_nav_notification }, icon = { R.drawable.rider_pro_nav_notification },
selectedIcon = { R.mipmap.rider_pro_nav_message_hl }, selectedIcon = { R.mipmap.bars_x_buttons_chat_s },
label = { stringResource(R.string.main_message) } label = { stringResource(R.string.main_message) }
) )
data object Profile : NavigationItem("Profile", data object Profile : NavigationItem("Profile",
icon = { R.drawable.rider_pro_nav_profile }, icon = { R.drawable.rider_pro_nav_profile },
selectedIcon = { R.mipmap.rider_pro_nav_profile_hl }, selectedIcon = { R.mipmap.bars_x_buttons_user_s },
label = { stringResource(R.string.main_profile) } label = { stringResource(R.string.main_profile) }
) )

View File

@@ -12,7 +12,6 @@ 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
@@ -29,15 +28,19 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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
@@ -71,11 +74,24 @@ import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.ResourceCleanupManager import com.aiosman.ravenow.utils.ResourceCleanupManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.zIndex import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items as gridItems
@OptIn(ExperimentalFoundationApi::class) // 检测是否接近列表底部的扩展函数
fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
val layoutInfo = this.layoutInfo
val totalItemsCount = layoutInfo.totalItemsCount
val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
return lastVisibleItemIndex >= (totalItemsCount - buffer)
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun Agent() { fun Agent() {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
@@ -89,25 +105,12 @@ 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) }
@@ -119,35 +122,38 @@ fun Agent() {
} }
} }
Box( val agentItems = viewModel.agentItems
modifier = Modifier.fillMaxSize() var selectedTabIndex by remember { mutableStateOf(0) }
) {
// 固定顶部搜索条 // 无限滚动状态
Box( val listState = rememberLazyListState()
modifier = Modifier
.fillMaxWidth() // 创建一个可观察的滚动到底部状态
.background(AppColors.background) val isScrolledToEnd by remember {
.zIndex(999.0f) derivedStateOf {
.height(44.dp + statusBarPaddingValues.calculateTopPadding()) listState.isScrolledToEnd()
) { }
Row( }
modifier = Modifier
.fillMaxWidth() // 检测滚动到底部并加载更多数据
.fillMaxHeight().padding(top = 32.dp, start = 16.dp, end = 16.dp), LaunchedEffect(isScrolledToEnd) {
horizontalArrangement = Arrangement.Start, if (isScrolledToEnd && !viewModel.isLoadingMore && agentItems.isNotEmpty() && viewModel.hasMoreData) {
verticalAlignment = Alignment.CenterVertically viewModel.loadMoreAgents()
) { }
}
Scaffold(
topBar = {
TopAppBar(
title = {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = "Rave AI", text = "Rave AI",
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.W900, fontWeight = FontWeight.W900,
color = AppColors.text, color = AppColors.text
modifier = Modifier
.align(Alignment.CenterVertically)
) )
},
Spacer(modifier = Modifier.weight(1f)) actions = {
Image( Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search), painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search", contentDescription = "search",
@@ -158,148 +164,160 @@ fun Agent() {
}, },
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )
} },
} colors = TopAppBarDefaults.topAppBarColors(
containerColor = AppColors.background
// 可滚动的内容区域 )
Column( )
},
containerColor = AppColors.background,
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { paddingValues ->
LazyColumn(
state = listState,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState) .padding(paddingValues)
.padding( .padding(
top = 44.dp + statusBarPaddingValues.calculateTopPadding() + 15.dp,
bottom = navigationBarPaddings, bottom = navigationBarPaddings,
start = 16.dp, start = 16.dp,
end = 16.dp end = 16.dp
), )
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
// // 搜索框
// 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
// )
// 类别标签页 - 吸顶
stickyHeader(key = "category_tabs") {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp) .background(AppColors.background)
.padding(top = 4.dp, bottom = 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))
// 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( LazyRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(bottom = 16.dp), .padding(bottom = 8.dp),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
viewModel.categories.forEachIndexed { index, category ->
item { item {
CustomTabItem( CustomTabItem(
text = category.getLocalizedName(), text = stringResource(R.string.agent_recommend),
isSelected = selectedTabIndex == index, isSelected = selectedTabIndex == 0,
onClick = { onClick = {
viewModel.selectCategory(index) selectedTabIndex = 0
viewModel.loadAllAgents()
}
)
}
item {
TabSpacer()
}
// 动态添加分类标签
viewModel.categories.take(4).forEachIndexed { index, category ->
item {
CustomTabItem(
text = category.name,
isSelected = selectedTabIndex == index + 1,
onClick = {
selectedTabIndex = index + 1
viewModel.loadAgentsByCategory(category.id)
} }
) )
} }
if (index < viewModel.categories.size - 1) {
item { item {
TabSpacer() TabSpacer()
} }
} }
item {
CustomTabItem(
text = "scenes",
isSelected = selectedTabIndex == 1,
onClick = {
selectedTabIndex = 1
} }
)
} }
// 显示当前选中分类的 Agent 数据 item {
TabSpacer()
}
item {
CustomTabItem(
text = "voices",
isSelected = selectedTabIndex == 6,
onClick = {
selectedTabIndex = 6
}
)
}
item {
TabSpacer()
}
item {
CustomTabItem(
text = "anime",
isSelected = selectedTabIndex == 7,
onClick = {
selectedTabIndex = 7
}
)
}
item {
TabSpacer()
}
item {
CustomTabItem(
text = "assist",
isSelected = selectedTabIndex == 8,
onClick = {
selectedTabIndex = 8
}
)
}
}
}
}
// 推荐内容区域
item {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
when {
selectedTabIndex == 0 -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
} }
selectedTabIndex in 1..viewModel.categories.size -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
}
else -> {
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
}
}
}
}
// 推荐聊天房间
ChatRoomsSection(
chatRooms = viewModel.chatRooms,
navController = LocalNavController.current
)
Spacer(modifier = Modifier.height(20.dp))
// "发现更多" 标题 - 吸顶
stickyHeader(key = "discover_more") {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(), .background(AppColors.background)
// center the tabs .padding(top = 8.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
@@ -307,7 +325,6 @@ fun Agent() {
painter = painterResource(R.mipmap.rider_pro_agent2), painter = painterResource(R.mipmap.rider_pro_agent2),
contentDescription = "agent", contentDescription = "agent",
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text( androidx.compose.material3.Text(
@@ -316,80 +333,69 @@ fun Agent() {
fontWeight = androidx.compose.ui.text.font.FontWeight.W600, fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text color = AppColors.text
) )
} }
Spacer(modifier = Modifier.height(20.dp)) }
// Agent两列网格布局 // Agent网格 - 使用行式布局
AgentGridLayout( items(
agentItems = viewModel.agentItems, items = agentItems.chunked(2),
key = { row -> row.firstOrNull()?.openId ?: "" }
) { rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
rowItems.forEach { agentItem ->
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel, viewModel = viewModel,
navController = LocalNavController.current navController = LocalNavController.current
) )
} }
} }
} // 如果这一行只有一个item添加一个空的占位符
if (rowItems.size == 1) {
@Composable
fun AgentGridLayout(
agentItems: List<AgentItem>,
viewModel: AgentViewModel,
navController: NavHostController
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
// 将agentItems按两列分组
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
bottom = 20.dp
),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一列
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[0],
viewModel = viewModel,
navController = navController
)
}
// 第二列(如果存在)
if (rowItems.size > 1) {
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[1],
viewModel = viewModel,
navController = navController
)
}
} else {
// 如果只有一列,添加空白占位
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
} }
} }
} }
// 加载更多指示器
if (viewModel.isLoadingMore) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
horizontalArrangement = Arrangement.Center
) {
androidx.compose.material3.CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.text,
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(12.dp))
androidx.compose.material3.Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
}
}
}
} }
} }
@SuppressLint("SuspiciousIndentation") @SuppressLint("SuspiciousIndentation")
@Composable @Composable
fun AgentCardSquare( fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navController: NavHostController) {
agentItem: AgentItem,
viewModel: AgentViewModel,
navController: NavHostController
) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val cardHeight = 180.dp val cardHeight = 200.dp
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一 val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
// 防抖状态 // 防抖状态
@@ -398,8 +404,9 @@ fun AgentCardSquare(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = avatarSize / 2)
.height(cardHeight) .height(cardHeight)
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp)) // 主题背景 .background(AppColors.nonActive, RoundedCornerShape(12.dp)) // 修改背景颜色
.clickable { .clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController) viewModel.goToProfile(agentItem.openId, navController)
@@ -414,10 +421,17 @@ fun AgentCardSquare(
modifier = Modifier modifier = Modifier
.offset(y = -avatarSize / 2) .offset(y = -avatarSize / 2)
.size(avatarSize) .size(avatarSize)
.background(AppColors.background, RoundedCornerShape(avatarSize / 2)) .background(Color.White, RoundedCornerShape(avatarSize / 2))
.clip(RoundedCornerShape(avatarSize / 2)), .clip(RoundedCornerShape(avatarSize / 2)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image(
painter = painterResource(R.mipmap.group_copy),
contentDescription = "默认头像",
modifier = Modifier.size(avatarSize),
contentScale = androidx.compose.ui.layout.ContentScale.Crop
)
if (agentItem.avatar.isNotEmpty()) { if (agentItem.avatar.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = agentItem.avatar, imageUrl = agentItem.avatar,
@@ -427,13 +441,6 @@ fun AgentCardSquare(
.clip(RoundedCornerShape(avatarSize / 2)), .clip(RoundedCornerShape(avatarSize / 2)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop contentScale = androidx.compose.ui.layout.ContentScale.Crop
) )
} else {
Image(
painter = painterResource(R.mipmap.rider_pro_agent),
contentDescription = "默认头像",
modifier = Modifier.size(avatarSize / 2),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
} }
} }
@@ -441,12 +448,12 @@ fun AgentCardSquare(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 48.dp), // 为底部聊天按钮留出空间 .padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = agentItem.title, text = agentItem.title,
fontSize = 16.sp, fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600, fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text, color = AppColors.text,
maxLines = 1, maxLines = 1,
@@ -455,25 +462,29 @@ fun AgentCardSquare(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.height(85.dp)
.fillMaxWidth()
) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = agentItem.desc, text = agentItem.desc,
fontSize = 14.sp, fontSize = 12.sp,
color = AppColors.secondaryText, color = AppColors.secondaryText,
maxLines = 2, maxLines = 5,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false)
) )
} }
// 聊天按钮,固定在底部居中,距离底部有一定边距 Spacer(modifier = Modifier.height(8.dp))
// 聊天按钮,位于底部居中
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .width(60.dp)
.padding(bottom = 12.dp) // 距离底部的边距
.width(80.dp)
.height(32.dp) .height(32.dp)
.background( .background(
color = AppColors.inputBackground, color = Color(0X147c7480),
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
) )
.clickable { .clickable {
@@ -502,11 +513,11 @@ fun AgentCardSquare(
) )
} }
} }
}
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) { fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
// 每页显示5个agent // 每页显示5个agent
@@ -520,7 +531,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel
// Agent内容 // Agent内容
Box( Box(
modifier = Modifier modifier = Modifier
.height(300.dp) .height(310.dp)
) { ) {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
@@ -542,7 +553,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage), agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
page = page, page = page,
modifier = Modifier modifier = Modifier
.height(300.dp) .height(310.dp)
.graphicsLayer { .graphicsLayer {
scaleX = scale scaleX = scale
scaleY = scale scaleY = scale
@@ -579,13 +590,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel
} }
@Composable @Composable
fun AgentPage( fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier,navController: NavHostController) {
viewModel: AgentViewModel,
agentItems: List<AgentItem>,
page: Int,
modifier: Modifier = Modifier,
navController: NavHostController
) {
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
@@ -593,11 +598,7 @@ fun AgentPage(
) { ) {
// 显示3个agent // 显示3个agent
agentItems.forEachIndexed { index, agentItem -> agentItems.forEachIndexed { index, agentItem ->
AgentCard2( AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current)
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))
} }
@@ -607,7 +608,7 @@ fun AgentPage(
@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
// 防抖状态 // 防抖状态
@@ -623,7 +624,7 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background(AppColors.secondaryBackground, RoundedCornerShape(24.dp)) .background(Color(0x00F5F5F5), RoundedCornerShape(24.dp))
.clickable { .clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController) viewModel.goToProfile(agentItem.openId, navController)
@@ -633,6 +634,12 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image(
painter = painterResource(R.mipmap.group_copy),
contentDescription = "默认头像",
modifier = Modifier.size(48.dp),
)
if (agentItem.avatar.isNotEmpty()) { if (agentItem.avatar.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = agentItem.avatar, imageUrl = agentItem.avatar,
@@ -642,13 +649,6 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
.clip(RoundedCornerShape(24.dp)), .clip(RoundedCornerShape(24.dp)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop contentScale = androidx.compose.ui.layout.ContentScale.Crop
) )
} else {
Image(
painter = painterResource(R.mipmap.rider_pro_agent),
contentDescription = "默认头像",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
} }
} }
@@ -687,7 +687,7 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
modifier = Modifier modifier = Modifier
.size(width = 60.dp, height = 32.dp) .size(width = 60.dp, height = 32.dp)
.background( .background(
color = AppColors.inputBackground, color = Color(0X147c7480),
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
) )
.clickable { .clickable {
@@ -717,135 +717,3 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
} }
} }
} }
@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,29 +6,18 @@ 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.data.Agent
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.data.ListContainer
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.CategoryTemplate
import com.aiosman.ravenow.data.api.RaveNowAPI import com.aiosman.ravenow.data.api.RaveNowAPI
import com.aiosman.ravenow.data.api.SingleChatRequestBody import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
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
@@ -36,198 +25,153 @@ 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()) var categories by mutableStateOf<List<CategoryItem>>(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 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 var isLoadingMore by mutableStateOf(false)
private set
var currentPage by mutableStateOf(1)
private set
var hasMoreData by mutableStateOf(true)
private set
private val pageSize = 20
private var currentCategoryId: Int? = null
init { init {
loadAgentData() loadAgentData()
loadChatRooms()
loadCategories() loadCategories()
} }
private fun loadAgentData(categoryIndex: Int = 0) { private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false) {
viewModelScope.launch { viewModelScope.launch {
if (isLoadMore) {
isLoadingMore = true
} else {
isLoading = true isLoading = true
errorMessage = null // 重置分页状态
currentPage = 1 currentPage = 1
hasMoreData = true hasMoreData = true
try { currentCategoryId = categoryId
val selectedCategory = if (categoryIndex < categories.size) categories[categoryIndex] else null }
val response = if (categoryIndex == 0 || selectedCategory == null) { errorMessage = null
// 推荐分类或无效分类,加载所有 Agent try {
apiClient.getAgent(page = currentPage, pageSize = 20, withWorkflow = "1", random = "true") val response = if (categoryId != null) {
} else { // 根据分类ID获取智能体
// 特定分类,使用 categoryName 参数
apiClient.getAgent( apiClient.getAgent(
page = currentPage, page = page,
pageSize = 20, pageSize = pageSize,
withWorkflow = "1", withWorkflow = 1,
categoryName = selectedCategory.name, categoryIds = listOf(categoryId)
random = "true"
) )
} else {
// 获取所有智能体
apiClient.getAgent(page = page, pageSize = pageSize, withWorkflow = 1)
} }
if (response.isSuccessful) { if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList() val responseData = response.body()?.data
agentItems = agents.map { agent -> val agents = responseData?.list ?: emptyList<Agent>()
val newAgentItems = agents.map { agent ->
AgentItem.fromAgent(agent) AgentItem.fromAgent(agent)
} }
hasMoreData = agents.size >= 20
if (isLoadMore) {
// 加载更多:追加到现有列表
agentItems = agentItems + newAgentItems
currentPage = page
} else {
// 首次加载或刷新:替换整个列表
agentItems = newAgentItems
currentPage = 1
}
// 检查是否还有更多数据
hasMoreData = agents.size >= pageSize
} else { } else {
errorMessage = "获取Agent数据失败: ${response.code()}" errorMessage = "获取Agent数据失败: ${response.code()}"
} }
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = "网络请求失败: ${e.message}" errorMessage = "网络请求失败: ${e.message}"
} finally { } finally {
if (isLoadMore) {
isLoadingMore = false
} else {
isLoading = false isLoading = false
} }
} }
} }
private fun loadChatRooms() {
viewModelScope.launch {
try {
val response = apiClient.getRooms(page = 1, pageSize = 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() { private fun loadCategories() {
viewModelScope.launch { viewModelScope.launch {
try { try {
val categoryBackend = CategoryBackend() val response = apiClient.getCategories(
val response = categoryBackend.getCategories(1) pageSize = 20,
if (response != null) { withChildren = false,
// 添加一个默认的"推荐"分类在第一位 withParent = false,
val recommendCategory = CategoryEntity( withCount = true,
id = 0, hideEmpty = true
name = "推荐",
description = "推荐内容",
avatar = "",
parentId = null,
sort = 0,
isActive = true,
promptCount = 0,
createdAt = "",
updatedAt = "",
translations = null
) )
categories = listOf(recommendCategory) + response.list println("分类数据请求完成,响应成功: ${response.isSuccessful}")
if (response.isSuccessful) {
// 分类加载完成后,重新加载当前选中分类的 Agent 数据 val categoryList = response.body()?.data?.list ?: emptyList()
if (agentItems.isEmpty()) { println("获取到 ${categoryList.size} 个分类")
loadAgentData(selectedCategoryIndex) categories = categoryList.map { category ->
CategoryItem.fromCategoryTemplate(category)
} }
println("成功处理并映射了 ${categories.size} 个分类")
} else { } else {
// 如果请求失败,使用默认分类 errorMessage = "获取分类数据失败: ${response.code()}"
categories = listOf() println("获取分类数据失败: ${response.code()}")
} }
} catch (e: Exception) { } catch (e: Exception) {
// 如果网络请求失败,使用默认分类 errorMessage = "获取分类数据失败: ${e.message}"
categories = listOf() println("获取分类数据异常: ${e.message}")
e.printStackTrace()
} }
} }
} }
fun loadAgentsByCategory(categoryId: Int) {
loadAgentData(categoryId)
}
fun loadAllAgents() {
loadAgentData()
}
/** /**
* 加载更多Agent数据 * 加载更多Agent数据
*/ */
fun loadMoreAgentData() { fun loadMoreAgents() {
if (!hasMoreData || isLoading) return // 检查是否正在加载或没有更多数据
if (isLoadingMore || !hasMoreData) {
return
}
viewModelScope.launch {
isLoading = true
try {
val nextPage = currentPage + 1 val nextPage = currentPage + 1
val selectedCategory = if (selectedCategoryIndex < categories.size) categories[selectedCategoryIndex] else null loadAgentData(
categoryId = currentCategoryId,
val response = if (selectedCategoryIndex == 0 || selectedCategory == null) {
// 推荐分类或无效分类,加载所有 Agent
apiClient.getAgent(page = nextPage, pageSize = 20, withWorkflow = "1")
} else {
// 特定分类,使用 categoryName 参数
apiClient.getAgent(
page = nextPage, page = nextPage,
pageSize = 20, isLoadMore = true
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,
) { ) {
@@ -286,13 +230,32 @@ 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
isLoadingMore = false
currentPage = 1 currentPage = 1
hasMoreData = true hasMoreData = true
currentCategoryId = null
} }
} }
data class CategoryItem(
val id: Int,
val name: String,
val description: String,
val avatar: String,
val promptCount: Int?
) {
companion object {
fun fromCategoryTemplate(template: CategoryTemplate): CategoryItem {
return CategoryItem(
id = template.id,
name = template.name,
description = template.description,
avatar = "${ApiClient.BASE_API_URL}${template.avatar}",
promptCount = template.promptCount
)
}
}
}

View File

@@ -78,7 +78,7 @@ import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.ui.index.tabs.message.tab.AllChatListScreen
/** /**
* 消息列表界面 * 消息列表界面
@@ -95,7 +95,7 @@ fun NotificationsScreen() {
val navController = LocalNavController.current val navController = LocalNavController.current
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val context = LocalContext.current val context = LocalContext.current
var pagerState = rememberPagerState (pageCount = { 3 }) var pagerState = rememberPagerState (pageCount = { 4 })
var scope = rememberCoroutineScope() var scope = rememberCoroutineScope()
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = { val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch { MessageListViewModel.viewModelScope.launch {
@@ -177,7 +177,7 @@ fun NotificationsScreen() {
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.noRippleClickable { .noRippleClickable {
// TODO: 实现通知功能 navController.navigate(NavigationRoute.NotificationScreen.route)
}, },
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )
@@ -324,7 +324,7 @@ fun NotificationsScreen() {
Box { Box {
TabItem( TabItem(
text = stringResource(R.string.chat_ai), text = stringResource(R.string.chat_all),
isSelected = pagerState.currentPage == 0, isSelected = pagerState.currentPage == 0,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
@@ -335,6 +335,38 @@ fun NotificationsScreen() {
} }
) )
// 全部未读消息红点
val totalUnreadCount = AgentChatListViewModel.totalUnreadCount +
GroupChatListViewModel.totalUnreadCount +
FriendChatListViewModel.totalUnreadCount
if (totalUnreadCount > 0) {
Box(
modifier = Modifier
.size(8.dp)
.background(
color = Color(0xFFFF3B30),
shape = CircleShape
)
.align(Alignment.TopEnd)
.offset(x = 8.dp, y = (-4).dp)
)
}
}
TabSpacer()
Box {
TabItem(
text = stringResource(R.string.chat_ai),
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
// 智能体未读消息红点 // 智能体未读消息红点
if (AgentChatListViewModel.totalUnreadCount > 0) { if (AgentChatListViewModel.totalUnreadCount > 0) {
Box( Box(
@@ -353,11 +385,11 @@ fun NotificationsScreen() {
Box { Box {
TabItem( TabItem(
text = stringResource(R.string.chat_group), text = stringResource(R.string.chat_group),
isSelected = pagerState.currentPage == 1, isSelected = pagerState.currentPage == 2,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { scope.launch {
pagerState.animateScrollToPage(1) pagerState.animateScrollToPage(2)
} }
} }
} }
@@ -378,14 +410,15 @@ fun NotificationsScreen() {
} }
} }
TabSpacer() TabSpacer()
Box { Box {
TabItem( TabItem(
text = stringResource(R.string.chat_friend), text = stringResource(R.string.chat_friend),
isSelected = pagerState.currentPage == 2, isSelected = pagerState.currentPage == 3,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { scope.launch {
pagerState.animateScrollToPage(2) pagerState.animateScrollToPage(3)
} }
} }
} }
@@ -414,14 +447,17 @@ fun NotificationsScreen() {
) { ) {
when (it) { when (it) {
0 -> { 0 -> {
AllChatListScreen()
}
1 -> {
AgentChatListScreen() AgentChatListScreen()
} }
1 -> { 2 -> {
GroupChatListScreen() GroupChatListScreen()
} }
2 -> { 3 -> {
FriendChatListScreen() FriendChatListScreen()
} }
} }

View File

@@ -91,11 +91,14 @@ fun AgentChatListScreen() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_znt_qs_as_img id = if(AppState.darkMode) R.mipmap.qs_znt_qs_as_img
else R.mipmap.qs_znt_qs_img), else R.mipmap.invalid_name_5),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(181.dp)
@@ -114,6 +117,35 @@ fun AgentChatListScreen() {
fontSize = 14.sp fontSize = 14.sp
) )
} }
else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
AgentChatListViewModel.refreshPager(context = context)
}
)
}
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View File

@@ -0,0 +1,390 @@
package com.aiosman.ravenow.ui.index.tabs.message.tab
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.ui.text.font.FontFamily
data class CombinedConversation(
val type: String, // "agent", "group", or "friend"
val agentConversation: AgentConversation? = null,
val groupConversation: GroupConversation? = null,
val friendConversation: FriendConversation? = null
) {
val id: String
get() = when (type) {
"agent" -> "agent_${agentConversation?.id ?: 0}"
"group" -> "group_${groupConversation?.id ?: 0}"
"friend" -> "friend_${friendConversation?.id ?: 0}"
else -> ""
}
val avatar: String
get() = when (type) {
"agent" -> agentConversation?.avatar ?: ""
"group" -> groupConversation?.avatar ?: ""
"friend" -> friendConversation?.avatar ?: ""
else -> ""
}
val name: String
get() = when (type) {
"agent" -> agentConversation?.nickname ?: ""
"group" -> groupConversation?.groupName ?: ""
"friend" -> friendConversation?.nickname ?: ""
else -> ""
}
val lastMessageTime: String
get() = when (type) {
"agent" -> agentConversation?.lastMessageTime ?: ""
"group" -> groupConversation?.lastMessageTime ?: ""
"friend" -> friendConversation?.lastMessageTime ?: ""
else -> ""
}
val displayText: String
get() = when (type) {
"agent" -> agentConversation?.displayText ?: ""
"group" -> groupConversation?.displayText ?: ""
"friend" -> friendConversation?.displayText ?: ""
else -> ""
}
val unreadCount: Int
get() = when (type) {
"agent" -> agentConversation?.unreadCount ?: 0
"group" -> groupConversation?.unreadCount ?: 0
"friend" -> friendConversation?.unreadCount ?: 0
else -> 0
}
val isSelf: Boolean
get() = when (type) {
"agent" -> agentConversation?.isSelf ?: false
"group" -> groupConversation?.isSelf ?: false
"friend" -> friendConversation?.isSelf ?: false
else -> false
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AllChatListScreen() {
val context = LocalContext.current
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current
var allConversations by remember { mutableStateOf<List<CombinedConversation>>(emptyList()) }
var refreshing by remember { mutableStateOf(false) }
var isLoading by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
val state = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = {
refreshing = true
refreshAllData(context,
onSuccess = { conversations ->
allConversations = conversations
refreshing = false
},
onError = { errorMsg ->
error = errorMsg
refreshing = false
}
)
}
)
LaunchedEffect(Unit) {
isLoading = true
refreshAllData(context,
onSuccess = { conversations ->
allConversations = conversations
isLoading = false
},
onError = { errorMsg ->
error = errorMsg
isLoading = false
}
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) {
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state)
) {
if (allConversations.isEmpty() && !isLoading) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img
else R.mipmap.invalid_name_2),
contentDescription = "null data",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_empty_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
)
} else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
isLoading = true
refreshAllData(context,
onSuccess = { conversations ->
allConversations = conversations
isLoading = false
},
onError = { errorMsg ->
error = errorMsg
isLoading = false
}
)
}
)
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
itemsIndexed(
items = allConversations,
key = { _, item -> item.id }
) { index, item ->
when (item.type) {
"agent" -> {
item.agentConversation?.let { agent ->
AgentChatItem(
conversation = agent,
onUserAvatarClick = { conv ->
AgentChatListViewModel.goToUserDetail(conv, navController)
},
onChatClick = { conv ->
if (NetworkUtils.isNetworkAvailable(context)) {
AgentChatListViewModel.createSingleChat(conv.trtcUserId)
AgentChatListViewModel.goToChatAi(conv.trtcUserId, navController)
} else {
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
}
}
)
}
}
"group" -> {
item.groupConversation?.let { group ->
GroupChatItem(
conversation = group,
onGroupAvatarClick = { conv ->
GroupChatListViewModel.goToGroupDetail(conv, navController)
},
onChatClick = { conv ->
if (NetworkUtils.isNetworkAvailable(context)) {
GroupChatListViewModel.goToChat(conv, navController)
} else {
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
}
}
)
}
}
"friend" -> {
item.friendConversation?.let { friend ->
FriendChatItem(
conversation = friend,
onUserAvatarClick = { conv ->
FriendChatListViewModel.goToUserDetail(conv, navController)
},
onChatClick = { conv ->
if (NetworkUtils.isNetworkAvailable(context)) {
FriendChatListViewModel.goToChat(conv, navController)
} else {
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
}
}
)
}
}
}
// 分隔线
// if (index < allConversations.size - 1) {
// HorizontalDivider(
// modifier = Modifier.padding(horizontal = 24.dp),
// color = AppColors.divider
// )
// }
}
if (isLoading && allConversations.isNotEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.main
)
}
}
}
}
}
PullRefreshIndicator(
refreshing = refreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
}
error?.let { errorMsg ->
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = errorMsg,
color = AppColors.error,
fontSize = 14.sp
)
}
}
}
}
fun refreshAllData(
context: android.content.Context,
onSuccess: (List<CombinedConversation>) -> Unit,
onError: (String) -> Unit
) {
try {
// 同时刷新所有类型的数据
AgentChatListViewModel.refreshPager(context = context)
GroupChatListViewModel.refreshPager(context = context)
FriendChatListViewModel.refreshPager(context = context)
val combinedList = mutableListOf<CombinedConversation>()
AgentChatListViewModel.agentChatList.forEach { agent ->
combinedList.add(CombinedConversation(type = "agent", agentConversation = agent))
}
GroupChatListViewModel.groupChatList.forEach { group ->
combinedList.add(CombinedConversation(type = "group", groupConversation = group))
}
FriendChatListViewModel.friendChatList.forEach { friend ->
val isDuplicate = combinedList.any {//判断重复
it.type == "agent" && it.agentConversation?.trtcUserId == friend.trtcUserId
}
if (!isDuplicate) {
combinedList.add(CombinedConversation(type = "friend", friendConversation = friend))
}
}
// 按最后消息时间排序
val sortedList = combinedList.sortedByDescending {
it.lastMessageTime
}
onSuccess(sortedList)
} catch (e: Exception) {
onError("刷新数据失败: ${e.message}")
}
}

View File

@@ -33,8 +33,14 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.search.ReloadButton
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.graphics.Brush
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.foundation.layout.PaddingValues
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -73,12 +79,14 @@ fun FriendChatListScreen() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
//verticalArrangement = Arrangement.Center //verticalArrangement = Arrangement.Center
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img
else R.mipmap.qs_py_qs_img), else R.mipmap.invalid_name_2),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(181.dp)
@@ -96,6 +104,34 @@ fun FriendChatListScreen() {
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontSize = 14.sp fontSize = 14.sp
) )
}else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
}
)
}
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -266,4 +302,43 @@ fun FriendChatItem(
} }
} }
} }
@Composable
fun ReloadButton(
onClick: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -71,11 +71,14 @@ fun GroupChatListScreen() {
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_ql_qs_as_img id = if(AppState.darkMode) R.mipmap.qs_ql_qs_as_img
else R.mipmap.qs_ql_qs_img), else R.mipmap.invalid_name_12),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(181.dp)
@@ -93,6 +96,34 @@ fun GroupChatListScreen() {
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontSize = 14.sp fontSize = 14.sp
) )
}else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
GroupChatListViewModel.refreshPager(context = context)
}
)
}
} }
} else { } else {
LazyColumn( LazyColumn(

View File

@@ -18,8 +18,6 @@ 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
@@ -46,25 +44,19 @@ 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.foundation.layout.wrapContentHeight
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
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.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
data class TabData(
val text: String,
val index: Int
)
/** /**
* 动态列表 * 动态列表
*/ */
@@ -76,8 +68,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()
// 游客模式下显示4个tabWorldwide、Hot、News、Recommend非游客模式显示5个tabWorldwide、Following、Hot、News、Recommend // 游客模式下显示timeline只显示2个tabDynamic、Hot // 游客模式下不显示timeline只显示3个tabExplore、Dynamic、Hot
val tabCount = if (AppStore.isGuest) 4 else 5 val tabCount = if (AppStore.isGuest) 2 else 3 // val tabCount = if (AppStore.isGuest) 3 else 4
var pagerState = rememberPagerState { tabCount } var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope() var scope = rememberCoroutineScope()
Column( Column(
@@ -130,6 +122,8 @@ fun MomentsList() {
// //
// } // }
// Spacer(modifier = Modifier.width(16.dp)) // Spacer(modifier = Modifier.width(16.dp))
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
Text( Text(
text = stringResource(R.string.moment), text = stringResource(R.string.moment),
fontSize = 20.sp, fontSize = 20.sp,
@@ -147,55 +141,93 @@ fun MomentsList() {
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.noRippleClickable { .noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
navController.navigate(NavigationRoute.Search.route) navController.navigate(NavigationRoute.Search.route)
}
}, },
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )
} }
// Spacer(modifier = Modifier.height(23.dp)) Spacer(modifier = Modifier.height(23.dp))
val tabDebouncer = rememberDebouncer() Row(
// 创建tab数据列表
val tabs = if (AppStore.isGuest) {
listOf(
TabData(stringResource(R.string.index_worldwide), 0),
TabData(stringResource(R.string.index_hot), 1),
TabData(stringResource(R.string.index_news), 2),
TabData(stringResource(R.string.index_recommend), 3)
)
} else {
listOf(
TabData(stringResource(R.string.index_worldwide), 0),
TabData(stringResource(R.string.index_following), 1),
TabData(stringResource(R.string.index_hot), 2),
TabData(stringResource(R.string.index_news), 3),
TabData(stringResource(R.string.index_recommend), 4)
)
}
LazyRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(start = 16.dp, bottom = 16.dp), .padding(start = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
items(tabs) { tab -> val tabDebouncer = rememberDebouncer()
UnderlineTabItem(
text = tab.text, // 新探索标签
isSelected = pagerState.currentPage == tab.index, Box {
CustomTabItem(
text = stringResource(R.string.index_worldwide),
isSelected = pagerState.currentPage == 0,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { scope.launch {
pagerState.animateScrollToPage(tab.index) pagerState.animateScrollToPage(0)
} }
} }
} }
) )
} }
TabSpacer()
// 只有非游客用户才显示"关注"tab
if (!AppStore.isGuest) {
Box {
CustomTabItem(
text = stringResource(R.string.index_following),
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
}
TabSpacer()
// 热门标签
Box {
CustomTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 2,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(2)
}
}
}
)
}
} else {
// 热门标签 (游客模式)
Box {
CustomTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
}
}
} }
HorizontalPager( HorizontalPager(
@@ -205,7 +237,7 @@ fun MomentsList() {
.weight(1f) .weight(1f)
) { ) {
if (AppStore.isGuest) { if (AppStore.isGuest) {
// 游客模式:Worldwide(0), Hot(1), News(2), Recommend(3) // 游客模式:Dynamic(0), Hot(1)
when (it) { when (it) {
0 -> { 0 -> {
Dynamic() Dynamic()
@@ -213,15 +245,9 @@ fun MomentsList() {
1 -> { 1 -> {
HotMomentsList() HotMomentsList()
} }
2 -> {
News()
}
3 -> {
Recommend()
}
} }
} else { } else {
// 正常用户:Worldwide(0), Following(1), Hot(2), News(3), Recommend(4) // 正常用户:Dynamic(0), Timeline(1), Hot(2)
when (it) { when (it) {
0 -> { 0 -> {
Dynamic() Dynamic()
@@ -232,52 +258,11 @@ fun MomentsList() {
2 -> { 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 @Composable
fun CustomTabItem( fun CustomTabItem(
text: String, text: String,

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

@@ -1,179 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,395 +0,0 @@
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

@@ -1,239 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -43,6 +43,8 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.MomentCard import com.aiosman.ravenow.ui.composables.MomentCard
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.platform.LocalContext
/** /**
* 动态列表 * 动态列表
@@ -76,7 +78,49 @@ fun TimelineMomentsList() {
model.loadMore() model.loadMore()
} }
} }
if (moments.isEmpty()) { val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 188.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
val exploreDebouncer = rememberDebouncer()
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ExploreButton(
onClick = {
exploreDebouncer {
/* TODO: 添加点击事件处理 */
} }
)
}
}
} else if (moments.isEmpty()) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -91,7 +135,7 @@ fun TimelineMomentsList() {
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img id = if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img
else R.mipmap.qst_gz_qs_img), else R.mipmap.invalid_name_4),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(140.dp) modifier = Modifier.size(140.dp)
) )
@@ -191,9 +235,9 @@ fun ExploreButton(
) { ) {
val gradientBrush = Brush.linearGradient( val gradientBrush = Brush.linearGradient(
colors = listOf( colors = listOf(
Color(0xFFee2a33), Color(0xFF7c45ed),
Color(0xFFd80264), Color(0xFF7c68ef),
Color(0xFF664c92) Color(0xFF7bd8f8)
) )
) )

View File

@@ -50,10 +50,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -96,7 +98,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import androidx.compose.foundation.rememberScrollState
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun ProfileV3( fun ProfileV3(
@@ -117,7 +119,6 @@ fun ProfileV3(
postCount: Int? = null, // 新增参数用于传递帖子总数 postCount: Int? = null, // 新增参数用于传递帖子总数
) { ) {
val model = MyProfileViewModel val model = MyProfileViewModel
val state = rememberCollapsingToolbarScaffoldState()
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 }) val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
val enabled by remember { mutableStateOf(true) } val enabled by remember { mutableStateOf(true) }
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
@@ -149,6 +150,15 @@ fun ProfileV3(
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val listState = rememberLazyListState() val listState = rememberLazyListState()
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
val scrollState = rememberScrollState()
val toolbarAlpha by remember {
derivedStateOf {
val maxScroll = 500f // 最大滚动距离,可调整
val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f)
progress
}
}
// observe list scrolling // observe list scrolling
val reachedListBottom by remember { val reachedListBottom by remember {
@@ -199,8 +209,6 @@ fun ProfileV3(
} }
} }
fun switchTheme() { fun switchTheme() {
// delay // delay
scope.launch { scope.launch {
@@ -279,43 +287,15 @@ fun ProfileV3(
Box( Box(
modifier = Modifier.pullRefresh(refreshState) modifier = Modifier.pullRefresh(refreshState)
) { ) {
CollapsingToolbarScaffold( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(AppColors.profileBackground), .verticalScroll(scrollState)
state = state,
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
toolbarScrollable = true,
enabled = enabled,
toolbar = { toolbarScrollState ->
Column(
modifier = Modifier
.fillMaxWidth()
.height(miniToolbarHeight.dp)
// 保持在最低高度和当前高度之间
.background(AppColors.profileBackground) .background(AppColors.profileBackground)
) { ) {
} // Banner
// header val banner = profile?.banner
Box( if (banner != null) {
modifier = Modifier
.parallax(0.5f)
.fillMaxWidth()
.height(if (isAiAccount) 600.dp else 700.dp)
.background(AppColors.profileBackground)
.verticalScroll(toolbarScrollState)
) {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer {
alpha = state.toolbarState.progress
}
) {
// banner
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -327,13 +307,11 @@ fun ProfileV3(
.fillMaxWidth() .fillMaxWidth()
.height(bannerHeight.dp - 24.dp) .height(bannerHeight.dp - 24.dp)
.let { .let {
if (isSelf&&isMain) { if (isSelf && isMain) {
it.noRippleClickable { it.noRippleClickable {
Intent(Intent.ACTION_PICK).apply { Intent(Intent.ACTION_PICK).apply {
type = "image/*" type = "image/*"
pickBannerImageLauncher.launch( pickBannerImageLauncher.launch(this)
this
)
} }
} }
} else { } else {
@@ -348,77 +326,25 @@ fun ProfileV3(
), ),
) )
) { ) {
val banner = profile?.banner
if (banner != null) {
CustomAsyncImage( CustomAsyncImage(
LocalContext.current, LocalContext.current,
banner, banner,
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize(),
contentDescription = "", contentDescription = "",
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
}
}
} else { } else {
Image( Spacer(modifier = Modifier.height(100.dp))
painter = painterResource(id = R.drawable.rave_now_profile_backgrount_demo_1),
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
}
}
if (isSelf&&isMain) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
start = 8.dp,
end = 8.dp
)
.noRippleClickable {
IndexViewModel.openDrawer = true
}
) {
Box(
modifier = Modifier
.padding(16.dp)
.clip(RoundedCornerShape(8.dp))
.background(
AppColors.background.copy(
alpha = 0.7f
)
)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
tint = AppColors.text
)
}
}
} }
} // 用户信息
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.profileBackground) .background(AppColors.profileBackground)
) { .padding(horizontal = 16.dp)
// user info
Column(
modifier = Modifier
.fillMaxWidth()
) {
// Spacer(modifier = Modifier.height(16.dp))
// 个人信息
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) { ) {
profile?.let { profile?.let {
UserItem( UserItem(
@@ -427,17 +353,20 @@ fun ProfileV3(
) )
} }
} }
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
// 操作按钮
profile?.let { profile?.let {
Box( Box(
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) { ) {
if (isSelf) { if (isSelf) {
SelfProfileAction( SelfProfileAction(
onEditProfile = { onEditProfile = {
navController.navigate( navController.navigate(NavigationRoute.AccountEdit.route)
NavigationRoute.AccountEdit.route
)
}, },
onPremiumClick = { onPremiumClick = {
navController.navigate(NavigationRoute.VipSelPage.route) navController.navigate(NavigationRoute.VipSelPage.route)
@@ -455,24 +384,20 @@ fun ProfileV3(
} }
) )
} }
} }
} }
} }
// 添加用户智能体行(智能体用户不显示) // 用户智能体行
if (!isAiAccount) { if (!isAiAccount) {
UserAgentsRow( UserAgentsRow(
userId = if (isSelf) null else profile?.id, userId = if (isSelf) null else profile?.id,
modifier = Modifier.padding(top = 16.dp), modifier = Modifier.padding(top = 16.dp),
onMoreClick = { onMoreClick = {
// 导航到智能体列表页面 // 导航到智能体列表页面
// TODO: 实现导航逻辑
}, },
onAgentClick = { agent -> onAgentClick = { agent ->
// 导航到智能体详情页面 // 导航到智能体详情页面
// TODO: 实现导航逻辑
}, },
onAvatarClick = { agent -> onAvatarClick = { agent ->
// 导航到智能体个人主页 // 导航到智能体个人主页
@@ -500,100 +425,13 @@ fun ProfileV3(
} }
) )
} }
}
} // 内容
}
}
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.graphicsLayer {
alpha = 1 - state.toolbarState.progress
}
.background(AppColors.profileBackground)
.onGloballyPositioned {
miniToolbarHeight = with(density) {
it.size.height.toDp().value.toInt()
}
}
) {
StatusBarSpacer()
Row(
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 8.dp,
).noRippleClickable {
},
verticalAlignment = Alignment.CenterVertically
) {
if (!isMain) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon), // Replace with your image resource
contentDescription = "Back",
modifier = Modifier
.noRippleClickable {
navController.navigateUp()
}
.size(24.dp),
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(8.dp))
CustomAsyncImage(
LocalContext.current,
profile?.avatar,
modifier = Modifier
.size(32.dp)
.clip(CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = profile?.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = AppColors.text
)
}
Spacer(modifier = Modifier.weight(1f))
if (isSelf&&isMain) {
Box(
modifier = Modifier.noRippleClickable {
IndexViewModel.openDrawer = true
}
) {
Box(
modifier = Modifier
.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
tint = AppColors.text
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(AppColors.profileBackground) .background(AppColors.profileBackground)
.padding(top = 8.dp)
) { ) {
UserContentPageIndicator( UserContentPageIndicator(
pagerState = pagerState, pagerState = pagerState,
@@ -602,6 +440,7 @@ fun ProfileV3(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.height(500.dp) // 固定滚动高度
) { idx -> ) { idx ->
when (idx) { when (idx) {
0 -> 0 ->
@@ -631,15 +470,116 @@ fun ProfileV3(
} }
} }
} }
} }
// 顶部导航栏
TopNavigationBar(
isMain = isMain,
isSelf = isSelf,
profile = profile,
navController = navController,
alpha = toolbarAlpha
)
PullRefreshIndicator( PullRefreshIndicator(
model.refreshing, model.refreshing,
refreshState, refreshState,
Modifier.align(Alignment.TopCenter) Modifier.align(Alignment.TopCenter)
) )
} }
}
//顶部导航栏组件
@Composable
fun TopNavigationBar(
isMain: Boolean,
isSelf: Boolean,
profile: AccountProfileEntity?,
navController: androidx.navigation.NavController,
alpha: Float
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer { this.alpha = alpha } // 应用透明度
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(appColors.profileBackground)
) {
StatusBarSpacer()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.noRippleClickable {
},
verticalAlignment = Alignment.CenterVertically
) {
if (!isMain) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
modifier = Modifier
.noRippleClickable {
navController.navigateUp()
}
.size(24.dp),
colorFilter = ColorFilter.tint(appColors.text)
)
Spacer(modifier = Modifier.width(8.dp))
CustomAsyncImage(
LocalContext.current,
profile?.avatar,
modifier = Modifier
.size(32.dp)
.clip(CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = profile?.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = appColors.text
)
}
Spacer(modifier = Modifier.weight(1f))
if (isSelf && isMain) {
Box(
modifier = Modifier
.size(24.dp)
.padding(16.dp)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
}
if (isSelf && isMain) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 32.dp, end = 16.dp)
.noRippleClickable {
IndexViewModel.openDrawer = true
}
) {
Box(
modifier = Modifier.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
tint = appColors.text
)
}
}
}
}
} }
/** /**

View File

@@ -37,9 +37,16 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils
@Composable @Composable
fun GalleryItem( fun GalleryItem(
moment: MomentEntity, moment: MomentEntity,
@@ -129,8 +136,50 @@ fun GalleryGrid(
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
val debouncer = rememberDebouncer() val debouncer = rememberDebouncer()
var refreshKey by remember { mutableStateOf(0) }
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (moments.isEmpty()) { if (!isNetworkAvailable) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
refreshKey++
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
}
} else if (moments.isEmpty()) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -141,7 +190,7 @@ fun GalleryGrid(
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_dt_qs_as_img id = if(AppState.darkMode) R.mipmap.qs_dt_qs_as_img
else R.mipmap.qs_dt_qs_img), else R.mipmap.invalid_name_7),
contentDescription = "暂无图片", contentDescription = "暂无图片",
modifier = Modifier.size(181.dp), modifier = Modifier.size(181.dp),
) )

View File

@@ -50,8 +50,8 @@ fun OtherProfileAction(
// 定义渐变色 // 定义渐变色
val followGradient = Brush.horizontalGradient( val followGradient = Brush.horizontalGradient(
colors = listOf( colors = listOf(
Color(0xFFE53E3E), // 红色 Color(0xFF7c45ed),
Color(0xFF9F7AEA) // 紫色 Color(0x777c68ef)
) )
) )
@@ -100,7 +100,7 @@ fun OtherProfileAction(
Text( Text(
text = if (profile.isFollowing) "已关注" else stringResource(R.string.follow_upper), text = if (profile.isFollowing) "已关注" else stringResource(R.string.follow_upper),
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W900,
color = if (profile.isFollowing) { color = if (profile.isFollowing) {
// 已关注状态 - 灰色文字 // 已关注状态 - 灰色文字
AppColors.text.copy(alpha = 0.6f) AppColors.text.copy(alpha = 0.6f)
@@ -133,11 +133,37 @@ fun OtherProfileAction(
Text( Text(
text = stringResource(R.string.chat_upper), text = stringResource(R.string.chat_upper),
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W900,
color = AppColors.text, // 使用主题文字颜色 color = AppColors.text, // 使用主题文字颜色
) )
} }
} }
// 分享按钮 - 灰色背景样式
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(8.dp))
.background(AppColors.nonActive)
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
// TODO: 添加分享逻辑
}
}
) {
Text(
text = stringResource(R.string.share),
fontSize = 14.sp,
fontWeight = FontWeight.W900,
color = AppColors.text, // 使用主题文字颜色
)
}
} }
} }

View File

@@ -29,11 +29,13 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable @Composable
fun SelfProfileAction( fun SelfProfileAction(
onEditProfile: () -> Unit, onEditProfile: () -> Unit,
onPremiumClick: (() -> Unit)? = null onPremiumClick: (() -> Unit),
onShare: (() -> Unit)? = null
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val editProfileDebouncer = rememberDebouncer() val editProfileDebouncer = rememberDebouncer()
val premiumClickDebouncer = rememberDebouncer() val premiumClickDebouncer = rememberDebouncer()
val shareDebouncer = rememberDebouncer()
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -46,9 +48,9 @@ fun SelfProfileAction(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(10.dp))
.background(AppColors.nonActive) .background(AppColors.nonActive)
.padding(horizontal = 16.dp, vertical = 12.dp) .padding(horizontal = 5.dp, vertical = 12.dp)
.noRippleClickable { .noRippleClickable {
editProfileDebouncer { editProfileDebouncer {
onEditProfile() onEditProfile()
@@ -58,39 +60,82 @@ fun SelfProfileAction(
Text( Text(
text = stringResource(R.string.edit_profile), text = stringResource(R.string.edit_profile),
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W900,
color = AppColors.text, color = AppColors.text,
) )
} }
// Rave Premium 按钮(右侧) // 预留按钮位置
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(10.dp))
.background(AppColors.premiumBackground)
.padding(horizontal = 16.dp, vertical = 12.dp) .padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable { .noRippleClickable {
premiumClickDebouncer {
onPremiumClick?.invoke() }
) {
Text(
text = "",
fontSize = 14.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
)
}
// 分享按钮
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(10.dp))
.background(AppColors.nonActive)
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
shareDebouncer {
// TODO: 添加分享逻辑
} }
} }
) { ) {
Image(
painter = painterResource(id = R.drawable.ic_member),
contentDescription = "",
modifier = Modifier.size(18.dp),
colorFilter = ColorFilter.tint(AppColors.premiumText)
)
Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = "Rave Premium", text = stringResource(R.string.share),
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W900,
color = AppColors.premiumText, color = AppColors.text,
) )
} }
// // Rave Premium 按钮(右侧)
// Row(
// verticalAlignment = Alignment.CenterVertically,
// horizontalArrangement = Arrangement.Center,
// modifier = Modifier
// .weight(1f)
// .clip(RoundedCornerShape(8.dp))
// .background(AppColors.premiumBackground)
// .padding(horizontal = 16.dp, vertical = 12.dp)
// .noRippleClickable {
// premiumClickDebouncer {
// onPremiumClick?.invoke()
// }
// }
// ) {
// Image(
// painter = painterResource(id = R.drawable.ic_member),
// contentDescription = "",
// modifier = Modifier.size(18.dp),
// colorFilter = ColorFilter.tint(AppColors.premiumText)
// )
// Spacer(modifier = Modifier.width(8.dp))
// Text(
// text = "Rave Premium",
// fontSize = 14.sp,
// fontWeight = FontWeight.W600,
// color = AppColors.premiumText,
// )
// }
} }
} }

View File

@@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -29,6 +30,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -44,7 +46,10 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.DebounceUtils import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.NetworkUtils
@Composable @Composable
fun UserAgentsList( fun UserAgentsList(
@@ -196,6 +201,7 @@ fun UserAgentCard(
@Composable @Composable
fun EmptyAgentsView() { fun EmptyAgentsView() {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
Column( Column(
modifier = Modifier modifier = Modifier
@@ -203,10 +209,11 @@ fun EmptyAgentsView() {
.padding(vertical = 60.dp), .padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (isNetworkAvailable) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qs_ai_qs_as_img id =if(AppState.darkMode) R.mipmap.qs_ai_qs_as_img
else R.mipmap.qs_ai_qs_img), else R.mipmap.ai),
contentDescription = "暂无Agent", contentDescription = "暂无Agent",
modifier = Modifier.size(181.dp), modifier = Modifier.size(181.dp),
) )
@@ -228,5 +235,37 @@ fun EmptyAgentsView() {
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400
) )
} else {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
}
} }
} }

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@@ -28,6 +29,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Tab import androidx.compose.material.Tab
import androidx.compose.material.TabRow import androidx.compose.material.TabRow
@@ -44,6 +47,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -53,6 +57,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -70,6 +75,7 @@ import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -304,6 +310,7 @@ fun MomentResultTab() {
var dataFlow = model.momentsFlow var dataFlow = model.momentsFlow
var moments = dataFlow.collectAsLazyPagingItems() var moments = dataFlow.collectAsLazyPagingItems()
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -317,10 +324,13 @@ fun MomentResultTab() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image( androidx.compose.foundation.Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.syss_yh_qs_img), else R.mipmap.invalid_name_1),
contentDescription = "No Comment", contentDescription = "No Comment",
modifier = Modifier.size(140.dp) modifier = Modifier.size(140.dp)
) )
@@ -337,6 +347,33 @@ fun MomentResultTab() {
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400
) )
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
}
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -369,6 +406,7 @@ fun UserResultTab() {
val model = SearchViewModel val model = SearchViewModel
val users = model.usersFlow.collectAsLazyPagingItems() val users = model.usersFlow.collectAsLazyPagingItems()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -380,10 +418,13 @@ fun UserResultTab() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image( androidx.compose.foundation.Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.syss_yh_qs_img), else R.mipmap.invalid_name_1),
contentDescription = "No Comment", contentDescription = "No Comment",
modifier = Modifier.size(140.dp) modifier = Modifier.size(140.dp)
) )
@@ -400,6 +441,33 @@ fun UserResultTab() {
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400
) )
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
}
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -497,3 +565,42 @@ fun UserItem(
} }
} }
} }
@Composable
fun ReloadButton(
onClick: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
)
}
}
}

View File

@@ -39,15 +39,14 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.AccountLikeEntity import com.aiosman.ravenow.entity.AccountLikeEntity
import com.aiosman.ravenow.exp.timeAgo import com.aiosman.ravenow.exp.timeAgo
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.BottomNavigationPlaceholder import com.aiosman.ravenow.ui.composables.BottomNavigationPlaceholder
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import java.util.Date import java.util.Date
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
@Preview @Preview
@Composable @Composable
fun LikeNoticeScreen() { fun LikeNoticeScreen() {
@@ -72,18 +71,47 @@ fun LikeNoticeScreen() {
.background(color = AppColors.background) .background(color = AppColors.background)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxSize()
.fillMaxWidth() .padding(top=149.dp),
.padding(vertical = 16.dp) contentAlignment = Alignment.TopCenter
) { ) {
NoticeScreenHeader( Column(
stringResource(R.string.like_upper), modifier = Modifier.fillMaxWidth(),
moreIcon = false horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
LikeNoticeViewModel.reload(force = true)
}
) )
} }
}
if (likes.itemCount == 0) { } else if (likes.itemCount == 0) {
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
.padding(top=149.dp), .padding(top=149.dp),
@@ -96,7 +124,7 @@ fun LikeNoticeScreen() {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_z_qs_as_img id =if(AppState.darkMode) R.mipmap.qst_z_qs_as_img
else R.mipmap.qst_z_qs_img), else R.mipmap.invalid_name_6),
contentDescription = "No Notice", contentDescription = "No Notice",
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp)
) )

View File

@@ -279,6 +279,7 @@ fun LoginPage() {
contentDescription = "Rave Now", contentDescription = "Rave Now",
modifier = Modifier modifier = Modifier
.size(52.dp) .size(52.dp)
.clip(RoundedCornerShape(10.dp))
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(

View File

@@ -0,0 +1,64 @@
package com.aiosman.ravenow.ui.network
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.R
@Composable
fun ReloadButton(
onClick: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -0,0 +1,144 @@
package com.aiosman.ravenow.ui.notification
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.follower.FollowerNoticeScreen
import com.aiosman.ravenow.ui.like.LikeNoticeScreen
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NotificationScreen() {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(pageCount = { 3 })
val Debouncer = rememberDebouncer()
Column(
modifier = Modifier
.fillMaxSize()
.background(color = AppColors.background)
) {
StatusBarSpacer()
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
Debouncer {
navController.popBackStack()
}
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(R.string.group_info_notice_setting),
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, top = 8.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
TabItem(
text = stringResource(R.string.like),
isSelected = pagerState.currentPage == 0,
onClick = {
scope.launch {
pagerState.animateScrollToPage(0)
}
}
)
TabSpacer()
TabItem(
text = stringResource(R.string.followers_upper),
isSelected = pagerState.currentPage == 1,
onClick = {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
)
TabSpacer()
TabItem(
text = stringResource(R.string.comment).uppercase(),
isSelected = pagerState.currentPage == 2,
onClick = {
scope.launch {
pagerState.animateScrollToPage(2)
}
}
)
}
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { page ->
when (page) {
0 -> LikeNoticeScreen()
1 -> FollowerNoticeScreen()
2 -> CommentNoticeScreen()
}
}
}
}

View File

@@ -799,7 +799,7 @@ fun CommentContent(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Image( Image(
painter = painterResource(id = R.mipmap.qs_plq_qs_img), painter = painterResource(id = R.mipmap.invalid_name_3),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp)
) )
@@ -917,7 +917,20 @@ fun Header(
Text( Text(
text = nickname ?: "", text = nickname ?: "",
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f), modifier = Modifier
.weight(1f)
.debouncedClickable(debounceTime = 1000L) {
userId?.let {
debouncedNavigation {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
)
}
}
},
color = AppColors.text, color = AppColors.text,
fontSize = 17.sp fontSize = 17.sp
) )
@@ -1196,7 +1209,7 @@ fun PostImageView(
) )
} }
// Navigation and Indicator container // 图片导航控件
if (images.size > 1) { if (images.size > 1) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -1347,91 +1360,19 @@ fun CommentItem(
} }
) {} ) {}
) { ) {
Row { Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text( Text(
text = commentEntity.name, text = commentEntity.name,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 11.sp, fontSize = 11.sp,
color = AppColors.text color = AppColors.text
) )
Spacer(modifier = Modifier.width(8.dp)) Column(
Text( horizontalAlignment = Alignment.End
text = commentEntity.date.timeAgo(context),
fontSize = 11.sp,
color = Color.Gray
)
}
Row (modifier = Modifier.padding(top = 4.dp)){
if (isChild) {
val annotatedText = buildAnnotatedString {
if (commentEntity.replyUserId != null) {
pushStringAnnotation(
tag = "replyUser",
annotation = commentEntity.replyUserId.toString()
)
withStyle(
style = SpanStyle(
fontWeight = FontWeight.W600,
color = Color(0xFF6F94AE)
)
) { ) {
append("@${commentEntity.replyUserNickname}")
}
pop()
}
append(" ${commentEntity.comment}")
}
Box {
CustomClickableText(
text = annotatedText,
onClick = { offset ->
annotatedText.getStringAnnotations(
tag = "replyUser",
start = offset,
end = offset
).firstOrNull()?.let {
debouncedNavigation {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
it.item
)
)
}
}
},
style = TextStyle(fontSize = 14.sp, color = AppColors.text),
onLongPress = {
onLongClick(commentEntity)
},
)
}
} else {
Text(
text = commentEntity.comment,
fontSize = 13.sp,
maxLines = Int.MAX_VALUE,
softWrap = true,
lineHeight = 20.sp,
color = AppColors.text,
modifier = Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onLongClick = {
onLongClick(
commentEntity
)
},
) {
}
)
}
}
Row (modifier = Modifier.padding(top = 12.dp),
verticalAlignment = Alignment.CenterVertically,){
AnimatedLikeIcon( AnimatedLikeIcon(
liked = commentEntity.liked, liked = commentEntity.liked,
onClick = { onClick = {
@@ -1446,29 +1387,54 @@ fun CommentItem(
}, },
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = commentEntity.likes.toString(), text = commentEntity.likes.toString(),
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = AppColors.text color = AppColors.text,
modifier = Modifier.padding(top = 4.dp,end = 4.dp)
) )
Text( }
text = stringResource(R.string.like), }
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = AppColors.nonActiveText,
)
Spacer(modifier = Modifier.width(27.dp))
Icon(
painter = painterResource(id = R.drawable.rider_pro_comment), Text(
contentDescription = "", text = commentEntity.comment,
modifier = Modifier.size(16.dp), fontSize = 13.sp,
tint = AppColors.nonActiveText maxLines = Int.MAX_VALUE,
softWrap = true,
lineHeight = 20.sp,
color = AppColors.text,
modifier = Modifier
.fillMaxWidth()
.padding(end = 50.dp)
.padding(top = 0.dp)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onLongClick = {
onLongClick(
commentEntity
) )
Spacer(modifier = Modifier.width(4.dp)) },
) {
}
)
Row (
modifier = Modifier.padding(top = 12.dp),
verticalAlignment = Alignment.CenterVertically,
){
Text(
text = commentEntity.date.timeAgo(context),
fontSize = 12.sp,
color = Color.Gray
)
Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = stringResource(R.string.reply), text = stringResource(R.string.reply),
fontSize = 12.sp, fontSize = 12.sp,
@@ -1541,6 +1507,7 @@ fun CommentItem(
} }
} }
@Composable @Composable
fun PostBottomBar( fun PostBottomBar(
onCreateCommentClick: () -> Unit = {}, onCreateCommentClick: () -> Unit = {},
@@ -1607,6 +1574,24 @@ fun PostBottomBar(
} }
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
AnimatedFavouriteIcon(
isFavourite = momentEntity?.isFavorite == true,
onClick = {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
onFavoriteClick()
}
},
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(text = momentEntity?.favoriteCount.toString(), color = AppColors.text)
Spacer(modifier = Modifier.width(16.dp))
AnimatedLikeIcon( AnimatedLikeIcon(
liked = momentEntity?.liked == true, liked = momentEntity?.liked == true,
onClick = { onClick = {
@@ -1623,24 +1608,6 @@ fun PostBottomBar(
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text(text = momentEntity?.likeCount.toString(), color = AppColors.text) Text(text = momentEntity?.likeCount.toString(), color = AppColors.text)
Spacer(modifier = Modifier.width(16.dp))
AnimatedFavouriteIcon(
isFavourite = momentEntity?.isFavorite == true,
onClick = {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
onFavoriteClick()
}
},
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(text = momentEntity?.favoriteCount.toString(), color = AppColors.text)
} }
BottomNavigationPlaceholder( BottomNavigationPlaceholder(
color = AppColors.background color = AppColors.background

View File

@@ -1,20 +1,19 @@
package com.aiosman.ravenow.ui.splash package com.aiosman.ravenow.ui.splash
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.size
import androidx.compose.material.Scaffold
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -22,38 +21,36 @@ import com.aiosman.ravenow.R
@Composable @Composable
fun SplashScreen() { fun SplashScreen() {
Scaffold {
it
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
// to bottom // 居中的图标
Box( Image(
contentAlignment = Alignment.TopCenter, painter = painterResource(id = R.mipmap.invalid_name),
modifier = Modifier.padding(top = 211.dp) contentDescription = "App Logo",
) { modifier = Modifier
.align(Alignment.Center)
.size(120.dp)
)
// 底部文字
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth() verticalArrangement = Arrangement.Bottom,
modifier = Modifier
.fillMaxSize()
.padding(bottom = 80.dp)
) { ) {
Image( Image(
painter = painterResource(id = R.mipmap.rider_pro_logo), painterResource(id = R.mipmap.kp_p_img),
contentDescription = "Rave Now", contentDescription = "",
modifier = Modifier modifier = Modifier.size(85.dp, 25.dp)
.width(108.dp)
.height(45.dp)
) )
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.padding(top = 16.dp))
Text( Text(
"Rave Now".uppercase(), stringResource(R.string.splash_title),
fontSize = 28.sp, fontSize = 13.sp
fontWeight = FontWeight.Bold
) )
Text("Your Night Starts Here".uppercase(), fontSize = 20.sp, fontWeight = FontWeight.W700)
} }
}
}
} }
} }

View File

@@ -63,23 +63,6 @@ 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

@@ -1,18 +0,0 @@
<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

@@ -1,36 +0,0 @@
<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

@@ -1,14 +0,0 @@
<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: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Some files were not shown because too many files have changed in this diff Show More