34 Commits

Author SHA1 Message Date
721b7aa9ab Revert "Feat/pr 20251104 154907" 2025-11-05 16:49:17 +08:00
f16be90cc3 Merge pull request #50 from Kevinlinpr/feat/pr-20251104-154907
修改底部导航栏颜色,暗夜模式缺省图
2025-11-05 16:44:23 +08:00
0bbfd9b739 修改底部导航栏颜色,暗夜模式缺省图 2025-11-04 18:59:18 +08:00
13593212df Merge pull request #48 from Kevinlinpr/feat/pr-20251104-154907
Feat/pr 20251104 154907
2025-11-04 16:01:05 +08:00
56f225702e Merge origin/main into feat/pr-20251104-154907 (prefer main); resolve gradlew via theirs 2025-11-04 15:58:50 +08:00
0a5b81cc5c test 2025-11-04 14:55:09 +08:00
10fc40373d test 2025-11-04 14:50:20 +08:00
f18ee9e360 test 2025-11-04 14:40:01 +08:00
9262288bc9 Merge pull request #47 from Kevinlinpr/atm
新增智能体规则相关数据类及API接口,包括创建、修改、删除规则功能和查询规则列表及配额信息
2025-11-04 11:01:25 +08:00
3273b17d15 新增智能体规则相关数据类及API接口,包括创建、修改、删除规则功能和查询规则列表及配额信息 2025-11-04 10:59:59 +08:00
4ef5a94d46 Merge pull request #46 from Kevinlinpr/zhong_1
实现新闻界面查看全文
2025-11-03 10:10:28 +08:00
4a684886fa 聊天自定义背景实现 2025-10-31 16:41:39 +08:00
d7f87c7c55 新增聊天设置页面;自定义背景UI 2025-10-31 11:57:49 +08:00
00933dadb8 实现新闻界面查看全文 2025-10-29 18:08:24 +08:00
d5cc186e27 Merge pull request #45 from Kevinlinpr/zhong_1
登录界面UI调整;新增新闻评论
2025-10-28 18:45:22 +08:00
90156745ad 添加新闻接口,UI调整 2025-10-28 18:42:05 +08:00
7095832722 登录界面UI调整;新增新闻评论 2025-10-27 18:51:42 +08:00
658b337d22 Merge pull request #44 from Kevinlinpr/zhong_1
动态、热门界面UI调整
2025-10-27 11:16:47 +08:00
f6a760371a 新增新闻标签页;修改底部导航栏背景颜色 2025-10-24 18:27:58 +08:00
13ed16078b 新增苹果账户登录;新增拉黑功能;UI调整 2025-10-23 17:56:24 +08:00
2a5174cbb6 动态、热门界面UI调整 2025-10-22 18:52:46 +08:00
ea26cb40a0 Merge pull request #43 from Kevinlinpr/zhong_1
热门聊天室实现;首页UI调整
2025-10-21 21:39:20 +08:00
eb8119b775 动态页面顶部标签 2025-10-21 18:35:22 +08:00
66da741eda 登录页面UI调整 2025-10-21 17:19:32 +08:00
18dd52e193 聊天室显示个数 2025-10-20 17:05:48 +08:00
f839a793a3 热门聊天室实现;首页UI调整 2025-10-20 16:54:23 +08:00
54df6c088b Merge pull request #41 from Kevinlinpr/home_page
主页推荐agent实现
2025-10-17 17:23:28 +08:00
28fb94a824 顶部 推荐 agent 列表实现 2025-10-17 17:21:52 +08:00
4bdbbb0231 主页分类动态获取 2025-10-17 16:56:25 +08:00
58a2013a8f Merge pull request #40 from Kevinlinpr/lottie_deps
lottie 依赖
2025-10-17 16:11:15 +08:00
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
0442925ae9 Merge pull request #37 from Kevinlinpr/zhong
解决问题:首页智能体头像为默认头像时显示为空
2025-10-14 18:20:22 +08:00
bd01ae39d0 Merge pull request #36 from Kevinlinpr/zhong
UI调整
2025-10-10 21:42:24 +08:00
108 changed files with 3498 additions and 1010 deletions

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -5,7 +5,8 @@ object ConstVars {
// Debug: http://192.168.0.201:8088
// Release: https://rider-pro.aiosman.com/beta_api
val BASE_SERVER = if (BuildConfig.DEBUG) {
"http://47.109.137.67:6363" // Debug环境
// "http://47.109.137.67:6363" // Debug环境
"https://rider-pro.aiosman.com/beta_api" // Release环境
} else {
"https://rider-pro.aiosman.com/beta_api" // Release环境
}

View File

@@ -32,6 +32,21 @@ data class Moment(
val time: String,
@SerializedName("isFollowed")
val isFollowed: Boolean,
// 新闻相关字段
@SerializedName("isNews")
val isNews: Boolean = false,
@SerializedName("newsTitle")
val newsTitle: String? = null,
@SerializedName("newsUrl")
val newsUrl: String? = null,
@SerializedName("newsSource")
val newsSource: String? = null,
@SerializedName("newsCategory")
val newsCategory: String? = null,
@SerializedName("newsLanguage")
val newsLanguage: String? = null,
@SerializedName("newsContent")
val newsContent: String? = null,
) {
fun toMomentItem(): MomentEntity {
return MomentEntity(
@@ -60,6 +75,14 @@ data class Moment(
authorId = user.id.toInt(),
liked = isLiked,
isFavorite = isFavorite,
// 新闻相关字段
isNews = isNews,
newsTitle = newsTitle ?: "",
newsUrl = newsUrl ?: "",
newsSource = newsSource ?: "",
newsCategory = newsCategory ?: "",
newsLanguage = newsLanguage ?: "",
newsContent = newsContent ?: ""
)
}
}

View File

@@ -67,7 +67,7 @@ data class SendChatAiRequestBody(
@SerializedName("skipTrtc")
val skipTrtc: Boolean? = true,
)
)
data class CreateGroupChatRequestBody(
@@ -77,7 +77,7 @@ data class CreateGroupChatRequestBody(
val userIds: List<String>,
@SerializedName("promptIds")
val promptIds: List<String>,
)
)
data class JoinGroupChatRequestBody(
@SerializedName("trtcId")
@@ -392,11 +392,13 @@ data class GroupChatResponse(
@SerializedName("source")
val source: String
)
class CategoryTemplateTranslation(
data class CategoryTranslation(
@SerializedName("name")
val name: String,
val name: String?,
@SerializedName("description")
val description: String,
val description: String?
)
data class CategoryTemplate(
@@ -425,18 +427,196 @@ data class CategoryTemplate(
@SerializedName("updatedAt")
val updatedAt: String,
@SerializedName("translations")
val translations: Map<String, CategoryTemplateTranslation>?
val translations: Map<String, CategoryTranslation>?
) {
/**
* 获取本地化名称,优先使用当前语言的翻译,如果没有则使用默认名称
* 根据语言代码获取翻译后的名称,如果没有翻译则返回默认名称
*/
fun getLocalizedName(): String {
// 这里可以根据需要添加国际化逻辑
// 目前直接返回默认名称
fun getLocalizedName(lang: String): String {
// 尝试获取完整的语言标记(如 "zh-CN"
val translation = translations?.get(lang)
if (translation?.name != null && translation.name.isNotEmpty()) {
return translation.name
}
// 如果没有找到,尝试语言代码的前缀(如 "zh"
val langPrefix = lang.split("-", "_").firstOrNull()
if (langPrefix != null) {
translations?.entries?.forEach { (key, value) ->
if (key.startsWith(langPrefix) && value.name != null && value.name.isNotEmpty()) {
return value.name
}
}
}
// 如果没有翻译,返回默认名称
return name
}
/**
* 根据语言代码获取翻译后的描述,如果没有翻译则返回默认描述
*/
fun getLocalizedDescription(lang: String): String {
// 尝试获取完整的语言标记(如 "zh-CN"
val translation = translations?.get(lang)
if (translation?.description != null && translation.description.isNotEmpty()) {
return translation.description
}
// 如果没有找到,尝试语言代码的前缀(如 "zh"
val langPrefix = lang.split("-", "_").firstOrNull()
if (langPrefix != null) {
translations?.entries?.forEach { (key, value) ->
if (key.startsWith(langPrefix) && value.description != null && value.description.isNotEmpty()) {
return value.description
}
}
}
// 如果没有翻译,返回默认描述
return description
}
}
data class CategoryListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Int,
@SerializedName("list")
val list: List<CategoryTemplate>
)
// ========== Prompt Rule 相关数据类 ==========
/**
* 创建规则请求体
* @param rule 规则内容,不能为空
* @param promptId 智能体ID与 openId 二选一promptId 优先
* @param openId 智能体的 OpenIDUUID格式与 promptId 二选一
*/
data class CreatePromptRuleRequestBody(
@SerializedName("rule")
val rule: String,
@SerializedName("promptId")
val promptId: Int? = null,
@SerializedName("openId")
val openId: String? = null
)
/**
* 修改规则请求体
* @param id 规则ID必填
* @param rule 新的规则内容,不能为空
* @param promptId 要更改关联的智能体ID可选
* @param openId 要更改关联的智能体 OpenID可选
*/
data class UpdatePromptRuleRequestBody(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String,
@SerializedName("promptId")
val promptId: Int? = null,
@SerializedName("openId")
val openId: String? = null
)
/**
* 规则关联的智能体信息
* @param id 智能体ID
* @param title 智能体标题
* @param avatar 智能体头像URL
*/
data class PromptRuleAgent(
@SerializedName("id")
val id: Int,
@SerializedName("title")
val title: String,
@SerializedName("avatar")
val avatar: String
)
/**
* 规则详情
* @param id 规则ID
* @param rule 规则内容
* @param creator 创建者名称
* @param creatorType 创建者类型(如 "user"
* @param scope 作用域(如 "personal"
* @param prompt 关联的智能体信息
* @param createdAt 创建时间ISO 8601 格式)
* @param updatedAt 更新时间ISO 8601 格式)
*/
data class PromptRule(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String,
@SerializedName("creator")
val creator: String,
@SerializedName("creator_type")
val creatorType: String,
@SerializedName("scope")
val scope: String,
@SerializedName("prompt")
val prompt: PromptRuleAgent,
@SerializedName("created_at")
val createdAt: String,
@SerializedName("updated_at")
val updatedAt: String
)
/**
* 规则列表响应
* @param page 当前页码
* @param pageSize 每页数量
* @param total 总记录数
* @param list 规则列表
*/
data class PromptRuleListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Int,
@SerializedName("list")
val list: List<PromptRule>
)
/**
* 规则配额信息
* @param promptId 智能体ID
* @param promptTitle 智能体标题
* @param baseMaxCount 基础条数限制(免费配额)
* @param purchasedCount 用户购买的额外条数
* @param totalMaxCount 总可用条数(基础+购买)
* @param currentCount 当前已创建的规则条数
* @param remainingCount 剩余可用条数
* @param usagePercent 使用百分比0-100
*/
data class PromptRuleQuota(
@SerializedName("promptId")
val promptId: Int,
@SerializedName("promptTitle")
val promptTitle: String,
@SerializedName("baseMaxCount")
val baseMaxCount: Int,
@SerializedName("purchasedCount")
val purchasedCount: Int,
@SerializedName("totalMaxCount")
val totalMaxCount: Int,
@SerializedName("currentCount")
val currentCount: Int,
@SerializedName("remainingCount")
val remainingCount: Int,
@SerializedName("usagePercent")
val usagePercent: Double
)
interface RaveNowAPI {
@GET("membership/config")
@retrofit2.http.Headers("X-Requires-Auth: true")
@@ -475,6 +655,7 @@ interface RaveNowAPI {
@Query("trend") trend: String? = null,
@Query("favouriteUserId") favouriteUserId: Int? = null,
@Query("explore") explore: String? = null,
@Query("newsFilter") newsFilter: String? = null,
): Response<ListContainer<Moment>>
@Multipart
@@ -716,26 +897,10 @@ interface RaveNowAPI {
suspend fun getAgent(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("order") order: String? = null,
@Query("orderKey") orderKey: String? = null,
@Query("createdAt") createdAt: String? = null,
@Query("updatedAt") updatedAt: String? = null,
@Query("createdStart") createdStart: String? = null,
@Query("createdEnd") createdEnd: String? = null,
@Query("updatedStart") updatedStart: String? = null,
@Query("updatedEnd") updatedEnd: String? = null,
@Query("title") title: String? = null,
@Query("withWorkflow") withWorkflow: Int = 1,
@Query("authorId") authorId: Int? = null,
@Query("authorOpenId") authorOpenId: String? = null,
@Query("showPrivate") showPrivate: String? = null,
@Query("explore") explore: String? = null,
@Query("desc") desc: String? = null,
@Query("withWorkflow") withWorkflow: String? = null,
@Query("hasAvatar") hasAvatar: String? = null,
@Query("random") random: String? = null,
@Query("categoryName") categoryName: String? = null,
@Query("categoryIds") categoryIds: List<Int>? = null,
@Query("uncategorized") uncategorized: String? = null,
@Query("random") random: Int? = null,
): Response<DataContainer<ListContainer<Agent>>>
@GET("outside/my/prompts")
@@ -803,8 +968,9 @@ interface RaveNowAPI {
@Query("withChildren") withChildren: Boolean? = null,
@Query("withParent") withParent: Boolean? = null,
@Query("withCount") withCount: Boolean? = null,
@Query("hideEmpty") hideEmpty: Boolean? = null
): Response<ListContainer<CategoryTemplate>>
@Query("hideEmpty") hideEmpty: Boolean? = null,
@Query("lang") lang: String? = null
): Response<CategoryListResponse>
@GET("outside/categories/tree")
suspend fun getCategoryTree(
@@ -817,6 +983,215 @@ interface RaveNowAPI {
@Path("id") id: Int
): Response<DataContainer<CategoryTemplate>>
@GET("outside/prompts")
suspend fun getPromptsByCategory(
@Query("categoryIds") categoryIds: List<Int>? = null,
@Query("categoryName") categoryName: String? = null,
@Query("uncategorized") uncategorized: String? = null,
@Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null
): Response<ListContainer<Agent>>
// ========== Prompt Rule API ==========
/**
* 创建智能体规则
*
* 功能说明:
* - 为指定的智能体创建一条新规则
* - 规则必须关联到存在的智能体(通过 promptId 或 openId 指定)
* - 创建前会自动检查配额限制,如果超出会尝试自动扩容
* - 只有创建者可以修改和删除该规则
*
* @param body 创建规则请求体
* - rule: 规则内容,不能为空
* - promptId: 智能体ID与 openId 二选一promptId 优先)
* - openId: 智能体的 OpenIDUUID格式与 promptId 二选一)
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 示例:
* ```kotlin
* val request = CreatePromptRuleRequestBody(
* rule = "禁止讨论政治话题",
* promptId = 123
* )
* val response = api.createPromptRule(request)
* ```
*/
@POST("outside/prompt/rule")
suspend fun createPromptRule(
@Body body: CreatePromptRuleRequestBody
): Response<Unit>
/**
* 修改智能体规则
*
* 功能说明:
* - 修改已存在的规则内容或关联的智能体
* - 只有规则的创建者可以修改
* - 可以同时修改规则内容和关联的智能体
* - 修改关联智能体时会重新验证配额限制
*
* @param body 修改规则请求体
* - id: 规则ID必填
* - rule: 新的规则内容,不能为空
* - promptId: 要更改关联的智能体ID可选
* - openId: 要更改关联的智能体 OpenID可选
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者creator_type 为 "user" 且 create_id 匹配)
*
* 示例:
* ```kotlin
* val request = UpdatePromptRuleRequestBody(
* id = 456,
* rule = "禁止讨论政治和敏感话题"
* )
* val response = api.updatePromptRule(request)
* ```
*/
@retrofit2.http.PUT("outside/prompt/rule")
suspend fun updatePromptRule(
@Body body: UpdatePromptRuleRequestBody
): Response<Unit>
/**
* 删除智能体规则
*
* 功能说明:
* - 删除指定的规则
* - 只有规则的创建者可以删除
* - 删除操作不可恢复,请谨慎操作
*
* @param id 规则ID
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者creator_type 为 "user" 且 create_id 匹配)
*
* 示例:
* ```kotlin
* val response = api.deletePromptRule(456)
* ```
*/
@DELETE("outside/prompt/rule/{id}")
suspend fun deletePromptRule(
@Path("id") id: Int
): Response<Unit>
/**
* 查询智能体规则列表
*
* 功能说明:
* - 查询指定智能体下当前用户创建的规则列表
* - 支持分页和关键词模糊搜索
* - 只返回当前用户创建的规则,不会返回其他用户的规则
*
* @param promptId 智能体ID支持数字ID或UUID格式的 openId
* @param rule 规则内容关键词(模糊搜索),可选
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
*
* @return 返回分页的规则列表,包含规则详情和关联的智能体信息
*
* 响应数据说明:
* - page: 当前页码
* - pageSize: 每页数量
* - total: 总记录数
* - list: 规则列表
* - id: 规则ID
* - rule: 规则内容
* - creator: 创建者名称
* - creator_type: 创建者类型
* - scope: 作用域
* - prompt: 关联的智能体信息id, title, avatar
* - created_at: 创建时间
* - updated_at: 更新时间
*
* 示例:
* ```kotlin
* // 使用数字ID查询
* val response1 = api.getPromptRuleList("123", page = 1, pageSize = 10)
*
* // 使用 OpenID 查询
* val response2 = api.getPromptRuleList(
* "550e8400-e29b-41d4-a716-446655440000",
* page = 1,
* pageSize = 10
* )
*
* // 带关键词搜索
* val response3 = api.getPromptRuleList(
* "123",
* rule = "政治",
* page = 1,
* pageSize = 10
* )
* ```
*/
@GET("outside/prompt/{promptId}/rule/list")
suspend fun getPromptRuleList(
@Path("promptId") promptId: String,
@Query("rule") rule: String? = null,
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 10
): Response<DataContainer<PromptRuleListResponse>>
/**
* 查询智能体规则配额信息
*
* 功能说明:
* - 查询指定智能体的规则条数使用情况
* - 包括基础配额、已购买配额、已使用数量等完整信息
* - 用于判断是否还能创建新规则
*
* @param promptId 智能体ID支持数字ID或UUID格式的 openId
*
* @return 返回配额详细信息
*
* 响应数据说明:
* - promptId: 智能体ID
* - promptTitle: 智能体标题
* - baseMaxCount: 基础条数限制(免费配额,由智能体等级决定)
* - purchasedCount: 用户购买的额外条数
* - totalMaxCount: 总可用条数baseMaxCount + purchasedCount
* - currentCount: 当前已创建的规则条数(只统计当前用户创建的)
* - remainingCount: 剩余可用条数totalMaxCount - currentCount
* - usagePercent: 使用百分比0-100currentCount / totalMaxCount * 100
*
* 使用场景:
* 1. 创建规则前检查是否有足够配额
* 2. 展示规则使用情况统计
* 3. 提示用户购买额外配额
*
* 示例:
* ```kotlin
* // 使用数字ID查询
* val response1 = api.getPromptRuleQuota("123")
*
* // 使用 OpenID 查询
* val response2 = api.getPromptRuleQuota("550e8400-e29b-41d4-a716-446655440000")
*
* // 处理响应
* response1.body()?.data?.let { quota ->
* if (quota.remainingCount > 0) {
* // 可以创建新规则
* println("还可以创建 ${quota.remainingCount} 条规则")
* } else {
* // 配额已用完
* println("规则配额已用完,已使用 ${quota.currentCount}/${quota.totalMaxCount}")
* }
* }
* ```
*/
@GET("outside/prompt/{promptId}/rule/count")
suspend fun getPromptRuleQuota(
@Path("promptId") promptId: String
): Response<DataContainer<PromptRuleQuota>>
}

View File

@@ -299,12 +299,21 @@ data class MomentEntity(
// 关联动态
var relMoment: MomentEntity? = null,
// 是否收藏
var isFavorite: Boolean = false
var isFavorite: Boolean = false,
// 新闻相关字段
val isNews: Boolean = false,
val newsTitle: String = "",
val newsUrl: String = "",
val newsSource: String = "",
val newsCategory: String = "",
val newsLanguage: String = "",
val newsContent: String = ""
)
class MomentLoaderExtraArgs(
val explore: Boolean? = false,
val timelineId: Int? = null,
val authorId : Int? = null
val authorId : Int? = null,
val newsOnly: Boolean? = null
)
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
override suspend fun fetchData(
@@ -317,7 +326,8 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
pageSize = pageSize,
explore = if (extra.explore == true) "true" else "",
timelineId = extra.timelineId,
authorId = extra.authorId
authorId = extra.authorId,
newsFilter = if (extra.newsOnly == true) "news_only" else ""
)
val data = result.body()?.let {
ListContainer(
@@ -355,6 +365,18 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
onListChanged?.invoke(this.list)
}
fun updateCommentCount(id: Int, delta: Int) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
val newCount = (momentItem.commentCount + delta).coerceAtLeast(0)
momentItem.copy(commentCount = newCount)
} else {
momentItem
}
}.toMutableList()
onListChanged?.invoke(this.list)
}
fun removeMoment(id: Int) {
this.list = this.list.filter { it.id != id }.toMutableList()
onListChanged?.invoke(this.list)

View File

@@ -30,6 +30,12 @@ object AppStore {
AppState.appTheme = DarkThemeColors()
}
// load chat background
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
if (savedBgUrl != null) {
AppState.chatBackgroundUrl = savedBgUrl
}
}
suspend fun saveData() {
@@ -54,5 +60,15 @@ object AppStore {
}.apply()
}
fun saveChatBackgroundUrl(url: String?) {
sharedPreferences.edit().apply {
if (url != null) {
putString("chatBackgroundUrl", url)
} else {
remove("chatBackgroundUrl")
}
}.apply()
AppState.chatBackgroundUrl = url
}
}

View File

@@ -41,6 +41,7 @@ import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
import com.aiosman.ravenow.ui.chat.ChatScreen
import com.aiosman.ravenow.ui.chat.GroupChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen
@@ -106,6 +107,7 @@ sealed class NavigationRoute(
data object FavouriteList : NavigationRoute("FavouriteList")
data object Chat : NavigationRoute("Chat/{id}")
data object ChatAi : NavigationRoute("ChatAi/{id}")
data object ChatSetting : NavigationRoute("ChatSetting")
data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
data object ImageCrop : NavigationRoute("ImageCrop")
@@ -491,6 +493,14 @@ fun NavigationController(
}
}
composable(route = NavigationRoute.ChatSetting.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
ChatSettingScreen()
}
}
composable(
route = NavigationRoute.ChatGroup.route,
arguments = listOf(navArgument("id") { type = NavType.StringType },

View File

@@ -79,11 +79,12 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.DropdownMenu
import com.aiosman.ravenow.ui.composables.MenuItem
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.AppState
import androidx.compose.ui.layout.ContentScale
import io.openim.android.sdk.enums.MessageType
import kotlinx.coroutines.launch
import java.util.UUID
@@ -91,10 +92,10 @@ import java.util.UUID
@Composable
fun ChatAiScreen(userId: String) {
var isMenuExpanded by remember { mutableStateOf(false) }
val navController = LocalNavController.current
val context = LocalNavController.current.context
val AppColors = LocalAppTheme.current
val chatBackgroundUrl = AppState.chatBackgroundUrl
var goToNewCount by remember { mutableStateOf(0) }
val viewModel = viewModel<ChatAiViewModel>(
key = "ChatAiViewModel_$userId",
@@ -158,14 +159,25 @@ fun ChatAiScreen(userId: String) {
}
Box(modifier = Modifier.fillMaxSize()) {
if (chatBackgroundUrl != null && chatBackgroundUrl.isNotEmpty()) {
CustomAsyncImage(
imageUrl = chatBackgroundUrl,
modifier = Modifier.fillMaxSize(),
contentDescription = "chat_background",
contentScale = ContentScale.Crop
)
}
Scaffold(
modifier = Modifier
.fillMaxSize(),
backgroundColor = Color.Transparent,
topBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.background(Color.Transparent)
) {
StatusBarSpacer()
Row(
@@ -213,49 +225,24 @@ fun ChatAiScreen(userId: String) {
modifier = Modifier
.size(28.dp)
.noRippleClickable {
isMenuExpanded = true
navController.navigate(NavigationRoute.ChatSetting.route)
},
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
)
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
menuItems = listOf(
MenuItem(
title = if (viewModel.notificationStrategy == "mute") "Unmute" else "Mute",
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
) {
isMenuExpanded = false
if (NetworkUtils.isNetworkAvailable(context)) {
viewModel.viewModelScope.launch {
if (viewModel.notificationStrategy == "mute") {
viewModel.updateNotificationStrategy("active")
} else {
viewModel.updateNotificationStrategy("mute")
}
}
} else {
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
}
}
),
)
}
}
}
},
bottomBar = {
val hasChatBackground = AppState.chatBackgroundUrl != null && AppState.chatBackgroundUrl!!.isNotEmpty()
Column(
modifier = Modifier
.fillMaxWidth()
.imePadding()
) {
if (!hasChatBackground) {
Box(
modifier = Modifier
.fillMaxWidth()
@@ -263,6 +250,7 @@ fun ChatAiScreen(userId: String) {
.background(
AppColors.decentBackground)
)
}
Spacer(modifier = Modifier.height(8.dp))
ChatAiInput(
onSendImage = {
@@ -283,7 +271,7 @@ fun ChatAiScreen(userId: String) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.background(Color.Transparent)
.padding(paddingValues)
) {
LazyColumn(
@@ -346,8 +334,7 @@ fun ChatAiScreen(userId: String) {
}
}
}
}
}
}
@@ -571,7 +558,8 @@ fun ChatAiInput(
}
Box( modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
.background(Color.Transparent)
.padding(start = 16.dp, end = 16.dp, bottom = 45.dp),){
Row(
modifier = Modifier

View File

@@ -64,6 +64,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
@@ -74,10 +75,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.DropdownMenu
import com.aiosman.ravenow.ui.composables.MenuItem
@@ -91,7 +94,6 @@ import java.util.UUID
@Composable
fun ChatScreen(userId: String) {
var isMenuExpanded by remember { mutableStateOf(false) }
val navController = LocalNavController.current
val context = LocalNavController.current.context
val AppColors = LocalAppTheme.current
@@ -158,14 +160,37 @@ fun ChatScreen(userId: String) {
}
Box(
modifier = Modifier
.fillMaxSize()
) {
// 背景图层
val bgUrl = AppState.chatBackgroundUrl
if (bgUrl != null) {
CustomAsyncImage(
imageUrl = bgUrl,
modifier = Modifier.fillMaxSize(),
contentDescription = "chat_background",
contentScale = ContentScale.Crop
)
} else {
// 无背景时使用主题背景色
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
)
}
Scaffold(
modifier = Modifier
.fillMaxSize(),
backgroundColor = Color.Transparent,
topBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.background(Color.Transparent)
) {
StatusBarSpacer()
Row(
@@ -214,39 +239,12 @@ fun ChatScreen(userId: String) {
modifier = Modifier
.size(28.dp)
.noRippleClickable {
isMenuExpanded = true
navController.navigate(NavigationRoute.ChatSetting.route)
},
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
)
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
menuItems = listOf(
MenuItem(
title = if (viewModel.notificationStrategy == "mute") "取消静音" else "静音",
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
) {
isMenuExpanded = false
if (NetworkUtils.isNetworkAvailable(context)) {
viewModel.viewModelScope.launch {
if (viewModel.notificationStrategy == "mute") {
viewModel.updateNotificationStrategy("active")
} else {
viewModel.updateNotificationStrategy("mute")
}
}
} else {
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
}
}
),
)
}
}
}
@@ -283,7 +281,7 @@ fun ChatScreen(userId: String) {
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
.background(Color.Transparent)
.padding(paddingValues)
) {
LazyColumn(
@@ -347,6 +345,7 @@ fun ChatScreen(userId: String) {
}
}
}
}
@@ -572,7 +571,7 @@ fun ChatInput(
}
Box( modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
.padding(start = 16.dp, end = 16.dp, bottom = 45.dp),){
Row(
modifier = Modifier

View File

@@ -0,0 +1,362 @@
package com.aiosman.ravenow.ui.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
@Composable
fun ChatSettingScreen() {
val appColors = LocalAppTheme.current
val navController = LocalNavController.current
var showThemeSheet by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.secondaryBackground)
) {
StatusBarSpacer()
Box(modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)) {
NoticeScreenHeader(title = stringResource(R.string.chat_settings), moreIcon = false)
}
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
SettingCard(
title = stringResource(R.string.chat_theme_settings),
onClick = { showThemeSheet = true }
)
Spacer(modifier = Modifier.height(12.dp))
SettingCard(
title = stringResource(R.string.report),
onClick = { /* TODO: 跳转举报 */ }
)
}
}
if (showThemeSheet) {
ThemePickerSheet(onClose = { showThemeSheet = false })
}
}
@Composable
private fun SettingCard(title: String, onClick: () -> Unit) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(appColors.background)
.clickable { onClick() }
.padding(horizontal = 12.dp, vertical = 14.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Text(
text = title,
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.weight(1f)
)
Icon(
painter = painterResource(id = R.drawable.rave_now_nav_right),
contentDescription = null,
tint = appColors.text,
modifier = Modifier.size(20.dp)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ThemePickerSheet(onClose: () -> Unit) {
val appColors = LocalAppTheme.current
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.9f
var previewUrl by remember { mutableStateOf<String?>(null) }
ModalBottomSheet(
onDismissRequest = onClose,
sheetState = sheetState,
containerColor = appColors.secondaryBackground,
dragHandle = null,
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.custom_background),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
modifier = Modifier.weight(1f).padding(start = 90.dp),
)
IconButton(onClick = onClose) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_close),
contentDescription = "close",
tint = appColors.text
)
}
}
// 从相册选择
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(appColors.background)
.clickable { /* TODO: 打开相册选择 */ }
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.select_from_gallery), color = appColors.text, fontSize = 15.sp, modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.drawable.group_info_edit),
contentDescription = null,
tint = appColors.text,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(text = stringResource(R.string.featured_backgrounds), color = appColors.text, fontSize = 12.sp)
Spacer(modifier = Modifier.height(8.dp))
val presets = remember {
listOf(
"https://picsum.photos/seed/ai1/400/600",
"https://picsum.photos/seed/ai2/400/600",
"https://picsum.photos/seed/ai3/400/600",
"https://picsum.photos/seed/ai4/400/600",
"https://picsum.photos/seed/ai5/400/600",
"https://picsum.photos/seed/ai6/400/600",
"https://picsum.photos/seed/ai7/400/600",
"https://picsum.photos/seed/ai8/400/600",
"https://picsum.photos/seed/ai9/400/600",
"https://picsum.photos/seed/ai10/400/600",
"https://picsum.photos/seed/ai11/400/600",
"https://picsum.photos/seed/ai12/400/600",
"https://picsum.photos/seed/ai13/400/600",
"https://picsum.photos/seed/ai14/400/600",
"https://picsum.photos/seed/ai15/400/600",
)
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
) {
items(presets) { url ->
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { previewUrl = url }
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(appColors.decentBackground)
) {
CustomAsyncImage(
imageUrl = url,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 4f),
contentDescription = "preset",
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "Heart Drive",
color = appColors.text,
fontSize = 12.sp
)
}
}
}
// 预览自定义背景弹窗
if (previewUrl != null) {
ModalBottomSheet(
onDismissRequest = { previewUrl = null },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = appColors.secondaryBackground,
dragHandle = null,
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight)
) {
Box(modifier = Modifier.fillMaxSize()) {
CustomAsyncImage(
imageUrl = previewUrl!!,
modifier = Modifier.fillMaxSize(),
contentDescription = "preview_bg",
contentScale = ContentScale.Crop
)
Column(modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 8.dp)) {
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = 12.dp, vertical = 16.dp)
) {
Text(text = stringResource(R.string.previewing_custom_background), color = Color.White, fontSize = 15.sp)
}
Column(modifier = Modifier.padding(8.dp)) {
Row {
Box(
modifier = Modifier
.clip(RoundedCornerShape(24.dp))
.background(Color.White)
.padding(horizontal = 14.dp, vertical = 8.dp)
) {
Text(text = stringResource(R.string.each_theme_unique_experience), color = Color.Black, fontSize = 12.sp)
}
}
Spacer(modifier = Modifier.height(12.dp))
Row {
Box(
modifier = Modifier
.clip(RoundedCornerShape(24.dp))
.background(Color.White)
.padding(horizontal = 14.dp, vertical = 8.dp)
) {
Text(text = stringResource(R.string.select_apply_to_use_theme), color = Color.Black, fontSize = 12.sp)
}
}
Spacer(modifier = Modifier.height(12.dp))
Row {
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.clip(RoundedCornerShape(20.dp))
.background(Color(0xFF7C4DFF))
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(text = stringResource(R.string.tap_cancel_to_preview_other_themes), color = Color.White, fontSize = 12.sp)
}
}
}
Spacer(modifier = Modifier.weight(1f))
// 底部按钮
Row(
modifier = Modifier.fillMaxWidth()
.padding(bottom = 60.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
.clickable { previewUrl = null }
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
Text(text = stringResource(R.string.cancel), color = Color.Black, fontSize = 14.sp)
}
Spacer(modifier = Modifier.size(12.dp))
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.background(
brush = Brush.horizontalGradient(
colors = listOf(
Color(0xFFEE2A33),
Color(0xFFD80264),
Color(0xFF664C92)
)
)
)
.clickable {
previewUrl?.let { url ->
com.aiosman.ravenow.AppStore.saveChatBackgroundUrl(url)
previewUrl = null
onClose()
}
}
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
Text(text = stringResource(R.string.moment_ai_apply), color = Color.White, fontSize = 14.sp)
}
}
}
}
}
}
}
}
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -32,6 +33,7 @@ fun ActionButton(
text: String,
color: Color? = null,
backgroundColor: Color? = null,
backgroundBrush: Brush? = null,
leading: @Composable (() -> Unit)? = null,
expandText: Boolean = false,
contentPadding: PaddingValues = PaddingValues(vertical = 16.dp),
@@ -65,7 +67,11 @@ fun ActionButton(
Box(
modifier = modifier
.clip(RoundedCornerShape(roundCorner.dp))
.background(animatedBackgroundColor)
.background(
brush = backgroundBrush ?: Brush.linearGradient(
colors = listOf(animatedBackgroundColor, animatedBackgroundColor)
)
)
.noRippleClickable {
if (enabled && !isLoading) {
click()

View File

@@ -56,15 +56,14 @@ fun AnimatedFavouriteIcon(
}) {
Image(
painter = if (isFavourite) {
painterResource(id = R.drawable.rider_pro_favourited)
painterResource(id = R.mipmap.icon_variant_2)
} else {
painterResource(id = R.drawable.rider_pro_favourite)
painterResource(id = R.mipmap.icon_collect)
},
contentDescription = "Favourite",
modifier = modifier.graphicsLayer {
rotationZ = animatableRotation.value
},
colorFilter = ColorFilter.tint(AppColors.text)
)
}
}

View File

@@ -51,7 +51,8 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun EditCommentBottomModal(
replyComment: CommentEntity? = null,
onSend: (String) -> Unit = {}
autoFocus: Boolean = false,
onSend: (String) -> Unit = {},
) {
val AppColors = LocalAppTheme.current
var text by remember { mutableStateOf("") }
@@ -59,9 +60,11 @@ fun EditCommentBottomModal(
val focusRequester = remember { FocusRequester() }
val context = LocalContext.current
LaunchedEffect(Unit) {
LaunchedEffect(autoFocus) {
if (autoFocus) {
focusRequester.requestFocus()
}
}
Column(
modifier = Modifier

View File

@@ -364,16 +364,6 @@ fun MomentContentGroup(
onPageChange: (Int) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
if (momentEntity.momentTextContent.isNotEmpty()) {
Text(
text = momentEntity.momentTextContent,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
fontSize = 16.sp,
color = AppColors.text
)
}
if (momentEntity.relMoment != null) {
RelPostCard(
momentEntity = momentEntity.relMoment!!,
@@ -389,6 +379,17 @@ fun MomentContentGroup(
)
}
}
if (momentEntity.momentTextContent.isNotEmpty()) {
Text(
text = momentEntity.momentTextContent,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
fontSize = 16.sp,
color = AppColors.text
)
}
}
@@ -401,8 +402,8 @@ fun MomentOperateBtn(@DrawableRes icon: Int, count: String) {
.size(width = 24.dp, height = 24.dp),
painter = painterResource(id = icon),
contentDescription = "",
colorFilter = ColorFilter.tint(AppColors.text)
)
if (count.isNotEmpty()) {
Text(
text = count,
modifier = Modifier.padding(start = 7.dp),
@@ -410,6 +411,7 @@ fun MomentOperateBtn(@DrawableRes icon: Int, count: String) {
color = AppColors.text
)
}
}
}
@Composable
@@ -510,49 +512,11 @@ fun MomentBottomOperateRowGroup(
.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
Row(
modifier = Modifier.weight(1f).fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
// 点赞按钮
MomentOperateBtn(count = momentEntity.likeCount.toString()) {
AnimatedLikeIcon(
modifier = Modifier.size(24.dp),
@@ -561,6 +525,44 @@ fun MomentBottomOperateRowGroup(
onLikeClick()
}
}
Spacer(modifier = Modifier.width(10.dp))
// 评论按钮
Box(
modifier = Modifier.noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
onCommentClick()
}
}
) {
MomentOperateBtn(
icon = R.mipmap.icon_comment,
count = momentEntity.commentCount.toString()
)
}
Spacer(modifier = Modifier.width(28.dp))
// 转发按钮
Box(
modifier = Modifier.noRippleClickable {
// TODO: 实现转发功能
}
) {
MomentOperateBtn(
icon = R.mipmap.icon_share,
count = ""
)
}
}
// 收藏按钮
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
AnimatedFavouriteIcon(
modifier = Modifier.size(24.dp),
isFavourite = momentEntity.isFavorite
) {
onFavoriteClick()
}
}
}
}

View File

@@ -1,10 +1,15 @@
package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
@@ -12,11 +17,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
/**
* 可复用的标签页组件
*/
@@ -54,3 +62,43 @@ fun TabItem(
fun TabSpacer() {
Spacer(modifier = Modifier.width(8.dp))
}
@Composable
fun UnderlineTabItem(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier
.noRippleClickable { onClick() },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = text,
fontSize = 15.sp,
fontWeight = FontWeight.ExtraBold,
color = if (isSelected) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 13.dp)
)
// 选中状态下显示图标
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
if (isSelected) {
Image(
painter = painterResource(id = R.mipmap.underline),
contentDescription = "selected indicator",
)
}
}
}
}

View File

@@ -134,7 +134,7 @@ fun FavouriteListPage() {
) {
Image(
painter = painterResource(
id = if (com.aiosman.ravenow.AppState.darkMode) R.mipmap.syss_yh_qs_as_img
id = if (com.aiosman.ravenow.AppState.darkMode) R.mipmap.invalid_dark
else R.mipmap.invalid_name_1),
contentDescription = "No favourites",
modifier = Modifier.size(110.dp)

View File

@@ -276,12 +276,15 @@ fun IndexScreen() {
bottomBar = {
NavigationBar(
modifier = Modifier.height(58.dp + navigationBarHeight),
containerColor = AppColors.background
containerColor = AppColors.tabUnselectedBackground
) {
item.forEachIndexed { idx, it ->
val isSelected = model.tabIndex == idx
// 定义新的选中颜色
val selectedColor = Color(0xFF7C45ED)
val iconTint by animateColorAsState(
targetValue = if (isSelected) AppColors.brandColorsColor else AppColors.text,
targetValue = if (isSelected) selectedColor else AppColors.text,
animationSpec = tween(durationMillis = 250), label = ""
)
@@ -343,7 +346,7 @@ fun IndexScreen() {
.width(48.dp)
.height(32.dp)
.background(
color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.15f) else Color.Transparent,
color = if (isSelected) selectedColor.copy(alpha = 0.15f) else Color.Transparent,
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
@@ -362,7 +365,7 @@ fun IndexScreen() {
Text(
text = it.label(),
fontSize = 10.sp,
color = if (isSelected) Color.Blue else AppColors.text,
color = if (isSelected) selectedColor else AppColors.text,
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
)
}

View File

@@ -16,9 +16,9 @@ sealed class NavigationItem(
) {
data object Home : NavigationItem("Home",
icon = { R.drawable.rider_pro_nav_home },
selectedIcon = { R.mipmap.bars_x_buttons_home_s },
label = { stringResource(R.string.main_home) }
icon = { R.mipmap.bars_x_buttons_home_n_copy },
selectedIcon = { R.mipmap.bars_x_buttons_home_n_copy_2 },
label = { stringResource(R.string.main_ai) }
)
data object Ai : NavigationItem("Ai",

View File

@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.material.Icon
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.DisposableEffect
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.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -49,10 +52,6 @@ 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 androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.material.CircularProgressIndicator
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.aiosman.ravenow.AppStore
@@ -79,7 +78,14 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.ui.zIndex
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.foundation.lazy.grid.items as gridItems
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.ui.draw.alpha
// 检测是否接近列表底部的扩展函数
fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
@@ -90,7 +96,7 @@ fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
return lastVisibleItemIndex >= (totalItemsCount - buffer)
}
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun Agent() {
val AppColors = LocalAppTheme.current
@@ -110,19 +116,6 @@ fun Agent() {
viewModel.ensureDataLoaded()
}
// 列表滚动状态(用于吸顶与加载更多)
val listState = rememberLazyListState()
// 监听滚动到底部,加载更多网格数据
LaunchedEffect(listState) {
snapshotFlow { listState.isScrolledToEnd() }
.collect { atEnd ->
if (atEnd && !viewModel.isLoading) {
viewModel.loadMoreGridAgentData()
}
}
}
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
@@ -134,61 +127,78 @@ fun Agent() {
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
// 固定顶部搜索条
Box(
val agentItems = viewModel.agentItems
var selectedTabIndex by remember { mutableStateOf(0) }
// 无限滚动状态
val listState = rememberLazyListState()
// 创建一个可观察的滚动到底部状态
val isScrolledToEnd by remember {
derivedStateOf {
listState.isScrolledToEnd()
}
}
// 检测滚动到底部并加载更多数据
LaunchedEffect(isScrolledToEnd) {
if (isScrolledToEnd && !viewModel.isLoadingMore && agentItems.isNotEmpty() && viewModel.hasMoreData) {
viewModel.loadMoreAgents()
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Image(
painter = painterResource(id = R.drawable.home_logo),
contentDescription = "Rave AI Logo",
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.zIndex(999.0f)
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
) {
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight().padding(top = 32.dp, start = 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Rave AI",
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
modifier = Modifier
.align(Alignment.CenterVertically)
.height(44.dp)
.padding(top =9.dp,bottom=9.dp)
.wrapContentSize(),
// colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.weight(1f))
},
actions = {
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier
.size(24.dp)
.size(44.dp)
.padding(top = 9.dp,bottom=9.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
},
colorFilter = ColorFilter.tint(AppColors.text)
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = AppColors.background
),
windowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
.padding(top = statusBarPaddingValues.calculateTopPadding())
)
},
containerColor = AppColors.background,
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { paddingValues ->
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(
top = 44.dp + statusBarPaddingValues.calculateTopPadding() + 15.dp,
bottom = navigationBarPaddings,
start = 16.dp,
end = 16.dp
start = 8.dp,
end = 8.dp
)
) {
// 动态标签页
// 类别标签页 - 吸顶
stickyHeader(key = "category_tabs") {
Column(
modifier = Modifier
@@ -196,33 +206,49 @@ fun Agent() {
.background(AppColors.background)
.padding(top = 4.dp, bottom = 8.dp)
) {
val selectedTabIndex = viewModel.selectedCategoryIndex
if (viewModel.categories.isNotEmpty()) {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 16.dp),
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
item {
CustomTabItem(
text = stringResource(R.string.agent_recommend),
isSelected = selectedTabIndex == 0,
onClick = {
selectedTabIndex = 0
viewModel.loadAllAgents()
}
)
}
item {
TabSpacer()
}
// 动态添加分类标签
viewModel.categories.forEachIndexed { index, category ->
item {
CustomTabItem(
text = category.getLocalizedName(),
isSelected = selectedTabIndex == index,
onClick = { viewModel.selectCategory(index) }
text = category.name,
isSelected = selectedTabIndex == index + 1,
onClick = {
selectedTabIndex = index + 1
viewModel.loadAgentsByCategory(category.id)
}
)
}
if (index < viewModel.categories.size - 1) {
item { TabSpacer() }
}
}
}
}
}
}
item {
TabSpacer()
}
}
}
}
}
// 推荐内容区域
item {
Column(
@@ -230,7 +256,18 @@ fun Agent() {
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
AgentViewPagerSection(agentItems = viewModel.topAgentItems, viewModel)
when {
selectedTabIndex == 0 -> {
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)
}
}
}
}
@@ -282,7 +319,7 @@ fun Agent() {
item { Spacer(modifier = Modifier.height(20.dp)) }
// 发现区域
// "发现更多" 标题 - 吸顶
stickyHeader(key = "discover_more") {
Row(
modifier = Modifier
@@ -301,47 +338,43 @@ fun Agent() {
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
}
item { Spacer(modifier = Modifier.height(20.dp)) }
// Agent 两列网格行
items(viewModel.gridAgentItems.chunked(2)) { rowItems ->
// Agent网格 - 使用行式布局
items(
items = agentItems.chunked(2),
key = { row -> row.firstOrNull()?.openId ?: "" }
) { rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = 20.dp,
bottom = 20.dp
),
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(modifier = Modifier.weight(1f)) {
rowItems.forEach { agentItem ->
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[0],
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
}
if (rowItems.size > 1) {
Box(modifier = Modifier.weight(1f)) {
AgentCardSquare(
agentItem = rowItems[1],
viewModel = viewModel,
navController = LocalNavController.current
)
}
} else {
// 如果这一行只有一个item添加一个空的占位符
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
// 加载更多指示器
if (viewModel.isLoading) {
if (viewModel.isLoadingMore) {
item {
Row(
modifier = Modifier
@@ -356,7 +389,7 @@ fun Agent() {
)
Spacer(modifier = Modifier.width(12.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_chat_loading),
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
@@ -499,20 +532,20 @@ fun AgentCardSquare(
)
}
// 聊天按钮,固定在底部居中,距离底部有一定边距
// 聊天按钮
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 12.dp) // 距离底部的边距
.padding(bottom = 12.dp)
.width(60.dp)
.height(32.dp)
.background(
color = AppColors.text,
shape = RoundedCornerShape(
topStart = 10.dp,
topEnd = 10.dp,
topStart = 14.dp,
topEnd = 14.dp,
bottomStart = 0.dp,
bottomEnd = 10.dp
bottomEnd = 14.dp
)
)
.clickable {
@@ -542,10 +575,9 @@ fun AgentCardSquare(
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current
// 每页显示5个agent
@@ -559,7 +591,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel
// Agent内容
Box(
modifier = Modifier
.height(300.dp)
.height(310.dp)
) {
HorizontalPager(
state = pagerState,
@@ -581,7 +613,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
page = page,
modifier = Modifier
.height(300.dp)
.height(310.dp)
.graphicsLayer {
scaleX = scale
scaleY = scale
@@ -618,13 +650,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel
}
@Composable
fun AgentPage(
viewModel: AgentViewModel,
agentItems: List<AgentItem>,
page: Int,
modifier: Modifier = Modifier,
navController: NavHostController
) {
fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier,navController: NavHostController) {
Column(
modifier = modifier
.fillMaxSize()
@@ -632,11 +658,7 @@ fun AgentPage(
) {
// 显示3个agent
agentItems.forEachIndexed { index, agentItem ->
AgentCard2(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current)
if (index < agentItems.size - 1) {
Spacer(modifier = Modifier.height(8.dp))
}
@@ -646,7 +668,7 @@ fun AgentPage(
@SuppressLint("SuspiciousIndentation")
@Composable
fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: NavHostController) {
fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) {
val AppColors = LocalAppTheme.current
// 防抖状态
@@ -662,7 +684,7 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
Box(
modifier = Modifier
.size(48.dp)
.background(AppColors.secondaryBackground, RoundedCornerShape(24.dp))
.background(Color(0x00F5F5F5), RoundedCornerShape(24.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
@@ -672,6 +694,12 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
},
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.mipmap.group_copy),
contentDescription = "默认头像",
modifier = Modifier.size(48.dp),
)
if (agentItem.avatar.isNotEmpty()) {
CustomAsyncImage(
imageUrl = agentItem.avatar,
@@ -681,13 +709,6 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
.clip(RoundedCornerShape(24.dp)),
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)
)
}
}
@@ -726,8 +747,13 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
modifier = Modifier
.size(width = 60.dp, height = 32.dp)
.background(
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
color = Color(0X147c7480),
shape = RoundedCornerShape(
topStart = 14.dp,
topEnd = 14.dp,
bottomStart = 0.dp,
bottomEnd = 14.dp
)
)
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
@@ -756,7 +782,6 @@ fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: N
}
}
}
@Composable
fun ChatRoomsSection(
chatRooms: List<ChatRoom>,
@@ -818,7 +843,7 @@ fun ChatRoomCard(
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
val cardSize = 160.dp
val cardSize = 180.dp
val viewModel: AgentViewModel = viewModel()
val context = LocalContext.current
@@ -866,7 +891,7 @@ fun ChatRoomCard(
Box(
modifier = modifier
.size(cardSize)
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
.clickable(enabled = !viewModel.isJoiningRoom) {
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 加入群聊房间
@@ -896,8 +921,13 @@ fun ChatRoomCard(
imageUrl = imageUrl,
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
modifier = Modifier
.size(cardSize)
.clip(RoundedCornerShape(12.dp)),
.width(cardSize)
.height(120.dp)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop
)
} else {
@@ -915,21 +945,47 @@ fun ChatRoomCard(
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)
.padding(bottom = 32.dp, start = 10.dp, end = 10.dp)
.clip(RoundedCornerShape(12.dp))
) {
androidx.compose.material3.Text(
text = chatRoom.name,
fontSize = 12.sp,
color = Color.White,
fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
textAlign = androidx.compose.ui.text.style.TextAlign.Left
)
}
// 显示人数
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = 10.dp, start = 10.dp, end = 10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(R.drawable.rider_pro_nav_profile),
contentDescription = "chat",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
fontSize = 12.sp,
modifier = Modifier.alpha(0.6f),
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
}
}
}

View File

@@ -1,52 +1,65 @@
package com.aiosman.ravenow.ui.index.tabs.ai
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CategoryTemplate
import com.aiosman.ravenow.data.api.CreateGroupChatRequestBody
import com.aiosman.ravenow.data.api.RaveNowAPI
import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.data.api.JoinGroupChatRequestBody
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
import kotlinx.coroutines.launch
import android.util.Log
import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.api.CreateGroupChatRequestBody
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.data.api.ApiErrorResponse
import com.google.gson.Gson
import android.content.Context
import android.widget.Toast
import kotlinx.coroutines.launch
import com.aiosman.ravenow.data.api.JoinGroupChatRequestBody
/**
* 缓存数据结构用于存储每个分类的Agent列表
*/
data class AgentCacheData(
val items: List<AgentItem>,
val currentPage: Int,
val hasMoreData: Boolean
)
data class ChatRoom(
val id: Int,
val name: String,
val avatar: String = "",
val banner: String = "",
val memberCount: Int = 0
val memberCount: Int
)
object AgentViewModel : ViewModel() {
object AgentViewModel: ViewModel() {
private val apiClient: RaveNowAPI = ApiClient.api
// 顶部Agent列表数据用于ViewPager
var topAgentItems by mutableStateOf<List<AgentItem>>(emptyList())
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
private set
// 底部网格布局Agent数据
var gridAgentItems by mutableStateOf<List<AgentItem>>(emptyList())
var categories by mutableStateOf<List<CategoryItem>>(emptyList())
private set
var errorMessage by mutableStateOf<String?>(null)
private set
var chatRooms by mutableStateOf<List<ChatRoom>>(emptyList())
@@ -55,133 +68,126 @@ object AgentViewModel : ViewModel() {
var rooms by mutableStateOf<List<Room>>(emptyList())
private set
var categories by mutableStateOf<List<CategoryTemplate>>(emptyList())
private set
var selectedCategoryIndex by mutableStateOf(0)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
var isRefreshing by mutableStateOf(false)
private set
var isLoading by mutableStateOf(false)
private set
// 分页相关状态
var isLoadingMore by mutableStateOf(false)
private set
var currentPage by mutableStateOf(1)
private set
var hasMoreData by mutableStateOf(true)
private set
var isJoiningRoom by mutableStateOf(false)
private set
private var topCurrentPage = 1
private var topHasMoreData = true
private val pageSize = 20
private var currentCategoryId: Int? = null
private var gridCurrentPage = 1
private var gridHasMoreData = true
// 缓存使用分类ID作为keynull表示推荐列表
private val agentCache = mutableMapOf<Int?, AgentCacheData>()
init {
loadTopAgentData()
loadGridAgentData()
loadChatRooms()
loadAgentData()
loadCategories()
loadChatRooms()
}
/**
* 加载顶部Agent列表数据用于ViewPager
*/
private fun loadTopAgentData(categoryIndex: Int = 0) {
private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false, forceRefresh: Boolean = false) {
viewModelScope.launch {
isLoading = true
errorMessage = null
topCurrentPage = 1
topHasMoreData = true
try {
val selectedCategory =
if (categoryIndex < categories.size) categories[categoryIndex] else null
// 如果不是强制刷新且不是加载更多,检查缓存
if (!forceRefresh && !isLoadMore) {
val cached = agentCache[categoryId]
if (cached != null && cached.items.isNotEmpty()) {
// 使用缓存数据
agentItems = cached.items
currentPage = cached.currentPage
hasMoreData = cached.hasMoreData
currentCategoryId = categoryId
println("使用缓存数据分类ID: $categoryId, 数据数量: ${cached.items.size}")
return@launch
}
}
val response = if (categoryIndex == 0 || selectedCategory == null) {
// 推荐分类或无效分类,加载所有 Agent
if (isLoadMore) {
isLoadingMore = true
} else {
isLoading = true
// 重置分页状态
currentPage = 1
hasMoreData = true
currentCategoryId = categoryId
}
errorMessage = null
try {
val response = if (categoryId != null) {
// 根据分类ID获取智能体
apiClient.getAgent(
page = topCurrentPage,
pageSize = 15,
withWorkflow = "1",
random = "1"
page = page,
pageSize = pageSize,
withWorkflow = 1,
categoryIds = listOf(categoryId),
random = 1
)
} else {
// 特定分类,使用 categoryName 参数
// 获取推荐智能体使用random=1
apiClient.getAgent(
page = topCurrentPage,
pageSize = 15,
withWorkflow = "1",
categoryName = selectedCategory.name,
random = "1"
page = page,
pageSize = pageSize,
withWorkflow = 1,
categoryIds = null,
random = 1
)
}
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
topAgentItems = agents.map { agent ->
val responseData = response.body()?.data
val agents = responseData?.list ?: emptyList<Agent>()
val newAgentItems = agents.map { agent ->
AgentItem.fromAgent(agent)
}
topHasMoreData = agents.size >= 15
if (isLoadMore) {
// 加载更多:追加到现有列表
agentItems = agentItems + newAgentItems
currentPage = page
} else {
errorMessage = "获取顶部Agent数据失败: ${response.code()}"
// 首次加载或刷新:替换整个列表
agentItems = newAgentItems
currentPage = 1
}
// 检查是否还有更多数据
hasMoreData = agents.size >= pageSize
// 更新缓存
agentCache[categoryId] = AgentCacheData(
items = agentItems,
currentPage = currentPage,
hasMoreData = hasMoreData
)
println("更新缓存分类ID: $categoryId, 数据数量: ${agentItems.size}")
} else {
errorMessage = "获取Agent数据失败: ${response.code()}"
}
} catch (e: Exception) {
errorMessage = "网络请求失败: ${e.message}"
} finally {
if (isLoadMore) {
isLoadingMore = false
} else {
isLoading = false
}
}
}
/**
* 加载底部网格布局Agent数据
*/
private fun loadGridAgentData(categoryIndex: Int = 0) {
viewModelScope.launch {
isLoading = true
errorMessage = null
gridCurrentPage = 1
gridHasMoreData = true
try {
val selectedCategory =
if (categoryIndex < categories.size) categories[categoryIndex] else null
val response = if (categoryIndex == 0 || selectedCategory == null) {
// 推荐分类或无效分类,加载所有 Agent
apiClient.getAgent(
page = gridCurrentPage,
pageSize = 20,
withWorkflow = "1",
random = "true"
)
} else {
// 特定分类,使用 categoryName 参数
apiClient.getAgent(
page = gridCurrentPage,
pageSize = 20,
withWorkflow = "1",
categoryName = selectedCategory.name,
random = "true"
)
}
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
gridAgentItems = agents.map { agent ->
AgentItem.fromAgent(agent)
}
gridHasMoreData = agents.size >= 20
} else {
errorMessage = "获取网格Agent数据失败: ${response.code()}"
}
} catch (e: Exception) {
errorMessage = "网络请求失败: ${e.message}"
} finally {
isLoading = false
}
}
}
private fun loadChatRooms() {
@@ -189,14 +195,13 @@ object AgentViewModel : ViewModel() {
try {
val response = apiClient.getRooms(
page = 1,
pageSize = 21,
pageSize = 20,
isRecommended = 1,
random = 1
) // 请求21个确保是3的倍数
)
if (response.isSuccessful) {
val allRooms = response.body()?.list ?: emptyList()
// 确保房间数量是3的倍数如果不足则截取如果超出则取前几个
val targetCount = (allRooms.size / 3) * 3 // 向下取整到最近的3的倍数
val targetCount = (allRooms.size / 2) * 2
rooms = allRooms.take(targetCount)
// 转换为ChatRoom格式用于兼容现有UI
@@ -221,138 +226,85 @@ object AgentViewModel : ViewModel() {
private fun loadCategories() {
viewModelScope.launch {
// 如果分类已经加载,不重复请求
if (categories.isNotEmpty()) {
println("使用已缓存的分类数据,数量: ${categories.size}")
return@launch
}
try {
val categoriesResponse = apiClient.getCategories(
// 获取完整的语言标记(如 "zh-CN"
val sysLang = com.aiosman.ravenow.utils.Utils.getPreferredLanguageTag()
val response = apiClient.getCategories(
page = 1,
pageSize = 100,
)
if (categoriesResponse.isSuccessful && categoriesResponse.body() != null) {
// 添加一个默认的"推荐"分类在第一位
val recommendCategory = createRecommendCategory()
val categoriesList = categoriesResponse.body()?.list ?: emptyList()
categories = listOf(recommendCategory) + categoriesList
// 分类加载完成后,重新加载当前选中分类的 Agent 数据
if (topAgentItems.isEmpty()) {
loadTopAgentData(selectedCategoryIndex)
}
if (gridAgentItems.isEmpty()) {
loadGridAgentData(selectedCategoryIndex)
}
} else {
// 如果请求失败,使用默认分类
categories = listOf(createRecommendCategory())
errorMessage = "获取分类失败: ${categoriesResponse.code()}"
}
} catch (e: Exception) {
// 如果网络请求失败,使用默认分类
categories = listOf(createRecommendCategory())
errorMessage = "网络请求失败: ${e.message}"
}
}
}
/**
* 创建推荐分类
*/
private fun createRecommendCategory(): CategoryTemplate {
return CategoryTemplate(
id = 0,
name = "推荐",
description = "推荐内容",
avatar = "",
parentId = null,
parent = null,
children = null,
sort = 0,
isActive = true,
promptCount = 0,
createdAt = "",
updatedAt = "",
translations = null
withChildren = false,
withParent = false,
withCount = true,
hideEmpty = true,
lang = sysLang
)
}
/**
* 加载更多网格Agent数据
*/
fun loadMoreGridAgentData() {
if (!gridHasMoreData || isLoading) return
viewModelScope.launch {
isLoading = true
try {
val nextPage = gridCurrentPage + 1
val selectedCategory =
if (selectedCategoryIndex < categories.size) categories[selectedCategoryIndex] else null
val response = if (selectedCategoryIndex == 0 || selectedCategory == null) {
apiClient.getAgent(
page = nextPage,
pageSize = 20,
withWorkflow = "1",
random = "true"
)
} else {
apiClient.getAgent(
page = nextPage,
pageSize = 20,
withWorkflow = "1",
categoryName = selectedCategory.name,
random = "true"
)
}
println("分类数据请求完成,响应成功: ${response.isSuccessful}, 语言标记: $sysLang")
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
val newAgentItems = agents.map { agent ->
AgentItem.fromAgent(agent)
val categoryList = response.body()?.list ?: emptyList()
println("获取到 ${categoryList.size} 个分类")
// 使用当前语言获取翻译后的分类名称
categories = categoryList.map { category ->
CategoryItem.fromCategoryTemplate(category, sysLang)
}
gridAgentItems = gridAgentItems + newAgentItems
gridCurrentPage = nextPage
gridHasMoreData = agents.size >= 20
println("成功处理并映射了 ${categories.size} 个分类")
} else {
errorMessage = "加载更多网格Agent数据失败: ${response.code()}"
errorMessage = "获取分类数据失败: ${response.code()}"
println("获取分类数据失败: ${response.code()}")
}
} catch (e: Exception) {
errorMessage = "网络请求失败: ${e.message}"
} finally {
isLoading = false
errorMessage = "获取分类数据失败: ${e.message}"
println("获取分类数据异常: ${e.message}")
e.printStackTrace()
}
}
}
fun loadAgentsByCategory(categoryId: Int) {
loadAgentData(categoryId)
}
fun loadAllAgents() {
loadAgentData()
}
/**
* 选择分类并加载对应的 Agent 数据
* 加载更多Agent数据
*/
fun selectCategory(categoryIndex: Int) {
if (categoryIndex != selectedCategoryIndex && categoryIndex >= 0 && categoryIndex < categories.size) {
selectedCategoryIndex = categoryIndex
// 同时加载顶部和网格的数据
loadTopAgentData(categoryIndex)
loadGridAgentData(categoryIndex)
}
fun loadMoreAgents() {
// 检查是否正在加载或没有更多数据
if (isLoadingMore || !hasMoreData) {
return
}
val nextPage = currentPage + 1
loadAgentData(
categoryId = currentCategoryId,
page = nextPage,
isLoadMore = true
)
}
fun createSingleChat(
openId: String,
) {
viewModelScope.launch {
val response =
ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
Log.d("debug", response.toString())
val response = ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
}
}
fun goToChatAi(
openId: String,
navController: NavHostController
) {
viewModelScope.launch {
val profile = userService.getUserProfileByOpenId(openId)
createGroup2ChatAi(profile.trtcUserId, "ai_group", navController, profile.id)
createGroup2ChatAi(profile.trtcUserId,"ai_group",navController,profile.id)
}
}
@@ -376,26 +328,20 @@ object AgentViewModel : ViewModel() {
}
/**
* 刷新推荐Agent数据
* 刷新当前分类的Agent数据(强制刷新,清除缓存)
*/
fun refreshAgentData() {
loadTopAgentData()
loadGridAgentData()
// 清除当前分类的缓存
agentCache.remove(currentCategoryId)
loadAgentData(categoryId = currentCategoryId, forceRefresh = true)
}
/**
* 检查数据是否为空,如果为空则重新加载
*/
fun ensureDataLoaded() {
if (topAgentItems.isEmpty() && !isLoading) {
loadTopAgentData()
}
if (gridAgentItems.isEmpty() && !isLoading) {
loadGridAgentData()
}
// 同时确保分类数据已加载
if (categories.isEmpty() && !isLoading) {
loadCategories()
if (agentItems.isEmpty() && !isLoading) {
loadAgentData()
}
}
@@ -478,18 +424,36 @@ object AgentViewModel : ViewModel() {
* 重置ViewModel状态用于登出或切换账号时清理数据
*/
fun ResetModel() {
topAgentItems = emptyList()
gridAgentItems = emptyList()
agentItems = emptyList()
categories = emptyList()
selectedCategoryIndex = 0
errorMessage = null
isRefreshing = false
isLoading = false
isJoiningRoom = false
topCurrentPage = 1
topHasMoreData = true
gridCurrentPage = 1
gridHasMoreData = true
isLoadingMore = false
currentPage = 1
hasMoreData = true
currentCategoryId = null
// 清空缓存
agentCache.clear()
}
}
data class CategoryItem(
val id: Int,
val name: String,
val description: String,
val avatar: String,
val promptCount: Int?
) {
companion object {
fun fromCategoryTemplate(template: CategoryTemplate, lang: String): CategoryItem {
return CategoryItem(
id = template.id,
name = template.getLocalizedName(lang),
description = template.getLocalizedDescription(lang),
avatar = "${ApiClient.BASE_API_URL}${template.avatar}",
promptCount = template.promptCount
)
}
}
}

View File

@@ -72,9 +72,12 @@ open class BaseMomentModel :ViewModel(){
}
suspend fun onAddComment(id: Int) {
// val currentPagingData = _momentsFlow.value
// updateCommentCount(id)
fun onAddComment(id: Int) {
momentLoader.updateCommentCount(id, +1)
}
fun onDeleteComment(id: Int) {
momentLoader.updateCommentCount(id, -1)
}
@@ -83,6 +86,7 @@ open class BaseMomentModel :ViewModel(){
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
}
suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id)
momentLoader.updateFavoriteCount(id, true)

View File

@@ -3,6 +3,7 @@ package com.aiosman.ravenow.ui.index.tabs.moment
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
@@ -45,6 +47,7 @@ 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.hot.HotMomentsList
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.NewsScreen
import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@@ -54,7 +57,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.composables.UnderlineTabItem
import com.aiosman.ravenow.ui.composables.rememberDebouncer
/**
@@ -68,8 +71,8 @@ fun MomentsList() {
val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
// 游客模式下不显示timeline只显示2个tabDynamic、Hot // 游客模式下不显示timeline只显示3个tabExplore、Dynamic、Hot
val tabCount = if (AppStore.isGuest) 2 else 3 // val tabCount = if (AppStore.isGuest) 3 else 4
// 现在有6个tab推荐、短视频、新闻、探索、关注、热门
val tabCount = 6
var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope()
Column(
@@ -81,60 +84,127 @@ fun MomentsList() {
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// 顶部区域:可滚动的标签页 + 搜索按钮
Row(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp),
// center the tabs
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 可滚动的标签页行
Row(
modifier = Modifier
.weight(1f)
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
//原探索//
// Column(
// modifier = Modifier
// .noRippleClickable {
// scope.launch {
// pagerState.animateScrollToPage(0)
// }
// }.padding(start = 16.dp),
// verticalArrangement = Arrangement.Center,
// horizontalAlignment = Alignment.CenterHorizontally
//
// ) {
// Text(
// text = stringResource(R.string.index_worldwide),
// fontSize = if (pagerState.currentPage == 0)18.sp else 16.sp,
// color = if (pagerState.currentPage == 0) AppColors.text else AppColors.nonActiveText,
// fontWeight = FontWeight.W600)
// Spacer(modifier = Modifier.height(4.dp))
//
// Image(
// painter = painterResource(
// if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
// else R.drawable.tab_indicator_unselected
// ),
// contentDescription = "tab indicator",
// modifier = Modifier
// .width(34.dp)
// .height(4.dp)
// )
//
// }
// Spacer(modifier = Modifier.width(16.dp))
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
Text(
text = stringResource(R.string.moment),
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
modifier = Modifier
.align(Alignment.CenterVertically)
val tabDebouncer = rememberDebouncer()
// 推荐标签
UnderlineTabItem(
text = stringResource(R.string.tab_recommend),
isSelected = pagerState.currentPage == 0,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(0)
}
}
}
)
Spacer(modifier = Modifier.weight(1f))
// 短视频标签
UnderlineTabItem(
text = stringResource(R.string.tab_short_video),
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
// 动态标签
UnderlineTabItem(
text = stringResource(R.string.moment),
isSelected = pagerState.currentPage == 2,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(2)
}
}
}
)
// 只有非游客用户才显示"关注"tab
if (!AppStore.isGuest) {
UnderlineTabItem(
text = stringResource(R.string.index_following),
isSelected = pagerState.currentPage == 3,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(3)
}
}
}
)
// 热门标签
UnderlineTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 4,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(4)
}
}
}
)
} else {
// 热门标签 (游客模式)
UnderlineTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 4,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(4)
}
}
}
)
}
// 新闻标签
UnderlineTabItem(
text = stringResource(R.string.tab_news),
isSelected = pagerState.currentPage == 5,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(5)
}
}
}
)
}
// 搜索按钮
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
@@ -151,113 +221,39 @@ fun MomentsList() {
)
}
Spacer(modifier = Modifier.height(23.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
val tabDebouncer = rememberDebouncer()
// 新探索标签
Box {
CustomTabItem(
text = stringResource(R.string.index_worldwide),
isSelected = pagerState.currentPage == 0,
onClick = {
tabDebouncer {
scope.launch {
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(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
if (AppStore.isGuest) {
// 游客模式Dynamic(0), Hot(1)
when (it) {
0 -> {
Dynamic()
// 推荐页面
NewsScreen()
}
1 -> {
HotMomentsList()
}
}
} else {
// 正常用户Dynamic(0), Timeline(1), Hot(2)
when (it) {
0 -> {
Dynamic()
}
1 -> {
TimelineMomentsList()
// 短视频页面
}
2 -> {
// 动态页面 - 暂时显示时间线内容
Dynamic()
}
3 -> {
// 关注页面 (仅非游客用户) 或 热门页面 (游客用户)
if (AppStore.isGuest) {
HotMomentsList()
} else {
TimelineMomentsList()
}
}
4 -> {
// 热门页面 (仅非游客用户)
HotMomentsList()
}
5 -> {
// 新闻页面
NewsScreen()
}
}
}

View File

@@ -168,6 +168,7 @@ fun Dynamic() {
)
}
}
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
}
}
}

View File

@@ -524,7 +524,7 @@ fun Explore() {
Row(
verticalAlignment = Alignment.CenterVertically
) {
/* Image(
Image(
painter = painterResource(R.drawable.rider_pro_nav_profile),
contentDescription = "chat",
modifier = Modifier.size(16.dp),
@@ -536,7 +536,7 @@ fun Explore() {
fontSize = 12.sp,
color = Color.White,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)*/
)
}
// 底部:标题和描述

View File

@@ -74,7 +74,7 @@ class ExploreViewModel : ViewModel() {
isRefreshing = true
errorMessage = null
try {
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
agentItems = agents.map { agent ->
@@ -118,7 +118,7 @@ class ExploreViewModel : ViewModel() {
isLoading = true
errorMessage = null
try {
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
agentItems = agents.map { agent ->

View File

@@ -4,6 +4,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
@@ -13,11 +14,12 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
@@ -39,6 +41,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.collectAsState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
@@ -104,8 +107,8 @@ fun DiscoverView() {
val isLoading by model.isLoading.collectAsState()
val context = LocalContext.current
val navController = LocalNavController.current
val gridState = rememberLazyGridState()
val gridState = rememberLazyStaggeredGridState()
val AppColors = LocalAppTheme.current
// 监听滚动到底部,自动加载更多
LaunchedEffect(gridState, moments.size) {
snapshotFlow {
@@ -124,18 +127,40 @@ fun DiscoverView() {
}
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
state = gridState,
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
// contentPadding = PaddingValues(8.dp)
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 4.dp)
) {
items(moments) { momentItem ->
val debouncer = rememberDebouncer()
val textContent = momentItem.momentTextContent
val textLines = if (textContent.isNotEmpty()) {
val estimatedCharsPerLine = 20
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1
minOf(estimatedLines, 2) // 最多2行
} else {
0
}
val baseHeight = 200.dp
val singleLineTextHeight = 20.dp
val doubleLineTextHeight = 40.dp
val authorInfoHeight = 25.dp
val paddingHeight = 10.dp
val paddingHeight2 =3.dp
val totalHeight = baseHeight + when (textLines) {
0 -> authorInfoHeight + paddingHeight
1 -> singleLineTextHeight + authorInfoHeight + paddingHeight
else -> doubleLineTextHeight + authorInfoHeight +paddingHeight2
}
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.height(totalHeight)
.padding(2.dp)
.noRippleClickable {
debouncer {
@@ -146,15 +171,69 @@ fun DiscoverView() {
)
}
}
) {
Column(
modifier = Modifier.fillMaxSize().background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
) {
CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail,
contentDescription = "",
modifier = Modifier
.fillMaxSize(),
.fillMaxWidth()
.height(baseHeight)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
context = context,
showShimmer = true
)
Column(
modifier = Modifier
.fillMaxWidth()
.height(totalHeight - baseHeight)
.padding(horizontal = 8.dp, vertical = 8.dp)
) {
if (momentItem.momentTextContent.isNotEmpty()) {
androidx.compose.material3.Text(
text = momentItem.momentTextContent,
modifier = Modifier.fillMaxWidth(),
fontSize = 12.sp,
color = AppColors.text,
maxLines = 2,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
imageUrl = momentItem.avatar,
contentDescription = "",
modifier = Modifier
.size(16.dp)
.clip(RoundedCornerShape(8.dp)),
context = context,
showShimmer = true
)
androidx.compose.material3.Text(
text = momentItem.nickname,
modifier = Modifier.padding(start = 4.dp),
fontSize = 11.sp,
color = AppColors.text.copy(alpha = 0.6f),
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
}
}
if (momentItem.images.size > 1) {
Box(
modifier = Modifier

View File

@@ -0,0 +1,205 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.exp.formatPostTime2
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FullArticleModal(
moment: MomentEntity,
onDismiss: () -> Unit
) {
val appColors = LocalAppTheme.current
val context = LocalContext.current
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.9f // 90% 高度
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
containerColor = appColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
Column(
modifier = Modifier
.fillMaxSize()
) {
// 滚动内容
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
// 新闻图片区域 - 固定高度和宽度
Box(
modifier = Modifier
.fillMaxWidth()
.height(250.dp)
.background(color = appColors.secondaryBackground)
) {
if (moment.images.isNotEmpty()) {
val firstImage = moment.images[0]
CustomAsyncImage(
context = context,
imageUrl = firstImage.url,
contentDescription = "新闻图片",
contentScale = ContentScale.Fit,
blurHash = firstImage.blurHash,
modifier = Modifier.fillMaxSize()
)
} else {
Image(
painter = androidx.compose.ui.res.painterResource(id = R.drawable.default_moment_img),
contentDescription = "默认图片",
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 新闻标题
Text(
text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = appColors.text,
lineHeight = 28.sp,
modifier = Modifier.padding(horizontal = 10.dp)
)
Spacer(modifier = Modifier.height(12.dp))
// 新闻来源和发布时间
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 来源按钮
Button(
onClick = { },
modifier = Modifier.height(28.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF7c68ef)
),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 12.dp, vertical = 4.dp),
shape = RoundedCornerShape(14.dp)
) {
Text(
text = if (moment.newsSource.isNotEmpty()) moment.newsSource else moment.nickname,
fontSize = 12.sp,
color = Color.White,
)
}
// 发布时间
Text(
text = moment.time.formatPostTime2(),
fontSize = 12.sp,
color = appColors.secondaryText
)
}
Spacer(modifier = Modifier.height(16.dp))
// 帖子内容
NewsContent(
content = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
images = moment.images,
context = context
)
Spacer(modifier = Modifier.height(200.dp))
}
}
}
}
@Composable
private fun NewsContent(
content: String,
images: List<com.aiosman.ravenow.entity.MomentImageEntity>,
context: android.content.Context
) {
val appColors = LocalAppTheme.current
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = content,
fontSize = 16.sp,
color = appColors.text,
lineHeight = 24.sp
)
// 图片内容
if (images.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
images.forEach { image ->
Spacer(modifier = Modifier.height(12.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
) {
CustomAsyncImage(
context = context,
imageUrl = image.url,
contentDescription = "内容图片",
contentScale = ContentScale.Fit,
blurHash = image.blurHash,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}

View File

@@ -0,0 +1,306 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.CommentService
import com.aiosman.ravenow.data.CommentServiceImpl
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
import com.aiosman.ravenow.ui.composables.debouncedClickable
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.post.CommentContent
import com.aiosman.ravenow.ui.post.CommentMenuModal
import com.aiosman.ravenow.ui.post.CommentsViewModel
import com.aiosman.ravenow.ui.post.OrderSelectionComponent
import kotlinx.coroutines.launch
class NewsCommentModalViewModel(
val postId: Int?
) : ViewModel() {
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId.toString())
var commentService: CommentService = CommentServiceImpl()
init {
commentsViewModel.preTransit()
}
fun likeComment(commentId: Int) {
viewModelScope.launch {
commentsViewModel.likeComment(commentId)
}
}
fun unlikeComment(commentId: Int) {
viewModelScope.launch {
commentsViewModel.unlikeComment(commentId)
}
}
fun createComment(
content: String,
parentCommentId: Int? = null,
replyUserId: Int? = null,
replyCommentId: Int? = null
) {
viewModelScope.launch {
commentsViewModel.createComment(
content = content,
parentCommentId = parentCommentId,
replyUserId = replyUserId,
replyCommentId = replyCommentId
)
}
}
fun deleteComment(commentId: Int) {
commentsViewModel.deleteComment(commentId)
}
}
// 新闻评论弹窗
// @param postId 新闻帖子ID
// @param commentCount 评论数量
// @param onDismiss 关闭回调
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewsCommentModal(
postId: Int? = null,
commentCount: Int = 0,
onDismiss: () -> Unit = {},
onCommentAdded: () -> Unit = {},
onCommentDeleted: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
val debouncedNavigation = rememberDebouncedNavigation()
// 实时评论数状态
var currentCommentCount by remember { mutableStateOf(commentCount) }
val model = viewModel<NewsCommentModalViewModel>(
key = "NewsCommentModalViewModel_$postId",
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NewsCommentModalViewModel(postId) as T
}
}
)
val commentViewModel = model.commentsViewModel
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
var showCommentMenu by remember { mutableStateOf(false) }
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
// 菜单弹窗
if (showCommentMenu) {
ModalBottomSheet(
onDismissRequest = {
showCommentMenu = false
},
containerColor = AppColors.background,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
CommentMenuModal(
onDeleteClick = {
showCommentMenu = false
contextComment?.let {
model.deleteComment(it.id)
onCommentDeleted()
currentCommentCount = (currentCommentCount - 1).coerceAtLeast(0)
}
},
commentEntity = contextComment,
onCloseClick = {
showCommentMenu = false
},
isSelf = AppState.UserId?.toLong() == contextComment?.author,
onLikeClick = {
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
showCommentMenu = false
contextComment?.let {
if (it.liked) {
model.unlikeComment(it.id)
} else {
model.likeComment(it.id)
}
}
}
},
onReplyClick = {
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
showCommentMenu = false
replyComment = contextComment
}
}
)
}
}
Column(
modifier = Modifier.background(AppColors.background)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${currentCommentCount}条评论",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
// 排序选择
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
OrderSelectionComponent {
commentViewModel.order = it
commentViewModel.reloadComment()
}
}
}
// 评论列表
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Box(
modifier = Modifier.fillMaxWidth()
) {
LazyColumn {
item {
CommentContent(
viewModel = commentViewModel,
onLongClick = { comment ->
showCommentMenu = true
contextComment = comment
},
onReply = { parentComment, _, _, _ ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
replyComment = parentComment
}
}
)
}
}
}
}
// 底部输入栏
Column(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
) {
HorizontalDivider(color = AppColors.inputBackground)
EditCommentBottomModal(
replyComment = replyComment,
autoFocus = false
) {
if (replyComment != null) {
if (replyComment?.parentCommentId != null) {
// 第三级评论
model.createComment(
content = it,
parentCommentId = replyComment?.parentCommentId,
replyUserId = replyComment?.author?.toInt(),
replyCommentId = replyComment?.id
)
} else {
// 子级评论
model.createComment(
content = it,
parentCommentId = replyComment?.id,
replyCommentId = replyComment?.id
)
}
} else {
// 顶级评论
model.createComment(content = it)
}
replyComment = null
onCommentAdded()
currentCommentCount++
}
Spacer(modifier = Modifier.height(navBarHeight))
}
}
}

View File

@@ -0,0 +1,436 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalConfiguration
import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.exp.timeAgo
import com.aiosman.ravenow.exp.formatPostTime2
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.NewsViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun NewsScreen() {
val model = NewsViewModel
val moments = model.moments
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
// 评论弹窗状态
var showCommentModal by remember { mutableStateOf(false) }
var selectedMoment by remember { mutableStateOf<MomentEntity?>(null) }
// 查看全文弹窗状态
var showFullArticleModal by remember { mutableStateOf(false) }
var selectedArticleMoment by remember { mutableStateOf<MomentEntity?>(null) }
// 垂直翻页状态
val pagerState = rememberPagerState(pageCount = { moments.size })
// 防抖器
val likeDebouncer = rememberDebouncer()
val favoriteDebouncer = rememberDebouncer()
// 初始化加载数据
LaunchedEffect(Unit) {
model.refreshPager()
}
// 监听数据变化,重置加载状态
LaunchedEffect(moments.size) {
// 当数据增加时如果接近列表末尾Pager会自动更新页数
}
// 当翻页接近末尾时加载更多
LaunchedEffect(pagerState.currentPage, moments.size) {
if (moments.isNotEmpty() && pagerState.currentPage >= moments.size - 2) {
model.loadMore()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) {
if (moments.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "暂无新闻内容", color = AppColors.text, fontSize = 16.sp)
}
} else {
VerticalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
val momentItem = moments.getOrNull(page) ?: return@VerticalPager
NewsItem(
moment = momentItem,
modifier = Modifier.fillMaxSize(),
onCommentClick = {
selectedMoment = momentItem
showCommentModal = true
},
onReadFullClick = {
selectedArticleMoment = momentItem
showFullArticleModal = true
},
onLikeClick = {
likeDebouncer {
// 检查游客模式
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
if (momentItem.liked) {
model.dislikeMoment(momentItem.id)
} else {
model.likeMoment(momentItem.id)
}
}
}
}
},
onFavoriteClick = {
favoriteDebouncer {
// 检查游客模式
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
if (momentItem.isFavorite) {
model.unfavoriteMoment(momentItem.id)
} else {
model.favoriteMoment(momentItem.id)
}
}
}
}
}
)
}
}
// 查看全文弹窗
if (showFullArticleModal && selectedArticleMoment != null) {
FullArticleModal(
moment = selectedArticleMoment!!,
onDismiss = {
showFullArticleModal = false
}
)
}
// 评论弹窗
if (showCommentModal && selectedMoment != null) {
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.67f // 三分之二高度
ModalBottomSheet(
onDismissRequest = {
showCommentModal = false
},
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
containerColor = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
NewsCommentModal(
postId = selectedMoment?.id,
commentCount = selectedMoment?.commentCount ?: 0,
onDismiss = {
showCommentModal = false
},
onCommentAdded = {
selectedMoment?.id?.let { model.onAddComment(it) }
},
onCommentDeleted = {
selectedMoment?.id?.let { model.onDeleteComment(it) }
}
)
}
}
}
}
//单个新闻项
@Composable
fun NewsItem(
moment: MomentEntity,
modifier: Modifier = Modifier,
onCommentClick: () -> Unit = {},
onReadFullClick: () -> Unit = {},
onLikeClick: () -> Unit = {},
onFavoriteClick: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
Column(
modifier = modifier
.fillMaxSize()
.background(AppColors.background)
.padding(vertical = 8.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.weight(1f)
.padding(bottom = 30.dp)
) {
// 新闻图片
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.padding(horizontal = 16.dp)
) {
if (moment.images.isNotEmpty()) {
CustomAsyncImage(
context = context,
imageUrl = moment.images[0].thumbnail,
contentDescription = "新闻图片",
contentScale = ContentScale.Crop,
blurHash = moment.images[0].blurHash,
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(8.dp))
)
} else {
Image(
painter = androidx.compose.ui.res.painterResource(id = R.drawable.default_moment_img),
contentDescription = "默认图片",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(8.dp))
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 新闻标题
Text(
text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 新闻内容(超出使用省略号)
Text(
text = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
fontSize = 14.sp,
color = AppColors.text,
lineHeight = 20.sp,
maxLines = 6,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 新闻信息
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 来源和时间(显示月份与具体时间)
Text(
text = if (moment.newsSource.isNotEmpty()) "${moment.newsSource}${moment.time.formatPostTime2()}" else "${moment.nickname}${moment.time.formatPostTime2()}",
fontSize = 12.sp,
color = AppColors.secondaryText,
modifier = Modifier
.weight(1f)
.padding(end = 8.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
// 查看全文
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.noRippleClickable { onReadFullClick() }
) {
Text(
text = stringResource(R.string.read_full_article),
fontSize = 13.sp,
fontWeight = FontWeight.W600,
color = Color(0xFF7c45ed)
)
Spacer(modifier = Modifier.width(4.dp))
Image(
painter = androidx.compose.ui.res.painterResource(id = R.mipmap.arrow),
contentDescription = "箭头",
modifier = Modifier.size(18.dp),
colorFilter = ColorFilter.tint(Color(0xFF7c45ed))
)
}
}
}
// 互动栏
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 25.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 点赞
NewsActionButton(
icon = if (moment.liked) R.drawable.rider_pro_moment_liked else R.drawable.rider_pro_moment_like,
count = moment.likeCount.toString(),
isActive = moment.liked,
modifier = Modifier.noRippleClickable { onLikeClick() }
)
// 评论
NewsActionButton(
icon = R.mipmap.icon_comment,
count = moment.commentCount.toString(),
isActive = false,
modifier = Modifier.noRippleClickable { onCommentClick() }
)
// 收藏
NewsActionButton(
icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect,
count = moment.favoriteCount.toString(),
isActive = moment.isFavorite,
modifier = Modifier.noRippleClickable { onFavoriteClick() }
)
// 分享
NewsActionButton(
icon = R.mipmap.icon_share,
count = "",
isActive = false,
text = stringResource(R.string.share),
textSize = 8.sp
)
}
}
}
// 互动栏按钮
@Composable
fun NewsActionButton(
icon: Int,
count: String,
isActive: Boolean,
modifier: Modifier = Modifier,
text: String? = null,
textSize: androidx.compose.ui.unit.TextUnit = 12.sp
) {
val AppColors = LocalAppTheme.current
Row(
modifier = modifier
.width(60.dp)
.background(
color = AppColors.secondaryBackground,
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Image(
painter = androidx.compose.ui.res.painterResource(id = icon),
contentDescription = "操作图标",
modifier = Modifier.size(16.dp)
)
if (count.isNotEmpty()) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = count,
fontSize = 12.sp,
color = AppColors.text
)
}
if (text != null) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = text,
fontSize = textSize,
color = AppColors.text
)
}
}
}

View File

@@ -0,0 +1,18 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
object NewsViewModel : BaseMomentModel() {
override fun extraArgs(): MomentLoaderExtraArgs {
// 只拉取新闻
return MomentLoaderExtraArgs(
explore = false,
timelineId = null,
authorId = null,
newsOnly = true
)
}
}

View File

@@ -0,0 +1,178 @@
package com.aiosman.ravenow.ui.index.tabs.profile
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
/**
* 拉黑确认弹窗
*/
@Composable
fun BlockConfirmDialog(
userProfile: AccountProfileEntity?,
onConfirmBlock: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
)
.padding(24.dp)
) {
// 用户头像
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp)
) {
CustomAsyncImage(
LocalContext.current,
userProfile?.avatar,
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.background(
color = AppColors.background,
shape = CircleShape
),
contentDescription = "用户头像",
contentScale = ContentScale.Crop
)
}
// 确认文本
Text(
text = stringResource(R.string.confirm_block_user, userProfile?.nickName ?: "该用户"),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 24.dp)
)
// 说明信息
Column(
modifier = Modifier.padding(bottom = 32.dp)
) {
// 第一条说明
Row(
modifier = Modifier.padding(bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_off_eye),
contentDescription = "",
tint = AppColors.text,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.block_description_1),
fontSize = 14.sp,
color = AppColors.text,
lineHeight = 20.sp
)
}
// 第二条说明
Row(
modifier = Modifier.padding(bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_notice_mute),
contentDescription = "",
tint = AppColors.text,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.block_description_2),
fontSize = 14.sp,
color = AppColors.text,
lineHeight = 20.sp
)
}
// 第三条说明
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_off_bell),
contentDescription = "",
tint = AppColors.text,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.block_description_3),
fontSize = 14.sp,
color = AppColors.text,
lineHeight = 20.sp
)
}
}
// 确认拉黑按钮
androidx.compose.material3.Button(
onClick = {
onConfirmBlock()
onDismiss()
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
containerColor = AppColors.text
),
shape = RoundedCornerShape(24.dp)
) {
Text(
stringResource(R.string.block),
color = AppColors.background,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}

View File

@@ -99,6 +99,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import androidx.compose.foundation.rememberScrollState
import androidx.compose.ui.res.stringResource
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
fun ProfileV3(
@@ -127,6 +129,8 @@ fun ProfileV3(
var showAgentMenu by remember { mutableStateOf(false) }
var contextAgent by remember { mutableStateOf<AgentEntity?>(null) }
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
var showOtherUserMenu by remember { mutableStateOf(false) }
var showBlockConfirmDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
@@ -154,11 +158,15 @@ fun ProfileV3(
val toolbarAlpha by remember {
derivedStateOf {
if (!isSelf) {
1f
} else {
val maxScroll = 500f // 最大滚动距离,可调整
val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f)
progress
}
}
}
// observe list scrolling
val reachedListBottom by remember {
@@ -478,7 +486,10 @@ fun ProfileV3(
isSelf = isSelf,
profile = profile,
navController = navController,
alpha = toolbarAlpha
alpha = toolbarAlpha,
onMenuClick = {
showOtherUserMenu = true
}
)
PullRefreshIndicator(
@@ -486,7 +497,69 @@ fun ProfileV3(
refreshState,
Modifier.align(Alignment.TopCenter)
)
// 其他用户菜单弹窗
if (showOtherUserMenu) {
ModalBottomSheet(
onDismissRequest = { showOtherUserMenu = false },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = Color.Transparent, // 设置容器背景透明
contentColor = Color.Transparent, // 设置内容背景透明
dragHandle = null, // 移除拖拽手柄
windowInsets = androidx.compose.foundation.layout.WindowInsets(0) // 移除窗口边距
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Transparent)
) {
OtherUserMenuModal(
onBlockClick = {
showBlockConfirmDialog = true
},
onReportClick = {
// 实现举报逻辑
},
onCancelClick = {
showOtherUserMenu = false
},
onDismiss = { showOtherUserMenu = false }
)
}
}
// 拉黑确认弹窗
if (showBlockConfirmDialog) {
ModalBottomSheet(
onDismissRequest = {
showBlockConfirmDialog = false
},
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = Color.Transparent,
contentColor = Color.Transparent,
dragHandle = null,
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Transparent)
) {
BlockConfirmDialog(
userProfile = profile,
onConfirmBlock = {
// 实现拉黑逻辑
},
onDismiss = {
showBlockConfirmDialog = false
showOtherUserMenu = false
}
)
}
}
}
}
}
}
//顶部导航栏组件
@@ -496,14 +569,14 @@ fun TopNavigationBar(
isSelf: Boolean,
profile: AccountProfileEntity?,
navController: androidx.navigation.NavController,
alpha: Float
alpha: Float,
onMenuClick: () -> Unit = {}
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer { this.alpha = alpha } // 应用透明度
.graphicsLayer { this.alpha = alpha }
) {
Column(
modifier = Modifier
@@ -555,6 +628,21 @@ fun TopNavigationBar(
.size(24.dp)
.padding(16.dp)
)
} else if (!isSelf) {
Box(
modifier = Modifier
.noRippleClickable {
onMenuClick()
}
.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "菜单",
tint = appColors.text,
modifier = Modifier.size(24.dp)
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
@@ -661,7 +749,7 @@ fun AgentMenuModal(
if (isSelf) {
MenuActionItem(
icon = R.drawable.rider_pro_moment_delete,
text = "删除"
text = stringResource(R.string.delete)
) {
onDeleteClick()
}
@@ -737,3 +825,96 @@ fun DeleteConfirmDialog(
containerColor = AppColors.background
)
}
/**
* 其他用户主页菜单弹窗
*/
@Composable
fun OtherUserMenuModal(
onBlockClick: () -> Unit = {},
onReportClick: () -> Unit = {},
onCancelClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.padding(bottom = 11.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = AppColors.background,
shape = RoundedCornerShape(8.dp)
)
) {
// 拉黑选项
androidx.compose.material3.TextButton(
onClick = {
onBlockClick()
},
modifier = Modifier.fillMaxWidth()
) {
Text(
stringResource(R.string.block),
color = AppColors.error,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
}
// 分割线
androidx.compose.material3.HorizontalDivider(
color = AppColors.divider,
thickness = 0.5.dp
)
// 举报选项
androidx.compose.material3.TextButton(
onClick = {
onReportClick()
onDismiss()
},
modifier = Modifier.fillMaxWidth()
) {
Text(
stringResource(R.string.report),
color = AppColors.error,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
}
}
Spacer(modifier = Modifier.height(8.dp))
// 取消按钮
androidx.compose.material3.TextButton(
onClick = {
onCancelClick()
onDismiss()
},
modifier = Modifier
.fillMaxWidth()
.background(
color = AppColors.background,
shape = RoundedCornerShape(8.dp)
)
) {
Text(
stringResource(R.string.cancel),
color = AppColors.text,
fontSize = 16.sp
)
}
}
}
}

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -47,66 +48,72 @@ fun SelfProfileAction(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(10.dp))
.background(AppColors.nonActive)
.padding(horizontal = 5.dp, vertical = 12.dp)
.width(60.dp).height(25.dp)
.clip(RoundedCornerShape(12.dp))
.background(androidx.compose.ui.graphics.Color(0x229284BD))
.noRippleClickable {
editProfileDebouncer {
onEditProfile()
}
}
) {
Image(
painter = painterResource(id = R.mipmap.fill_and_sign),
contentDescription = "",
modifier = Modifier.size(12.dp),
colorFilter = ColorFilter.tint(androidx.compose.ui.graphics.Color(0xFF9284BD))
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(R.string.edit_profile),
fontSize = 14.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
fontSize = 12.sp,
fontWeight = FontWeight.W600,
color = androidx.compose.ui.graphics.Color(0xFF9284BD),
)
}
// 预留按钮位置
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(10.dp))
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
}
) {
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: 添加分享逻辑
}
}
) {
Text(
text = stringResource(R.string.share),
fontSize = 14.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
)
}
// // 预留按钮位置
// Row(
// verticalAlignment = Alignment.CenterVertically,
// horizontalArrangement = Arrangement.Center,
// modifier = Modifier
// .weight(1f)
// .clip(RoundedCornerShape(10.dp))
// .padding(horizontal = 16.dp, vertical = 12.dp)
// .noRippleClickable {
//
// }
// ) {
// 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: 添加分享逻辑
// }
// }
// ) {
// Text(
// text = stringResource(R.string.share),
// fontSize = 14.sp,
// fontWeight = FontWeight.W900,
// color = AppColors.text,
// )
// }
// // Rave Premium 按钮(右侧)
// Row(

View File

@@ -212,10 +212,12 @@ fun EmptyAgentsView() {
if (isNetworkAvailable) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qs_ai_qs_as_img
id =if(AppState.darkMode) R.mipmap.ai_dark
else R.mipmap.ai),
contentDescription = "暂无Agent",
modifier = Modifier.size(181.dp),
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(24.dp))

View File

@@ -65,94 +65,94 @@ fun UserAgentsRow(
viewModel.loadUserAgents(userId)
}
// 总是显示智能体区域,即使没有数据也显示标题和状态
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Text(
text = if (isSelf) "我的智能体" else "TA的智能体",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = AppColors.text,
modifier = Modifier.padding(bottom = 12.dp)
)
when {
viewModel.isLoading -> {
// 显示加载状态
Box(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
fontSize = 14.sp,
color = AppColors.text.copy(alpha = 0.6f)
)
}
}
viewModel.error != null -> {
// 显示错误状态
Box(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "加载失败: ${viewModel.error}",
fontSize = 14.sp,
color = AppColors.text.copy(alpha = 0.6f)
)
}
}
viewModel.agents.isEmpty() -> {
// 显示空状态
Box(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
contentAlignment = Alignment.Center
) {
Text(
text = if (isSelf) "您还没有创建智能体" else "TA还没有创建智能体",
fontSize = 14.sp,
color = AppColors.text.copy(alpha = 0.6f)
)
}
}
else -> {
// 显示智能体列表
LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth()
) {
// 显示智能体项目
items(viewModel.agents) { agent ->
AgentItem(
agent = agent,
onClick = { onAgentClick(agent) },
onAvatarClick = { onAvatarClick(agent) },
onLongClick = { onAgentLongClick(agent) }
)
}
// 添加"更多"按钮
item {
MoreAgentItem(
onClick = onMoreClick
)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
// // 总是显示智能体区域,即使没有数据也显示标题和状态
// Column(
// modifier = modifier
// .fillMaxWidth()
// .padding(horizontal = 16.dp)
// ) {
// Text(
// text = if (isSelf) "我的智能体" else "TA的智能体",
// fontSize = 16.sp,
// fontWeight = FontWeight.W600,
// color = AppColors.text,
// modifier = Modifier.padding(bottom = 12.dp)
// )
//
// when {
// viewModel.isLoading -> {
// // 显示加载状态
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .height(60.dp),
// contentAlignment = Alignment.Center
// ) {
// Text(
// text = "加载中...",
// fontSize = 14.sp,
// color = AppColors.text.copy(alpha = 0.6f)
// )
// }
// }
// viewModel.error != null -> {
// // 显示错误状态
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .height(60.dp),
// contentAlignment = Alignment.Center
// ) {
// Text(
// text = "加载失败: ${viewModel.error}",
// fontSize = 14.sp,
// color = AppColors.text.copy(alpha = 0.6f)
// )
// }
// }
// viewModel.agents.isEmpty() -> {
// // 显示空状态
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .height(60.dp),
// contentAlignment = Alignment.Center
// ) {
// Text(
// text = if (isSelf) "您还没有创建智能体" else "TA还没有创建智能体",
// fontSize = 14.sp,
// color = AppColors.text.copy(alpha = 0.6f)
// )
// }
// }
// else -> {
// // 显示智能体列表
// LazyRow(
// horizontalArrangement = Arrangement.spacedBy(12.dp),
// modifier = Modifier.fillMaxWidth()
// ) {
// // 显示智能体项目
// items(viewModel.agents) { agent ->
// AgentItem(
// agent = agent,
// onClick = { onAgentClick(agent) },
// onAvatarClick = { onAvatarClick(agent) },
// onLongClick = { onAgentLongClick(agent) }
// )
// }
//
// // 添加"更多"按钮
// item {
// MoreAgentItem(
// onClick = onMoreClick
// )
// }
// }
// }
// }
//
// Spacer(modifier = Modifier.height(16.dp))
// }
}
@OptIn(ExperimentalFoundationApi::class)

View File

@@ -6,9 +6,11 @@ import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -40,12 +42,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -70,6 +74,11 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
@Preview
@Composable
fun LoginPage() {
@@ -209,14 +218,14 @@ fun LoginPage() {
saveData()
}
// 显示成功提示
coroutineScope.launch(Dispatchers.Main) {
Toast.makeText(
context,
"游客登录成功",
Toast.LENGTH_SHORT
).show()
}
// // 显示成功提示
// coroutineScope.launch(Dispatchers.Main) {
// Toast.makeText(
// context,
// "游客登录成功",
// Toast.LENGTH_SHORT
// ).show()
// }
// 初始化应用状态游客模式会自动跳过推送和TRTC初始化
try {
@@ -259,14 +268,38 @@ fun LoginPage() {
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 60.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.fillMaxSize()
.size(30.dp)
.background(
color = AppColors.text.copy(alpha = 0.1f),
shape = androidx.compose.foundation.shape.CircleShape
)
.noRippleClickable {
guestLogin()
},
contentAlignment = Alignment.Center
) {
val localContext = LocalContext.current // 获取 Context
MovingImageWall(localContext.resources) // 将 resources 传递给 MovingImageWall
Image(
painter = painterResource(id = R.drawable.rider_pro_close),
contentDescription = "Close",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.text)
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Column(
modifier = Modifier
.fillMaxSize()
@@ -274,78 +307,109 @@ fun LoginPage() {
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(id = R.mipmap.invalid_name),
contentDescription = "Rave Now",
Box(
modifier = Modifier
.size(52.dp)
.clip(RoundedCornerShape(10.dp))
.fillMaxWidth()
.height(400.dp)
) {
val lottieFile = if (AppState.darkMode) "login.lottie" else "login_light.lottie"
LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset(lottieFile)).value,
iterations = LottieConstants.IterateForever,
modifier = Modifier.fillMaxSize()
)
Spacer(modifier = Modifier.height(8.dp))
}
Text(
"Rave Now",
fontSize = 28.sp,
fontWeight = FontWeight.W900,
text = stringResource(R.string.join_party_carnival),
fontSize = 17.sp,
fontWeight = FontWeight.W600,
color = AppColors.text
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Your Night Starts Here",
fontSize = 20.sp,
fontWeight = FontWeight.W700,
color = AppColors.text
)
Spacer(modifier = Modifier.height(8.dp))
// Image(
// painter = painterResource(id = R.mipmap.invalid_name),
// contentDescription = "Rave Now",
// modifier = Modifier
// .size(52.dp)
// .clip(RoundedCornerShape(10.dp))
// )
// Spacer(modifier = Modifier.height(8.dp))
// Text(
// "Rave Now",
// fontSize = 28.sp,
// fontWeight = FontWeight.W900,
// color = AppColors.text
// )
// Spacer(modifier = Modifier.height(16.dp))
// Text(
// "Your Night Starts Here",
// fontSize = 20.sp,
// fontWeight = FontWeight.W700,
// color = AppColors.text
// )
//注册tab
Spacer(modifier = Modifier.height(48.dp))
ActionButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.sign_up_upper),
color = AppColors.mainText,
backgroundColor = AppColors.main
color = if (AppState.darkMode) Color.Black else Color.White,
backgroundColor = if (AppState.darkMode) Color.White else Color.Black
) {
navController.navigate(
NavigationRoute.EmailSignUp.route,
)
}
if (showGoogleLogin) {
//谷歌登录tab
Spacer(modifier = Modifier.height(16.dp))
ActionButton(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.border(
width = 1.5.dp,
color = if (AppState.darkMode) Color.White else Color.Black,
shape = RoundedCornerShape(24.dp)
),
text = stringResource(R.string.sign_in_with_google),
color = AppColors.text,
color = if (AppState.darkMode) Color.White else Color.Black,
backgroundColor = if (AppState.darkMode) Color.Black else Color.White,
leading = {
Image(
painter = painterResource(id = R.drawable.rider_pro_google),
painter = painterResource(id = R.mipmap.rider_pro_signup_google),
contentDescription = "Google",
modifier = Modifier.size(36.dp)
modifier = Modifier.size(18.dp),
)
},
expandText = true,
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 10.dp)
) {
googleLogin()
}
}
//登录tab
Spacer(modifier = Modifier.height(24.dp))
ActionButton(
modifier = Modifier.fillMaxWidth(),
Text(
text = stringResource(R.string.login_upper),
color = AppColors.text,
) {
color = AppColors.text.copy(alpha = 0.5f),
fontSize = 16.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.noRippleClickable {
navController.navigate(
NavigationRoute.UserAuth.route,
)
}
// 游客登录按钮
Spacer(modifier = Modifier.height(16.dp))
ActionButton(
modifier = Modifier.fillMaxWidth(),
text = "游客模式",
color = AppColors.text.copy(alpha = 0.7f),
) {
guestLogin()
}
)
// // 游客登录按钮
// Spacer(modifier = Modifier.height(16.dp))
// ActionButton(
// modifier = Modifier.fillMaxWidth(),
// text = "游客模式",
// color = AppColors.text.copy(alpha = 0.7f),
// ) {
// guestLogin()
// }
Spacer(modifier = Modifier.height(70.dp))
}
}

View File

@@ -22,6 +22,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -60,7 +62,7 @@ fun UserAuthScreen() {
val AppColors = LocalAppTheme.current
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) }
var rememberMe by remember { mutableStateOf(true) }
val accountService: AccountService = AccountServiceImpl()
val captchaService: CaptchaService = CaptchaServiceImpl()
val scope = rememberCoroutineScope()
@@ -299,32 +301,38 @@ fun UserAuthScreen() {
ActionButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.lets_ride_upper),
backgroundColor = AppColors.main,
backgroundBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0x777c68ef),
Color(0x777bd8f8)
)
),
color = AppColors.mainText,
) {
onLogin()
}
if (AppState.enableGoogleLogin) {
Spacer(modifier = Modifier.height(16.dp))
Text(stringResource(R.string.or_login_with), color = AppColors.secondaryText)
Spacer(modifier = Modifier.height(16.dp))
ActionButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.sign_in_with_google),
color = AppColors.text,
leading = {
Image(
painter = painterResource(id = R.drawable.rider_pro_google),
contentDescription = "Google",
modifier = Modifier.size(36.dp)
)
},
expandText = true,
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
) {
googleLogin()
}
}
// if (AppState.enableGoogleLogin) {
// Spacer(modifier = Modifier.height(16.dp))
// Text(stringResource(R.string.or_login_with), color = AppColors.secondaryText)
// Spacer(modifier = Modifier.height(16.dp))
// ActionButton(
// modifier = Modifier.fillMaxWidth(),
// text = stringResource(R.string.sign_in_with_google),
// color = AppColors.text,
// leading = {
// Image(
// painter = painterResource(id = R.drawable.rider_pro_google),
// contentDescription = "Google",
// modifier = Modifier.size(36.dp)
// )
// },
// expandText = true,
// contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
// ) {
// googleLogin()
// }
// }
}

View File

@@ -63,6 +63,23 @@ object Utils {
return Locale.getDefault().language
}
/**
* 获取完整的语言标记,如 "zh-CN", "en-US"
* 优先使用完整的 BCP-47 语言标记,提升与后端 translations 键的匹配率
*/
fun getPreferredLanguageTag(): String {
val locale = Locale.getDefault()
val language = locale.language
val country = locale.country
// 如果有国家/地区代码,返回完整的语言标记
return if (country.isNotEmpty()) {
"$language-$country"
} else {
language
}
}
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
val inputStream = context.contentResolver.openInputStream(uri)
val originalBitmap = BitmapFactory.decodeStream(inputStream)

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -147,6 +147,7 @@
<string name="chat_group">グループ</string>
<string name="chat_friend">友達</string>
<string name="chat_all">すべて</string>
<string name="chatting_now">人はおしゃべりをしている…</string>
<string name="agent_chat_list_title">AIエージェントチャット</string>
<string name="agent_chat_empty_title">AIエージェントチャットがありません</string>
<string name="agent_chat_empty_subtitle">AIエージェントと対話してみましょう</string>
@@ -157,7 +158,6 @@
<string name="agent_chat_file">[ファイル]</string>
<string name="agent_chat_message">[メッセージ]</string>
<string name="agent_chat_load_failed">読み込みに失敗しました</string>
<string name="agent_chat_loading">読み込み中</string>
<string name="agent_chat_load_more_failed">さらに読み込むのに失敗しました</string>
<string name="agent_chat_user_info_failed">ユーザー情報の取得に失敗しました: %s</string>
<string name="group_chat_empty">グループチャットがありません</string>
@@ -192,6 +192,9 @@
<string name="agent_createing">作成中…</string>
<string name="agent_find">発見</string>
<string name="text_error_password_too_long">パスワードは%1$d文字を超えることはできません</string>
<string name="block">ブロック</string>
<string name="read_full_article">全文を読む</string>
<!-- Create Bottom Sheet -->
<string name="create_title">作成</string>
@@ -219,5 +222,31 @@
<string name="friend_chat_no_network_title">オフラインだ..</string>
<string name="friend_chat_no_network_subtitle">ネットワークを確認して、この宇宙に接続してください</string>
<string name="Reload">再ロード</string>
<!-- Login page -->
<string name="join_party_carnival">パーティーに参加して、一緒に盛り上がろう</string>
<!-- Tab labels -->
<string name="tab_recommend">おすすめ</string>
<string name="tab_short_video">ショート動画</string>
<string name="tab_news">ニュース</string>
<!-- Block Confirm Dialog -->
<string name="confirm_block_user">%1$sをブロックしますか</string>
<string name="block_description_1">相手はあなたにメッセージを送信したり、あなたのプロフィールやコンテンツを見つけることができなくなります</string>
<string name="block_description_2">相手はあなたにブロックされた通知を受け取りません</string>
<string name="block_description_3">「設定」でいつでもブロックを解除できます</string>
<!-- Chat Settings -->
<string name="chat_settings">チャット設定</string>
<string name="chat_theme_settings">チャットテーマを設定</string>
<string name="custom_background">カスタム背景</string>
<string name="select_from_gallery">ギャラリーから選択</string>
<string name="featured_backgrounds">おすすめ背景</string>
<string name="previewing_custom_background">カスタム背景をプレビュー中</string>
<string name="each_theme_unique_experience">各テーマには独自の体験があります</string>
<string name="select_apply_to_use_theme">「適用」を選択してこのテーマを使用</string>
<string name="tap_cancel_to_preview_other_themes">「キャンセル」をタップして他のテーマをプレビュー</string>
</resources>

View File

@@ -32,7 +32,7 @@
<string name="text_hint_password">输入密码</string>
<string name="sign_up_upper">注册</string>
<string name="sign_in_with_email">使用邮箱注册</string>
<string name="sign_in_with_google">使用 Google 账号登录</string>
<string name="sign_in_with_google">使用Google账号登录</string>
<string name="back_upper">返回</string>
<string name="text_hint_confirm_password">再次输入密码</string>
<string name="login_confirm_password_label">再次输入密码</string>
@@ -48,7 +48,7 @@
<string name="error_not_accept_term">"为了提供更好的服务,请您在注册前仔细阅读并同意《用户协议》。 "</string>
<string name="empty_my_post_title">还没有发布任何动态</string>
<string name="empty_my_post_content">发布一个动态吧</string>
<string name="edit_profile">编辑资料</string>
<string name="edit_profile">编辑</string>
<string name="share">分享</string>
<string name="logout">登出</string>
<string name="change_password">修改密码</string>
@@ -127,7 +127,7 @@
<string name="index_following">关注</string>
<string name="index_hot">热门</string>
<string name="main_home">首页</string>
<string name="main_ai">智能体</string>
<string name="main_ai">AI</string>
<string name="main_message">消息</string>
<string name="main_profile">我的</string>
<string name="agent_mine">我的</string>
@@ -149,7 +149,7 @@
<string name="chat_group">群聊</string>
<string name="chat_friend">朋友</string>
<string name="chat_all">全部</string>
<string name="favourites_null">咦,什么都没有...</string>
<string name="favourites_null">暂无数据</string>
<string name="agent_chat_list_title">智能体聊天</string>
<string name="agent_chat_empty_title">AI 在等你的开场白</string>
@@ -161,15 +161,14 @@
<string name="agent_chat_file">[文件]</string>
<string name="agent_chat_message">[消息]</string>
<string name="agent_chat_load_failed">加载失败</string>
<string name="agent_chat_loading">加载中</string>
<string name="agent_chat_load_more_failed">加载更多失败</string>
<string name="agent_chat_user_info_failed">获取用户信息失败: %s</string>
<string name="group_chat_empty">没有群聊,宇宙好安静</string>
<string name="group_chat_empty_title">没有群聊消息的宇宙太安静了</string>
<string name="group_chat_empty_subtitle">在首页探索感兴趣的主题房间</string>
<string name="group_chat_empty_join">去首页探索感兴趣的高能对话</string>
<string name="friend_chat_empty_title">和朋友,还没说第一句话呢</string>
<string name="friend_chat_empty_subtitle">一段崭新的友谊 等待被唤醒</string>
<string name="friend_chat_empty_title">和朋友,还没有对话哦~</string>
<string name="friend_chat_empty_subtitle">点击好友头像,即刻发起聊天</string>
<string name="friend_chat_me_prefix">我: </string>
<string name="friend_chat_load_failed">加载失败</string>
<string name="create_group_chat">创建群聊</string>
@@ -192,9 +191,13 @@
<string name="group_room_enter">进入</string>
<string name="group_room_enter_success">成功加入房间</string>
<string name="group_room_enter_fail">加入房间失败</string>
<string name="agent_createing">创建中...</string>
<string name="agent_createing">创建中</string>
<string name="agent_find">发现</string>
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
<string name="chatting_now">人正在热聊…</string>
<string name="block">拉黑</string>
<string name="read_full_article">查看全文</string>
<!-- Create Bottom Sheet -->
<string name="create_title">创建</string>
@@ -223,4 +226,28 @@
<string name="friend_chat_no_network_subtitle">确认一下网络,连接这个宇宙</string>
<string name="Reload">重新加载</string>
<!-- Login page -->
<string name="join_party_carnival">加入派派,一起狂欢</string>
<!-- Tab labels -->
<string name="tab_recommend">推荐</string>
<string name="tab_short_video">短视频</string>
<string name="tab_news">新闻</string>
<!-- Block Confirm Dialog -->
<string name="confirm_block_user">确认拉黑%s</string>
<string name="block_description_1">对方将无法发消息给你,也无法找到你的主页或内容</string>
<string name="block_description_2">对方不会收到自己被你拉黑的通知</string>
<string name="block_description_3">你可以随时"设置"中取消拉黑TA</string>
<!-- Chat Settings -->
<string name="chat_settings">聊天设置</string>
<string name="chat_theme_settings">设置聊天主题</string>
<string name="custom_background">自定义背景</string>
<string name="select_from_gallery">从相册选择</string>
<string name="featured_backgrounds">精选背景</string>
<string name="previewing_custom_background">正在预览自定义背景</string>
<string name="each_theme_unique_experience">每个主题都有自己独特的体验</string>
<string name="select_apply_to_use_theme">选择"应用"可选中这个主题</string>
<string name="tap_cancel_to_preview_other_themes">轻触"取消"可预览其他主题</string>
</resources>

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