37 Commits

Author SHA1 Message Date
2ba4c2ec02 Merge branch 'main' into atm 2025-11-05 23:15:50 +08:00
28061617da Merge pull request #49 from Kevinlinpr/zhong_1
记忆管理功能实现
2025-11-05 22:30:50 +08:00
c9e411c4ad Merge branch 'main' into zhong_1 2025-11-05 22:30:29 +08:00
995e061b6f Merge pull request #52 from Kevinlinpr/feat/pr-20251104-154907
添加深色模式缺省图
2025-11-05 22:25:09 +08:00
9c9eb66b71 新增智能体和房间规则管理功能
- 新增智能体(Agent)规则的增删改查(CRUD)和服务实现。
- 新增房间(Room)规则的增删改查(CRUD)和服务实现。
- 将 `PromptRule` 相关命名重构为 `AgentRule`,以提高代码清晰度。
- 将相关数据实体中表示总数的字段类型从 `Int` 修改为 `Long`。
2025-11-05 22:24:03 +08:00
f90bfbfa0f 添加深色模式缺省图 2025-11-05 21:31:11 +08:00
9ea03cee34 群记忆项编辑功能;群权限设置UI;群记忆管理缺省图 2025-11-05 21:29:18 +08:00
1de8bb825c Merge pull request #51 from Kevinlinpr/revert-48-feat/pr-20251104-154907
Revert "Feat/pr 20251104 154907"
2025-11-05 16:57:59 +08:00
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
4a1c15747c 文本资源文件替换 2025-11-04 19:01:11 +08:00
0bbfd9b739 修改底部导航栏颜色,暗夜模式缺省图 2025-11-04 18:59:18 +08:00
cff6b78c30 记忆管理功能实现 2025-11-04 18:28:24 +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
179 changed files with 6816 additions and 882 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-17T06:25:35.585100400Z"> <DropdownSelection timestamp="2025-11-05T12:24:27.034893100Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=192.168.0.216:5555;connection=698a7727" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=c328a150" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -2,8 +2,16 @@ package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.AgentRule
import com.aiosman.ravenow.data.api.AgentRuleListResponse
import com.aiosman.ravenow.data.api.AgentRuleQuota
import com.aiosman.ravenow.data.api.AgentRuleAgent
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
import com.aiosman.ravenow.data.api.InsufficientBalanceError
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.ProfileEntity import com.aiosman.ravenow.entity.ProfileEntity
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class Agent( data class Agent(
@@ -83,9 +91,16 @@ data class Profile(
) )
} }
} }
/**
* 智能体服务
*/
interface AgentService { interface AgentService {
/** /**
* 获取智能体列表 * 获取智能体列表
* @param pageNumber 页码
* @param pageSize 每页数量,默认 20
* @param authorId 作者ID可选参数用于筛选特定作者的智能体
* @return 智能体列表容器,包含分页信息和智能体列表,失败时返回 null
*/ */
suspend fun getAgent( suspend fun getAgent(
pageNumber: Int, pageNumber: Int,
@@ -96,3 +111,206 @@ interface AgentService {
} }
// ========== Agent 规则 - 领域实体 ==========
data class AgentRuleAgentInfo(
val id: Int,
val title: String,
val avatar: String,
)
data class AgentRuleEntity(
val id: Int,
val rule: String,
val creator: String,
val creatorType: String,
val scope: String,
val agent: AgentRuleAgentInfo,
val createdAt: String,
val updatedAt: String,
)
data class AgentRuleListResult(
val page: Int,
val pageSize: Int,
val total: Int,
val list: List<AgentRuleEntity>,
)
data class AgentRuleQuotaEntity(
val agentId: Int,
val agentTitle: String,
val baseMaxCount: Int,
val purchasedCount: Int,
val totalMaxCount: Int,
val currentCount: Int,
val remainingCount: Int,
val usagePercent: Double,
)
// ========== Agent 规则 - Service 接口 ==========
/**
* Agent 规则服务
*/
interface AgentRuleService {
/**
* 根据 OpenId 创建 Agent 规则
* @param openId Agent 的 OpenId
* @param rule 规则内容,不能为空
* @throws ServiceException 创建失败时抛出异常(包括余额不足等情况)
*/
suspend fun createAgentRuleByOpenId(openId: String, rule: String)
/**
* 修改 Agent 规则
* @param id 规则ID
* @param rule 新的规则内容,不能为空
* @param openId Agent 的 OpenId可选参数
* @throws ServiceException 修改失败时抛出异常(包括余额不足等情况)
*/
suspend fun updateAgentRule(id: Int, rule: String, openId: String? = null)
/**
* 删除 Agent 规则
* @param id 规则ID
* @throws ServiceException 删除失败时抛出异常
*/
suspend fun deleteAgentRule(id: Int)
/**
* 查询 Agent 规则列表
* @param openId Agent 的 OpenId
* @param keyword 关键词搜索,可选参数
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
* @return 规则列表响应,包含分页信息和规则列表
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getAgentRuleList(
openId: String,
keyword: String? = null,
page: Int = 1,
pageSize: Int = 10
): AgentRuleListResult
/**
* 查询 Agent 规则配额使用情况
* @param openId Agent 的 OpenId
* @return 规则配额信息,包含基础配额、已购买配额、当前使用量等
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getAgentRuleQuota(openId: String): AgentRuleQuotaEntity
}
class AgentRuleServiceImpl : AgentRuleService {
private val gson = Gson()
override suspend fun createAgentRuleByOpenId(openId: String, rule: String) {
val body = CreateAgentRuleRequestBody(
rule = rule,
promptId = null,
openId = openId
)
val resp = ApiClient.api.createAgentRule(body)
if (!resp.isSuccessful) {
val errorText = resp.errorBody()?.string()
try {
val err = gson.fromJson(errorText, InsufficientBalanceError::class.java)
if (err != null && err.code == 35600) {
throw ServiceException(err.message)
}
} catch (_: Exception) {
// ignore parse error
}
throw ServiceException("创建 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun updateAgentRule(id: Int, rule: String, openId: String?) {
val body = UpdateAgentRuleRequestBody(
id = id,
rule = rule,
promptId = null,
openId = openId
)
val resp = ApiClient.api.updateAgentRule(body)
if (!resp.isSuccessful) {
val errorText = resp.errorBody()?.string()
try {
val err = gson.fromJson(errorText, InsufficientBalanceError::class.java)
if (err != null && err.code == 35600) {
throw ServiceException(err.message)
}
} catch (_: Exception) {
// ignore parse error
}
throw ServiceException("更新 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun deleteAgentRule(id: Int) {
val resp = ApiClient.api.deleteAgentRule(id)
if (!resp.isSuccessful) {
throw ServiceException("删除 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun getAgentRuleList(
openId: String,
keyword: String?,
page: Int,
pageSize: Int
): AgentRuleListResult {
val resp = ApiClient.api.getAgentRuleList(
promptId = openId,
rule = keyword,
page = page,
pageSize = pageSize
)
val data = resp.body()?.data ?: throw ServiceException("获取 Agent 规则列表失败")
return data.toEntity()
}
override suspend fun getAgentRuleQuota(openId: String): AgentRuleQuotaEntity {
val resp = ApiClient.api.getAgentRuleQuota(openId)
val data = resp.body()?.data ?: throw ServiceException("获取 Agent 规则配额失败")
return data.toEntity()
}
private fun AgentRuleListResponse.toEntity(): AgentRuleListResult = AgentRuleListResult(
page = page,
pageSize = pageSize,
total = total,
list = list.map { it.toEntity() }
)
private fun AgentRule.toEntity(): AgentRuleEntity = AgentRuleEntity(
id = id,
rule = rule,
creator = creator,
creatorType = creatorType,
scope = scope,
agent = prompt.toEntity(),
createdAt = createdAt,
updatedAt = updatedAt,
)
private fun AgentRuleAgent.toEntity(): AgentRuleAgentInfo = AgentRuleAgentInfo(
id = id,
title = title,
avatar = avatar,
)
private fun AgentRuleQuota.toEntity(): AgentRuleQuotaEntity = AgentRuleQuotaEntity(
agentId = promptId,
agentTitle = promptTitle,
baseMaxCount = baseMaxCount,
purchasedCount = purchasedCount,
totalMaxCount = totalMaxCount,
currentCount = currentCount,
remainingCount = remainingCount,
usagePercent = usagePercent,
)
}

View File

@@ -9,7 +9,7 @@ import com.google.gson.annotations.SerializedName
data class ListContainer<T>( data class ListContainer<T>(
// 总数 // 总数
@SerializedName("total") @SerializedName("total")
val total: Int, val total: Long,
// 当前页 // 当前页
@SerializedName("page") @SerializedName("page")
val page: Int, val page: Int,

View File

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

View File

@@ -1,11 +1,16 @@
package com.aiosman.ravenow.data package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.data.api.CreateRoomRuleRequestBody
import com.aiosman.ravenow.data.api.UpdateRoomRuleRequestBody
import com.aiosman.ravenow.data.api.RoomRuleQuota
import com.aiosman.ravenow.data.api.RoomRule
import com.aiosman.ravenow.data.api.RoomRuleCreator
import com.aiosman.ravenow.entity.CreatorEntity import com.aiosman.ravenow.entity.CreatorEntity
import com.aiosman.ravenow.entity.ProfileEntity
import com.aiosman.ravenow.entity.RoomEntity import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.entity.RoomRuleEntity
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
import com.aiosman.ravenow.entity.RoomRuleQuotaEntity
import com.aiosman.ravenow.entity.UsersEntity import com.aiosman.ravenow.entity.UsersEntity
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
@@ -106,6 +111,190 @@ data class Users(
} }
} }
/**
* 房间规则相关服务
*/
interface RoomService {
/**
* 创建房间规则
* @param rule 规则内容,不能为空
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @throws ServiceException 创建失败时抛出异常
*/
suspend fun createRoomRule(
rule: String,
roomId: Int? = null,
trtcId: String? = null
)
/**
* 修改房间规则
* @param id 规则ID
* @param rule 新的规则内容,不能为空
* @throws ServiceException 修改失败时抛出异常
*/
suspend fun updateRoomRule(
id: Int,
rule: String
)
/**
* 删除房间规则
* @param id 规则ID
* @throws ServiceException 删除失败时抛出异常
*/
suspend fun deleteRoomRule(id: Int)
/**
* 查询房间规则列表
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
* @return 规则列表响应,包含分页信息和规则列表
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getRoomRuleList(
roomId: Int? = null,
trtcId: String? = null,
page: Int = 1,
pageSize: Int = 10
): ListContainer<RoomRuleEntity>
/**
* 查询规则配额使用情况
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @return 规则配额信息
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getRoomRuleQuota(
roomId: Int? = null,
trtcId: String? = null
): RoomRuleQuotaEntity
}
/**
* 房间规则服务实现类
*/
class RoomServiceImpl : RoomService {
override suspend fun createRoomRule(
rule: String,
roomId: Int?,
trtcId: String?
) {
val resp = ApiClient.api.createRoomRule(
CreateRoomRuleRequestBody(
rule = rule,
roomId = roomId,
trtcId = trtcId
)
)
if (!resp.isSuccessful) {
throw ServiceException("创建房间规则失败")
}
}
override suspend fun updateRoomRule(
id: Int,
rule: String
) {
val resp = ApiClient.api.updateRoomRule(
UpdateRoomRuleRequestBody(
id = id,
rule = rule
)
)
if (!resp.isSuccessful) {
throw ServiceException("修改房间规则失败")
}
}
override suspend fun deleteRoomRule(id: Int) {
val resp = ApiClient.api.deleteRoomRule(id)
if (!resp.isSuccessful) {
throw ServiceException("删除房间规则失败")
}
}
override suspend fun getRoomRuleList(
roomId: Int?,
trtcId: String?,
page: Int,
pageSize: Int
): ListContainer<RoomRuleEntity> {
val resp = ApiClient.api.getRoomRuleList(
roomId = roomId,
trtcId = trtcId,
page = page,
pageSize = pageSize
)
val body = resp.body() ?: throw ServiceException("获取房间规则列表失败")
return ListContainer(
list = body.list.map { it.toRoomRuleEntity() },
page = body.page,
total = body.total,
pageSize = body.pageSize
)
}
override suspend fun getRoomRuleQuota(
roomId: Int?,
trtcId: String?
): RoomRuleQuotaEntity {
val resp = ApiClient.api.getRoomRuleQuota(
roomId = roomId,
trtcId = trtcId
)
val body = resp.body() ?: throw ServiceException("获取规则配额信息失败")
val data = body.data ?: throw ServiceException("规则配额数据为空")
return data.toRoomRuleQuotaEntity()
}
}
/**
* RoomRule 扩展函数,转换为 RoomRuleEntity
*/
fun RoomRule.toRoomRuleEntity(): RoomRuleEntity {
return RoomRuleEntity(
id = id,
rule = rule,
creator = creator?.toRoomRuleCreatorEntity(),
creatorType = creatorType,
roomId = roomId,
createdAt = createdAt,
updatedAt = updatedAt
)
}
/**
* RoomRuleCreator 扩展函数,转换为 RoomRuleCreatorEntity
*/
fun RoomRuleCreator.toRoomRuleCreatorEntity(): RoomRuleCreatorEntity {
return RoomRuleCreatorEntity(
id = id,
nickname = nickname,
avatar = avatar
)
}
/**
* RoomRuleQuota 扩展函数,转换为 RoomRuleQuotaEntity
*/
fun RoomRuleQuota.toRoomRuleQuotaEntity(): RoomRuleQuotaEntity {
return RoomRuleQuotaEntity(
baseMaxCount = baseMaxCount,
purchasedCount = purchasedCount,
totalMaxCount = totalMaxCount,
currentCount = currentCount,
remainingCount = remainingCount,
usagePercent = usagePercent
)
}

View File

@@ -81,7 +81,9 @@ data class CreateGroupChatRequestBody(
data class JoinGroupChatRequestBody( data class JoinGroupChatRequestBody(
@SerializedName("trtcId") @SerializedName("trtcId")
val trtcId: String, val trtcId: String? = null,
@SerializedName("roomId")
val roomId: Int? = null,
) )
data class LoginUserRequestBody( data class LoginUserRequestBody(
@@ -271,6 +273,127 @@ data class RemoveAccountRequestBody(
val password: String, val password: String,
) )
// API 错误响应(用于加入房间等接口的错误处理)
data class ApiErrorResponse(
@SerializedName("err")
val error: String,
@SerializedName("success")
val success: Boolean
)
// 群聊中的用户信息
data class GroupChatUser(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("userSessionId")
val userSessionId: String,
@SerializedName("sessions")
val sessions: Any?, // 根据实际需要可以定义具体类型
@SerializedName("prompts")
val prompts: Any?, // 根据实际需要可以定义具体类型
@SerializedName("isAgent")
val isAgent: Boolean
)
// 智能体角色信息
data class GroupChatPrompt(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("Title")
val title: String,
@SerializedName("Desc")
val desc: String,
@SerializedName("Value")
val value: String,
@SerializedName("Enable")
val enable: Boolean,
@SerializedName("UserSessions")
val userSessions: Any?, // 根据实际需要可以定义具体类型
@SerializedName("Avatar")
val avatar: String,
@SerializedName("AuthorId")
val authorId: Int?,
@SerializedName("Author")
val author: Any?, // 根据实际需要可以定义具体类型
@SerializedName("TokenCount")
val tokenCount: Int,
@SerializedName("OpenId")
val openId: String,
@SerializedName("Public")
val public: Boolean,
@SerializedName("BreakMode")
val breakMode: Boolean,
@SerializedName("DocNamespace")
val docNamespace: String,
@SerializedName("UseRag")
val useRag: Boolean,
@SerializedName("RagThreshold")
val ragThreshold: Double,
@SerializedName("WorkflowId")
val workflowId: Int?,
@SerializedName("Workflow")
val workflow: Any?, // 根据实际需要可以定义具体类型
@SerializedName("WorkflowInputs")
val workflowInputs: Any?, // 根据实际需要可以定义具体类型
@SerializedName("Source")
val source: String,
@SerializedName("categories")
val categories: Any? // 根据实际需要可以定义具体类型
)
// 群聊详细信息响应
data class GroupChatResponse(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("name")
val name: String,
@SerializedName("description")
val description: String,
@SerializedName("creatorId")
val creatorId: Int,
@SerializedName("creator")
val creator: Any?, // 根据实际需要可以定义具体类型
@SerializedName("trtcRoomId")
val trtcRoomId: String,
@SerializedName("trtcType")
val trtcType: String,
@SerializedName("cover")
val cover: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("recommendBanner")
val recommendBanner: String,
@SerializedName("isRecommended")
val isRecommended: Boolean,
@SerializedName("allowInHot")
val allowInHot: Boolean,
@SerializedName("users")
val users: List<GroupChatUser>,
@SerializedName("prompts")
val prompts: List<GroupChatPrompt>,
@SerializedName("source")
val source: String
)
data class CategoryTranslation( data class CategoryTranslation(
@SerializedName("name") @SerializedName("name")
val name: String?, val name: String?,
@@ -366,6 +489,278 @@ data class CategoryListResponse(
val list: List<CategoryTemplate> val list: List<CategoryTemplate>
) )
// ========== Agent Rule 相关数据类 ==========
/**
* 创建规则请求体
* @param rule 规则内容,不能为空
* @param promptId 智能体ID与 openId 二选一promptId 优先
* @param openId 智能体的 OpenIDUUID格式与 promptId 二选一
*/
data class CreateAgentRuleRequestBody(
@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 UpdateAgentRuleRequestBody(
@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 AgentRuleAgent(
@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 AgentRule(
@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: AgentRuleAgent,
@SerializedName("created_at")
val createdAt: String,
@SerializedName("updated_at")
val updatedAt: String
)
/**
* 规则列表响应
* @param page 当前页码
* @param pageSize 每页数量
* @param total 总记录数
* @param list 规则列表
*/
data class AgentRuleListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Int,
@SerializedName("list")
val list: List<AgentRule>
)
/**
* 规则配额信息
* @param promptId 智能体ID
* @param promptTitle 智能体标题
* @param baseMaxCount 基础条数限制(免费配额)
* @param purchasedCount 用户购买的额外条数
* @param totalMaxCount 总可用条数(基础+购买)
* @param currentCount 当前已创建的规则条数
* @param remainingCount 剩余可用条数
* @param usagePercent 使用百分比0-100
*/
data class AgentRuleQuota(
@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
)
// ========== Room Rule 相关数据类 ==========
/**
* 创建房间规则请求体
* @param rule 规则内容,不能为空
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID字符串格式与 roomId 二选一
*/
data class CreateRoomRuleRequestBody(
@SerializedName("rule")
val rule: String,
@SerializedName("roomId")
val roomId: Int? = null,
@SerializedName("trtcId")
val trtcId: String? = null
)
/**
* 修改房间规则请求体
* @param id 规则ID必填
* @param rule 新的规则内容,不能为空
*/
data class UpdateRoomRuleRequestBody(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String
)
/**
* 房间规则创建者信息
* @param id 创建者ID
* @param nickname 创建者昵称
* @param avatar 创建者头像文件名
*/
data class RoomRuleCreator(
@SerializedName("id")
val id: Int,
@SerializedName("nickname")
val nickname: String,
@SerializedName("avatar")
val avatar: String
)
/**
* 房间规则详情
* @param id 规则ID
* @param rule 规则内容
* @param creator 创建者信息(可能为 null
* @param creatorType 创建者类型(如 "user"
* @param roomId 所属房间ID
* @param createdAt 创建时间ISO 8601 格式)
* @param updatedAt 更新时间ISO 8601 格式)
*/
data class RoomRule(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String,
@SerializedName("creator")
val creator: RoomRuleCreator?,
@SerializedName("creatorType")
val creatorType: String,
@SerializedName("roomId")
val roomId: Int,
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String
)
/**
* 房间规则列表响应
* @param page 当前页码
* @param pageSize 每页数量
* @param total 总记录数
* @param list 规则列表
*/
data class RoomRuleListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Long,
@SerializedName("list")
val list: List<RoomRule>
)
/**
* 房间规则配额信息
* @param baseMaxCount 基础条数限制(系统配置的免费配额)
* @param purchasedCount 用户购买的额外条数(通过自动扩容机制)
* @param totalMaxCount 总可用条数baseMaxCount + purchasedCount
* @param currentCount 当前已创建的规则条数(该用户在该房间)
* @param remainingCount 剩余可用条数totalMaxCount - currentCount
* @param usagePercent 使用百分比0-100
*/
data class RoomRuleQuota(
@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
)
/**
* 积分不足错误响应
*
* 当创建房间规则时积分不足,服务器会返回此特殊错误格式。
* 包含当前余额和所需金额信息,便于客户端提示用户。
*
* @param success 是否成功,固定为 false
* @param code 错误码,固定为 35600积分不足
* @param message 错误消息,包含所需积分和当前余额的详细说明
* @param err 错误类型,固定为 "InsufficientBalance"
* @param currentBalance 当前积分余额
* @param requiredAmount 所需积分数量
* @param traceId 追踪ID可选
*/
data class InsufficientBalanceError(
@SerializedName("success")
val success: Boolean,
@SerializedName("code")
val code: Int,
@SerializedName("message")
val message: String,
@SerializedName("err")
val err: String,
@SerializedName("currentBalance")
val currentBalance: Int,
@SerializedName("requiredAmount")
val requiredAmount: Int,
@SerializedName("traceId")
val traceId: String?
)
interface RaveNowAPI { interface RaveNowAPI {
@GET("membership/config") @GET("membership/config")
@retrofit2.http.Headers("X-Requires-Auth: true") @retrofit2.http.Headers("X-Requires-Auth: true")
@@ -404,6 +799,7 @@ interface RaveNowAPI {
@Query("trend") trend: String? = null, @Query("trend") trend: String? = null,
@Query("favouriteUserId") favouriteUserId: Int? = null, @Query("favouriteUserId") favouriteUserId: Int? = null,
@Query("explore") explore: String? = null, @Query("explore") explore: String? = null,
@Query("newsFilter") newsFilter: String? = null,
): Response<ListContainer<Moment>> ): Response<ListContainer<Moment>>
@Multipart @Multipart
@@ -677,7 +1073,10 @@ interface RaveNowAPI {
suspend fun agentMoment(@Body body: AgentMomentRequestBody): Response<DataContainer<String>> suspend fun agentMoment(@Body body: AgentMomentRequestBody): Response<DataContainer<String>>
@GET("outside/rooms/open") @GET("outside/rooms/open")
suspend fun createGroupChatAi(@Query("trtcGroupId") trtcGroupId: String): Response<DataContainer<Unit>> suspend fun createGroupChatAi(
@Query("trtcGroupId") trtcGroupId: String? = null,
@Query("roomId") roomId: Int? = null
): Response<DataContainer<GroupChatResponse>>
@POST("outside/rooms/create-single-chat") @POST("outside/rooms/create-single-chat")
suspend fun createSingleChat(@Body body: SingleChatRequestBody): Response<DataContainer<Unit>> suspend fun createSingleChat(@Body body: SingleChatRequestBody): Response<DataContainer<Unit>>
@@ -692,6 +1091,7 @@ interface RaveNowAPI {
suspend fun getRooms(@Query("page") page: Int = 1, suspend fun getRooms(@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20, @Query("pageSize") pageSize: Int = 20,
@Query("isRecommended") isRecommended: Int = 1, @Query("isRecommended") isRecommended: Int = 1,
@Query("random") random: Int? = null,
): Response<ListContainer<Room>> ): Response<ListContainer<Room>>
@GET("outside/rooms/detail") @GET("outside/rooms/detail")
@@ -736,5 +1136,461 @@ interface RaveNowAPI {
@Query("pageSize") pageSize: Int? = null @Query("pageSize") pageSize: Int? = null
): Response<ListContainer<Agent>> ): Response<ListContainer<Agent>>
// ========== Agent 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 createAgentRule(
@Body body: CreateAgentRuleRequestBody
): 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 updateAgentRule(
@Body body: UpdateAgentRuleRequestBody
): 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 deleteAgentRule(
@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 getAgentRuleList(
@Path("promptId") promptId: String,
@Query("rule") rule: String? = null,
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 10
): Response<DataContainer<AgentRuleListResponse>>
/**
* 查询智能体规则配额信息
*
* 功能说明:
* - 查询指定智能体的规则条数使用情况
* - 包括基础配额、已购买配额、已使用数量等完整信息
* - 用于判断是否还能创建新规则
*
* @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 getAgentRuleQuota(
@Path("promptId") promptId: String
): Response<DataContainer<AgentRuleQuota>>
// ========== Room Rule API ==========
/**
* 创建房间规则
*
* 功能说明:
* - 为指定的房间创建一条新规则
* - 规则会被添加到AI对话上下文中用于约束房间内的行为和交互方式
* - 必须是房间成员或房间创建者才能创建规则
* - 每个用户在每个房间有基础条数限制,达到限制后会自动扣费扩容
* - 如果积分不足,会返回特殊的错误响应(错误码 35600
*
* @param body 创建规则请求体
* - rule: 规则内容,不能为空
* - roomId: 房间ID与 trtcId 二选一)
* - trtcId: TRTC房间ID与 roomId 二选一)
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 400 (40002): 规则内容为空
* - 400 (40003): 规则内容超出字数限制
* - 400 (35600): 积分不足(自动扩容失败),返回 InsufficientBalanceError 格式
* - 401 (40101): 未登录
* - 403 (40301): 无权限为该房间创建规则
* - 400 (40005): 房间不存在
*
* 示例:
* ```kotlin
* // 使用 roomId 创建规则
* val request1 = CreateRoomRuleRequestBody(
* rule = "禁止在房间内讨论政治话题",
* roomId = 123
* )
* val response1 = api.createRoomRule(request1)
*
* // 使用 trtcId 创建规则
* val request2 = CreateRoomRuleRequestBody(
* rule = "请使用文明用语",
* trtcId = "room_trtc_123"
* )
* val response2 = api.createRoomRule(request2)
*
* // 处理积分不足错误
* if (!response1.isSuccessful && response1.code() == 400) {
* val errorBody = response1.errorBody()?.string()
* // 解析为 InsufficientBalanceError 获取余额信息
* }
* ```
*/
@POST("outside/room/rule")
suspend fun createRoomRule(
@Body body: CreateRoomRuleRequestBody
): Response<Unit>
/**
* 修改房间规则
*
* 功能说明:
* - 修改指定的房间规则内容
* - 只能修改自己创建的规则creator_type 为 "user"
* - 修改不会增加规则条数,因此不会触发扣费
*
* @param body 修改规则请求体
* - id: 规则ID必填
* - rule: 新的规则内容,不能为空
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者
* - 规则必须是用户类型creator_type = "user"
*
* 错误响应说明:
* - 400 (40002): 规则内容为空
* - 400 (40003): 规则内容超出字数限制
* - 401 (40101): 未登录
* - 403 (40301): 无权限修改该规则
* - 404 (40401): 规则不存在
*
* 示例:
* ```kotlin
* val request = UpdateRoomRuleRequestBody(
* id = 456,
* rule = "请保持友善交流"
* )
* val response = api.updateRoomRule(request)
* ```
*/
@retrofit2.http.PUT("outside/room/rule")
suspend fun updateRoomRule(
@Body body: UpdateRoomRuleRequestBody
): Response<Unit>
/**
* 删除房间规则
*
* 功能说明:
* - 删除指定的房间规则
* - 只能删除自己创建的规则creator_type 为 "user"
* - 删除操作不可恢复,请谨慎操作
* - 删除规则不会退还已扣除的积分
*
* @param id 规则ID
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者
* - 规则必须是用户类型creator_type = "user"
*
* 错误响应说明:
* - 400 (40006): 无效的规则ID
* - 401 (40101): 未登录
* - 403 (40301): 无权限删除该规则
*
* 示例:
* ```kotlin
* val response = api.deleteRoomRule(456)
* ```
*/
@DELETE("outside/room/rule/{id}")
suspend fun deleteRoomRule(
@Path("id") id: Int
): Response<Unit>
/**
* 查询房间规则列表
*
* 功能说明:
* - 查询指定房间的规则列表,支持分页
* - 返回该房间所有用户创建的规则,包含创建者信息
* - 必须是房间成员或房间创建者才能查询
*
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
*
* @return 返回分页的规则列表,包含规则详情和创建者信息
*
* 响应数据说明:
* - page: 当前页码
* - pageSize: 每页数量
* - total: 总记录数
* - list: 规则列表
* - id: 规则ID
* - rule: 规则内容
* - creator: 创建者信息id, nickname, avatar
* - creatorType: 创建者类型("user" 等)
* - roomId: 所属房间ID
* - createdAt: 创建时间ISO 8601格式
* - updatedAt: 更新时间ISO 8601格式
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 401 (40101): 未登录
* - 403 (40301): 无权限查看该房间的规则
*
* 示例:
* ```kotlin
* // 使用 roomId 查询
* val response1 = api.getRoomRuleList(
* roomId = 123,
* page = 1,
* pageSize = 10
* )
*
* // 使用 trtcId 查询
* val response2 = api.getRoomRuleList(
* trtcId = "room_trtc_123",
* page = 1,
* pageSize = 10
* )
*
* // 处理响应
* response1.body()?.let { result ->
* println("共 ${result.total} 条规则,当前第 ${result.page} 页")
* result.list.forEach { rule ->
* println("规则: ${rule.rule}, 创建者: ${rule.creator?.nickname}")
* }
* }
* ```
*/
@GET("outside/room/rule")
suspend fun getRoomRuleList(
@Query("roomId") roomId: Int? = null,
@Query("trtcId") trtcId: String? = null,
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 10
): Response<RoomRuleListResponse>
/**
* 查询规则额度使用情况
*
* 功能说明:
* - 查询当前用户在指定房间的规则额度使用情况
* - 包括基础条数、已购买条数、当前使用数等完整信息
* - 用于判断用户是否还能创建新规则
* - 必须是房间成员或房间创建者才能查询
*
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
*
* @return 返回配额详细信息
*
* 响应数据说明:
* - baseMaxCount: 基础条数限制(系统配置的免费配额)
* - purchasedCount: 用户购买的额外条数(通过自动扩容机制)
* - totalMaxCount: 总可用条数baseMaxCount + purchasedCount
* - currentCount: 当前已创建的规则条数(该用户在该房间)
* - remainingCount: 剩余可用条数totalMaxCount - currentCount
* - usagePercent: 使用百分比0-100currentCount / totalMaxCount * 100
*
* 使用场景:
* 1. 创建规则前检查是否有足够配额
* 2. 展示规则使用情况统计
* 3. 提示用户即将达到配额上限
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 401 (40101): 未登录
* - 403 (40301): 无权限查看该房间的规则条数信息
*
* 示例:
* ```kotlin
* // 使用 roomId 查询
* val response1 = api.getRoomRuleQuota(roomId = 123)
*
* // 使用 trtcId 查询
* val response2 = api.getRoomRuleQuota(trtcId = "room_trtc_123")
*
* // 处理响应
* response1.body()?.data?.let { quota ->
* if (quota.remainingCount > 0) {
* // 可以创建新规则
* println("还可以创建 ${quota.remainingCount} 条规则")
* println("使用率: ${quota.usagePercent}%")
* } else {
* // 配额已用完,需要扩容
* println("规则配额已用完,已使用 ${quota.currentCount}/${quota.totalMaxCount}")
* println("创建新规则将自动扣除积分进行扩容")
* }
* }
* ```
*/
@GET("outside/room/rule/length-info")
suspend fun getRoomRuleQuota(
@Query("roomId") roomId: Int? = null,
@Query("trtcId") trtcId: String? = null
): Response<DataContainer<RoomRuleQuota>>
} }

View File

@@ -7,18 +7,13 @@ import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.ListContainer import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.AgentService import com.aiosman.ravenow.data.AgentService
import com.aiosman.ravenow.data.DataContainer import com.aiosman.ravenow.data.DataContainer
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.http.Part
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -160,7 +155,7 @@ class AgentBackend {
return if (authorId != null) { return if (authorId != null) {
// getAgent 返回 DataContainer<ListContainer<Agent>> // getAgent 返回 DataContainer<ListContainer<Agent>>
val dataContainer = val dataContainer =
body as com.aiosman.ravenow.data.DataContainer<com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>> body as DataContainer<ListContainer<Agent>>
val listContainer = dataContainer.data val listContainer = dataContainer.data
ListContainer( ListContainer(
total = listContainer.total, total = listContainer.total,
@@ -171,7 +166,7 @@ class AgentBackend {
} else { } else {
// getMyAgent 返回 ListContainer<Agent> // getMyAgent 返回 ListContainer<Agent>
val listContainer = val listContainer =
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent> body as ListContainer<Agent>
ListContainer( ListContainer(
total = listContainer.total, total = listContainer.total,
page = pageNumber, page = pageNumber,
@@ -251,7 +246,7 @@ class AgentLoader : DataLoader<AgentEntity, AgentLoaderExtraArgs>() {
} else { } else {
// getMyAgent 返回 ListContainer<Agent> // getMyAgent 返回 ListContainer<Agent>
val listContainer = val listContainer =
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent> body as ListContainer<Agent>
ListContainer( ListContainer(
list = listContainer.list.map { it.toAgentEntity() }, list = listContainer.list.map { it.toAgentEntity() },
total = listContainer.total, total = listContainer.total,

View File

@@ -9,7 +9,7 @@ import com.aiosman.ravenow.data.ListContainer
abstract class DataLoader<T,ET> { abstract class DataLoader<T,ET> {
var list: MutableList<T> = mutableListOf() var list: MutableList<T> = mutableListOf()
var page by mutableStateOf(1) var page by mutableStateOf(1)
var total by mutableStateOf(0) var total by mutableStateOf(0L)
var pageSize by mutableStateOf(10) var pageSize by mutableStateOf(10)
var hasNext by mutableStateOf(true) var hasNext by mutableStateOf(true)
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)

View File

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

View File

@@ -3,10 +3,6 @@ package com.aiosman.ravenow.entity
import com.aiosman.ravenow.data.ListContainer import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.ServiceException import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
/** /**
* 群聊房间 * 群聊房间
@@ -56,6 +52,40 @@ data class ProfileEntity(
val aiAccount: Boolean, val aiAccount: Boolean,
) )
/**
* 房间规则创建者信息
*/
data class RoomRuleCreatorEntity(
val id: Int,
val nickname: String,
val avatar: String
)
/**
* 房间规则详情
*/
data class RoomRuleEntity(
val id: Int,
val rule: String,
val creator: RoomRuleCreatorEntity?,
val creatorType: String,
val roomId: Int,
val createdAt: String,
val updatedAt: String
)
/**
* 房间规则配额信息
*/
data class RoomRuleQuotaEntity(
val baseMaxCount: Int,
val purchasedCount: Int,
val totalMaxCount: Int,
val currentCount: Int,
val remainingCount: Int,
val usagePercent: Double
)
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() { class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
override suspend fun fetchData( override suspend fun fetchData(
page: Int, page: Int,

View File

@@ -30,6 +30,12 @@ object AppStore {
AppState.appTheme = DarkThemeColors() AppState.appTheme = DarkThemeColors()
} }
// load chat background
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
if (savedBgUrl != null) {
AppState.chatBackgroundUrl = savedBgUrl
}
} }
suspend fun saveData() { suspend fun saveData() {
@@ -54,5 +60,40 @@ object AppStore {
}.apply() }.apply()
} }
fun saveChatBackgroundUrl(url: String?) {
sharedPreferences.edit().apply {
if (url != null) {
putString("chatBackgroundUrl", url)
} else {
remove("chatBackgroundUrl")
}
}.apply()
AppState.chatBackgroundUrl = url
}
// ===================== 用户本地扩展信息 =====================
// 后端暂未提供 MBTI 与星座字段,使用本地持久化按用户维度进行存储
private fun mbtiKey(userId: Int) = "mbti_user_$userId"
private fun zodiacKey(userId: Int) = "zodiac_user_$userId"
fun getUserMbti(userId: Int): String? {
return sharedPreferences.getString(mbtiKey(userId), null)
}
fun setUserMbti(userId: Int, mbti: String?) {
sharedPreferences.edit().apply {
if (mbti.isNullOrEmpty()) remove(mbtiKey(userId)) else putString(mbtiKey(userId), mbti)
}.apply()
}
fun getUserZodiac(userId: Int): String? {
return sharedPreferences.getString(zodiacKey(userId), null)
}
fun setUserZodiac(userId: Int, zodiac: String?) {
sharedPreferences.edit().apply {
if (zodiac.isNullOrEmpty()) remove(zodiacKey(userId)) else putString(zodiacKey(userId), zodiac)
}.apply()
}
} }

View File

@@ -35,12 +35,15 @@ import com.aiosman.ravenow.LocalSharedTransitionScope
import com.aiosman.ravenow.ui.about.AboutScreen import com.aiosman.ravenow.ui.about.AboutScreen
import com.aiosman.ravenow.ui.account.AccountEditScreen2 import com.aiosman.ravenow.ui.account.AccountEditScreen2
import com.aiosman.ravenow.ui.account.AccountSetting import com.aiosman.ravenow.ui.account.AccountSetting
import com.aiosman.ravenow.ui.account.MbtiSelectScreen
import com.aiosman.ravenow.ui.account.RemoveAccountScreen import com.aiosman.ravenow.ui.account.RemoveAccountScreen
import com.aiosman.ravenow.ui.account.ResetPasswordScreen import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.account.ZodiacSelectScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
import com.aiosman.ravenow.ui.chat.ChatScreen import com.aiosman.ravenow.ui.chat.ChatScreen
import com.aiosman.ravenow.ui.chat.GroupChatScreen import com.aiosman.ravenow.ui.chat.GroupChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen import com.aiosman.ravenow.ui.comment.CommentsScreen
@@ -106,6 +109,7 @@ sealed class NavigationRoute(
data object FavouriteList : NavigationRoute("FavouriteList") data object FavouriteList : NavigationRoute("FavouriteList")
data object Chat : NavigationRoute("Chat/{id}") data object Chat : NavigationRoute("Chat/{id}")
data object ChatAi : NavigationRoute("ChatAi/{id}") data object ChatAi : NavigationRoute("ChatAi/{id}")
data object ChatSetting : NavigationRoute("ChatSetting")
data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}") data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen") data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
data object ImageCrop : NavigationRoute("ImageCrop") data object ImageCrop : NavigationRoute("ImageCrop")
@@ -118,6 +122,8 @@ sealed class NavigationRoute(
data object VipSelPage : NavigationRoute("VipSelPage") data object VipSelPage : NavigationRoute("VipSelPage")
data object RemoveAccountScreen: NavigationRoute("RemoveAccount") data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
data object NotificationScreen : NavigationRoute("NotificationScreen") data object NotificationScreen : NavigationRoute("NotificationScreen")
data object MbtiSelect : NavigationRoute("MbtiSelect")
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
} }
@@ -419,6 +425,12 @@ fun NavigationController(
composable(route = NavigationRoute.RemoveAccountScreen.route) { composable(route = NavigationRoute.RemoveAccountScreen.route) {
RemoveAccountScreen() RemoveAccountScreen()
} }
composable(route = NavigationRoute.MbtiSelect.route) {
MbtiSelectScreen()
}
composable(route = NavigationRoute.ZodiacSelect.route) {
ZodiacSelectScreen()
}
composable(route = NavigationRoute.VipSelPage.route) { composable(route = NavigationRoute.VipSelPage.route) {
VipSelPage() VipSelPage()
} }
@@ -491,6 +503,14 @@ fun NavigationController(
} }
} }
composable(route = NavigationRoute.ChatSetting.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
ChatSettingScreen()
}
}
composable( composable(
route = NavigationRoute.ChatGroup.route, route = NavigationRoute.ChatGroup.route,
arguments = listOf(navArgument("id") { type = NavType.StringType }, arguments = listOf(navArgument("id") { type = NavType.StringType },

View File

@@ -26,6 +26,9 @@ object AccountEditViewModel : ViewModel() {
var croppedBitmap by mutableStateOf<Bitmap?>(null) var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false) var isUpdating by mutableStateOf(false)
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
// 本地扩展字段
var mbti by mutableStateOf<String?>(null)
var zodiac by mutableStateOf<String?>(null)
suspend fun reloadProfile(updateTrtcProfile:Boolean = false) { suspend fun reloadProfile(updateTrtcProfile:Boolean = false) {
Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料") Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料")
isLoading = true isLoading = true
@@ -38,6 +41,12 @@ object AccountEditViewModel : ViewModel() {
bio = it.bio bio = it.bio
// 清除之前裁剪的图片 // 清除之前裁剪的图片
croppedBitmap = null croppedBitmap = null
// 读取本地扩展字段
try {
val uid = it.id // 使用 profile 的 id确保非空
mbti = com.aiosman.ravenow.AppStore.getUserMbti(uid)
zodiac = com.aiosman.ravenow.AppStore.getUserZodiac(uid)
} catch (_: Exception) { }
if (updateTrtcProfile) { if (updateTrtcProfile) {
TrtcHelper.updateTrtcProfile( TrtcHelper.updateTrtcProfile(
it.nickName, it.nickName,
@@ -84,6 +93,13 @@ object AccountEditViewModel : ViewModel() {
nickName = newName, nickName = newName,
bio = cleanBio bio = cleanBio
) )
// 保存本地扩展字段
try {
profile?.id?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
}
} catch (_: Exception) { }
// 刷新用户资料 // 刷新用户资料
reloadProfile() reloadProfile()
// 刷新个人资料页面的用户资料 // 刷新个人资料页面的用户资料

View File

@@ -0,0 +1,136 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
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.runtime.remember
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.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
// MBTI类型列表
val MBTI_TYPES = listOf(
"INTJ", "INTP", "ENTJ", "ENTP",
"INFJ", "INFP", "ENFJ", "ENFP",
"ISTJ", "ISFJ", "ESTJ", "ESFJ",
"ISTP", "ISFP", "ESTP", "ESFP"
)
@Composable
fun MbtiSelectScreen() {
val navController = LocalNavController.current
val appColors = LocalAppTheme.current
val model = AccountEditViewModel
val currentMbti = model.mbti
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.profileBackground)
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.choose_mbti),
moreIcon = false
)
}
// 列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(MBTI_TYPES) { mbti ->
MBTIItem(
mbti = mbti,
isSelected = mbti == currentMbti,
onClick = {
model.mbti = mbti
navController.navigateUp()
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
fun MBTIItem(
mbti: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = mbti,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
}
}

View File

@@ -0,0 +1,132 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
// 星座列表
val ZODIAC_SIGNS = listOf(
"白羊座", "金牛座", "双子座", "巨蟹座",
"狮子座", "处女座", "天秤座", "天蝎座",
"射手座", "摩羯座", "水瓶座", "双鱼座"
)
@Composable
fun ZodiacSelectScreen() {
val navController = LocalNavController.current
val appColors = LocalAppTheme.current
val model = AccountEditViewModel
val currentZodiac = model.zodiac
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.profileBackground)
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.choose_zodiac),
moreIcon = false
)
}
// 列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(ZODIAC_SIGNS) { zodiac ->
ZodiacItem(
zodiac = zodiac,
isSelected = zodiac == currentZodiac,
onClick = {
model.zodiac = zodiac
navController.navigateUp()
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
fun ZodiacItem(
zodiac: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = zodiac,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
}
}

View File

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

View File

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

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

@@ -125,12 +125,13 @@ fun CommentNoticeScreen() {
) { ) {
androidx.compose.foundation.Image( androidx.compose.foundation.Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_pl_qs_as_img id = if(AppState.darkMode) R.mipmap.tietie_dark
else R.mipmap.invalid_name_11), else R.mipmap.invalid_name_11),
contentDescription = "No Comment", contentDescription = "No Comment",
modifier = Modifier.size(181.dp) modifier = Modifier
.size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( Text(
text = "等一位旅人~", text = "等一位旅人~",
color = AppColors.text, color = AppColors.text,

View File

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

View File

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

View File

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

View File

@@ -364,16 +364,6 @@ fun MomentContentGroup(
onPageChange: (Int) -> Unit = {} onPageChange: (Int) -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
if (momentEntity.momentTextContent.isNotEmpty()) {
Text(
text = momentEntity.momentTextContent,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
fontSize = 16.sp,
color = AppColors.text
)
}
if (momentEntity.relMoment != null) { if (momentEntity.relMoment != null) {
RelPostCard( RelPostCard(
momentEntity = momentEntity.relMoment!!, momentEntity = momentEntity.relMoment!!,
@@ -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), .size(width = 24.dp, height = 24.dp),
painter = painterResource(id = icon), painter = painterResource(id = icon),
contentDescription = "", contentDescription = "",
colorFilter = ColorFilter.tint(AppColors.text)
) )
if (count.isNotEmpty()) {
Text( Text(
text = count, text = count,
modifier = Modifier.padding(start = 7.dp), modifier = Modifier.padding(start = 7.dp),
@@ -410,6 +411,7 @@ fun MomentOperateBtn(@DrawableRes icon: Int, count: String) {
color = AppColors.text color = AppColors.text
) )
} }
}
} }
@Composable @Composable
@@ -510,49 +512,11 @@ fun MomentBottomOperateRowGroup(
.weight(1f), .weight(1f),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Box( Row(
modifier = Modifier modifier = Modifier.weight(1f).fillMaxHeight(),
.weight(1f) verticalAlignment = Alignment.CenterVertically
.fillMaxHeight(),
contentAlignment = Alignment.CenterStart
) {
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
AnimatedFavouriteIcon(
modifier = Modifier.size(24.dp),
isFavourite = momentEntity.isFavorite
) {
onFavoriteClick()
}
}
}
Box(
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight()
.noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
onCommentClick()
}
},
contentAlignment = Alignment.CenterEnd
) {
MomentOperateBtn(
icon = R.drawable.rider_pro_comment,
count = momentEntity.commentCount.toString()
)
}
Spacer(modifier = Modifier.width(24.dp))
Box(
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight(),
contentAlignment = Alignment.CenterEnd
) { ) {
// 点赞按钮
MomentOperateBtn(count = momentEntity.likeCount.toString()) { MomentOperateBtn(count = momentEntity.likeCount.toString()) {
AnimatedLikeIcon( AnimatedLikeIcon(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
@@ -561,6 +525,44 @@ fun MomentBottomOperateRowGroup(
onLikeClick() 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 package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -12,11 +17,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
/** /**
* 可复用的标签页组件 * 可复用的标签页组件
*/ */
@@ -54,3 +62,43 @@ fun TabItem(
fun TabSpacer() { fun TabSpacer() {
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
@Composable
fun UnderlineTabItem(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier
.noRippleClickable { onClick() },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = text,
fontSize = 15.sp,
fontWeight = FontWeight.ExtraBold,
color = if (isSelected) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 13.dp)
)
// 选中状态下显示图标
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
if (isSelected) {
Image(
painter = painterResource(id = R.mipmap.underline),
contentDescription = "selected indicator",
)
}
}
}
}

View File

@@ -134,12 +134,12 @@ fun FavouriteListPage() {
) { ) {
Image( Image(
painter = painterResource( 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), else R.mipmap.invalid_name_1),
contentDescription = "No favourites", contentDescription = "No favourites",
modifier = Modifier.size(110.dp) modifier = Modifier.size(181.dp, 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
Text( Text(
text = stringResource(R.string.favourites_null), text = stringResource(R.string.favourites_null),
color = AppColors.text, color = AppColors.text,

View File

@@ -124,21 +124,21 @@ fun FollowerListScreen(userId: Int) {
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img id = if(AppState.darkMode) R.mipmap.frame_4
else R.mipmap.invalid_name_8), else R.mipmap.invalid_name_8),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp, 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text( androidx.compose.material.Text(
text = "还没有人关注", text = "还没有人关注你呢",
color = appColors.text, color = appColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600
) )
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "去发布动态,吸引更多粉丝", text = "试着发信号出来,某人就会被吸引啦",
color = appColors.text, color = appColors.text,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400

View File

@@ -114,21 +114,22 @@ fun FollowerNoticeScreen() {
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img id = if(AppState.darkMode) R.mipmap.frame_4
else R.mipmap.invalid_name_8), else R.mipmap.invalid_name_8),
contentDescription = "No Followers", contentDescription = "No Followers",
modifier = Modifier.size(181.dp) modifier = Modifier
.size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "还没有人关注", text = "还没有人关注你呢",
color = AppColors.text, color = AppColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600
) )
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "去发布动态,吸引更多粉丝", text = "试着发信号出来,某人就会被吸引啦",
color = AppColors.text, color = AppColors.text,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400

View File

@@ -126,14 +126,14 @@ fun FollowingListScreen(userId: Int) {
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img_my id = if(AppState.darkMode) R.mipmap.frame_3
else R.mipmap.invalid_name_9), else R.mipmap.invalid_name_9),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp, 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text( androidx.compose.material.Text(
text = "没有关注任何灵魂", text = "没有关注任何灵魂",
color = appColors.text, color = appColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.ui.group package com.aiosman.ravenow.ui.group
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -8,6 +9,10 @@ import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ChatState import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CreatePromptRuleRequestBody
import com.aiosman.ravenow.data.api.PromptRule
import com.aiosman.ravenow.data.api.PromptRuleQuota
import com.aiosman.ravenow.data.parseErrorResponse
import com.aiosman.ravenow.entity.ChatNotification import com.aiosman.ravenow.entity.ChatNotification
import com.aiosman.ravenow.entity.GroupInfo import com.aiosman.ravenow.entity.GroupInfo
import com.aiosman.ravenow.entity.GroupMember import com.aiosman.ravenow.entity.GroupMember
@@ -22,9 +27,36 @@ class GroupChatInfoViewModel(
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var error by mutableStateOf<String?>(null) var error by mutableStateOf<String?>(null)
var chatNotification by mutableStateOf<ChatNotification?>(null) var chatNotification by mutableStateOf<ChatNotification?>(null)
var isAddingMemory by mutableStateOf(false)
var addMemoryError by mutableStateOf<String?>(null)
var addMemorySuccess by mutableStateOf(false)
val notificationStrategy get() = chatNotification?.strategy ?: "default" val notificationStrategy get() = chatNotification?.strategy ?: "default"
// 记忆管理相关状态
var memoryQuota by mutableStateOf<PromptRuleQuota?>(null)
var memoryList by mutableStateOf<List<PromptRule>>(emptyList())
var isLoadingMemory by mutableStateOf(false)
var memoryError by mutableStateOf<String?>(null)
var promptOpenId by mutableStateOf<String?>(null)
init { init {
loadGroupInfo() loadGroupInfo()
loadPromptOpenId()
}
/**
* 获取群聊中智能体的 OpenID
*/
private fun loadPromptOpenId() {
viewModelScope.launch {
try {
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
promptOpenId = prompts?.firstOrNull()?.openId
} catch (e: Exception) {
Log.e("GroupChatInfoViewModel", "获取智能体OpenID失败: ${e.message}", e)
}
}
} }
suspend fun updateNotificationStrategy(strategy: String) { suspend fun updateNotificationStrategy(strategy: String) {
val result = ChatState.updateChatNotification(groupId.hashCode(), strategy) val result = ChatState.updateChatNotification(groupId.hashCode(), strategy)
@@ -71,4 +103,218 @@ class GroupChatInfoViewModel(
} }
} }
} }
/**
* 添加群记忆
* @param memoryText 记忆内容
* @param promptOpenId 智能体的 OpenID可选如果不提供则从群聊信息中获取
*/
fun addGroupMemory(memoryText: String, promptOpenId: String? = null) {
viewModelScope.launch {
try {
isAddingMemory = true
addMemoryError = null
addMemorySuccess = false
// 如果没有提供 promptOpenId需要先获取群聊的智能体信息
val openId = promptOpenId ?: run {
// 通过 createGroupChatAi 接口获取群聊详细信息(包含 prompts
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
if (prompts.isNullOrEmpty()) {
throw Exception("群聊中没有找到智能体,无法添加记忆")
}
// 使用第一个智能体的 openId
prompts.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
}
if (openId.isBlank()) {
throw Exception("智能体ID不能为空")
}
// 创建智能体规则(群记忆)
val requestBody = CreatePromptRuleRequestBody(
rule = memoryText,
openId = openId
)
val response = ApiClient.api.createPromptRule(requestBody)
if (response.isSuccessful) {
addMemorySuccess = true
Log.d("GroupChatInfoViewModel", "群记忆添加成功")
// 刷新记忆列表和配额
loadMemoryQuota(openId)
loadMemoryList(openId)
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "添加群记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
addMemoryError = e.message ?: "添加群记忆失败"
Log.e("GroupChatInfoViewModel", "添加群记忆失败: ${e.message}", e)
} finally {
isAddingMemory = false
}
}
}
/**
* 获取记忆配额信息
*/
fun loadMemoryQuota(openId: String? = null) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val targetOpenId = openId ?: promptOpenId
if (targetOpenId.isNullOrBlank()) {
// 如果还没有获取到 openId先获取
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
val fetchedOpenId = prompts?.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId
val quotaResponse = ApiClient.api.getPromptRuleQuota(fetchedOpenId)
if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data
} else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
}
} else {
val quotaResponse = ApiClient.api.getPromptRuleQuota(targetOpenId)
if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data
} else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
}
}
} catch (e: Exception) {
memoryError = e.message ?: "获取配额信息失败"
Log.e("GroupChatInfoViewModel", "获取配额信息失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 获取记忆列表
*/
fun loadMemoryList(openId: String? = null, page: Int = 1, pageSize: Int = 20) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val targetOpenId = openId ?: promptOpenId
if (targetOpenId.isNullOrBlank()) {
// 如果还没有获取到 openId先获取
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
val fetchedOpenId = prompts?.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId
val listResponse = ApiClient.api.getPromptRuleList(fetchedOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList()
} else {
throw Exception("获取记忆列表失败: ${listResponse.code()}")
}
} else {
val listResponse = ApiClient.api.getPromptRuleList(targetOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList()
} else {
throw Exception("获取记忆列表失败: ${listResponse.code()}")
}
}
} catch (e: Exception) {
memoryError = e.message ?: "获取记忆列表失败"
Log.e("GroupChatInfoViewModel", "获取记忆列表失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 删除记忆
*/
fun deleteMemory(ruleId: Int) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val response = ApiClient.api.deletePromptRule(ruleId)
if (response.isSuccessful) {
// 刷新记忆列表和配额
promptOpenId?.let { openId ->
loadMemoryQuota(openId)
loadMemoryList(openId)
}
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "删除记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
memoryError = e.message ?: "删除记忆失败"
Log.e("GroupChatInfoViewModel", "删除记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 更新记忆
*/
fun updateMemory(ruleId: Int, newRuleText: String, targetOpenId: String? = null) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val openId = targetOpenId ?: promptOpenId
?: throw Exception("无法获取智能体ID")
val requestBody = com.aiosman.ravenow.data.api.UpdatePromptRuleRequestBody(
id = ruleId,
rule = newRuleText,
openId = openId
)
val response = ApiClient.api.updatePromptRule(requestBody)
if (response.isSuccessful) {
// 刷新记忆列表和配额
loadMemoryQuota(openId)
loadMemoryList(openId)
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "更新记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
memoryError = e.message ?: "更新记忆失败"
Log.e("GroupChatInfoViewModel", "更新记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
} }

View File

@@ -0,0 +1,625 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
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.graphics.ColorFilter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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.foundation.Image
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import android.widget.Toast
import androidx.compose.ui.graphics.Brush
@Composable
fun GroupMemoryManageContent(
groupId: String,
viewModel: GroupChatInfoViewModel,
onAddMemoryClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.95f
val context = LocalContext.current
// 编辑记忆的状态 - 存储正在编辑的记忆ID
var editingMemoryId by remember { mutableStateOf<Int?>(null) }
// 加载配额和列表数据
LaunchedEffect(Unit) {
viewModel.loadMemoryQuota()
viewModel.loadMemoryList()
}
val quota = viewModel.memoryQuota
val memoryList = viewModel.memoryList
val isLoading = viewModel.isLoadingMemory
Column(
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight)
.background(Color(0xFFFAF9FB))
) {
// 顶部栏:返回按钮 + 标题 + 加号按钮
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp)
) {
// 中间标题 - 绝对居中,不受其他组件影响
Text(
text = "记忆管理",
style = TextStyle(
color = Color.Black,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold
),
modifier = Modifier.align(Alignment.Center),
textAlign = TextAlign.Center
)
// 左侧返回按钮
Row(
modifier = Modifier
.align(Alignment.CenterStart)
.clip(RoundedCornerShape(296.dp))
.background(Color.White) // 浅灰色背景
.noRippleClickable { onDismiss() }
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(R.drawable.rider_pro_back_icon),
contentDescription = "返回",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(Color.Black)
)
Text(
text = "返回",
style = TextStyle(
color = Color.Black,
fontSize = 15.sp,
fontWeight = FontWeight.Normal
)
)
}
// 右侧圆形加号按钮
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.size(32.dp)
.clip(CircleShape)
.background(Color.White)
.noRippleClickable { onAddMemoryClick() },
contentAlignment = Alignment.Center
) {
Text(
text = "+",
style = TextStyle(
color = Color.Black,
fontSize = 20.sp,
fontWeight = FontWeight.Medium
)
)
}
}
// 浅黄色提示栏 - 显示真实的配额数据
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFFBF8EF))
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("已付费:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.purchasedCount ?: 0}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("已使用:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.currentCount ?: 0}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("可用上限:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"50",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
}
// 记忆列表或空状态
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
if (isLoading) {
// 加载中状态
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(40.dp),
color = Color(0xFFFF8D28)
)
}
} else if (memoryList.isNotEmpty()) {
// 显示记忆列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(memoryList) { memory ->
MemoryItem(
memory = memory,
isEditing = editingMemoryId == memory.id,
onEdit = {
editingMemoryId = memory.id
},
onCancel = {
editingMemoryId = null
},
onSave = { newText ->
viewModel.updateMemory(memory.id, newText)
editingMemoryId = null
},
onDelete = {
viewModel.deleteMemory(memory.id)
}
)
}
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.mipmap.group),
contentDescription = "暂无记忆",
modifier = Modifier
.height(150.dp).width(180.dp)
)
Spacer(Modifier.height(10.dp))
Text(
text = "暂无记忆",
style = TextStyle(color = Color.Black, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
)
Spacer(Modifier.height(6.dp))
Text(
text = "点击上方按钮添加群记忆",
style = TextStyle(color = Color.Black, fontSize = 14.sp, fontWeight = FontWeight.Normal)
)
}
}
}
}
}
/**
* 编辑记忆对话框
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditGroupMemoryDialog(
memory: com.aiosman.ravenow.data.api.PromptRule,
viewModel: GroupChatInfoViewModel,
onDismiss: () -> Unit,
onUpdateMemory: (String) -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf(memory.rule) }
val maxLength = 500
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val gradientColors = listOf(
Color(0xFF7C45ED),
Color(0xFF7C57EE),
Color(0xFF7BD8F8)
)
val gradientBrush = Brush.horizontalGradient(colors = gradientColors)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color(0xFFFAF9FB),
dragHandle = {
Box(
modifier = Modifier
.width(36.dp)
.height(5.dp)
.padding(top = 5.dp)
.background(
Color(0xFFCCCCCC),
RoundedCornerShape(100.dp)
)
)
},
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(88.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(brush = gradientBrush)
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
.clip(RoundedCornerShape(15.dp))
.background(Color.White)
.padding(12.dp),
contentAlignment = Alignment.TopStart
) {
BasicTextField(
value = memoryText,
onValueChange = { newText ->
if (newText.length <= maxLength) {
memoryText = newText
}
},
cursorBrush = SolidColor(Color.Black),
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 13.sp,
color = Color.Black,
lineHeight = 18.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
innerTextField()
}
}
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0x147C7480))
.noRippleClickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
style = TextStyle(
fontSize = 15.sp,
color = Color.Black
)
)
}
// 保存按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0xFF110C13))
.noRippleClickable {
if (memoryText.isNotBlank() && memoryText != memory.rule) {
onUpdateMemory(memoryText)
Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show()
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
style = TextStyle(
fontSize = 15.sp,
color = Color.White
)
)
}
}
}
}
}
/**
* 记忆项组件
*/
@Composable
fun MemoryItem(
memory: com.aiosman.ravenow.data.api.PromptRule,
isEditing: Boolean = false,
onEdit: () -> Unit = {},
onCancel: () -> Unit = {},
onSave: (String) -> Unit = {},
onDelete: () -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf(memory.rule) }
val maxLength = 500
// 渐变边框颜色
val gradientColors = listOf(
Color(0xFF7C45ED),
Color(0xFF7C57EE),
Color(0xFF7BD8F8)
)
val gradientBrush = Brush.horizontalGradient(colors = gradientColors)
// 当进入编辑模式时,重置文本
LaunchedEffect(isEditing) {
if (isEditing) {
memoryText = memory.rule
}
}
if (isEditing) {
// 编辑模式:显示编辑界面
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 文本输入框 - 带渐变边框
Box(
modifier = Modifier
.fillMaxWidth()
.height(88.dp)
) {
// 渐变边框层
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(brush = gradientBrush)
)
// 内容层 - 白色背景通过padding形成边框效果
Box(
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
.clip(RoundedCornerShape(15.dp))
.background(Color.White)
.padding(12.dp),
contentAlignment = Alignment.TopStart
) {
BasicTextField(
value = memoryText,
onValueChange = { newText ->
if (newText.length <= maxLength) {
memoryText = newText
}
},
cursorBrush = SolidColor(Color.Black),
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 13.sp,
color = Color.Black,
lineHeight = 18.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
innerTextField()
}
}
)
}
}
// 按钮行:取消和保存
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0x147C7480))
.noRippleClickable { onCancel() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
style = TextStyle(
fontSize = 15.sp,
color = Color.Black
)
)
}
// 保存按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0xFF110C13))
.noRippleClickable {
if (memoryText.isNotBlank() && memoryText != memory.rule) {
onSave(memoryText)
Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show()
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
style = TextStyle(
fontSize = 15.sp,
color = Color.White
)
)
}
}
}
} else {
// 显示模式:显示记忆内容
// 格式化日期:从 "2025-10-20T10:30:00Z" 格式转换为 "2025年10月20日"
val formattedDate = try {
if (memory.createdAt.length >= 10) {
val dateStr = memory.createdAt.substring(0, 10)
val parts = dateStr.split("-")
if (parts.size == 3) {
"${parts[0]}${parts[1].toInt()}${parts[2].toInt()}"
} else {
dateStr
}
} else {
memory.createdAt
}
} catch (e: Exception) {
if (memory.createdAt.length >= 10) memory.createdAt.substring(0, 10) else memory.createdAt
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 16.dp)
) {
// 主文本 - 顶部
Text(
text = memory.rule,
style = TextStyle(
color = Color.Black,
fontSize = 13.sp,
lineHeight = 18.sp
),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 底部行:日期 + 编辑删除按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom
) {
// 日期文本 - 左侧
Text(
text = formattedDate,
style = TextStyle(
color = Color(0x993C3C43),
fontSize = 11.sp
)
)
// 编辑和删除图标 - 右侧
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 编辑图标
Image(
painter = painterResource(R.mipmap.icons_infor_edit),
contentDescription = "编辑",
modifier = Modifier
.size(20.dp)
.noRippleClickable { onEdit() },
colorFilter = ColorFilter.tint(Color.Black)
)
Image(
painter = painterResource(R.mipmap.iconsdelete),
contentDescription = "删除",
modifier = Modifier
.size(20.dp)
.noRippleClickable { onDelete() },
colorFilter = ColorFilter.tint(Color(0xFFEE2A33))
)
}
}
}
}
}

View File

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

View File

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

View File

@@ -80,7 +80,12 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.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.foundation.lazy.grid.items as gridItems
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.ui.draw.alpha
// 检测是否接近列表底部的扩展函数 // 检测是否接近列表底部的扩展函数
fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean { fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
@@ -266,6 +271,53 @@ fun Agent() {
} }
} }
// 热门聊天室
stickyHeader(key = "hot_rooms_header") {
Row(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.padding(top = 8.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.rider_pro_hot_room),
contentDescription = "chat room",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.hot_rooms),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
}
// 热门聊天室网格
items(viewModel.chatRooms.chunked(2)) { rowRooms ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowRooms.forEach { chatRoom ->
ChatRoomCard(
chatRoom = chatRoom,
navController = LocalNavController.current,
modifier = Modifier.weight(1f)
)
}
if (rowRooms.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
item { Spacer(modifier = Modifier.height(20.dp)) }
// "发现更多" 标题 - 吸顶 // "发现更多" 标题 - 吸顶
stickyHeader(key = "discover_more") { stickyHeader(key = "discover_more") {
@@ -278,15 +330,15 @@ fun Agent() {
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
Image( Image(
painter = painterResource(R.mipmap.rider_pro_agent2), painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2),
contentDescription = "agent", contentDescription = "agent",
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp)
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = stringResource(R.string.agent_find), text = stringResource(R.string.agent_find),
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600, fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text color = AppColors.text
) )
} }
@@ -347,11 +399,67 @@ fun Agent() {
} }
} }
} }
@Composable
fun AgentGridLayout(
agentItems: List<AgentItem>,
viewModel: AgentViewModel,
navController: NavHostController
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
// 将agentItems按两列分组
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
bottom = 20.dp
),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一列
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[0],
viewModel = viewModel,
navController = navController
)
}
// 第二列(如果存在)
if (rowItems.size > 1) {
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[1],
viewModel = viewModel,
navController = navController
)
}
} else {
// 如果只有一列,添加空白占位
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@SuppressLint("SuspiciousIndentation") @SuppressLint("SuspiciousIndentation")
@Composable @Composable
fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navController: NavHostController) { fun AgentCardSquare(
agentItem: AgentItem,
viewModel: AgentViewModel,
navController: NavHostController
) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val cardHeight = 200.dp val cardHeight = 180.dp
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一 val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
// 防抖状态 // 防抖状态
@@ -360,9 +468,8 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = avatarSize / 2)
.height(cardHeight) .height(cardHeight)
.background(AppColors.nonActive, RoundedCornerShape(12.dp)) // 修改背景颜色 .background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
.clickable { .clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController) viewModel.goToProfile(agentItem.openId, navController)
@@ -372,12 +479,11 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
}, },
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
// 头像,位于方块上方居中,部分悬于方块外部
Box( Box(
modifier = Modifier modifier = Modifier
.offset(y = -avatarSize / 2) .offset(y = 4.dp)
.size(avatarSize) .size(avatarSize)
.background(Color.White, RoundedCornerShape(avatarSize / 2)) .background(AppColors.background, RoundedCornerShape(avatarSize / 2))
.clip(RoundedCornerShape(avatarSize / 2)), .clip(RoundedCornerShape(avatarSize / 2)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -385,9 +491,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
painter = painterResource(R.mipmap.group_copy), painter = painterResource(R.mipmap.group_copy),
contentDescription = "默认头像", contentDescription = "默认头像",
modifier = Modifier.size(avatarSize), modifier = Modifier.size(avatarSize),
contentScale = androidx.compose.ui.layout.ContentScale.Crop
) )
if (agentItem.avatar.isNotEmpty()) { if (agentItem.avatar.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = agentItem.avatar, imageUrl = agentItem.avatar,
@@ -404,7 +508,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 8.dp), .padding(top = 4.dp + avatarSize + 8.dp, start = 8.dp, end = 8.dp, bottom = 48.dp), // 为底部聊天按钮留出空间
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
@@ -418,30 +522,31 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.height(85.dp)
.fillMaxWidth()
) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = agentItem.desc, text = agentItem.desc,
fontSize = 12.sp, fontSize = 12.sp,
color = AppColors.secondaryText, color = AppColors.secondaryText,
maxLines = 5, maxLines = 2,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false)
) )
} }
Spacer(modifier = Modifier.height(8.dp)) // 聊天按钮
// 聊天按钮,位于底部居中
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 12.dp)
.width(60.dp) .width(60.dp)
.height(32.dp) .height(32.dp)
.background( .background(
color = Color(0X147c7480), color = AppColors.text,
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(
topStart = 14.dp,
topEnd = 14.dp,
bottomStart = 0.dp,
bottomEnd = 14.dp
)
) )
.clickable { .clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
@@ -463,13 +568,12 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
) { ) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = stringResource(R.string.chat), text = stringResource(R.string.chat),
fontSize = 12.sp, fontSize = 15.sp,
color = AppColors.text, color = AppColors.background,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500 fontWeight = androidx.compose.ui.text.font.FontWeight.W500
) )
} }
} }
}
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -644,7 +748,12 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
.size(width = 60.dp, height = 32.dp) .size(width = 60.dp, height = 32.dp)
.background( .background(
color = Color(0X147c7480), color = Color(0X147c7480),
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(
topStart = 14.dp,
topEnd = 14.dp,
bottomStart = 0.dp,
bottomEnd = 14.dp
)
) )
.clickable { .clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
@@ -673,3 +782,210 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
} }
} }
} }
@Composable
fun ChatRoomsSection(
chatRooms: List<ChatRoom>,
navController: NavHostController
) {
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier.fillMaxWidth()
) {
// 标题
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.rider_pro_hot_room),
contentDescription = "chat room",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.hot_rooms),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
Column(
modifier = Modifier.fillMaxWidth()
) {
chatRooms.chunked(2).forEach { rowRooms ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowRooms.forEach { chatRoom ->
ChatRoomCard(
chatRoom = chatRoom,
navController = navController,
modifier = Modifier.weight(1f)
)
}
}
}
}
}
}
@Composable
fun ChatRoomCard(
chatRoom: ChatRoom,
navController: NavHostController,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
val cardSize = 180.dp
val viewModel: AgentViewModel = viewModel()
val context = LocalContext.current
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
// Loading 对话框
if (viewModel.isJoiningRoom) {
Dialog(
onDismissRequest = { /* 阻止用户关闭对话框 */ },
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
) {
Box(
modifier = Modifier
.size(120.dp)
.background(
color = AppColors.background,
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(32.dp),
color = AppColors.main
)
Spacer(modifier = Modifier.height(12.dp))
androidx.compose.material3.Text(
text = "加入中...",
fontSize = 14.sp,
color = AppColors.text
)
}
}
}
}
// 正方形卡片,文字重叠在底部
Box(
modifier = modifier
.size(cardSize)
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
.clickable(enabled = !viewModel.isJoiningRoom) {
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 加入群聊房间
viewModel.joinRoom(
id = chatRoom.id,
name = chatRoom.name,
avatar = chatRoom.avatar,
context = context,
navController = navController,
onSuccess = {
// 成功加入房间
},
onError = { errorMsg ->
// 处理错误可以显示Toast或其他提示
}
)
}) {
lastClickTime = System.currentTimeMillis()
}
}
) {
// 优先显示banner如果没有banner则显示头像
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
if (imageUrl.isNotEmpty()) {
CustomAsyncImage(
imageUrl = imageUrl,
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
modifier = Modifier
.width(cardSize)
.height(120.dp)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop
)
} else {
// 默认房间图标
Image(
painter = painterResource(R.mipmap.rider_pro_agent),
contentDescription = "默认房间图标",
modifier = Modifier.size(cardSize * 0.4f),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
}
// 房间名称,重叠在底部
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = 32.dp, start = 10.dp, end = 10.dp)
.clip(RoundedCornerShape(12.dp))
) {
androidx.compose.material3.Text(
text = chatRoom.name,
fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
textAlign = androidx.compose.ui.text.style.TextAlign.Left
)
}
// 显示人数
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = 10.dp, start = 10.dp, end = 10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(R.drawable.rider_pro_nav_profile),
contentDescription = "chat",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
fontSize = 12.sp,
modifier = Modifier.alpha(0.6f),
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
}
}
}

View File

@@ -17,6 +17,21 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.util.Log
import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.api.CreateGroupChatRequestBody
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.data.api.ApiErrorResponse
import com.google.gson.Gson
import android.content.Context
import android.widget.Toast
import kotlinx.coroutines.launch
import com.aiosman.ravenow.data.api.JoinGroupChatRequestBody
/** /**
* 缓存数据结构用于存储每个分类的Agent列表 * 缓存数据结构用于存储每个分类的Agent列表
@@ -26,6 +41,13 @@ data class AgentCacheData(
val currentPage: Int, val currentPage: Int,
val hasMoreData: Boolean val hasMoreData: Boolean
) )
data class ChatRoom(
val id: Int,
val name: String,
val avatar: String = "",
val banner: String = "",
val memberCount: Int
)
object AgentViewModel: ViewModel() { object AgentViewModel: ViewModel() {
@@ -40,6 +62,11 @@ object AgentViewModel: ViewModel() {
var errorMessage by mutableStateOf<String?>(null) var errorMessage by mutableStateOf<String?>(null)
private set private set
var chatRooms by mutableStateOf<List<ChatRoom>>(emptyList())
private set
var rooms by mutableStateOf<List<Room>>(emptyList())
private set
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set private set
@@ -57,6 +84,9 @@ object AgentViewModel: ViewModel() {
var hasMoreData by mutableStateOf(true) var hasMoreData by mutableStateOf(true)
private set private set
var isJoiningRoom by mutableStateOf(false)
private set
private val pageSize = 20 private val pageSize = 20
private var currentCategoryId: Int? = null private var currentCategoryId: Int? = null
@@ -66,6 +96,7 @@ object AgentViewModel: ViewModel() {
init { init {
loadAgentData() loadAgentData()
loadCategories() loadCategories()
loadChatRooms()
} }
private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false, forceRefresh: Boolean = false) { private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false, forceRefresh: Boolean = false) {
@@ -159,6 +190,40 @@ object AgentViewModel: ViewModel() {
} }
} }
private fun loadChatRooms() {
viewModelScope.launch {
try {
val response = apiClient.getRooms(
page = 1,
pageSize = 20,
isRecommended = 1,
random = 1
)
if (response.isSuccessful) {
val allRooms = response.body()?.list ?: emptyList()
val targetCount = (allRooms.size / 2) * 2
rooms = allRooms.take(targetCount)
// 转换为ChatRoom格式用于兼容现有UI
chatRooms = rooms.map { room ->
ChatRoom(
id = room.id,
name = room.name,
avatar = room.avatar,
banner = ConstVars.BASE_SERVER + "/api/v1/outside/" + room.recommendBanner + "?token=${AppStore.token}",
memberCount = room.userCount
)
}
} else {
}
} catch (e: Exception) {
// 如果网络请求失败,使用默认数据
}
}
}
private fun loadCategories() { private fun loadCategories() {
viewModelScope.launch { viewModelScope.launch {
// 如果分类已经加载,不重复请求 // 如果分类已经加载,不重复请求
@@ -280,6 +345,81 @@ object AgentViewModel: ViewModel() {
} }
} }
/**
* 加入房间
*/
fun joinRoom(
id: Int,
name: String,
avatar: String,
context: Context,
navController: NavHostController,
onSuccess: () -> Unit,
onError: (String) -> Unit
) {
// 防止重复点击
if (isJoiningRoom) return
viewModelScope.launch {
try {
isJoiningRoom = true
val response = apiClient.joinRoom(JoinGroupChatRequestBody(roomId = id))
if (response.isSuccessful) {
// 打开房间
val openRoomResponse = apiClient.createGroupChatAi(
roomId = id
)
if (openRoomResponse.isSuccessful){
val respData = openRoomResponse.body()
respData?.let {
viewModelScope.launch {
try {
// 群聊直接使用群ID进行导航
navController.navigateToGroupChat(
id = respData.data.trtcRoomId,
name = name,
avatar = avatar
)
} catch (e: Exception) {
onError("加入房间失败")
e.printStackTrace()
}
}
}
}
onSuccess()
} else {
// 处理错误响应
try {
val errorBody = response.errorBody()?.string()
if (errorBody != null) {
val gson = Gson()
val errorResponse = gson.fromJson(errorBody, ApiErrorResponse::class.java)
// 在主线程显示 Toast
Toast.makeText(context, errorResponse.error, Toast.LENGTH_LONG).show()
onError(errorResponse.error)
} else {
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
onError("加入房间失败")
}
} catch (parseException: Exception) {
// 如果解析错误响应失败,显示默认错误信息
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
onError("加入房间失败")
}
}
} catch (e: Exception) {
Toast.makeText(context, "网络请求失败:${e.message}", Toast.LENGTH_SHORT).show()
onError("网络请求失败:${e.message}")
} finally {
isJoiningRoom = false
}
}
}
/** /**
* 重置ViewModel状态用于登出或切换账号时清理数据 * 重置ViewModel状态用于登出或切换账号时清理数据
*/ */

View File

@@ -97,13 +97,13 @@ fun AgentChatListScreen() {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_znt_qs_as_img id = if(AppState.darkMode) R.mipmap.juhao_dark
else R.mipmap.invalid_name_5), else R.mipmap.invalid_name_5),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = stringResource(R.string.agent_chat_empty_title), text = stringResource(R.string.agent_chat_empty_title),
color = AppColors.text, color = AppColors.text,

View File

@@ -175,13 +175,13 @@ fun AllChatListScreen() {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img id = if(AppState.darkMode) R.mipmap.piao_dark
else R.mipmap.invalid_name_2), else R.mipmap.invalid_name_2),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = stringResource(R.string.friend_chat_empty_title), text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text, color = AppColors.text,

View File

@@ -85,13 +85,13 @@ fun FriendChatListScreen() {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img id = if(AppState.darkMode) R.mipmap.piao_dark
else R.mipmap.invalid_name_2), else R.mipmap.invalid_name_2),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = stringResource(R.string.friend_chat_empty_title), text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text, color = AppColors.text,

View File

@@ -77,13 +77,13 @@ fun GroupChatListScreen() {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_ql_qs_as_img id = if(AppState.darkMode) R.mipmap.fei_dark
else R.mipmap.invalid_name_12), else R.mipmap.invalid_name_12),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = stringResource(R.string.group_chat_empty), text = stringResource(R.string.group_chat_empty),
color = AppColors.text, color = AppColors.text,

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.index.tabs.message.tab
import android.content.Context import android.content.Context
import android.icu.util.Calendar import android.icu.util.Calendar
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -167,10 +168,12 @@ object GroupChatListViewModel : ViewModel() {
} }
fun createGroupChat( fun createGroupChat(
trtcGroupId: String, trtcGroupId: String? = null,
roomId: Int? = null
) { ) {
viewModelScope.launch { viewModelScope.launch {
val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId) val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId,roomId = roomId)
Log.d("debug",response.toString())
} }
} }

View File

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

View File

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

View File

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

View File

@@ -369,6 +369,7 @@ fun Explore() {
trtcId = roomItem.trtcId.toString(), trtcId = roomItem.trtcId.toString(),
name = roomItem.title, name = roomItem.title,
avatar = roomItem.avatar, avatar = roomItem.avatar,
context = context,
navController = navController, navController = navController,
onSuccess = { onSuccess = {
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show() Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()
@@ -523,7 +524,7 @@ fun Explore() {
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
/* Image( Image(
painter = painterResource(R.drawable.rider_pro_nav_profile), painter = painterResource(R.drawable.rider_pro_nav_profile),
contentDescription = "chat", contentDescription = "chat",
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
@@ -535,7 +536,7 @@ fun Explore() {
fontSize = 12.sp, fontSize = 12.sp,
color = Color.White, color = Color.White,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500 fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)*/ )
} }
// 底部:标题和描述 // 底部:标题和描述
@@ -636,6 +637,7 @@ fun Explore() {
trtcId = bannerItem.trtcId.toString(), trtcId = bannerItem.trtcId.toString(),
name = bannerItem.title, name = bannerItem.title,
avatar = bannerItem.avatar, avatar = bannerItem.avatar,
context = context,
navController = navController, navController = navController,
onSuccess = { onSuccess = {
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show() Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()

View File

@@ -17,6 +17,10 @@ import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userServic
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
import com.aiosman.ravenow.ui.navigateToGroupChat import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.data.api.ApiErrorResponse
import com.google.gson.Gson
import android.content.Context
import android.widget.Toast
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ExploreViewModel : ViewModel() { class ExploreViewModel : ViewModel() {
@@ -130,21 +134,24 @@ class ExploreViewModel : ViewModel() {
} }
} }
} }
fun createSingleChat( fun createSingleChat(
openId: String, openId: String,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val response = ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId)) val response =
ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
} }
} }
fun goToChatAi( fun goToChatAi(
openId: String, openId: String,
navController: NavHostController navController: NavHostController
) { ) {
viewModelScope.launch { viewModelScope.launch {
val profile = userService.getUserProfileByOpenId(openId) val profile = userService.getUserProfileByOpenId(openId)
createGroup2ChatAi(profile.trtcUserId,"ai_group",navController,profile.id) createGroup2ChatAi(profile.trtcUserId, "ai_group", navController, profile.id)
} }
} }
@@ -152,6 +159,7 @@ class ExploreViewModel : ViewModel() {
trtcId: String, trtcId: String,
name: String, name: String,
avatar: String, avatar: String,
context: Context,
navController: NavHostController, navController: NavHostController,
onSuccess: () -> Unit, onSuccess: () -> Unit,
onError: (String) -> Unit onError: (String) -> Unit
@@ -164,9 +172,11 @@ class ExploreViewModel : ViewModel() {
try { try {
createGroupChat(trtcGroupId = trtcId) createGroupChat(trtcGroupId = trtcId)
// 群聊直接使用群ID进行导航 // 群聊直接使用群ID进行导航
navController.navigateToGroupChat( id = trtcId, navController.navigateToGroupChat(
id = trtcId,
name = name, name = name,
avatar = avatar) avatar = avatar
)
} catch (e: Exception) { } catch (e: Exception) {
onError("加入房间失败") onError("加入房间失败")
e.printStackTrace() e.printStackTrace()
@@ -175,9 +185,28 @@ class ExploreViewModel : ViewModel() {
onSuccess() onSuccess()
} else { } else {
// 处理错误响应
try {
val errorBody = response.errorBody()?.string()
if (errorBody != null) {
val gson = Gson()
val errorResponse = gson.fromJson(errorBody, ApiErrorResponse::class.java)
// 在主线程显示 Toast
Toast.makeText(context, errorResponse.error, Toast.LENGTH_LONG).show()
onError(errorResponse.error)
} else {
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
onError("加入房间失败") onError("加入房间失败")
} }
} catch (parseException: Exception) {
// 如果解析错误响应失败,显示默认错误信息
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
onError("加入房间失败")
}
}
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(context, "网络请求失败:${e.message}", Toast.LENGTH_SHORT).show()
onError("网络请求失败:${e.message}") onError("网络请求失败:${e.message}")
} }
} }

View File

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

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

View File

@@ -189,16 +189,17 @@ fun GalleryGrid(
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_dt_qs_as_img id = if(AppState.darkMode) R.mipmap.shuihu_dark
else R.mipmap.invalid_name_7), else R.mipmap.invalid_name_7),
contentDescription = "暂无图片", contentDescription = "暂无图片",
modifier = Modifier.size(181.dp), modifier = Modifier
.size(width = 181.dp, height = 153.dp),
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = "故事还没开始", text = "你的故事还没开始",
fontSize = 16.sp, fontSize = 16.sp,
color = AppColors.text, color = AppColors.text,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600

View File

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

View File

@@ -212,13 +212,16 @@ fun EmptyAgentsView() {
if (isNetworkAvailable) { if (isNetworkAvailable) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qs_ai_qs_as_img id = if(AppState.darkMode) R.mipmap.ai_dark
else R.mipmap.ai), else R.mipmap.ai),
contentDescription = "暂无Agent", contentDescription = "暂无Agent",
modifier = Modifier.size(181.dp), modifier = Modifier
.size(width = 181.dp, height = 153.dp)
.align(Alignment.CenterHorizontally),
) )
Spacer(modifier = Modifier.height(24.dp)) // 根据是否为深色模式调整间距
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = "专属AI等你召唤", text = "专属AI等你召唤",

View File

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

View File

@@ -33,7 +33,7 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable @Composable
fun UserItem( fun UserItem(
accountProfileEntity: AccountProfileEntity, accountProfileEntity: AccountProfileEntity,
postCount: Int = 0 postCount: Long = 0
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current

View File

@@ -123,14 +123,15 @@ fun LikeNoticeScreen() {
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_z_qs_as_img id = if(AppState.darkMode) R.mipmap.sanqiu_dark
else R.mipmap.invalid_name_6), else R.mipmap.invalid_name_6),
contentDescription = "No Notice", contentDescription = "No Notice",
modifier = Modifier.size(181.dp) modifier = Modifier
.size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = "你的赞在路上", text = "你的赞在赶来的路上",
color = AppColors.text, color = AppColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W600,

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ fun SplashScreen() {
) { ) {
// 居中的图标 // 居中的图标
Image( Image(
painter = painterResource(id = R.mipmap.invalid_name), painter = painterResource(id = R.mipmap.kp_logo_img),
contentDescription = "App Logo", contentDescription = "App Logo",
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

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: 551 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: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

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: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

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: 419 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: 1.2 KiB

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