Compare commits
178 Commits
launch-spe
...
update_tik
| Author | SHA1 | Date | |
|---|---|---|---|
| b06f2d6c4a | |||
| a215d79ce8 | |||
| 0e0d622864 | |||
| a1196715d0 | |||
| 68273ae166 | |||
| 6c7be4ba47 | |||
| cf25540417 | |||
| eca85c8377 | |||
| f8be622ba6 | |||
| f3c841779b | |||
| 57e4614ce8 | |||
| 922d6e72d6 | |||
| c41c097d41 | |||
| 5218ca7046 | |||
| ce6ee7bf82 | |||
| e00deb5661 | |||
| 95d6522a54 | |||
| d231f3678c | |||
| cd35562244 | |||
| 21cb512237 | |||
| 0aa3069efe | |||
| b79073b295 | |||
| 1a41cb7aef | |||
| 1d632fb757 | |||
| ef04450696 | |||
| ef6fb6348f | |||
| e61fb2ad79 | |||
| b4ed311978 | |||
| 20ff6df3bf | |||
| 18a0bb8494 | |||
| 9dceb99a98 | |||
| 9f14b35847 | |||
| 9b7349a761 | |||
| 23f3baf238 | |||
| 1f1101e260 | |||
| ca9f4a372b | |||
| d93373d8fa | |||
| ae7254163a | |||
| e49e509c38 | |||
| d703b5ae05 | |||
| 824be5fad8 | |||
| 79547de2db | |||
| 16f95782f8 | |||
| 3aa0172580 | |||
| a950504d6f | |||
| 288728f142 | |||
| db85deea96 | |||
| 7882ab8f19 | |||
| 2b0d33fdbe | |||
| 0ac3312fea | |||
| 2f61850c33 | |||
| 6c743a6cac | |||
| c202f0d280 | |||
| c20b6ba682 | |||
| ab43f154f5 | |||
| 9c7c87722b | |||
| 337bda46a2 | |||
| 2e176605f7 | |||
| 6721d715e7 | |||
| b9fac2c1ee | |||
| 118a6cf986 | |||
| 36d322ef8c | |||
| 5c12982908 | |||
| 4d319351b7 | |||
| 76bcb1e7fc | |||
| 83cff9d56c | |||
| 484d641554 | |||
| 12b3c15083 | |||
| cb253d3276 | |||
| 2907e7f9a6 | |||
| 281169cfcb | |||
| ba7eeeca90 | |||
| 00824ff7b4 | |||
| 3777a76c44 | |||
| 40ccd70e80 | |||
| 5759d4ec95 | |||
| 21200910c1 | |||
| a79628026c | |||
| 7c0d35ec8c | |||
| da5fdcbd57 | |||
|
|
fdf8c1fa5a | ||
|
|
2a7d310be5 | ||
|
|
52e571da01 | ||
| 5d4a95bf07 | |||
| f2ab55d545 | |||
|
|
df75c710e5 | ||
|
|
77033854f0 | ||
|
|
4be63f3428 | ||
| 52d32a8510 | |||
| bd2d291164 | |||
|
|
9fb79b3881 | ||
|
|
5ee1897739 | ||
|
|
edcab76fdb | ||
| ed9dd9ab3e | |||
| a9066bd473 | |||
| 3e9353f07b | |||
|
|
8f8c2ff2e9 | ||
|
|
791b24b2fb | ||
|
|
2de8127882 | ||
|
|
112efa74e7 | ||
|
|
d8c091b19b | ||
|
|
cd3fc03524 | ||
|
|
bc7a897cec | ||
|
|
2d518cbd68 | ||
|
|
697af504b7 | ||
|
|
54ca1d3f1c | ||
| 1bb0adeb90 | |||
|
|
f6a796e2bc | ||
|
|
38759eb3e4 | ||
|
|
b837c704e5 | ||
|
|
a944bd0fa3 | ||
|
|
993604bfc1 | ||
| dc9c013383 | |||
| 91c34a0acf | |||
| a2254e503e | |||
|
|
daea7824af | ||
| 0f5d3d7960 | |||
| 6433d4a23c | |||
| 873001ce28 | |||
| 7ea75a4755 | |||
| a80711a475 | |||
|
|
3e544844bb | ||
|
|
29d2bb753f | ||
|
|
6ec732b996 | ||
| 9f93e6dc14 | |||
| 50fb1874e7 | |||
| 76e7bbb84a | |||
| cc7a65e016 | |||
| 784427cfba | |||
| c54d5c914a | |||
| 79fccda1aa | |||
| 6c19f83cfb | |||
| 3a68a51f3c | |||
| 324e04881e | |||
| 5cacfbdd95 | |||
| 9fac012d11 | |||
| ca5629043b | |||
| b697082732 | |||
| a243abb8a2 | |||
| e75b181818 | |||
| 4cdb7f28b3 | |||
| 074244c0f8 | |||
| 914cfca6be | |||
| e62e83c566 | |||
| 5b2613cacc | |||
| ee88708860 | |||
| 34ce648102 | |||
| 7152bfd9c1 | |||
| d5e210b080 | |||
| 3cef90c887 | |||
| d49e037f51 | |||
| 7ed3c51118 | |||
| 560f4655df | |||
| 720bafe787 | |||
| 068ca693ea | |||
| d6e2db7edb | |||
| 5b3d663a73 | |||
| be18a60b2b | |||
| e1ba8709de | |||
| e8ddd67219 | |||
| 2012668361 | |||
| 01fb092e83 | |||
| fe2bd2f382 | |||
| 5c1cca6a69 | |||
| 9838439ff1 | |||
| 994e493035 | |||
| fd2f1cfc1d | |||
| 8dca3e8de1 | |||
| e38c36aa2c | |||
| ed66f15be7 | |||
| d3518b1a82 | |||
| 5e65f217bb | |||
| c183c2fb9d | |||
| 684078515f | |||
| 9cf6a338ea | |||
| 9e55043a17 | |||
| fde7a0ad95 | |||
| dad032e233 |
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
|||||||
RiderPro
|
RaveNow
|
||||||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="17" />
|
<bytecodeTargetLevel target="21" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2024-08-11T15:57:44.196893Z">
|
<DropdownSelection timestamp="2025-09-09T09:51:06.656104400Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/kevinlinpr/.android/avd/Pixel_8_Pro_API_34.avd" />
|
<DeviceId pluginId="Default" identifier="serial=192.168.0.227:45035;connection=094cb92e" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -4,6 +4,7 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
|
|||||||
4
.idea/inspectionProfiles/Project_Default.xml
generated
4
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -65,6 +65,10 @@
|
|||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
|
|||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.0" />
|
<option name="version" value="1.9.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
4
.idea/runConfigurations.xml
generated
4
.idea/runConfigurations.xml
generated
@@ -5,8 +5,12 @@
|
|||||||
<set>
|
<set>
|
||||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
176
MIGRATION_GUIDE.md
Normal file
176
MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# 腾讯云 IM SDK 到 OpenIM 迁移指南
|
||||||
|
|
||||||
|
## 迁移状态
|
||||||
|
|
||||||
|
### ✅ 已完成的工作
|
||||||
|
|
||||||
|
1. **依赖项迁移**
|
||||||
|
- 从 `build.gradle.kts` 中移除了腾讯云 IM SDK 依赖 (`libs.imsdk.plus`)
|
||||||
|
- 从 `gradle/libs.versions.toml` 中移除了相关版本定义
|
||||||
|
- 保留了 OpenIM SDK 依赖 (`io.openim:android-sdk`, `io.openim:core-sdk`)
|
||||||
|
|
||||||
|
2. **核心组件迁移**
|
||||||
|
- ✅ `TrtcHelper.kt` - 完全迁移到 OpenIM API
|
||||||
|
- ✅ `Chat.kt` 实体类 - 更新为 OpenIM 消息模型
|
||||||
|
- ✅ `AgentChatListViewModel.kt` - 部分迁移到 OpenIM API
|
||||||
|
- ✅ `OpenIMManager.kt` - 完整的 OpenIM 管理器
|
||||||
|
- ✅ `OpenIMService.kt` - OpenIM 后台服务
|
||||||
|
- ✅ `AppState.kt` - 已使用 OpenIM 进行初始化和登录
|
||||||
|
|
||||||
|
3. **兼容层创建**
|
||||||
|
- 创建了 `TencentIMCompat.kt` 兼容层,避免编译错误
|
||||||
|
- 所有使用腾讯云 IM 的文件都已添加兼容层导入
|
||||||
|
|
||||||
|
4. **配置清理**
|
||||||
|
- AndroidManifest.xml 已经是干净的,无需额外清理
|
||||||
|
|
||||||
|
### 🔄 需要进一步完成的工作
|
||||||
|
|
||||||
|
#### 1. 完整的 ViewModel 迁移
|
||||||
|
以下文件仍在使用兼容层,需要完全迁移到 OpenIM:
|
||||||
|
|
||||||
|
- `ChatViewModel.kt` - 聊天功能核心
|
||||||
|
- `ChatAiViewModel.kt` - AI 聊天功能
|
||||||
|
- `GroupChatViewModel.kt` - 群聊功能
|
||||||
|
- `FriendChatListViewModel.kt` - 好友聊天列表
|
||||||
|
- `GroupChatListViewModel.kt` - 群聊列表
|
||||||
|
- `MessageListViewModel.kt` - 消息列表
|
||||||
|
- `MineAgentViewModel.kt` - 我的智能体
|
||||||
|
- `CreateGroupChatViewModel.kt` - 创建群聊
|
||||||
|
|
||||||
|
#### 2. UI 组件迁移
|
||||||
|
以下 Screen 文件需要更新以使用新的数据模型:
|
||||||
|
|
||||||
|
- `ChatScreen.kt`
|
||||||
|
- `ChatAiScreen.kt`
|
||||||
|
- `GroupChatScreen.kt`
|
||||||
|
|
||||||
|
#### 3. 消息类型映射
|
||||||
|
需要完善 OpenIM 消息类型到应用内部类型的映射:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// OpenIM 消息类型
|
||||||
|
101 -> TEXT
|
||||||
|
102 -> IMAGE
|
||||||
|
103 -> AUDIO
|
||||||
|
104 -> VIDEO
|
||||||
|
105 -> FILE
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenIM 集成状态
|
||||||
|
|
||||||
|
### ✅ 已集成的功能
|
||||||
|
|
||||||
|
1. **SDK 初始化** - `OpenIMManager.initSDK()`
|
||||||
|
2. **用户登录** - `AppState.loginToOpenIM()`
|
||||||
|
3. **连接监听** - 连接状态、踢下线、token 过期
|
||||||
|
4. **消息监听** - 新消息、消息撤回、已读回执等
|
||||||
|
5. **会话管理** - 会话变化、未读数统计
|
||||||
|
6. **用户信息管理** - 用户资料更新
|
||||||
|
7. **好友关系管理** - 好友申请、添加、删除等
|
||||||
|
8. **群组管理** - 群信息变更、成员管理等
|
||||||
|
|
||||||
|
### 🔧 需要实现的功能
|
||||||
|
|
||||||
|
1. **消息发送**
|
||||||
|
```kotlin
|
||||||
|
// 需要实现
|
||||||
|
OpenIMClient.getInstance().messageManager.sendMessage(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **历史消息获取**
|
||||||
|
```kotlin
|
||||||
|
// 需要实现
|
||||||
|
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **会话列表获取**
|
||||||
|
```kotlin
|
||||||
|
// 已在 AgentChatListViewModel 中部分实现
|
||||||
|
OpenIMClient.getInstance().conversationManager.getAllConversationList(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **图片消息处理**
|
||||||
|
- 需要适配 OpenIM 的 `PictureElem` 结构
|
||||||
|
- 更新图片显示逻辑
|
||||||
|
|
||||||
|
## 迁移步骤建议
|
||||||
|
|
||||||
|
### 第一阶段:核心聊天功能
|
||||||
|
1. 完成 `ChatViewModel.kt` 的完整迁移
|
||||||
|
2. 实现消息发送和接收
|
||||||
|
3. 实现历史消息加载
|
||||||
|
4. 测试基本聊天功能
|
||||||
|
|
||||||
|
### 第二阶段:会话管理
|
||||||
|
1. 完成各种 ChatListViewModel 的迁移
|
||||||
|
2. 实现会话列表的正确显示
|
||||||
|
3. 实现未读消息统计
|
||||||
|
|
||||||
|
### 第三阶段:高级功能
|
||||||
|
1. 群聊功能迁移
|
||||||
|
2. 文件、图片等多媒体消息
|
||||||
|
3. 消息状态和已读回执
|
||||||
|
|
||||||
|
### 第四阶段:清理和优化
|
||||||
|
1. 删除兼容层 `TencentIMCompat.kt`
|
||||||
|
2. 清理所有临时代码
|
||||||
|
3. 性能优化和测试
|
||||||
|
|
||||||
|
## 关键 API 对比
|
||||||
|
|
||||||
|
### 消息发送
|
||||||
|
```kotlin
|
||||||
|
// 腾讯云 IM
|
||||||
|
V2TIMManager.getMessageManager().sendMessage(message, receiver, null, ...)
|
||||||
|
|
||||||
|
// OpenIM
|
||||||
|
OpenIMClient.getInstance().messageManager.sendMessage(message, receiver, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取会话列表
|
||||||
|
```kotlin
|
||||||
|
// 腾讯云 IM
|
||||||
|
V2TIMManager.getConversationManager().getConversationList(...)
|
||||||
|
|
||||||
|
// OpenIM
|
||||||
|
OpenIMClient.getInstance().conversationManager.getAllConversationList(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息监听
|
||||||
|
```kotlin
|
||||||
|
// 腾讯云 IM
|
||||||
|
V2TIMManager.getMessageManager().addAdvancedMsgListener(listener)
|
||||||
|
|
||||||
|
// OpenIM
|
||||||
|
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(listener)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **数据结构差异**:OpenIM 和腾讯云 IM 的数据结构有所不同,需要仔细映射
|
||||||
|
2. **回调机制**:OpenIM 使用不同的回调接口
|
||||||
|
3. **消息 ID**:OpenIM 使用 `clientMsgID`,腾讯云使用 `msgID`
|
||||||
|
4. **时间戳**:注意时间戳的单位和格式差异
|
||||||
|
5. **用户 ID**:确保用户 ID 在两个系统中的一致性
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
1. **单元测试**:为每个迁移的组件编写测试
|
||||||
|
2. **集成测试**:测试完整的聊天流程
|
||||||
|
3. **兼容性测试**:确保与现有数据的兼容性
|
||||||
|
4. **性能测试**:对比迁移前后的性能表现
|
||||||
|
|
||||||
|
## 删除兼容层的时机
|
||||||
|
|
||||||
|
当以下条件都满足时,可以安全删除 `TencentIMCompat.kt`:
|
||||||
|
|
||||||
|
1. 所有 ViewModel 都已完全迁移到 OpenIM
|
||||||
|
2. 所有功能都已测试通过
|
||||||
|
3. 没有编译错误
|
||||||
|
4. 应用运行正常
|
||||||
|
|
||||||
|
删除步骤:
|
||||||
|
1. 删除 `app/src/main/java/com/aiosman/ravenow/compat/TencentIMCompat.kt`
|
||||||
|
2. 从所有文件中移除 `import com.aiosman.ravenow.compat.*`
|
||||||
|
3. 清理所有相关的临时注释
|
||||||
@@ -7,15 +7,15 @@ plugins {
|
|||||||
|
|
||||||
}
|
}
|
||||||
android {
|
android {
|
||||||
namespace = "com.aiosman.riderpro"
|
namespace = "com.aiosman.ravenow"
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.aiosman.riderpro"
|
applicationId = "com.aiosman.ravenow"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1000014
|
versionCode = 1000019
|
||||||
versionName = "1.0.000.14"
|
versionName = "1.0.000.19"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -24,7 +24,7 @@ android {
|
|||||||
addManifestPlaceholders(
|
addManifestPlaceholders(
|
||||||
mapOf(
|
mapOf(
|
||||||
"JPUSH_PKGNAME " to applicationId!!,
|
"JPUSH_PKGNAME " to applicationId!!,
|
||||||
"JPUSH_APPKEY" to "ad805ee9f2760376f4f47178",
|
"JPUSH_APPKEY" to "cbd968cae60346065e03f9d7",
|
||||||
"JPUSH_CHANNEL" to "developer-default",
|
"JPUSH_CHANNEL" to "developer-default",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,6 +32,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
isDebuggable = true
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
@@ -49,9 +52,10 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.1"
|
kotlinCompilerExtensionVersion = "1.5.3"
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
@@ -93,27 +97,35 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
implementation(libs.androidx.animation)
|
implementation(libs.androidx.animation)
|
||||||
implementation("io.coil-kt:coil-compose:2.7.0")
|
implementation(libs.coil.compose)
|
||||||
implementation("io.coil-kt:coil:2.7.0")
|
implementation(libs.coil)
|
||||||
implementation("com.google.android.gms:play-services-auth:21.2.0")
|
implementation(libs.play.services.auth)
|
||||||
implementation("io.github.serpro69:kotlin-faker:2.0.0-rc.5")
|
implementation(libs.kotlin.faker)
|
||||||
implementation("androidx.compose.material:material:1.6.8")
|
implementation(libs.androidx.material)
|
||||||
implementation("net.engawapg.lib:zoomable:1.6.1")
|
implementation(libs.zoomable)
|
||||||
implementation("com.squareup.retrofit2:retrofit:2.11.0")
|
implementation(libs.retrofit)
|
||||||
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
|
implementation(libs.converter.gson)
|
||||||
implementation("androidx.credentials:credentials:1.2.2")
|
implementation(libs.androidx.credentials)
|
||||||
implementation("androidx.credentials:credentials-play-services-auth:1.2.2")
|
implementation(libs.androidx.credentials.play.services.auth)
|
||||||
implementation("com.auth0.android:jwtdecode:2.0.2")
|
implementation(libs.jwtdecode)
|
||||||
|
|
||||||
|
implementation(platform(libs.firebase.bom))
|
||||||
|
implementation(libs.firebase.crashlytics)
|
||||||
|
implementation(libs.firebase.analytics)
|
||||||
|
implementation(libs.firebase.perf)
|
||||||
|
|
||||||
|
implementation(libs.firebase.messaging.ktx)
|
||||||
|
|
||||||
|
implementation (libs.jpush.google)
|
||||||
|
implementation (libs.im.sdk)
|
||||||
|
implementation (libs.im.core.sdk)
|
||||||
|
implementation (libs.gson)
|
||||||
|
implementation(libs.imagecropview)
|
||||||
|
implementation(libs.androidx.core.splashscreen) // 添加 SplashScreen 依赖
|
||||||
|
// 添加 lifecycle-runtime-ktx 依赖
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx.v262)
|
||||||
|
implementation (libs.eventbus)
|
||||||
|
|
||||||
implementation(platform("com.google.firebase:firebase-bom:33.2.0"))
|
|
||||||
implementation("com.google.firebase:firebase-crashlytics")
|
|
||||||
implementation("com.google.firebase:firebase-analytics")
|
|
||||||
implementation("com.google.firebase:firebase-perf")
|
|
||||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
|
||||||
implementation ("cn.jiguang.sdk:jpush-google:5.4.0")
|
|
||||||
api ("com.tencent.imsdk:imsdk-plus:8.1.6116")
|
|
||||||
implementation("io.github.rroohit:ImageCropView:3.0.1")
|
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1") // 添加 SplashScreen 依赖
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"client_info": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:987156664714:android:2c29c11b9cd8be78b9f873",
|
"mobilesdk_app_id": "1:987156664714:android:2c29c11b9cd8be78b9f873",
|
||||||
"android_client_info": {
|
"android_client_info": {
|
||||||
"package_name": "com.aiosman.riderpro"
|
"package_name": "com.aiosman.ravenow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [],
|
"oauth_client": [],
|
||||||
|
|||||||
21
app/proguard-rules.pro
vendored
21
app/proguard-rules.pro
vendored
@@ -20,4 +20,23 @@
|
|||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
-keep class com.tencent.imsdk.** { *; }
|
# OpenIM SDK ProGuard rules
|
||||||
|
-keep class io.openim.android.sdk.** { *; }
|
||||||
|
-keep class io.openim.core.** { *; }
|
||||||
|
-keepclassmembers class io.openim.android.sdk.** { *; }
|
||||||
|
-keepclassmembers class io.openim.core.** { *; }
|
||||||
|
|
||||||
|
# Keep OpenIM models and listeners
|
||||||
|
-keep class io.openim.android.sdk.models.** { *; }
|
||||||
|
-keep class io.openim.android.sdk.listener.** { *; }
|
||||||
|
-keep class io.openim.android.sdk.enums.** { *; }
|
||||||
|
|
||||||
|
# Keep OpenIM Client and managers
|
||||||
|
-keep class io.openim.android.sdk.OpenIMClient { *; }
|
||||||
|
-keep class io.openim.android.sdk.manager.** { *; }
|
||||||
|
|
||||||
|
# Prevent obfuscation of callback methods
|
||||||
|
-keepclassmembers class * implements io.openim.android.sdk.listener.** {
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("com.aiosman.riderpro", appContext.packageName)
|
assertEquals("com.aiosman.ravenow", appContext.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,18 +4,20 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".RaveNowApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/rider_pro_logo_red"
|
android:icon="@mipmap/invalid_name"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/rider_pro_logo_red_round"
|
android:roundIcon="@mipmap/rider_pro_logo_next_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.RiderPro"
|
android:theme="@style/Theme.RaveNow"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -34,11 +36,20 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
android:value="Default Message" />
|
android:value="Default Message" />
|
||||||
|
<meta-data android:name="com.google.ar.core" android:value="optional" />
|
||||||
|
<!-- Firebase Performance 配置:禁用自动网络请求监控 -->
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_performance_collection_enabled"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_performance_logcat_enabled"
|
||||||
|
android:value="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.App.Starting"
|
android:theme="@style/Theme.App.Starting"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -52,6 +63,13 @@
|
|||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<receiver
|
||||||
|
android:name=".model.ApkInstallReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".MyFirebaseMessagingService"
|
android:name=".MyFirebaseMessagingService"
|
||||||
@@ -70,7 +88,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".TrtcService"
|
android:name=".OpenIMService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
@@ -79,14 +97,14 @@
|
|||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="cn.jpush.android.intent.RECEIVER_MESSAGE" />
|
<action android:name="cn.jpush.android.intent.RECEIVER_MESSAGE" />
|
||||||
<category android:name="com.aiosman.riderpro" />
|
<category android:name="com.aiosman.ravenow" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="com.aiosman.riderpro.fileprovider"
|
android:authorities="com.aiosman.ravenow.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
236
app/src/main/java/com/aiosman/ravenow/AppState.kt
Normal file
236
app/src/main/java/com/aiosman/ravenow/AppState.kt
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.icu.util.Calendar
|
||||||
|
import android.icu.util.TimeZone
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.DictService
|
||||||
|
import com.aiosman.ravenow.data.DictServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel
|
||||||
|
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeViewModel
|
||||||
|
import com.aiosman.ravenow.ui.follower.FollowerNoticeViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.IndexViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.dynamic.DynamicViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.Explore
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
|
import com.aiosman.ravenow.ui.account.AccountEditViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.search.DiscoverViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.ai.AgentViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel
|
||||||
|
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
|
||||||
|
import com.aiosman.ravenow.utils.Utils
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import com.aiosman.ravenow.im.OpenIMManager
|
||||||
|
import io.openim.android.sdk.OpenIMClient
|
||||||
|
import io.openim.android.sdk.models.InitConfig
|
||||||
|
|
||||||
|
|
||||||
|
object AppState {
|
||||||
|
var UserId: Int? = null
|
||||||
|
var profile: AccountProfileEntity? = null
|
||||||
|
var darkMode by mutableStateOf(false)
|
||||||
|
var appTheme by mutableStateOf<AppThemeData>(LightThemeColors())
|
||||||
|
var googleClientId: String? = null
|
||||||
|
var enableGoogleLogin: Boolean = false
|
||||||
|
var enableChat = false
|
||||||
|
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
||||||
|
// 如果是游客模式,使用简化的初始化流程
|
||||||
|
if (AppStore.isGuest) {
|
||||||
|
initWithGuestAccount()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
// 获取用户认证信息
|
||||||
|
val resp = accountService.getMyAccount()
|
||||||
|
// 更新必要的用户信息
|
||||||
|
val calendar: Calendar = Calendar.getInstance()
|
||||||
|
val tz: TimeZone = calendar.timeZone
|
||||||
|
val offsetInMillis: Int = tz.rawOffset
|
||||||
|
accountService.updateUserExtra(
|
||||||
|
Utils.getCurrentLanguage(),
|
||||||
|
// 时区偏移量单位是秒
|
||||||
|
offsetInMillis / 1000,
|
||||||
|
tz.displayName
|
||||||
|
)
|
||||||
|
// 设置当前登录用户 ID
|
||||||
|
UserId = resp.id
|
||||||
|
try {
|
||||||
|
var profileResult = accountService.getMyAccountProfile()
|
||||||
|
profile = profileResult
|
||||||
|
|
||||||
|
} catch (e:Exception) {
|
||||||
|
Log.e("AppState", "getMyAccountProfile Error:"+ e.message )
|
||||||
|
}
|
||||||
|
// 获取当前用户资料
|
||||||
|
|
||||||
|
// 注册 JPush
|
||||||
|
Messaging.registerDevice(scope, context)
|
||||||
|
initChat(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游客模式的简化初始化
|
||||||
|
*/
|
||||||
|
private fun initWithGuestAccount() {
|
||||||
|
// 游客模式下,不初始化推送和TRTC
|
||||||
|
// 设置默认的用户信息
|
||||||
|
UserId = 0
|
||||||
|
profile = null
|
||||||
|
enableChat = false
|
||||||
|
Log.d("AppState", "Guest mode initialized without push notifications and TRTC")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun initChat(context: Context){
|
||||||
|
val dictService :DictService = DictServiceImpl()
|
||||||
|
val enableItem = dictService.getDictByKey(ConstVars.DICT_KEY_ENABLE_TRTC)
|
||||||
|
val isEnableTrtc = enableItem.value as? Boolean
|
||||||
|
if (isEnableTrtc != true) {
|
||||||
|
enableChat = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
|
||||||
|
|
||||||
|
val initConfig = InitConfig(
|
||||||
|
"https://im.ravenow.ai/api",//SDK api地址
|
||||||
|
"wss://im.ravenow.ai/msg_gateway",//SDK WebSocket地址
|
||||||
|
OpenIMManager.getStorageDir(context),//SDK数据库存储目录
|
||||||
|
)
|
||||||
|
// initConfig.isLogStandardOutput = true;
|
||||||
|
// initConfig.logLevel = 6
|
||||||
|
// 使用 OpenIMManager 初始化 SDK
|
||||||
|
OpenIMManager.initSDK(context, initConfig)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (profile?.chatToken != null && profile?.trtcUserId != null) {
|
||||||
|
loginToOpenIM(profile!!.trtcUserId, profile!!.chatToken!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startService(Intent(context, OpenIMService::class.java))
|
||||||
|
|
||||||
|
enableChat = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
enableChat = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loginToOpenIM(userId: String, imToken: String): Boolean {
|
||||||
|
return suspendCoroutine { continuation ->
|
||||||
|
OpenIMClient.getInstance().login(object : io.openim.android.sdk.listener.OnBase<String> {
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.e("AppState", "OpenIM 登录失败: code=$code, error=$error")
|
||||||
|
continuation.resumeWith(Result.failure(Exception("OpenIM Login failed: $code, $error")))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: String?) {
|
||||||
|
Log.d("AppState", "OpenIM 登录成功: $data")
|
||||||
|
//其他api调用必须保证登录回调成功后操作
|
||||||
|
continuation.resumeWith(Result.success(true))
|
||||||
|
}
|
||||||
|
}, userId, imToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// suspend fun updateTrtcUserProfile() {
|
||||||
|
// val accountService: AccountService = AccountServiceImpl()
|
||||||
|
// val profile = accountService.getMyAccountProfile()
|
||||||
|
// val info = V2TIMUserFullInfo()
|
||||||
|
// info.setNickname(profile.nickName)
|
||||||
|
// info.faceUrl = profile.rawAvatar
|
||||||
|
// info.selfSignature = profile.bio
|
||||||
|
// return suspendCoroutine { continuation ->
|
||||||
|
// V2TIMManager.getInstance().setSelfInfo(info, object : V2TIMCallback {
|
||||||
|
// override fun onError(code: Int, desc: String?) {
|
||||||
|
// continuation.resumeWith(Result.failure(Exception("Update user profile failed: $code, $desc")))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onSuccess() {
|
||||||
|
// continuation.resumeWith(Result.success(Unit))
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun switchTheme() {
|
||||||
|
darkMode = !darkMode
|
||||||
|
appTheme = if (darkMode) {
|
||||||
|
DarkThemeColors()
|
||||||
|
} else {
|
||||||
|
LightThemeColors()
|
||||||
|
}
|
||||||
|
AppStore.saveDarkMode(darkMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是游客模式,并且是否需要登录
|
||||||
|
* @return true 如果是游客模式
|
||||||
|
*/
|
||||||
|
fun isGuestMode(): Boolean {
|
||||||
|
return AppStore.isGuest
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查游客模式并提示登录
|
||||||
|
* @param onGuestMode 当是游客模式时的回调
|
||||||
|
* @return true 如果是游客模式
|
||||||
|
*/
|
||||||
|
fun checkGuestModeAndPromptLogin(onGuestMode: (() -> Unit)? = null): Boolean {
|
||||||
|
if (AppStore.isGuest) {
|
||||||
|
onGuestMode?.invoke()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ReloadAppState(context: Context) {
|
||||||
|
// 重置动态列表页面
|
||||||
|
TimelineMomentViewModel.ResetModel()
|
||||||
|
DynamicViewModel.ResetModel()
|
||||||
|
HotMomentViewModel.resetModel()
|
||||||
|
|
||||||
|
// 重置我的页面
|
||||||
|
MyProfileViewModel.ResetModel()
|
||||||
|
// 重置编辑资料页面 - 暂时注释掉看是否是这里导致的问题
|
||||||
|
// AccountEditViewModel.ResetModel()
|
||||||
|
// 重置发现页面
|
||||||
|
DiscoverViewModel.ResetModel()
|
||||||
|
// 重置搜索页面
|
||||||
|
SearchViewModel.ResetModel()
|
||||||
|
// 重置消息页面
|
||||||
|
MessageListViewModel.ResetModel()
|
||||||
|
// 重置点赞通知页面
|
||||||
|
LikeNoticeViewModel.ResetModel()
|
||||||
|
// 重置收藏页面
|
||||||
|
FavouriteListViewModel.ResetModel()
|
||||||
|
// 重置收藏通知页面
|
||||||
|
FavouriteNoticeViewModel.ResetModel()
|
||||||
|
// 重置粉丝通知页面
|
||||||
|
FollowerNoticeViewModel.ResetModel()
|
||||||
|
// 重置关注通知页面
|
||||||
|
IndexViewModel.ResetModel()
|
||||||
|
// 重置AI Agent相关页面
|
||||||
|
AgentViewModel.ResetModel()
|
||||||
|
MineAgentViewModel.ResetModel()
|
||||||
|
UserId = null
|
||||||
|
|
||||||
|
// 清除游客状态
|
||||||
|
AppStore.isGuest = false
|
||||||
|
|
||||||
|
context.stopService(Intent(context, OpenIMService::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import com.aiosman.riderpro.data.ChatService
|
import com.aiosman.ravenow.data.ChatService
|
||||||
import com.aiosman.riderpro.data.ChatServiceImpl
|
import com.aiosman.ravenow.data.ChatServiceImpl
|
||||||
import com.aiosman.riderpro.entity.ChatNotification
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存一些关于聊天的状态
|
* 保存一些关于聊天的状态
|
||||||
130
app/src/main/java/com/aiosman/ravenow/Colors.kt
Normal file
130
app/src/main/java/com/aiosman/ravenow/Colors.kt
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
//var AppColors = LightThemeColors()
|
||||||
|
//var AppColors = if (AppState.darkMode) DarkThemeColors() else LightThemeColors()
|
||||||
|
|
||||||
|
open class AppThemeData(
|
||||||
|
var main: Color,
|
||||||
|
var mainText: Color,
|
||||||
|
var basicMain: Color,
|
||||||
|
var nonActive: Color,
|
||||||
|
var text: Color,
|
||||||
|
var nonActiveText: Color,
|
||||||
|
var secondaryText: Color,
|
||||||
|
var loadingMain: Color,
|
||||||
|
var loadingText: Color,
|
||||||
|
var disabledBackground: Color,
|
||||||
|
var background: Color,
|
||||||
|
var secondaryBackground: Color,
|
||||||
|
var decentBackground: Color,
|
||||||
|
var divider: Color,
|
||||||
|
var inputBackground: Color,
|
||||||
|
var inputBackground2: Color,
|
||||||
|
var inputHint: Color,
|
||||||
|
var error: Color,
|
||||||
|
var checkedBackground: Color,
|
||||||
|
var unCheckedBackground: Color,
|
||||||
|
var checkedText: Color,
|
||||||
|
var chatActionColor: Color,
|
||||||
|
var brandColorsColor: Color,
|
||||||
|
var tabSelectedBackground: Color,
|
||||||
|
var tabUnselectedBackground: Color,
|
||||||
|
var tabSelectedText: Color,
|
||||||
|
var tabUnselectedText: Color,
|
||||||
|
var bubbleBackground: Color,
|
||||||
|
var profileBackground:Color,
|
||||||
|
// Premium 按钮相关颜色
|
||||||
|
var premiumText: Color,
|
||||||
|
var premiumBackground: Color,
|
||||||
|
// VIP 权益强调色(用于 2X / 勾选高亮)
|
||||||
|
var vipHave: Color,
|
||||||
|
// 价格卡片颜色
|
||||||
|
var priceCardSelectedBorder: Color,
|
||||||
|
var priceCardSelectedBackground: Color,
|
||||||
|
var priceCardUnselectedBorder: Color,
|
||||||
|
var priceCardUnselectedBackground: Color,
|
||||||
|
)
|
||||||
|
|
||||||
|
class LightThemeColors : AppThemeData(
|
||||||
|
main = Color(0xffD80264),
|
||||||
|
mainText = Color(0xffffffff),
|
||||||
|
basicMain = Color(0xfff0f0f0),
|
||||||
|
nonActive = Color(0xfff5f5f5),
|
||||||
|
text = Color(0xff333333),
|
||||||
|
nonActiveText = Color(0xff3C3C43),
|
||||||
|
secondaryText = Color(0x99000000),
|
||||||
|
loadingMain = Color(0xFFD95757),
|
||||||
|
loadingText = Color(0xffffffff),
|
||||||
|
disabledBackground = Color(0xFFD0D0D0),
|
||||||
|
background = Color(0xFFFFFFFF),
|
||||||
|
secondaryBackground = Color(0xFFF7f7f7),
|
||||||
|
divider = Color(0xFFEbEbEb),
|
||||||
|
inputBackground = Color(0xFFF7f7f7),
|
||||||
|
inputBackground2 = Color(0xFFFFFFFF),
|
||||||
|
inputHint = Color(0xffdadada),
|
||||||
|
error = Color(0xffFF0000),
|
||||||
|
checkedBackground = Color(0xff000000),
|
||||||
|
unCheckedBackground = Color(0xFFECEAEC),
|
||||||
|
checkedText = Color(0xffFFFFFF),
|
||||||
|
decentBackground = Color(0xfff5f5f5),
|
||||||
|
chatActionColor = Color(0xffe0e0e0),
|
||||||
|
brandColorsColor = Color(0xffD80264),
|
||||||
|
tabSelectedBackground = Color(0xff110C13),
|
||||||
|
tabUnselectedBackground = Color(0xfffaf9fb),
|
||||||
|
tabSelectedText = Color(0xffffffff),
|
||||||
|
tabUnselectedText = Color(0xff000000),
|
||||||
|
bubbleBackground = Color(0xfff5f5f5),
|
||||||
|
profileBackground = Color(0xffffffff),
|
||||||
|
premiumText = Color(0xFFCD7B00),
|
||||||
|
premiumBackground = Color(0xFFFFF5D4),
|
||||||
|
vipHave = Color(0xFFFAAD14),
|
||||||
|
priceCardSelectedBorder = Color(0xFF000000),
|
||||||
|
priceCardSelectedBackground = Color(0xFFFFF5D4),
|
||||||
|
priceCardUnselectedBorder = Color(0xFFF0EEF1),
|
||||||
|
priceCardUnselectedBackground = Color(0xFFFAF9FB),
|
||||||
|
)
|
||||||
|
|
||||||
|
class DarkThemeColors : AppThemeData(
|
||||||
|
main = Color(0xffda3832),
|
||||||
|
mainText = Color(0xffffffff),
|
||||||
|
basicMain = Color(0xFF1C1C1C),
|
||||||
|
nonActive = Color(0xff1f1f1f),
|
||||||
|
text = Color(0xffffffff),
|
||||||
|
nonActiveText = Color(0xff888888),
|
||||||
|
secondaryText = Color(0x99ffffff),
|
||||||
|
loadingMain = Color(0xFFD95757),
|
||||||
|
loadingText = Color(0xff000000),
|
||||||
|
disabledBackground = Color(0xFF3A3A3A),
|
||||||
|
background = Color(0xFF121212),
|
||||||
|
secondaryBackground = Color(0xFF1C1C1C),
|
||||||
|
divider = Color(0xFF282828),
|
||||||
|
inputBackground = Color(0xFF1C1C1C),
|
||||||
|
inputBackground2 = Color(0xFF1C1C1C),
|
||||||
|
inputHint = Color(0xff888888),
|
||||||
|
error = Color(0xffFF0000),
|
||||||
|
checkedBackground = Color(0xffffffff),
|
||||||
|
unCheckedBackground = Color(0xFF7C7480),
|
||||||
|
checkedText = Color(0xff000000),
|
||||||
|
decentBackground = Color(0xFF171717),
|
||||||
|
chatActionColor = Color(0xFF3D3D3D),
|
||||||
|
brandColorsColor = Color(0xffD80264),
|
||||||
|
tabSelectedBackground = Color(0xffffffff),
|
||||||
|
tabUnselectedBackground = Color(0x2E7C7480),
|
||||||
|
tabSelectedText = Color(0xff000000),
|
||||||
|
tabUnselectedText = Color(0xffffffff),
|
||||||
|
bubbleBackground = Color(0xff2d2c2e),
|
||||||
|
profileBackground = Color(0xff100c12),
|
||||||
|
// 暗色模式下的Premium按钮颜色 - 使用更暗的黄色调
|
||||||
|
premiumText = Color(0xFF000000),
|
||||||
|
premiumBackground = Color(0xFFFAAD14),
|
||||||
|
// VIP权益强调色 - 保持金黄色但调整透明度
|
||||||
|
vipHave = Color(0xFFFAAD14),
|
||||||
|
// 暗色模式下的价格卡片颜色
|
||||||
|
priceCardSelectedBorder = Color(0xFFFAAD14),
|
||||||
|
priceCardSelectedBackground = Color(0xFF2A2A2A),
|
||||||
|
priceCardUnselectedBorder = Color(0xFF3A3A3A),
|
||||||
|
priceCardUnselectedBackground = Color(0xFF1C1C1C),
|
||||||
|
|
||||||
|
)
|
||||||
77
app/src/main/java/com/aiosman/ravenow/Const.kt
Normal file
77
app/src/main/java/com/aiosman/ravenow/Const.kt
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
|
object ConstVars {
|
||||||
|
// api 地址 - 根据构建类型自动选择
|
||||||
|
// Debug: http://192.168.0.201:8088
|
||||||
|
// Release: https://rider-pro.aiosman.com/beta_api
|
||||||
|
// val BASE_SERVER = if (!BuildConfig.DEBUG) {
|
||||||
|
// "http://47.109.137.67:6363" // Debug环境
|
||||||
|
// } else {
|
||||||
|
// "https://rider-pro.aiosman.com/beta_api" // Release环境
|
||||||
|
// }
|
||||||
|
val BASE_SERVER = "https://rider-pro.aiosman.com/beta_api"
|
||||||
|
|
||||||
|
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
|
||||||
|
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传头像图片大小限制
|
||||||
|
* 10M
|
||||||
|
*/
|
||||||
|
const val AVATAR_FILE_SIZE_LIMIT = 1024 * 1024 * 10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传头像图片压缩时最大的尺寸
|
||||||
|
* 512
|
||||||
|
*/
|
||||||
|
const val AVATAR_IMAGE_MAX_SIZE = 512
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传 banner 图片大小限制
|
||||||
|
*/
|
||||||
|
const val BANNER_IMAGE_MAX_SIZE = 2048
|
||||||
|
|
||||||
|
// 用户协议地址
|
||||||
|
const val DICT_KEY_PRIVATE_POLICY_URL = "private_policy"
|
||||||
|
// 重置邮箱间隔
|
||||||
|
const val DIC_KEY_RESET_EMAIL_INTERVAL = "send_reset_password_timeout"
|
||||||
|
// 开启google登录
|
||||||
|
const val DICT_KEY_ENABLE_GOOGLE_LOGIN = "enable_google_login"
|
||||||
|
// google登录clientid
|
||||||
|
const val DICT_KEY_GOOGLE_LOGIN_CLIENT_ID = "google_login_client_id"
|
||||||
|
// trtc功能开启
|
||||||
|
const val DICT_KEY_ENABLE_TRTC = "enable_chat"
|
||||||
|
// 举报选项
|
||||||
|
const val DICT_KEY_REPORT_OPTIONS = "report_reasons"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class GuestLoginCheckOutScene {
|
||||||
|
CREATE_POST,
|
||||||
|
CREATE_AGENT,
|
||||||
|
VIEW_MESSAGES,
|
||||||
|
VIEW_PROFILE,
|
||||||
|
JOIN_GROUP_CHAT,
|
||||||
|
CHAT_WITH_AGENT,
|
||||||
|
LIKE_MOMENT,
|
||||||
|
COMMENT_MOMENT,
|
||||||
|
FOLLOW_USER,
|
||||||
|
REPORT_CONTENT
|
||||||
|
}
|
||||||
|
|
||||||
|
object GuestLoginCheckOut {
|
||||||
|
var NeedLoginScene = listOf<GuestLoginCheckOutScene>(
|
||||||
|
GuestLoginCheckOutScene.CREATE_POST,
|
||||||
|
GuestLoginCheckOutScene.CREATE_AGENT,
|
||||||
|
GuestLoginCheckOutScene.VIEW_MESSAGES,
|
||||||
|
GuestLoginCheckOutScene.VIEW_PROFILE,
|
||||||
|
GuestLoginCheckOutScene.JOIN_GROUP_CHAT,
|
||||||
|
GuestLoginCheckOutScene.CHAT_WITH_AGENT,
|
||||||
|
GuestLoginCheckOutScene.LIKE_MOMENT,
|
||||||
|
GuestLoginCheckOutScene.COMMENT_MOMENT,
|
||||||
|
GuestLoginCheckOutScene.FOLLOW_USER,
|
||||||
|
GuestLoginCheckOutScene.REPORT_CONTENT
|
||||||
|
)
|
||||||
|
fun needLogin(scene: GuestLoginCheckOutScene): Boolean {
|
||||||
|
return AppStore.isGuest && NeedLoginScene.contains(scene)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import cn.jpush.android.service.JCommonService
|
import cn.jpush.android.service.JCommonService
|
||||||
|
|
||||||
class JpushService : JCommonService() {
|
class JpushService : JCommonService() {
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -14,9 +15,12 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.animation.AnimatedContentScope
|
import androidx.compose.animation.AnimatedContentScope
|
||||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
@@ -24,15 +28,18 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
|||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import cn.jiguang.api.utils.JCollectionAuth
|
import cn.jiguang.api.utils.JCollectionAuth
|
||||||
import cn.jpush.android.api.JPushInterface
|
import cn.jpush.android.api.JPushInterface
|
||||||
import com.aiosman.riderpro.data.AccountService
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.riderpro.data.UserService
|
import com.aiosman.ravenow.data.UserService
|
||||||
import com.aiosman.riderpro.data.UserServiceImpl
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
import com.aiosman.riderpro.ui.Navigation
|
import com.aiosman.ravenow.ui.Navigation
|
||||||
import com.aiosman.riderpro.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.riderpro.ui.navigateToPost
|
import com.aiosman.ravenow.ui.dialogs.CheckUpdateDialog
|
||||||
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
|
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||||
|
import com.google.firebase.Firebase
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
|
import com.google.firebase.analytics.analytics
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -42,6 +49,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
// Firebase Analytics
|
// Firebase Analytics
|
||||||
private lateinit var analytics: FirebaseAnalytics
|
private lateinit var analytics: FirebaseAnalytics
|
||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
val context = this
|
||||||
|
|
||||||
// 请求通知权限
|
// 请求通知权限
|
||||||
private val requestPermissionLauncher = registerForActivityResult(
|
private val requestPermissionLauncher = registerForActivityResult(
|
||||||
@@ -67,8 +75,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// 设置屏幕方向为竖屏
|
||||||
|
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
// 监听应用生命周期
|
// 监听应用生命周期
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(MainActivityLifecycleObserver())
|
ProcessLifecycleOwner.get().lifecycle.addObserver(MainActivityLifecycleObserver())
|
||||||
// 创建通知渠道
|
// 创建通知渠道
|
||||||
@@ -78,7 +92,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
// 初始化 Places SDK
|
// 初始化 Places SDK
|
||||||
|
|
||||||
// 初始化 Firebase Analytics
|
// 初始化 Firebase Analytics
|
||||||
// analytics = Firebase.analytics
|
analytics = Firebase.analytics
|
||||||
// 请求通知权限
|
// 请求通知权限
|
||||||
askNotificationPermission()
|
askNotificationPermission()
|
||||||
// 加载一些本地化的配置
|
// 加载一些本地化的配置
|
||||||
@@ -91,22 +105,27 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
JPushInterface.init(this)
|
JPushInterface.init(this)
|
||||||
|
|
||||||
|
if (AppState.darkMode) {
|
||||||
|
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
|
||||||
|
}
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
// 初始化腾讯云通信 SDK
|
|
||||||
|
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// 检查是否有登录态
|
// 检查是否有登录态
|
||||||
val isAccountValidate = getAccount()
|
val isAccountValidate = getAccount()
|
||||||
var startDestination = NavigationRoute.Login.route
|
var startDestination = NavigationRoute.Login.route
|
||||||
// 如果有登录态,且记住登录状态,且账号有效,则初始化 FCM,下一步进入首页
|
// 如果有登录态,且记住登录状态,且账号有效,则初始化应用状态,下一步进入首页
|
||||||
if (AppStore.token != null && AppStore.rememberMe && isAccountValidate) {
|
if (AppStore.token != null && AppStore.rememberMe && (isAccountValidate || AppStore.isGuest)) {
|
||||||
|
// 根据用户类型进行相应的初始化(游客模式会跳过推送和TRTC初始化)
|
||||||
AppState.initWithAccount(scope, this@MainActivity)
|
AppState.initWithAccount(scope, this@MainActivity)
|
||||||
startDestination = NavigationRoute.Index.route
|
startDestination = NavigationRoute.Index.route
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAppTheme provides AppState.appTheme
|
||||||
|
) {
|
||||||
|
CheckUpdateDialog()
|
||||||
Navigation(startDestination) { navController ->
|
Navigation(startDestination) { navController ->
|
||||||
// 处理带有 postId 的通知点击
|
// 处理带有 postId 的通知点击
|
||||||
val postId = intent.getStringExtra("POST_ID")
|
val postId = intent.getStringExtra("POST_ID")
|
||||||
@@ -126,7 +145,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
sender?.let {
|
sender?.let {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val profile = userService.getUserProfileByTrtcUserId(it)
|
val profile = userService.getUserProfileByTrtcUserId(it,0)
|
||||||
navController.navigate(NavigationRoute.Chat.route.replace(
|
navController.navigate(NavigationRoute.Chat.route.replace(
|
||||||
"{id}",
|
"{id}",
|
||||||
profile.id.toString()
|
profile.id.toString()
|
||||||
@@ -163,12 +182,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求通知权限
|
* 请求通知权限
|
||||||
*/
|
*/
|
||||||
@@ -218,3 +240,6 @@ val LocalAnimatedContentScope = compositionLocalOf<AnimatedContentScope> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val LocalAppTheme = compositionLocalOf<AppThemeData> {
|
||||||
|
error("AppThemeData not provided")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import cn.jpush.android.api.JPushInterface
|
import cn.jpush.android.api.JPushInterface
|
||||||
import com.aiosman.riderpro.data.AccountService
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.google.android.gms.tasks.OnCompleteListener
|
import com.google.android.gms.tasks.OnCompleteListener
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -6,7 +6,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
@@ -31,7 +30,7 @@ fun showLikeNotification(context: Context, title: String, message: String, postI
|
|||||||
)
|
)
|
||||||
|
|
||||||
val notificationBuilder = NotificationCompat.Builder(context, channelId)
|
val notificationBuilder = NotificationCompat.Builder(context, channelId)
|
||||||
.setSmallIcon(R.drawable.rider_pro_favoriate)
|
.setSmallIcon(R.drawable.rider_pro_favourite)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText(message)
|
.setContentText(message)
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
302
app/src/main/java/com/aiosman/ravenow/OpenIMService.kt
Normal file
302
app/src/main/java/com/aiosman/ravenow/OpenIMService.kt
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import io.openim.android.sdk.OpenIMClient
|
||||||
|
import io.openim.android.sdk.listener.OnAdvanceMsgListener
|
||||||
|
import io.openim.android.sdk.models.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM 服务
|
||||||
|
* 负责处理 OpenIM 的后台消息监听和通知
|
||||||
|
*/
|
||||||
|
class OpenIMService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "OpenIMService"
|
||||||
|
private const val CHANNEL_ID = "openim_notification"
|
||||||
|
private const val NOTIFICATION_ID = 1001
|
||||||
|
}
|
||||||
|
|
||||||
|
private var openIMMessageListener: OnAdvanceMsgListener? = null
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
Log.d(TAG, "OpenIMService onStartCommand")
|
||||||
|
createNotificationChannel()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
registerMessageListener(applicationContext)
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
Log.d(TAG, "OpenIMService onDestroy")
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
unRegisterMessageListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 OpenIM 消息监听器
|
||||||
|
*/
|
||||||
|
private fun registerMessageListener(context: Context) {
|
||||||
|
val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
openIMMessageListener = object : OnAdvanceMsgListener {
|
||||||
|
override fun onRecvNewMessage(msg: Message?) {
|
||||||
|
Log.d(TAG, "收到新消息: ${msg?.toString()}")
|
||||||
|
msg?.let {
|
||||||
|
// 如果应用在前台,不显示通知
|
||||||
|
if (MainActivityLifecycle.isForeground) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
// 检查通知策略
|
||||||
|
val shouldNotify = shouldShowNotification(it)
|
||||||
|
if (shouldNotify) {
|
||||||
|
sendNotification(context, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvC2CReadReceipt(list: List<C2CReadReceiptInfo>?) {
|
||||||
|
Log.d(TAG, "收到C2C已读回执,数量: ${list?.size}")
|
||||||
|
// 处理已读回执,可以更新消息状态
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvGroupMessageReadReceipt(groupMessageReceipt: GroupMessageReceipt?) {
|
||||||
|
Log.d(TAG, "收到群组消息已读回执")
|
||||||
|
// 处理群组消息已读回执
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvMessageRevokedV2(info: RevokedInfo?) {
|
||||||
|
Log.d(TAG, "消息被撤回: ${info?.clientMsgID}")
|
||||||
|
// 处理消息撤回,可以更新UI
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvMessageExtensionsChanged(msgID: String?, list: List<KeyValue>?) {
|
||||||
|
Log.d(TAG, "消息扩展信息变更: $msgID")
|
||||||
|
// 处理消息扩展信息变更
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvMessageExtensionsDeleted(msgID: String?, list: List<String>?) {
|
||||||
|
Log.d(TAG, "消息扩展信息删除: $msgID")
|
||||||
|
// 处理消息扩展信息删除
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvMessageExtensionsAdded(msgID: String?, list: List<KeyValue>?) {
|
||||||
|
Log.d(TAG, "消息扩展信息添加: $msgID")
|
||||||
|
// 处理消息扩展信息添加
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMsgDeleted(message: Message?) {
|
||||||
|
Log.d(TAG, "消息被删除: ${message?.clientMsgID}")
|
||||||
|
// 处理消息删除
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvOfflineNewMessage(msg: List<Message>?) {
|
||||||
|
Log.d(TAG, "收到离线新消息,数量: ${msg?.size}")
|
||||||
|
// 处理离线新消息
|
||||||
|
msg?.forEach { message ->
|
||||||
|
// 为离线消息也可以发送通知
|
||||||
|
if (!MainActivityLifecycle.isForeground) {
|
||||||
|
scope.launch {
|
||||||
|
val shouldNotify = shouldShowNotification(message)
|
||||||
|
if (shouldNotify) {
|
||||||
|
sendNotification(context, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvOnlineOnlyMessage(s: String?) {
|
||||||
|
Log.d(TAG, "收到仅在线消息: $s")
|
||||||
|
// 处理仅在线消息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加消息监听器
|
||||||
|
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(openIMMessageListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消注册消息监听器
|
||||||
|
*/
|
||||||
|
private fun unRegisterMessageListener() {
|
||||||
|
openIMMessageListener?.let {
|
||||||
|
// OpenIM SDK 可能需要不同的方法来移除监听器
|
||||||
|
// 这里假设有类似的方法,具体需要根据SDK文档调整
|
||||||
|
Log.d(TAG, "取消注册消息监听器")
|
||||||
|
}
|
||||||
|
openIMMessageListener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否应该显示通知
|
||||||
|
* @param message 消息对象
|
||||||
|
* @return 是否显示通知
|
||||||
|
*/
|
||||||
|
private suspend fun shouldShowNotification(message: Message): Boolean {
|
||||||
|
// 这里可以根据用户设置、消息类型等判断是否显示通知
|
||||||
|
// 类似于 TrtcService 中的策略检查
|
||||||
|
|
||||||
|
// 示例:检查是否是系统消息或者用户设置了免打扰
|
||||||
|
return try {
|
||||||
|
// 可以根据发送者ID或会话ID检查通知策略
|
||||||
|
val senderId = message.sendID
|
||||||
|
|
||||||
|
// 这里可以调用类似 ChatState.getStrategyByTargetTrtcId 的方法
|
||||||
|
// 暂时返回 true,表示默认显示通知
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "检查通知策略失败", e)
|
||||||
|
true // 默认显示通知
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建通知渠道
|
||||||
|
*/
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val name = "OpenIM 消息通知"
|
||||||
|
val descriptionText = "OpenIM 即时消息通知"
|
||||||
|
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
|
||||||
|
description = descriptionText
|
||||||
|
enableVibration(true)
|
||||||
|
enableLights(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送通知
|
||||||
|
* @param context 上下文
|
||||||
|
* @param message OpenIM 消息对象
|
||||||
|
*/
|
||||||
|
private fun sendNotification(context: Context, message: Message) {
|
||||||
|
try {
|
||||||
|
// 创建点击通知后的意图
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
putExtra("ACTION", "OPENIM_NEW_MESSAGE")
|
||||||
|
putExtra("SENDER_ID", message.sendID)
|
||||||
|
putExtra("CONVERSATION_ID", message.sessionType)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
// 构建通知内容
|
||||||
|
val senderName = getSenderDisplayName(message)
|
||||||
|
val messageContent = getMessageDisplayContent(message)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.mipmap.rider_pro_log_round)
|
||||||
|
.setContentTitle(senderName)
|
||||||
|
.setContentText(messageContent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setVibrate(longArrayOf(0, 300, 300, 300))
|
||||||
|
.setLights(0xFF0000FF.toInt(), 300, 300)
|
||||||
|
|
||||||
|
// 发送通知
|
||||||
|
with(NotificationManagerCompat.from(context)) {
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
Log.w(TAG, "没有通知权限,无法发送通知")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用消息ID的哈希值作为通知ID,确保每条消息都有唯一的通知
|
||||||
|
val notificationId = message.clientMsgID?.hashCode() ?: NOTIFICATION_ID
|
||||||
|
notify(notificationId, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "发送通知成功: $senderName - $messageContent")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "发送通知失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取发送者显示名称
|
||||||
|
* @param message 消息对象
|
||||||
|
* @return 发送者名称
|
||||||
|
*/
|
||||||
|
private fun getSenderDisplayName(message: Message): String {
|
||||||
|
return try {
|
||||||
|
// 尝试获取发送者的昵称或显示名
|
||||||
|
message.senderNickname?.takeIf { it.isNotEmpty() }
|
||||||
|
?: message.sendID
|
||||||
|
?: "未知用户"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "获取发送者名称失败", e)
|
||||||
|
"未知用户"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息显示内容
|
||||||
|
* @param message 消息对象
|
||||||
|
* @return 消息内容
|
||||||
|
*/
|
||||||
|
private fun getMessageDisplayContent(message: Message): String {
|
||||||
|
return try {
|
||||||
|
when (message.contentType) {
|
||||||
|
101 -> message.textElem.content ?: "[文本消息]" // 文本消息
|
||||||
|
102 -> "[图片]"
|
||||||
|
103 -> "[语音]"
|
||||||
|
104 -> "[视频]"
|
||||||
|
105 -> "[文件]"
|
||||||
|
106 -> "[位置]"
|
||||||
|
107 -> "[自定义消息]"
|
||||||
|
108 -> "[合并消息]"
|
||||||
|
109 -> "[名片]"
|
||||||
|
110 -> "[引用消息]"
|
||||||
|
else -> "[消息]"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "获取消息内容失败", e)
|
||||||
|
"[消息]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/src/main/java/com/aiosman/ravenow/RaveNowApplication.kt
Normal file
57
app/src/main/java/com/aiosman/ravenow/RaveNowApplication.kt
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.firebase.FirebaseApp
|
||||||
|
import com.google.firebase.perf.FirebasePerformance
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义Application类,用于处理多进程中的Firebase初始化
|
||||||
|
*/
|
||||||
|
class RaveNowApplication : Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
// 获取当前进程名
|
||||||
|
val processName = getCurrentProcessName()
|
||||||
|
Log.d("RaveNowApplication", "当前进程: $processName")
|
||||||
|
|
||||||
|
// 在所有进程中初始化Firebase
|
||||||
|
try {
|
||||||
|
if (FirebaseApp.getApps(this).isEmpty()) {
|
||||||
|
FirebaseApp.initializeApp(this)
|
||||||
|
Log.d("RaveNowApplication", "Firebase已在进程 $processName 中初始化")
|
||||||
|
|
||||||
|
// 如果是pushcore进程,禁用Firebase Performance监控
|
||||||
|
if (processName.contains(":pushcore")) {
|
||||||
|
try {
|
||||||
|
FirebasePerformance.getInstance().isPerformanceCollectionEnabled = false
|
||||||
|
Log.d("RaveNowApplication", "已在pushcore进程中禁用Firebase Performance监控")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("RaveNowApplication", "禁用Firebase Performance监控失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d("RaveNowApplication", "Firebase已在进程 $processName 中存在")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RaveNowApplication", "Firebase初始化失败在进程 $processName", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前进程名
|
||||||
|
*/
|
||||||
|
private fun getCurrentProcessName(): String {
|
||||||
|
return try {
|
||||||
|
val pid = android.os.Process.myPid()
|
||||||
|
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
|
||||||
|
val processes = activityManager.runningAppProcesses
|
||||||
|
processes?.find { it.pid == pid }?.processName ?: "unknown"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,30 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.aiosman.riderpro.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.riderpro.data.api.AppConfig
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.riderpro.data.api.CaptchaInfo
|
import com.aiosman.ravenow.data.api.AppConfig
|
||||||
import com.aiosman.riderpro.data.api.ChangePasswordRequestBody
|
import com.aiosman.ravenow.data.api.CaptchaInfo
|
||||||
import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody
|
import com.aiosman.ravenow.data.api.ChangePasswordRequestBody
|
||||||
import com.aiosman.riderpro.data.api.LoginUserRequestBody
|
import com.aiosman.ravenow.data.api.GoogleRegisterRequestBody
|
||||||
import com.aiosman.riderpro.data.api.RegisterMessageChannelRequestBody
|
import com.aiosman.ravenow.data.api.GuestLoginRequestBody
|
||||||
import com.aiosman.riderpro.data.api.RegisterRequestBody
|
import com.aiosman.ravenow.data.api.LoginUserRequestBody
|
||||||
import com.aiosman.riderpro.data.api.ResetPasswordRequestBody
|
import com.aiosman.ravenow.data.api.RegisterMessageChannelRequestBody
|
||||||
import com.aiosman.riderpro.data.api.TrtcSignResponseBody
|
import com.aiosman.ravenow.data.api.RegisterRequestBody
|
||||||
import com.aiosman.riderpro.data.api.UnRegisterMessageChannelRequestBody
|
import com.aiosman.ravenow.data.api.RemoveAccountRequestBody
|
||||||
import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody
|
import com.aiosman.ravenow.data.api.ResetPasswordRequestBody
|
||||||
import com.aiosman.riderpro.data.api.UpdateUserLangRequestBody
|
import com.aiosman.ravenow.data.api.TrtcSignResponseBody
|
||||||
import com.aiosman.riderpro.entity.AccountFavouriteEntity
|
import com.aiosman.ravenow.data.api.UnRegisterMessageChannelRequestBody
|
||||||
import com.aiosman.riderpro.entity.AccountLikeEntity
|
import com.aiosman.ravenow.data.api.UpdateNoticeRequestBody
|
||||||
import com.aiosman.riderpro.entity.AccountProfileEntity
|
import com.aiosman.ravenow.data.DataContainer
|
||||||
import com.aiosman.riderpro.entity.NoticeCommentEntity
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
import com.aiosman.riderpro.entity.NoticePostEntity
|
import com.aiosman.ravenow.data.api.UpdateUserLangRequestBody
|
||||||
import com.aiosman.riderpro.entity.NoticeUserEntity
|
import com.aiosman.ravenow.entity.AccountFavouriteEntity
|
||||||
|
import com.aiosman.ravenow.entity.AccountLikeEntity
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.NoticeCommentEntity
|
||||||
|
import com.aiosman.ravenow.entity.NoticePostEntity
|
||||||
|
import com.aiosman.ravenow.entity.NoticeUserEntity
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
@@ -52,6 +57,13 @@ data class AccountProfile(
|
|||||||
val banner: String?,
|
val banner: String?,
|
||||||
// trtcUserId
|
// trtcUserId
|
||||||
val trtcUserId: String,
|
val trtcUserId: String,
|
||||||
|
|
||||||
|
val openImToken: String?,
|
||||||
|
|
||||||
|
// aiAccount true:ai false:普通用户
|
||||||
|
val aiAccount: Boolean,
|
||||||
|
|
||||||
|
val chatAIId: String,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* 转换为Entity
|
* 转换为Entity
|
||||||
@@ -62,7 +74,8 @@ data class AccountProfile(
|
|||||||
followerCount = followerCount,
|
followerCount = followerCount,
|
||||||
followingCount = followingCount,
|
followingCount = followingCount,
|
||||||
nickName = nickname,
|
nickName = nickname,
|
||||||
avatar = "${ApiClient.BASE_SERVER}$avatar",
|
avatar =
|
||||||
|
"${ApiClient.BASE_SERVER}$avatar",
|
||||||
bio = bio,
|
bio = bio,
|
||||||
country = "Worldwide",
|
country = "Worldwide",
|
||||||
isFollowing = isFollowing,
|
isFollowing = isFollowing,
|
||||||
@@ -72,7 +85,11 @@ data class AccountProfile(
|
|||||||
}
|
}
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
trtcUserId = trtcUserId
|
trtcUserId = trtcUserId,
|
||||||
|
chatToken = openImToken,
|
||||||
|
aiAccount = aiAccount,
|
||||||
|
rawAvatar = avatar,
|
||||||
|
chatAIId = chatAIId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,6 +305,13 @@ interface AccountService {
|
|||||||
*/
|
*/
|
||||||
suspend fun loginUserWithGoogle(googleId: String): UserAuth
|
suspend fun loginUserWithGoogle(googleId: String): UserAuth
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游客登录
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param deviceInfo 设备信息
|
||||||
|
*/
|
||||||
|
suspend fun guestLogin(deviceId: String, deviceInfo: String? = null): UserAuth
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录
|
* 退出登录
|
||||||
*/
|
*/
|
||||||
@@ -385,13 +409,38 @@ interface AccountService {
|
|||||||
suspend fun getMyTrtcSign(): TrtcSignResponseBody
|
suspend fun getMyTrtcSign(): TrtcSignResponseBody
|
||||||
|
|
||||||
suspend fun getAppConfig(): AppConfig
|
suspend fun getAppConfig(): AppConfig
|
||||||
|
|
||||||
|
suspend fun removeAccount(password: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取AI智能体列表
|
||||||
|
* @param page 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
*/
|
||||||
|
suspend fun getAgent(page: Int, pageSize: Int): retrofit2.Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建群聊
|
||||||
|
* @param name 群聊名称
|
||||||
|
* @param userIds 用户ID列表
|
||||||
|
* @param promptIds AI智能体ID列表
|
||||||
|
*/
|
||||||
|
suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>): retrofit2.Response<DataContainer<Unit>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountServiceImpl : AccountService {
|
class AccountServiceImpl : AccountService {
|
||||||
override suspend fun getMyAccountProfile(): AccountProfileEntity {
|
override suspend fun getMyAccountProfile(): AccountProfileEntity {
|
||||||
|
// 如果已有缓存,直接返回缓存结果
|
||||||
|
AppState.profile?.let { return it }
|
||||||
|
|
||||||
|
// 第一次调用,获取数据并缓存
|
||||||
val resp = ApiClient.api.getMyAccount()
|
val resp = ApiClient.api.getMyAccount()
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
return body.data.toAccountProfileEntity()
|
val profile = body.data.toAccountProfileEntity()
|
||||||
|
|
||||||
|
// 缓存结果到共享状态
|
||||||
|
AppState.profile = profile
|
||||||
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMyAccount(): UserAuth {
|
override suspend fun getMyAccount(): UserAuth {
|
||||||
@@ -423,12 +472,31 @@ class AccountServiceImpl : AccountService {
|
|||||||
override suspend fun loginUserWithGoogle(googleId: String): UserAuth {
|
override suspend fun loginUserWithGoogle(googleId: String): UserAuth {
|
||||||
val resp = ApiClient.api.login(LoginUserRequestBody(googleId = googleId))
|
val resp = ApiClient.api.login(LoginUserRequestBody(googleId = googleId))
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to login")
|
val body = resp.body() ?: throw ServiceException("Failed to login")
|
||||||
|
|
||||||
return UserAuth(0, body.token)
|
return UserAuth(0, body.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun guestLogin(deviceId: String, deviceInfo: String?): UserAuth {
|
||||||
|
val resp = ApiClient.api.guestLogin(GuestLoginRequestBody(
|
||||||
|
deviceId = deviceId,
|
||||||
|
deviceInfo = deviceInfo
|
||||||
|
))
|
||||||
|
if (!resp.isSuccessful) {
|
||||||
|
parseErrorResponse(resp.errorBody())?.let {
|
||||||
|
throw it.toServiceException()
|
||||||
|
}
|
||||||
|
throw ServiceException("Failed to guest login")
|
||||||
|
}
|
||||||
|
val body = resp.body() ?: throw ServiceException("Failed to guest login")
|
||||||
|
return UserAuth(0, body.token, isGuest = true)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun regiterUserWithGoogleAccount(idToken: String) {
|
override suspend fun regiterUserWithGoogleAccount(idToken: String) {
|
||||||
val resp = ApiClient.api.registerWithGoogle(GoogleRegisterRequestBody(idToken))
|
val resp = ApiClient.api.registerWithGoogle(GoogleRegisterRequestBody(idToken))
|
||||||
if (resp.code() != 200) {
|
if (!resp.isSuccessful) {
|
||||||
|
parseErrorResponse(resp.errorBody())?.let {
|
||||||
|
throw it.toServiceException()
|
||||||
|
}
|
||||||
throw ServiceException("Failed to register")
|
throw ServiceException("Failed to register")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,7 +540,13 @@ class AccountServiceImpl : AccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun changeAccountPassword(oldPassword: String, newPassword: String) {
|
override suspend fun changeAccountPassword(oldPassword: String, newPassword: String) {
|
||||||
ApiClient.api.changePassword(ChangePasswordRequestBody(oldPassword, newPassword))
|
val resp = ApiClient.api.changePassword(ChangePasswordRequestBody(oldPassword, newPassword))
|
||||||
|
if (!resp.isSuccessful) {
|
||||||
|
parseErrorResponse(resp.errorBody())?.let {
|
||||||
|
throw it.toServiceException()
|
||||||
|
}
|
||||||
|
throw ServiceException("Failed to change password")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMyLikeNotice(page: Int, pageSize: Int): ListContainer<AccountLike> {
|
override suspend fun getMyLikeNotice(page: Int, pageSize: Int): ListContainer<AccountLike> {
|
||||||
@@ -543,4 +617,29 @@ class AccountServiceImpl : AccountService {
|
|||||||
val body = resp.body() ?: throw ServiceException("Failed to get app config")
|
val body = resp.body() ?: throw ServiceException("Failed to get app config")
|
||||||
return body.data
|
return body.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun removeAccount(password: String) {
|
||||||
|
val resp = ApiClient.api.deleteAccount(
|
||||||
|
RemoveAccountRequestBody(password)
|
||||||
|
)
|
||||||
|
if (!resp.isSuccessful) {
|
||||||
|
parseErrorResponse(resp.errorBody())?.let {
|
||||||
|
throw it.toServiceException()
|
||||||
|
}
|
||||||
|
throw ServiceException("Failed to remove account")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAgent(page: Int, pageSize: Int): retrofit2.Response<DataContainer<ListContainer<Agent>>> {
|
||||||
|
return ApiClient.api.getAgent(page, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>): retrofit2.Response<DataContainer<Unit>> {
|
||||||
|
val requestBody = com.aiosman.ravenow.data.api.CreateGroupChatRequestBody(
|
||||||
|
name = name,
|
||||||
|
userIds = userIds,
|
||||||
|
promptIds = promptIds
|
||||||
|
)
|
||||||
|
return ApiClient.api.createGroupChat(requestBody)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
98
app/src/main/java/com/aiosman/ravenow/data/AgentService.kt
Normal file
98
app/src/main/java/com/aiosman/ravenow/data/AgentService.kt
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
|
import com.aiosman.ravenow.entity.ProfileEntity
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class Agent(
|
||||||
|
/*@SerializedName("author")
|
||||||
|
val author: String,*/
|
||||||
|
@SerializedName("avatar")
|
||||||
|
val avatar: String,
|
||||||
|
@SerializedName("breakMode")
|
||||||
|
val breakMode: Boolean,
|
||||||
|
@SerializedName("createdAt")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("desc")
|
||||||
|
val desc: String,
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("isPublic")
|
||||||
|
val isPublic: Boolean,
|
||||||
|
@SerializedName("openId")
|
||||||
|
val openId: String,
|
||||||
|
@SerializedName("title")
|
||||||
|
val title: String,
|
||||||
|
@SerializedName("updatedAt")
|
||||||
|
val updatedAt: String,
|
||||||
|
@SerializedName("useCount")
|
||||||
|
val useCount: Int
|
||||||
|
|
||||||
|
) {
|
||||||
|
fun toAgentEntity(): AgentEntity {
|
||||||
|
return AgentEntity(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
desc = desc,
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
avatar = "${ApiClient.BASE_API_URL+"/outside"}$avatar"+"?token="+"${AppStore.token}",
|
||||||
|
//author = author,
|
||||||
|
isPublic = isPublic,
|
||||||
|
openId = openId,
|
||||||
|
breakMode = breakMode,
|
||||||
|
useCount = useCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class Profile(
|
||||||
|
@SerializedName("aiAccount")
|
||||||
|
val aiAccount: Boolean,
|
||||||
|
@SerializedName("avatar")
|
||||||
|
val avatar: String,
|
||||||
|
@SerializedName("banner")
|
||||||
|
val banner: String,
|
||||||
|
@SerializedName("bio")
|
||||||
|
val bio: String,
|
||||||
|
@SerializedName("chatAIId")
|
||||||
|
val chatAIId: String,
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("nickname")
|
||||||
|
val nickname: String,
|
||||||
|
@SerializedName("trtcUserId")
|
||||||
|
val trtcUserId: String,
|
||||||
|
@SerializedName("username")
|
||||||
|
val username: String
|
||||||
|
){
|
||||||
|
fun toProfileEntity(): ProfileEntity {
|
||||||
|
return ProfileEntity(
|
||||||
|
id = id,
|
||||||
|
username = username,
|
||||||
|
nickname = nickname,
|
||||||
|
avatar = "${ApiClient.BASE_SERVER}$avatar",
|
||||||
|
bio = bio,
|
||||||
|
banner = "${ApiClient.BASE_SERVER}$banner",
|
||||||
|
trtcUserId = trtcUserId,
|
||||||
|
chatAIId = chatAIId,
|
||||||
|
aiAccount = aiAccount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface AgentService {
|
||||||
|
/**
|
||||||
|
* 获取智能体列表
|
||||||
|
*/
|
||||||
|
suspend fun getAgent(
|
||||||
|
pageNumber: Int,
|
||||||
|
pageSize: Int = 20,
|
||||||
|
authorId: Int? = null
|
||||||
|
): ListContainer<AgentEntity>?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.riderpro.data.api.CaptchaRequestBody
|
import com.aiosman.ravenow.data.api.CaptchaRequestBody
|
||||||
import com.aiosman.riderpro.data.api.CaptchaResponseBody
|
import com.aiosman.ravenow.data.api.CaptchaResponseBody
|
||||||
import com.aiosman.riderpro.data.api.CheckLoginCaptchaRequestBody
|
import com.aiosman.ravenow.data.api.CheckLoginCaptchaRequestBody
|
||||||
import com.aiosman.riderpro.data.api.GenerateLoginCaptchaRequestBody
|
import com.aiosman.ravenow.data.api.GenerateLoginCaptchaRequestBody
|
||||||
|
|
||||||
|
|
||||||
interface CaptchaService {
|
interface CaptchaService {
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类翻译数据模型
|
||||||
|
*/
|
||||||
|
data class CategoryTranslation(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类翻译集合
|
||||||
|
*/
|
||||||
|
data class CategoryTranslations(
|
||||||
|
@SerializedName("ja")
|
||||||
|
val ja: CategoryTranslation? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据模型
|
||||||
|
*/
|
||||||
|
data class Category(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String,
|
||||||
|
@SerializedName("avatar")
|
||||||
|
val avatar: String,
|
||||||
|
@SerializedName("parentId")
|
||||||
|
val parentId: Int? = null,
|
||||||
|
@SerializedName("sort")
|
||||||
|
val sort: Int,
|
||||||
|
@SerializedName("isActive")
|
||||||
|
val isActive: Boolean,
|
||||||
|
@SerializedName("promptCount")
|
||||||
|
val promptCount: Int,
|
||||||
|
@SerializedName("createdAt")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("updatedAt")
|
||||||
|
val updatedAt: String,
|
||||||
|
@SerializedName("translations")
|
||||||
|
val translations: CategoryTranslations? = null
|
||||||
|
)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.riderpro.data.api.UpdateChatNotificationRequestBody
|
import com.aiosman.ravenow.data.api.UpdateChatNotificationRequestBody
|
||||||
import com.aiosman.riderpro.entity.ChatNotification
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
|
|
||||||
interface ChatService {
|
interface ChatService {
|
||||||
suspend fun getChatNotifications(
|
suspend fun getChatNotifications(
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.riderpro.data.api.CommentRequestBody
|
import com.aiosman.ravenow.data.api.CommentRequestBody
|
||||||
import com.aiosman.riderpro.entity.CommentEntity
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
/**
|
/**
|
||||||
50
app/src/main/java/com/aiosman/ravenow/data/CommonService.kt
Normal file
50
app/src/main/java/com/aiosman/ravenow/data/CommonService.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.aiosman.ravenow.ConstVars
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.data.api.CreateReportRequestBody
|
||||||
|
import com.aiosman.ravenow.entity.ReportReasons
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class ReportReasonList(
|
||||||
|
@SerializedName("reasons") var reasons: ArrayList<ReportReasons>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface CommonService {
|
||||||
|
suspend fun getReportReasons(): ReportReasonList
|
||||||
|
suspend fun createReport(
|
||||||
|
reportReasonId: Int,
|
||||||
|
reportType: String,
|
||||||
|
reportId: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommonServiceImpl : CommonService {
|
||||||
|
private val dictService: DictService = DictServiceImpl()
|
||||||
|
override suspend fun getReportReasons(): ReportReasonList {
|
||||||
|
val dictItem = dictService.getDictByKey(ConstVars.DICT_KEY_REPORT_OPTIONS)
|
||||||
|
val rawJson: String = dictItem.value as? String ?: throw Exception("parse report reasons error")
|
||||||
|
val gson = Gson()
|
||||||
|
val list = gson.fromJson(rawJson, ReportReasonList::class.java)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun createReport(
|
||||||
|
reportReasonId: Int,
|
||||||
|
reportType: String,
|
||||||
|
reportId: Int,
|
||||||
|
) {
|
||||||
|
ApiClient.api.createReport(
|
||||||
|
CreateReportRequestBody(
|
||||||
|
reportType = reportType,
|
||||||
|
reportId = reportId,
|
||||||
|
reason = reportReasonId,
|
||||||
|
extra = "",
|
||||||
|
base64Images = emptyList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用接口返回数据
|
* 通用接口返回数据
|
||||||
29
app/src/main/java/com/aiosman/ravenow/data/DictService.kt
Normal file
29
app/src/main/java/com/aiosman/ravenow/data/DictService.kt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.data.api.DictItem
|
||||||
|
|
||||||
|
|
||||||
|
interface DictService {
|
||||||
|
/**
|
||||||
|
* 获取字典项
|
||||||
|
*/
|
||||||
|
suspend fun getDictByKey(key: String): DictItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典列表
|
||||||
|
*/
|
||||||
|
suspend fun getDistList(keys: List<String>): List<DictItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DictServiceImpl : DictService {
|
||||||
|
override suspend fun getDictByKey(key: String): DictItem {
|
||||||
|
val resp = ApiClient.api.getDict(key)
|
||||||
|
return resp.body()?.data ?: throw Exception("failed to get dict")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDistList(keys: List<String>): List<DictItem> {
|
||||||
|
val resp = ApiClient.api.getDicts(keys.joinToString(","))
|
||||||
|
return resp.body()?.list ?: throw Exception("failed to get dict list")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.data.api.ErrorCode
|
||||||
|
import com.aiosman.ravenow.data.api.toErrorCode
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
@@ -13,6 +15,7 @@ class ServiceException(
|
|||||||
val data: Any? = null,
|
val data: Any? = null,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
|
val errorType: ErrorCode = ErrorCode.UNKNOWN
|
||||||
) : Exception(
|
) : Exception(
|
||||||
message
|
message
|
||||||
)
|
)
|
||||||
@@ -30,7 +33,8 @@ data class ApiErrorResponse(
|
|||||||
message = error ?: name ?: "",
|
message = error ?: name ?: "",
|
||||||
code = code,
|
code = code,
|
||||||
error = error,
|
error = error,
|
||||||
name = name
|
name = name,
|
||||||
|
errorType = (code ?: 0).toErrorCode()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.riderpro.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.riderpro.entity.MomentImageEntity
|
import com.aiosman.ravenow.entity.MomentImageEntity
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -32,6 +32,28 @@ data class Moment(
|
|||||||
val time: String,
|
val time: String,
|
||||||
@SerializedName("isFollowed")
|
@SerializedName("isFollowed")
|
||||||
val isFollowed: Boolean,
|
val isFollowed: Boolean,
|
||||||
|
@SerializedName("isNews")
|
||||||
|
val isNews: Boolean? = null,
|
||||||
|
@SerializedName("newsTitle")
|
||||||
|
val newsTitle: String? = null,
|
||||||
|
@SerializedName("newsUrl")
|
||||||
|
val newsUrl: String? = null,
|
||||||
|
@SerializedName("newsSource")
|
||||||
|
val newsSource: String? = null,
|
||||||
|
@SerializedName("newsCategory")
|
||||||
|
val newsCategory: String? = null,
|
||||||
|
@SerializedName("newsLanguage")
|
||||||
|
val newsLanguage: String? = null,
|
||||||
|
@SerializedName("newsContent")
|
||||||
|
val newsContent: String? = null,
|
||||||
|
@SerializedName("hasFullText")
|
||||||
|
val hasFullText: Boolean? = null,
|
||||||
|
@SerializedName("summary")
|
||||||
|
val summary: String? = null,
|
||||||
|
@SerializedName("publishedAt")
|
||||||
|
val publishedAt: String? = null,
|
||||||
|
@SerializedName("imageCached")
|
||||||
|
val imageCached: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
fun toMomentItem(): MomentEntity {
|
fun toMomentItem(): MomentEntity {
|
||||||
return MomentEntity(
|
return MomentEntity(
|
||||||
@@ -60,6 +82,17 @@ data class Moment(
|
|||||||
authorId = user.id.toInt(),
|
authorId = user.id.toInt(),
|
||||||
liked = isLiked,
|
liked = isLiked,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
|
isNews = isNews,
|
||||||
|
newsTitle = newsTitle,
|
||||||
|
newsUrl = newsUrl,
|
||||||
|
newsSource = newsSource,
|
||||||
|
newsCategory = newsCategory,
|
||||||
|
newsLanguage = newsLanguage,
|
||||||
|
newsContent = newsContent,
|
||||||
|
hasFullText = hasFullText,
|
||||||
|
summary = summary,
|
||||||
|
publishedAt = publishedAt,
|
||||||
|
imageCached = imageCached,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,6 +154,7 @@ interface MomentService {
|
|||||||
* @param timelineId 用户时间线ID,指定用户 ID 的时间线
|
* @param timelineId 用户时间线ID,指定用户 ID 的时间线
|
||||||
* @param contentSearch 内容搜索,过滤条件
|
* @param contentSearch 内容搜索,过滤条件
|
||||||
* @param trend 是否趋势动态
|
* @param trend 是否趋势动态
|
||||||
|
* @param explore 是否探索动态
|
||||||
* @return 动态列表
|
* @return 动态列表
|
||||||
*/
|
*/
|
||||||
suspend fun getMoments(
|
suspend fun getMoments(
|
||||||
@@ -129,6 +163,7 @@ interface MomentService {
|
|||||||
timelineId: Int? = null,
|
timelineId: Int? = null,
|
||||||
contentSearch: String? = null,
|
contentSearch: String? = null,
|
||||||
trend: Boolean? = false,
|
trend: Boolean? = false,
|
||||||
|
explore: Boolean? = false,
|
||||||
favoriteUserId: Int? = null
|
favoriteUserId: Int? = null
|
||||||
): ListContainer<MomentEntity>
|
): ListContainer<MomentEntity>
|
||||||
|
|
||||||
@@ -146,6 +181,10 @@ interface MomentService {
|
|||||||
relPostId: Int? = null
|
relPostId: Int? = null
|
||||||
): MomentEntity
|
): MomentEntity
|
||||||
|
|
||||||
|
suspend fun agentMoment(
|
||||||
|
content: String,
|
||||||
|
): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 收藏动态
|
* 收藏动态
|
||||||
* @param id 动态ID
|
* @param id 动态ID
|
||||||
111
app/src/main/java/com/aiosman/ravenow/data/RoomService.kt
Normal file
111
app/src/main/java/com/aiosman/ravenow/data/RoomService.kt
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
|
import com.aiosman.ravenow.entity.CreatorEntity
|
||||||
|
import com.aiosman.ravenow.entity.ProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.RoomEntity
|
||||||
|
import com.aiosman.ravenow.entity.UsersEntity
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class Room(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String,
|
||||||
|
@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("creator")
|
||||||
|
val creator: Creator,
|
||||||
|
@SerializedName("userCount")
|
||||||
|
val userCount: Int,
|
||||||
|
@SerializedName("maxMemberLimit")
|
||||||
|
val maxMemberLimit: Int,
|
||||||
|
@SerializedName("canJoin")
|
||||||
|
val canJoin: Boolean,
|
||||||
|
@SerializedName("canJoinCode")
|
||||||
|
val canJoinCode: Int,
|
||||||
|
@SerializedName("users")
|
||||||
|
val users: List<Users>
|
||||||
|
|
||||||
|
) {
|
||||||
|
fun toRoomtEntity(): RoomEntity {
|
||||||
|
return RoomEntity(
|
||||||
|
id= id,
|
||||||
|
name = name,
|
||||||
|
description = description ,
|
||||||
|
trtcRoomId = trtcRoomId,
|
||||||
|
trtcType = trtcType,
|
||||||
|
cover = cover,
|
||||||
|
avatar = avatar,
|
||||||
|
recommendBanner = recommendBanner,
|
||||||
|
isRecommended = isRecommended,
|
||||||
|
allowInHot = allowInHot,
|
||||||
|
creator = creator.toCreatorEntity(),
|
||||||
|
userCount = userCount,
|
||||||
|
maxMemberLimit = maxMemberLimit,
|
||||||
|
canJoin = canJoin,
|
||||||
|
canJoinCode = canJoinCode,
|
||||||
|
users = users.map { it.toUsersEntity() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class Creator(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("userId")
|
||||||
|
val userId: String,
|
||||||
|
@SerializedName("trtcUserId")
|
||||||
|
val trtcUserId: String,
|
||||||
|
@SerializedName("profile")
|
||||||
|
val profile: Profile
|
||||||
|
){
|
||||||
|
fun toCreatorEntity(): CreatorEntity {
|
||||||
|
return CreatorEntity(
|
||||||
|
id = id,
|
||||||
|
userId = userId,
|
||||||
|
trtcUserId = trtcUserId,
|
||||||
|
profile = profile.toProfileEntity()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Users(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("userId")
|
||||||
|
val userId: String,
|
||||||
|
@SerializedName("trtcUserId")
|
||||||
|
val trtcUserId: String,
|
||||||
|
@SerializedName("profile")
|
||||||
|
val profile: Profile
|
||||||
|
){
|
||||||
|
fun toUsersEntity(): UsersEntity {
|
||||||
|
return UsersEntity(
|
||||||
|
id = id,
|
||||||
|
userId = userId,
|
||||||
|
profile = profile.toProfileEntity()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package com.aiosman.riderpro.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.riderpro.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
|
||||||
data class UserAuth(
|
data class UserAuth(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val token: String? = null
|
val token: String? = null,
|
||||||
|
val isGuest: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +49,22 @@ interface UserService {
|
|||||||
followingId: Int? = null
|
followingId: Int? = null
|
||||||
): ListContainer<AccountProfileEntity>
|
): ListContainer<AccountProfileEntity>
|
||||||
|
|
||||||
suspend fun getUserProfileByTrtcUserId(id: String):AccountProfileEntity
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param id 用户ID
|
||||||
|
* @return 用户信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
suspend fun getUserProfileByTrtcUserId(id: String,includeAI: Int):AccountProfileEntity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param id 用户ID
|
||||||
|
* @return 用户信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
suspend fun getUserProfileByOpenId(id: String):AccountProfileEntity
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +108,14 @@ class UserServiceImpl : UserService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUserProfileByTrtcUserId(id: String): AccountProfileEntity {
|
override suspend fun getUserProfileByTrtcUserId(id: String,includeAI: Int): AccountProfileEntity {
|
||||||
val resp = ApiClient.api.getAccountProfileByTrtcUserId(id)
|
val resp = ApiClient.api.getAccountProfileByTrtcUserId(id,includeAI)
|
||||||
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
|
return body.data.toAccountProfileEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUserProfileByOpenId(id: String): AccountProfileEntity {
|
||||||
|
val resp = ApiClient.api.getAccountProfileByOpenId(id)
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
return body.data.toAccountProfileEntity()
|
return body.data.toAccountProfileEntity()
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.aiosman.riderpro.data.api
|
package com.aiosman.ravenow.data.api
|
||||||
|
|
||||||
import android.icu.text.SimpleDateFormat
|
import android.icu.text.SimpleDateFormat
|
||||||
import android.icu.util.TimeZone
|
import android.icu.util.TimeZone
|
||||||
import com.aiosman.riderpro.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.riderpro.ConstVars
|
import com.aiosman.ravenow.ConstVars
|
||||||
import com.auth0.android.jwt.JWT
|
import com.auth0.android.jwt.JWT
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
@@ -11,54 +11,24 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import java.security.cert.CertificateException
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.net.ssl.SSLContext
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.net.ssl.TrustManager
|
|
||||||
import javax.net.ssl.X509TrustManager
|
|
||||||
|
|
||||||
fun getUnsafeOkHttpClient(
|
fun getSafeOkHttpClient(
|
||||||
authInterceptor: AuthInterceptor? = null
|
authInterceptor: AuthInterceptor? = null,
|
||||||
|
timeoutSeconds: Long = 30
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
return try {
|
return OkHttpClient.Builder()
|
||||||
// Create a trust manager that does not validate certificate chains
|
|
||||||
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
|
|
||||||
@Throws(CertificateException::class)
|
|
||||||
override fun checkClientTrusted(
|
|
||||||
chain: Array<java.security.cert.X509Certificate>,
|
|
||||||
authType: String
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(CertificateException::class)
|
|
||||||
override fun checkServerTrusted(
|
|
||||||
chain: Array<java.security.cert.X509Certificate>,
|
|
||||||
authType: String
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> = arrayOf()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Install the all-trusting trust manager
|
|
||||||
val sslContext = SSLContext.getInstance("SSL")
|
|
||||||
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
|
|
||||||
// Create an ssl socket factory with our all-trusting manager
|
|
||||||
val sslSocketFactory = sslContext.socketFactory
|
|
||||||
|
|
||||||
OkHttpClient.Builder()
|
|
||||||
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
|
|
||||||
.hostnameVerifier { _, _ -> true }
|
|
||||||
.apply {
|
.apply {
|
||||||
authInterceptor?.let {
|
authInterceptor?.let {
|
||||||
addInterceptor(it)
|
addInterceptor(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
} catch (e: Exception) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthInterceptor() : Interceptor {
|
class AuthInterceptor() : Interceptor {
|
||||||
@@ -81,6 +51,7 @@ class AuthInterceptor() : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestBuilder.addHeader("Authorization", "Bearer ${AppStore.token}")
|
requestBuilder.addHeader("Authorization", "Bearer ${AppStore.token}")
|
||||||
|
requestBuilder.addHeader("DEVICE-OS", "Android")
|
||||||
|
|
||||||
val response = chain.proceed(requestBuilder.build())
|
val response = chain.proceed(requestBuilder.build())
|
||||||
return response
|
return response
|
||||||
@@ -90,9 +61,9 @@ class AuthInterceptor() : Interceptor {
|
|||||||
val client = Retrofit.Builder()
|
val client = Retrofit.Builder()
|
||||||
.baseUrl(ApiClient.RETROFIT_URL)
|
.baseUrl(ApiClient.RETROFIT_URL)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.client(getUnsafeOkHttpClient())
|
.client(getSafeOkHttpClient(timeoutSeconds = 30))
|
||||||
.build()
|
.build()
|
||||||
.create(RiderProAPI::class.java)
|
.create(RaveNowAPI::class.java)
|
||||||
|
|
||||||
val resp = client.refreshToken(AppStore.token ?: "")
|
val resp = client.refreshToken(AppStore.token ?: "")
|
||||||
val newToken = resp.body()?.token
|
val newToken = resp.body()?.token
|
||||||
@@ -104,12 +75,15 @@ class AuthInterceptor() : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object ApiClient {
|
object ApiClient {
|
||||||
const val BASE_SERVER = ConstVars.BASE_SERVER
|
val BASE_SERVER = ConstVars.BASE_SERVER
|
||||||
const val BASE_API_URL = "${BASE_SERVER}/api/v1"
|
val BASE_API_URL = "${BASE_SERVER}/api/v1"
|
||||||
const val RETROFIT_URL = "${BASE_API_URL}/"
|
val RETROFIT_URL = "${BASE_API_URL}/"
|
||||||
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
|
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
|
||||||
private val okHttpClient: OkHttpClient by lazy {
|
private val okHttpClient: OkHttpClient by lazy {
|
||||||
getUnsafeOkHttpClient(authInterceptor = AuthInterceptor())
|
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 30)
|
||||||
|
}
|
||||||
|
private val longTimeoutOkHttpClient: OkHttpClient by lazy {
|
||||||
|
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 120)
|
||||||
}
|
}
|
||||||
private val retrofit: Retrofit by lazy {
|
private val retrofit: Retrofit by lazy {
|
||||||
Retrofit.Builder()
|
Retrofit.Builder()
|
||||||
@@ -118,8 +92,18 @@ object ApiClient {
|
|||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
val api: RiderProAPI by lazy {
|
private val longTimeoutRetrofit: Retrofit by lazy {
|
||||||
retrofit.create(RiderProAPI::class.java)
|
Retrofit.Builder()
|
||||||
|
.baseUrl(RETROFIT_URL)
|
||||||
|
.client(longTimeoutOkHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
val api: RaveNowAPI by lazy {
|
||||||
|
retrofit.create(RaveNowAPI::class.java)
|
||||||
|
}
|
||||||
|
val longTimeoutApi: RaveNowAPI by lazy {
|
||||||
|
longTimeoutRetrofit.create(RaveNowAPI::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatTime(date: Date): String {
|
fun formatTime(date: Date): String {
|
||||||
42
app/src/main/java/com/aiosman/ravenow/data/api/Error.kt
Normal file
42
app/src/main/java/com/aiosman/ravenow/data/api/Error.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package com.aiosman.ravenow.data.api
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
|
||||||
|
//
|
||||||
|
enum class ErrorCode(val code: Int) {
|
||||||
|
USER_EXIST(40001),
|
||||||
|
USER_NOT_EXIST(40002),
|
||||||
|
InvalidateCaptcha(40004),
|
||||||
|
IncorrectOldPassword(40005),
|
||||||
|
// 未知错误
|
||||||
|
UNKNOWN(99999)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ErrorCode.toErrorMessage(context: Context): String {
|
||||||
|
return context.getErrorMessageCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ErrorCode.showToast(context: Context) {
|
||||||
|
Toast.makeText(context, toErrorMessage(context), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// code to ErrorCode
|
||||||
|
fun Int.toErrorCode(): ErrorCode {
|
||||||
|
return when (this) {
|
||||||
|
40001 -> ErrorCode.USER_EXIST
|
||||||
|
40002 -> ErrorCode.USER_NOT_EXIST
|
||||||
|
40004 -> ErrorCode.InvalidateCaptcha
|
||||||
|
40005 -> ErrorCode.IncorrectOldPassword
|
||||||
|
else -> ErrorCode.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getErrorMessageCode(code: Int?): String {
|
||||||
|
return when (code) {
|
||||||
|
40001 -> getString(R.string.error_10001_user_exist)
|
||||||
|
ErrorCode.IncorrectOldPassword.code -> getString(R.string.error_incorrect_old_password)
|
||||||
|
else -> getString(R.string.error_unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
package com.aiosman.riderpro.data.api
|
package com.aiosman.ravenow.data.api
|
||||||
|
|
||||||
import com.aiosman.riderpro.data.AccountFavourite
|
import com.aiosman.ravenow.data.AccountFavourite
|
||||||
import com.aiosman.riderpro.data.AccountFollow
|
import com.aiosman.ravenow.data.AccountFollow
|
||||||
import com.aiosman.riderpro.data.AccountLike
|
import com.aiosman.ravenow.data.AccountLike
|
||||||
import com.aiosman.riderpro.data.AccountNotice
|
import com.aiosman.ravenow.data.AccountNotice
|
||||||
import com.aiosman.riderpro.data.AccountProfile
|
import com.aiosman.ravenow.data.AccountProfile
|
||||||
import com.aiosman.riderpro.data.Comment
|
import com.aiosman.ravenow.data.Agent
|
||||||
import com.aiosman.riderpro.data.DataContainer
|
import com.aiosman.ravenow.data.Category
|
||||||
import com.aiosman.riderpro.data.ListContainer
|
import com.aiosman.ravenow.data.Comment
|
||||||
import com.aiosman.riderpro.data.Moment
|
import com.aiosman.ravenow.data.DataContainer
|
||||||
import com.aiosman.riderpro.entity.ChatNotification
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
import com.aiosman.ravenow.data.Moment
|
||||||
|
import com.aiosman.ravenow.data.Room
|
||||||
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
|
import com.aiosman.ravenow.data.membership.MembershipConfigData
|
||||||
|
import com.aiosman.ravenow.data.membership.ValidateData
|
||||||
|
import com.aiosman.ravenow.data.membership.ValidateProductRequestBody
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
@@ -30,6 +36,68 @@ data class RegisterRequestBody(
|
|||||||
@SerializedName("password")
|
@SerializedName("password")
|
||||||
val password: String
|
val password: String
|
||||||
)
|
)
|
||||||
|
data class AgentMomentRequestBody(
|
||||||
|
@SerializedName("generateText")
|
||||||
|
val generateText: String,
|
||||||
|
@SerializedName("sessionId")
|
||||||
|
val sessionId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GenerateAgentInfoRequestBody(
|
||||||
|
@SerializedName("descriptionText")
|
||||||
|
val descriptionText: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GenerateAgentInfoResponseBody(
|
||||||
|
@SerializedName("title")
|
||||||
|
val title: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String,
|
||||||
|
@SerializedName("content")
|
||||||
|
val content: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SingleChatRequestBody(
|
||||||
|
@SerializedName("agentOpenId")
|
||||||
|
val agentOpenId: String? = null,
|
||||||
|
@SerializedName("agentTrtcId")
|
||||||
|
val agentTrtcId: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GroupChatRequestBody(
|
||||||
|
@SerializedName("trtcGroupId")
|
||||||
|
val trtcGroupId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class SendChatAiRequestBody(
|
||||||
|
@SerializedName("trtcGroupId")
|
||||||
|
val trtcGroupId: String? = null,
|
||||||
|
@SerializedName("fromTrtcUserId")
|
||||||
|
val fromTrtcUserId: String? = null,
|
||||||
|
@SerializedName("toTrtcUserId")
|
||||||
|
val toTrtcUserId: String? = null,
|
||||||
|
@SerializedName("message")
|
||||||
|
val message: String,
|
||||||
|
@SerializedName("skipTrtc")
|
||||||
|
val skipTrtc: Boolean? = true,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class CreateGroupChatRequestBody(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("userIds")
|
||||||
|
val userIds: List<String>,
|
||||||
|
@SerializedName("promptIds")
|
||||||
|
val promptIds: List<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class JoinGroupChatRequestBody(
|
||||||
|
@SerializedName("trtcId")
|
||||||
|
val trtcId: String,
|
||||||
|
)
|
||||||
|
|
||||||
data class LoginUserRequestBody(
|
data class LoginUserRequestBody(
|
||||||
@SerializedName("username")
|
@SerializedName("username")
|
||||||
@@ -42,6 +110,19 @@ data class LoginUserRequestBody(
|
|||||||
val captcha: CaptchaInfo? = null,
|
val captcha: CaptchaInfo? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class GuestLoginRequestBody(
|
||||||
|
@SerializedName("deviceID")
|
||||||
|
val deviceId: String,
|
||||||
|
@SerializedName("platform")
|
||||||
|
val platform: String = "android",
|
||||||
|
@SerializedName("deviceInfo")
|
||||||
|
val deviceInfo: String? = null,
|
||||||
|
@SerializedName("userAgent")
|
||||||
|
val userAgent: String? = null,
|
||||||
|
@SerializedName("ipAddress")
|
||||||
|
val ipAddress: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class GoogleRegisterRequestBody(
|
data class GoogleRegisterRequestBody(
|
||||||
@SerializedName("idToken")
|
@SerializedName("idToken")
|
||||||
val idToken: String
|
val idToken: String
|
||||||
@@ -131,7 +212,7 @@ data class DictItem(
|
|||||||
@SerializedName("key")
|
@SerializedName("key")
|
||||||
val key: String,
|
val key: String,
|
||||||
@SerializedName("value")
|
@SerializedName("value")
|
||||||
val value: String,
|
val value: Any,
|
||||||
@SerializedName("desc")
|
@SerializedName("desc")
|
||||||
val desc: String,
|
val desc: String,
|
||||||
)
|
)
|
||||||
@@ -186,13 +267,44 @@ data class UpdateChatNotificationRequestBody(
|
|||||||
val strategy: String,
|
val strategy: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface RiderProAPI {
|
|
||||||
|
data class CreateReportRequestBody(
|
||||||
|
@SerializedName("reportType")
|
||||||
|
val reportType: String,
|
||||||
|
@SerializedName("reportId")
|
||||||
|
val reportId: Int,
|
||||||
|
@SerializedName("reason")
|
||||||
|
val reason: Int,
|
||||||
|
@SerializedName("extra")
|
||||||
|
val extra: String,
|
||||||
|
@SerializedName("base64Images")
|
||||||
|
val base64Images: List<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RemoveAccountRequestBody(
|
||||||
|
@SerializedName("password")
|
||||||
|
val password: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface RaveNowAPI {
|
||||||
|
@GET("membership/config")
|
||||||
|
@retrofit2.http.Headers("X-Requires-Auth: true")
|
||||||
|
suspend fun getMembershipConfig(): Response<DataContainer<MembershipConfigData>>
|
||||||
|
|
||||||
|
@POST("membership/android/product/validate")
|
||||||
|
@retrofit2.http.Headers("X-Requires-Auth: true")
|
||||||
|
suspend fun validateAndroidProduct(
|
||||||
|
@Body body: ValidateProductRequestBody
|
||||||
|
): Response<DataContainer<ValidateData>>
|
||||||
@POST("register")
|
@POST("register")
|
||||||
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
|
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
|
||||||
|
|
||||||
@POST("login")
|
@POST("login")
|
||||||
suspend fun login(@Body body: LoginUserRequestBody): Response<AuthResult>
|
suspend fun login(@Body body: LoginUserRequestBody): Response<AuthResult>
|
||||||
|
|
||||||
|
@POST("guest/login")
|
||||||
|
suspend fun guestLogin(@Body body: GuestLoginRequestBody): Response<AuthResult>
|
||||||
|
|
||||||
@GET("auth/token")
|
@GET("auth/token")
|
||||||
suspend fun checkToken(): Response<ValidateTokenResult>
|
suspend fun checkToken(): Response<ValidateTokenResult>
|
||||||
|
|
||||||
@@ -205,12 +317,23 @@ interface RiderProAPI {
|
|||||||
suspend fun getPosts(
|
suspend fun getPosts(
|
||||||
@Query("page") page: Int = 1,
|
@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("id") postId: Int? = null,
|
||||||
@Query("timelineId") timelineId: Int? = null,
|
@Query("timelineId") timelineId: Int? = null,
|
||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
@Query("contentSearch") contentSearch: String? = null,
|
@Query("contentSearch") contentSearch: String? = null,
|
||||||
@Query("postUser") postUser: Int? = null,
|
@Query("postUser") postUser: Int? = null,
|
||||||
@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("imageTag") imageTag: String? = null,
|
||||||
|
@Query("search") search: String? = null,
|
||||||
|
@Query("advancedSearch") advancedSearch: String? = null,
|
||||||
|
@Query("newsFilter") newsFilter: String? = null,
|
||||||
|
@Query("onlyNews") onlyNews: Boolean? = null,
|
||||||
|
@Query("newsSource") newsSource: String? = null,
|
||||||
|
@Query("newsLanguage") newsLanguage: String? = null,
|
||||||
|
@Query("newsCategory") newsCategory: String? = null,
|
||||||
|
@Query("requireImageCache") requireImageCache: Boolean? = null,
|
||||||
): Response<ListContainer<Moment>>
|
): Response<ListContainer<Moment>>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@@ -338,6 +461,13 @@ interface RiderProAPI {
|
|||||||
|
|
||||||
@GET("profile/trtc/{id}")
|
@GET("profile/trtc/{id}")
|
||||||
suspend fun getAccountProfileByTrtcUserId(
|
suspend fun getAccountProfileByTrtcUserId(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("includeAI") includeAI: Int
|
||||||
|
): Response<DataContainer<AccountProfile>>
|
||||||
|
|
||||||
|
|
||||||
|
@GET("profile/aichat/profile/{id}")
|
||||||
|
suspend fun getAccountProfileByOpenId(
|
||||||
@Path("id") id: String
|
@Path("id") id: String
|
||||||
): Response<DataContainer<AccountProfile>>
|
): Response<DataContainer<AccountProfile>>
|
||||||
|
|
||||||
@@ -358,6 +488,8 @@ interface RiderProAPI {
|
|||||||
@Query("nickname") search: String? = null,
|
@Query("nickname") search: String? = null,
|
||||||
@Query("followerId") followerId: Int? = null,
|
@Query("followerId") followerId: Int? = null,
|
||||||
@Query("followingId") followingId: Int? = null,
|
@Query("followingId") followingId: Int? = null,
|
||||||
|
@Query("includeAI") includeAI: Boolean? = false,
|
||||||
|
@Query("chatSessionIdNotNull") chatSessionIdNotNull: Boolean? = true,
|
||||||
): Response<ListContainer<AccountProfile>>
|
): Response<ListContainer<AccountProfile>>
|
||||||
|
|
||||||
@POST("register/google")
|
@POST("register/google")
|
||||||
@@ -399,6 +531,11 @@ interface RiderProAPI {
|
|||||||
@Query("key") key: String
|
@Query("key") key: String
|
||||||
): Response<DataContainer<DictItem>>
|
): Response<DataContainer<DictItem>>
|
||||||
|
|
||||||
|
@GET("dicts")
|
||||||
|
suspend fun getDicts(
|
||||||
|
@Query("keys") keys: String
|
||||||
|
): Response<ListContainer<DictItem>>
|
||||||
|
|
||||||
@POST("captcha/generate")
|
@POST("captcha/generate")
|
||||||
suspend fun generateCaptcha(
|
suspend fun generateCaptcha(
|
||||||
@Body body: CaptchaRequestBody
|
@Body body: CaptchaRequestBody
|
||||||
@@ -423,5 +560,106 @@ interface RiderProAPI {
|
|||||||
suspend fun updateChatNotification(
|
suspend fun updateChatNotification(
|
||||||
@Body body: UpdateChatNotificationRequestBody
|
@Body body: UpdateChatNotificationRequestBody
|
||||||
): Response<DataContainer<ChatNotification>>
|
): Response<DataContainer<ChatNotification>>
|
||||||
|
|
||||||
|
@POST("reports")
|
||||||
|
suspend fun createReport(
|
||||||
|
@Body body: CreateReportRequestBody
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
@POST("account/my/delete")
|
||||||
|
suspend fun deleteAccount(
|
||||||
|
@Body body: RemoveAccountRequestBody
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@GET("outside/prompts")
|
||||||
|
suspend fun getAgent(
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("order") order: String? = null,
|
||||||
|
@Query("orderKey") orderKey: String? = null,
|
||||||
|
@Query("createdAt") createdAt: String? = null,
|
||||||
|
@Query("updatedAt") updatedAt: String? = null,
|
||||||
|
@Query("createdStart") createdStart: String? = null,
|
||||||
|
@Query("createdEnd") createdEnd: String? = null,
|
||||||
|
@Query("updatedStart") updatedStart: String? = null,
|
||||||
|
@Query("updatedEnd") updatedEnd: String? = null,
|
||||||
|
@Query("title") title: String? = null,
|
||||||
|
@Query("authorId") authorId: Int? = null,
|
||||||
|
@Query("authorOpenId") authorOpenId: String? = null,
|
||||||
|
@Query("showPrivate") showPrivate: String? = null,
|
||||||
|
@Query("explore") explore: String? = null,
|
||||||
|
@Query("desc") desc: String? = null,
|
||||||
|
@Query("withWorkflow") withWorkflow: String? = null,
|
||||||
|
@Query("hasAvatar") hasAvatar: String? = null,
|
||||||
|
@Query("random") random: String? = null,
|
||||||
|
@Query("categoryName") categoryName: String? = null,
|
||||||
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
|
@Query("uncategorized") uncategorized: String? = null,
|
||||||
|
): Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
|
@GET("outside/my/prompts")
|
||||||
|
suspend fun getMyAgent(
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("withWorkflow") withWorkflow: String = "1",
|
||||||
|
): Response<ListContainer<Agent>>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("outside/prompts")
|
||||||
|
suspend fun createAgent(
|
||||||
|
@Part avatar: MultipartBody.Part?,
|
||||||
|
@Part("title") title: RequestBody?,
|
||||||
|
@Part("value") value: RequestBody?,
|
||||||
|
@Part("desc") desc: RequestBody?,
|
||||||
|
@Part("workflowId") workflowId: RequestBody?,
|
||||||
|
@Part("public") isPublic: RequestBody?,
|
||||||
|
@Part("breakMode") breakMode: RequestBody?,
|
||||||
|
@Part("useWorkflow") useWorkflow: RequestBody?,
|
||||||
|
@Part("workflowInputs") workflowInputs: RequestBody?,
|
||||||
|
): Response<DataContainer<Agent>>
|
||||||
|
|
||||||
|
|
||||||
|
@POST("generate/postText")
|
||||||
|
suspend fun agentMoment(@Body body: AgentMomentRequestBody): Response<DataContainer<String>>
|
||||||
|
|
||||||
|
@GET("outside/rooms/open")
|
||||||
|
suspend fun createGroupChatAi(@Query("trtcGroupId") trtcGroupId: String): Response<DataContainer<Unit>>
|
||||||
|
|
||||||
|
@POST("outside/rooms/create-single-chat")
|
||||||
|
suspend fun createSingleChat(@Body body: SingleChatRequestBody): Response<DataContainer<Unit>>
|
||||||
|
|
||||||
|
@POST("outside/rooms/message")
|
||||||
|
suspend fun sendChatAiMessage(@Body body: SendChatAiRequestBody): Response<DataContainer<Unit>>
|
||||||
|
|
||||||
|
@POST("outside/rooms")
|
||||||
|
suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response<DataContainer<Unit>>
|
||||||
|
|
||||||
|
@GET("outside/rooms")
|
||||||
|
suspend fun getRooms(@Query("page") page: Int = 1,
|
||||||
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("isRecommended") isRecommended: Int = 1,
|
||||||
|
@Query("random") random: Int? = null
|
||||||
|
): Response<ListContainer<Room>>
|
||||||
|
|
||||||
|
@GET("outside/rooms/detail")
|
||||||
|
suspend fun getRoomDetail(@Query("trtcId") trtcId: String,
|
||||||
|
): Response<DataContainer<Room>>
|
||||||
|
|
||||||
|
@POST("outside/rooms/join")
|
||||||
|
suspend fun joinRoom(@Body body: JoinGroupChatRequestBody,
|
||||||
|
): Response<DataContainer<Room>>
|
||||||
|
|
||||||
|
@POST("outside/generate/agent-info")
|
||||||
|
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
|
||||||
|
|
||||||
|
@GET("outside/categories")
|
||||||
|
suspend fun getCategories(
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("lang") lang: String? = null
|
||||||
|
): Response<ListContainer<Category>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package com.aiosman.ravenow.data.membership
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class MembershipConfigData(
|
||||||
|
@SerializedName("id") val id: Int,
|
||||||
|
@SerializedName("name") val name: String,
|
||||||
|
@SerializedName("description") val description: String,
|
||||||
|
@SerializedName("version") val version: String,
|
||||||
|
@SerializedName("config_data") val configData: ConfigData,
|
||||||
|
@SerializedName("createdAt") val createdAt: String,
|
||||||
|
@SerializedName("updatedAt") val updatedAt: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ConfigData(
|
||||||
|
@SerializedName("members") val members: List<Member>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Member(
|
||||||
|
@SerializedName("name") val name: String,
|
||||||
|
@SerializedName("benefits") val benefits: List<Benefit>,
|
||||||
|
@SerializedName("goods") val goods: List<Good>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Benefit(
|
||||||
|
@SerializedName("level") val level: Int,
|
||||||
|
@SerializedName("name") val name: String,
|
||||||
|
@SerializedName("value") val value: JsonElement,
|
||||||
|
@SerializedName("order") val order: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Good(
|
||||||
|
@SerializedName("description") val description: String,
|
||||||
|
@SerializedName("discount") val discount: Double,
|
||||||
|
@SerializedName("goods_id") val goodsId: String,
|
||||||
|
@SerializedName("originalPrice") val originalPrice: Double,
|
||||||
|
@SerializedName("period") val period: String,
|
||||||
|
@SerializedName("price") val price: Double,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ValidateProductRequestBody(
|
||||||
|
@SerializedName("plan_id") val planId: String,
|
||||||
|
@SerializedName("product_id") val productId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ValidateData(
|
||||||
|
@SerializedName("isValid") val isValid: Boolean,
|
||||||
|
@SerializedName("mapped") val mapped: Boolean,
|
||||||
|
@SerializedName("hasUnfinishedOrder") val hasUnfinishedOrder: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VipPriceModel(
|
||||||
|
val title: String,
|
||||||
|
val proPrice: String,
|
||||||
|
val standardPrice: String,
|
||||||
|
val proDesc: String,
|
||||||
|
val standardDesc: String,
|
||||||
|
val id: String,
|
||||||
|
val proGoodsId: String?,
|
||||||
|
val standardGoodsId: String?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val MONTH_ID = "monthly"
|
||||||
|
const val YEAR_ID = "yearly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VipPageDataModel(
|
||||||
|
val title: String,
|
||||||
|
val proHave: Boolean?,
|
||||||
|
val proDesc: String,
|
||||||
|
val standardHave: Boolean?,
|
||||||
|
val standardDesc: String,
|
||||||
|
val freeHave: Boolean?,
|
||||||
|
val freeDesc: String,
|
||||||
|
val order: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
object VipModelMapper {
|
||||||
|
fun generatePageDataList(members: List<Member>): List<VipPageDataModel> {
|
||||||
|
if (members.size < 3) return emptyList()
|
||||||
|
val free = members[0]
|
||||||
|
val standard = members[1]
|
||||||
|
val pro = members[2]
|
||||||
|
|
||||||
|
val names = (members.flatMap { it.benefits.map { b -> b.name } }).toSet().sorted()
|
||||||
|
val list = names.map { name ->
|
||||||
|
val freeB = free.benefits.firstOrNull { it.name == name }
|
||||||
|
val stdB = standard.benefits.firstOrNull { it.name == name }
|
||||||
|
val proB = pro.benefits.firstOrNull { it.name == name }
|
||||||
|
|
||||||
|
val order = proB?.order ?: stdB?.order ?: freeB?.order ?: 0
|
||||||
|
VipPageDataModel(
|
||||||
|
title = name,
|
||||||
|
proHave = proB?.value?.asBooleanOrNull(),
|
||||||
|
proDesc = proB?.value?.asStringOrEmpty() ?: "",
|
||||||
|
standardHave = stdB?.value?.asBooleanOrNull(),
|
||||||
|
standardDesc = stdB?.value?.asStringOrEmpty() ?: "",
|
||||||
|
freeHave = freeB?.value?.asBooleanOrNull(),
|
||||||
|
freeDesc = freeB?.value?.asStringOrEmpty() ?: "",
|
||||||
|
order = order,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return list.sortedBy { it.order }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generatePriceDataList(members: List<Member>): List<VipPriceModel> {
|
||||||
|
if (members.size < 3) return emptyList()
|
||||||
|
val standard = members[1]
|
||||||
|
val pro = members[2]
|
||||||
|
|
||||||
|
val list = mutableListOf<VipPriceModel>()
|
||||||
|
// 首月(示例:如果后端 period = "first_month")
|
||||||
|
val stdFirst = standard.goods.firstOrNull { it.period == "first_month" }
|
||||||
|
val proFirst = pro.goods.firstOrNull { it.period == "first_month" }
|
||||||
|
if (stdFirst != null && proFirst != null) {
|
||||||
|
list.add(
|
||||||
|
VipPriceModel(
|
||||||
|
title = "首月",
|
||||||
|
proPrice = proFirst.price.toInt().toString(),
|
||||||
|
standardPrice = stdFirst.price.toInt().toString(),
|
||||||
|
proDesc = proFirst.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||||
|
standardDesc = stdFirst.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||||
|
id = "first_month",
|
||||||
|
proGoodsId = proFirst.goodsId,
|
||||||
|
standardGoodsId = stdFirst.goodsId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val stdMonth = standard.goods.firstOrNull { it.period == "month" }
|
||||||
|
val proMonth = pro.goods.firstOrNull { it.period == "month" }
|
||||||
|
if (stdMonth != null && proMonth != null) {
|
||||||
|
list.add(
|
||||||
|
VipPriceModel(
|
||||||
|
title = "月付",
|
||||||
|
proPrice = proMonth.price.toInt().toString(),
|
||||||
|
standardPrice = stdMonth.price.toInt().toString(),
|
||||||
|
proDesc = proMonth.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||||
|
standardDesc = stdMonth.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||||
|
id = VipPriceModel.MONTH_ID,
|
||||||
|
proGoodsId = proMonth.goodsId,
|
||||||
|
standardGoodsId = stdMonth.goodsId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 半年
|
||||||
|
val stdHalf = standard.goods.firstOrNull { it.period == "half_year" }
|
||||||
|
val proHalf = pro.goods.firstOrNull { it.period == "half_year" }
|
||||||
|
if (stdHalf != null && proHalf != null) {
|
||||||
|
list.add(
|
||||||
|
VipPriceModel(
|
||||||
|
title = "半年",
|
||||||
|
proPrice = proHalf.price.toInt().toString(),
|
||||||
|
standardPrice = stdHalf.price.toInt().toString(),
|
||||||
|
proDesc = proHalf.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||||
|
standardDesc = stdHalf.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||||
|
id = "half_year",
|
||||||
|
proGoodsId = proHalf.goodsId,
|
||||||
|
standardGoodsId = stdHalf.goodsId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val stdYear = standard.goods.firstOrNull { it.period == "year" }
|
||||||
|
val proYear = pro.goods.firstOrNull { it.period == "year" }
|
||||||
|
if (stdYear != null && proYear != null) {
|
||||||
|
list.add(
|
||||||
|
VipPriceModel(
|
||||||
|
title = "每年",
|
||||||
|
proPrice = proYear.price.toInt().toString(),
|
||||||
|
standardPrice = stdYear.price.toInt().toString(),
|
||||||
|
proDesc = proYear.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||||
|
standardDesc = stdYear.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||||
|
id = VipPriceModel.YEAR_ID,
|
||||||
|
proGoodsId = proYear.goodsId,
|
||||||
|
standardGoodsId = stdYear.goodsId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonElement.asStringOrEmpty(): String {
|
||||||
|
return try { if (isJsonPrimitive && asJsonPrimitive.isString) asString else "" } catch (_: Exception) { "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonElement.asBooleanOrNull(): Boolean? {
|
||||||
|
return try { if (isJsonPrimitive && asJsonPrimitive.isBoolean) asBoolean else null } catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.aiosman.riderpro.entity
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.aiosman.riderpro.data.AccountFollow
|
import com.aiosman.ravenow.data.AccountFollow
|
||||||
import com.aiosman.riderpro.data.AccountService
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.riderpro.data.Image
|
import com.aiosman.ravenow.data.Image
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@@ -61,6 +61,12 @@ data class AccountProfileEntity(
|
|||||||
val banner: String?,
|
val banner: String?,
|
||||||
// trtcUserId
|
// trtcUserId
|
||||||
val trtcUserId: String,
|
val trtcUserId: String,
|
||||||
|
val chatToken: String?,
|
||||||
|
|
||||||
|
val aiAccount: Boolean,
|
||||||
|
val rawAvatar: String,
|
||||||
|
|
||||||
|
val chatAIId: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
263
app/src/main/java/com/aiosman/ravenow/entity/Agent.kt
Normal file
263
app/src/main/java/com/aiosman/ravenow/entity/Agent.kt
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.data.Agent
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
import com.aiosman.ravenow.data.AgentService
|
||||||
|
import com.aiosman.ravenow.data.DataContainer
|
||||||
|
import com.aiosman.ravenow.data.MomentService
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.data.UploadImage
|
||||||
|
import com.aiosman.ravenow.data.UserService
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import retrofit2.http.Part
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体
|
||||||
|
*/
|
||||||
|
suspend fun createAgent(
|
||||||
|
title: String,
|
||||||
|
desc: String,
|
||||||
|
avatar: UploadImage? = null,
|
||||||
|
workflowId: Int = 1,
|
||||||
|
isPublic: Boolean = true,
|
||||||
|
breakMode: Boolean = false,
|
||||||
|
useWorkflow: Boolean = true,
|
||||||
|
): AgentEntity {
|
||||||
|
val textTitle = title.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val textDesc = desc.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val workflowIdRequestBody =
|
||||||
|
workflowId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val isPublicRequestBody = isPublic.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val breakModeRequestBody = breakMode.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val useWorkflowRequestBody =
|
||||||
|
useWorkflow.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val workflowInputsValue = "{\"si\":\"$desc\"}"
|
||||||
|
val workflowInputsRequestBody =
|
||||||
|
workflowInputsValue.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
|
||||||
|
val avatarField: MultipartBody.Part? = avatar?.let {
|
||||||
|
createMultipartBody(it.file, it.filename, "avatar")
|
||||||
|
}
|
||||||
|
val response = ApiClient.api.createAgent(
|
||||||
|
avatarField,
|
||||||
|
textTitle,
|
||||||
|
textDesc,
|
||||||
|
textDesc,
|
||||||
|
workflowIdRequestBody,
|
||||||
|
isPublicRequestBody,
|
||||||
|
breakModeRequestBody,
|
||||||
|
useWorkflowRequestBody,
|
||||||
|
workflowInputsRequestBody
|
||||||
|
)
|
||||||
|
val body = response.body()?.data ?: throw ServiceException("Failed to create agent")
|
||||||
|
return body.toAgentEntity()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体信息分页加载器
|
||||||
|
*/
|
||||||
|
class AgentPagingSource(
|
||||||
|
private val agentRemoteDataSource: AgentRemoteDataSource,
|
||||||
|
private val authorId: Int? = null
|
||||||
|
) : PagingSource<Int, AgentEntity>() {
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AgentEntity> {
|
||||||
|
return try {
|
||||||
|
val currentPage = params.key ?: 1
|
||||||
|
val users = agentRemoteDataSource.getAgent(
|
||||||
|
pageNumber = currentPage,
|
||||||
|
authorId = authorId
|
||||||
|
)
|
||||||
|
LoadResult.Page(
|
||||||
|
data = users?.list ?: listOf(),
|
||||||
|
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||||
|
nextKey = if (users?.list?.isNotEmpty() == true) users.page + 1 else null
|
||||||
|
)
|
||||||
|
} catch (exception: IOException) {
|
||||||
|
return LoadResult.Error(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, AgentEntity>): Int? {
|
||||||
|
return state.anchorPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AgentRemoteDataSource(
|
||||||
|
private val agentService: AgentService,
|
||||||
|
) {
|
||||||
|
suspend fun getAgent(
|
||||||
|
pageNumber: Int,
|
||||||
|
authorId: Int? = null
|
||||||
|
): ListContainer<AgentEntity>? {
|
||||||
|
return agentService.getAgent(
|
||||||
|
pageNumber = pageNumber,
|
||||||
|
authorId = authorId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AgentServiceImpl() : AgentService {
|
||||||
|
val agentBackend = AgentBackend()
|
||||||
|
|
||||||
|
override suspend fun getAgent(
|
||||||
|
pageNumber: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
authorId: Int?
|
||||||
|
): ListContainer<AgentEntity>? {
|
||||||
|
return agentBackend.getAgent(
|
||||||
|
pageNumber = pageNumber,
|
||||||
|
authorId = authorId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AgentBackend {
|
||||||
|
val DataBatchSize = 20
|
||||||
|
suspend fun getAgent(
|
||||||
|
pageNumber: Int,
|
||||||
|
authorId: Int? = null
|
||||||
|
): ListContainer<AgentEntity>? {
|
||||||
|
// 如果是游客模式且获取我的Agent(authorId为null),返回空列表
|
||||||
|
if (authorId == null && AppStore.isGuest) {
|
||||||
|
return ListContainer(
|
||||||
|
total = 0,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
list = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val resp = if (authorId != null) {
|
||||||
|
ApiClient.api.getAgent(
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
authorId = authorId
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ApiClient.api.getMyAgent(
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = resp.body() ?: return null
|
||||||
|
|
||||||
|
// 处理不同的返回类型
|
||||||
|
return if (authorId != null) {
|
||||||
|
// getAgent 返回 DataContainer<ListContainer<Agent>>
|
||||||
|
val dataContainer =
|
||||||
|
body as com.aiosman.ravenow.data.DataContainer<com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>>
|
||||||
|
val listContainer = dataContainer.data
|
||||||
|
ListContainer(
|
||||||
|
total = listContainer.total,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
list = listContainer.list.map { it.toAgentEntity() }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// getMyAgent 返回 ListContainer<Agent>
|
||||||
|
val listContainer =
|
||||||
|
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>
|
||||||
|
ListContainer(
|
||||||
|
total = listContainer.total,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
list = listContainer.list.map { it.toAgentEntity() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AgentEntity(
|
||||||
|
//val author: String,
|
||||||
|
val avatar: String,
|
||||||
|
val breakMode: Boolean,
|
||||||
|
val createdAt: String,
|
||||||
|
val desc: String,
|
||||||
|
val id: Int,
|
||||||
|
val isPublic: Boolean,
|
||||||
|
val openId: String,
|
||||||
|
//val profile: ProfileEntity,
|
||||||
|
val title: String,
|
||||||
|
val updatedAt: String,
|
||||||
|
val useCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
fun createMultipartBody(file: File, filename: String, name: String): MultipartBody.Part {
|
||||||
|
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
|
||||||
|
return MultipartBody.Part.createFormData(name, filename, requestFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AgentLoaderExtraArgs(
|
||||||
|
val authorId: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
class AgentLoader : DataLoader<AgentEntity, AgentLoaderExtraArgs>() {
|
||||||
|
override suspend fun fetchData(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
extra: AgentLoaderExtraArgs
|
||||||
|
): ListContainer<AgentEntity> {
|
||||||
|
// 如果是游客模式且获取我的Agent(authorId为null),返回空列表
|
||||||
|
if (extra.authorId == null && AppStore.isGuest) {
|
||||||
|
return ListContainer(
|
||||||
|
total = 0,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
list = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = if (extra.authorId != null) {
|
||||||
|
ApiClient.api.getAgent(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
authorId = extra.authorId
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ApiClient.api.getMyAgent(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = result.body() ?: throw ServiceException("Failed to get agent")
|
||||||
|
|
||||||
|
return if (extra.authorId != null) {
|
||||||
|
// getAgent 返回 DataContainer<ListContainer<Agent>>
|
||||||
|
val dataContainer = body as DataContainer<ListContainer<Agent>>
|
||||||
|
val listContainer = dataContainer.data
|
||||||
|
ListContainer(
|
||||||
|
list = listContainer.list.map { it.toAgentEntity() },
|
||||||
|
total = listContainer.total,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// getMyAgent 返回 ListContainer<Agent>
|
||||||
|
val listContainer =
|
||||||
|
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>
|
||||||
|
ListContainer(
|
||||||
|
list = listContainer.list.map { it.toAgentEntity() },
|
||||||
|
total = listContainer.total,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
app/src/main/java/com/aiosman/ravenow/entity/Category.kt
Normal file
160
app/src/main/java/com/aiosman/ravenow/entity/Category.kt
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.data.Category
|
||||||
|
import com.aiosman.ravenow.data.CategoryTranslation
|
||||||
|
import com.aiosman.ravenow.data.CategoryTranslations
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.utils.Utils
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类实体类
|
||||||
|
*/
|
||||||
|
data class CategoryEntity(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val avatar: String,
|
||||||
|
val parentId: Int? = null,
|
||||||
|
val sort: Int,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val promptCount: Int,
|
||||||
|
val createdAt: String,
|
||||||
|
val updatedAt: String,
|
||||||
|
val translations: CategoryTranslations? = null
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* 获取本地化名称
|
||||||
|
*/
|
||||||
|
fun getLocalizedName(): String {
|
||||||
|
return when (Utils.getCurrentLanguage()) {
|
||||||
|
"ja" -> translations?.ja?.name ?: name
|
||||||
|
else -> name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本地化描述
|
||||||
|
*/
|
||||||
|
fun getLocalizedDescription(): String {
|
||||||
|
return when (Utils.getCurrentLanguage()) {
|
||||||
|
"ja" -> translations?.ja?.description ?: description
|
||||||
|
else -> description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category 数据模型扩展函数
|
||||||
|
*/
|
||||||
|
fun Category.toCategoryEntity(): CategoryEntity {
|
||||||
|
return CategoryEntity(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
avatar = if (avatar.isNotEmpty()) "${ApiClient.BASE_API_URL}/outside$avatar?token=${AppStore.token}" else "",
|
||||||
|
parentId = parentId,
|
||||||
|
sort = sort,
|
||||||
|
isActive = isActive,
|
||||||
|
promptCount = promptCount,
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
translations = translations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据后端服务
|
||||||
|
*/
|
||||||
|
class CategoryBackend {
|
||||||
|
private val DataBatchSize = 20
|
||||||
|
|
||||||
|
suspend fun getCategories(pageNumber: Int): ListContainer<CategoryEntity>? {
|
||||||
|
try {
|
||||||
|
val resp = ApiClient.api.getCategories(
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
lang = Utils.getCurrentLanguageTag()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!resp.isSuccessful) {
|
||||||
|
throw ServiceException("获取分类失败: ${resp.code()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = resp.body() ?: return null
|
||||||
|
|
||||||
|
return ListContainer(
|
||||||
|
total = body.total,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
list = body.list.map { it.toCategoryEntity() }
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ServiceException("网络请求失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据加载器参数
|
||||||
|
*/
|
||||||
|
class CategoryLoaderExtraArgs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据加载器
|
||||||
|
*/
|
||||||
|
class CategoryLoader : DataLoader<CategoryEntity, CategoryLoaderExtraArgs>() {
|
||||||
|
override suspend fun fetchData(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
extra: CategoryLoaderExtraArgs
|
||||||
|
): ListContainer<CategoryEntity> {
|
||||||
|
val backend = CategoryBackend()
|
||||||
|
return backend.getCategories(page) ?: ListContainer(
|
||||||
|
total = 0,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
list = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类分页数据源
|
||||||
|
*/
|
||||||
|
class CategoryPagingSource(
|
||||||
|
private val backend: CategoryBackend
|
||||||
|
) : PagingSource<Int, CategoryEntity>() {
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, CategoryEntity>): Int? {
|
||||||
|
return state.anchorPosition?.let { anchorPosition ->
|
||||||
|
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
||||||
|
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CategoryEntity> {
|
||||||
|
return try {
|
||||||
|
val page = params.key ?: 1
|
||||||
|
val response = backend.getCategories(page)
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
LoadResult.Error(IOException("获取分类数据失败"))
|
||||||
|
} else {
|
||||||
|
val hasMore = response.list.size == response.pageSize
|
||||||
|
LoadResult.Page(
|
||||||
|
data = response.list,
|
||||||
|
prevKey = if (page == 1) null else page - 1,
|
||||||
|
nextKey = if (hasMore) page + 1 else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
app/src/main/java/com/aiosman/ravenow/entity/Chat.kt
Normal file
114
app/src/main/java/com/aiosman/ravenow/entity/Chat.kt
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.icu.util.Calendar
|
||||||
|
import com.aiosman.ravenow.ConstVars
|
||||||
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import io.openim.android.sdk.models.Message
|
||||||
|
import io.openim.android.sdk.models.PictureElem
|
||||||
|
|
||||||
|
data class ChatItem(
|
||||||
|
val message: String,
|
||||||
|
val avatar: String,
|
||||||
|
val time: String,
|
||||||
|
val userId: String,
|
||||||
|
val nickname: String,
|
||||||
|
val timeCategory: String = "",
|
||||||
|
val timestamp: Long = 0,
|
||||||
|
val imageList: MutableList<PictureInfo> = emptyList<PictureInfo>().toMutableList(),
|
||||||
|
val messageType: Int = 0,
|
||||||
|
val textDisplay: String = "",
|
||||||
|
val msgId: String, // Add this property
|
||||||
|
var showTimestamp: Boolean = false,
|
||||||
|
var showTimeDivider: Boolean = false
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
// OpenIM 消息类型常量
|
||||||
|
const val MESSAGE_TYPE_TEXT = 101
|
||||||
|
const val MESSAGE_TYPE_IMAGE = 102
|
||||||
|
const val MESSAGE_TYPE_AUDIO = 103
|
||||||
|
const val MESSAGE_TYPE_VIDEO = 104
|
||||||
|
const val MESSAGE_TYPE_FILE = 105
|
||||||
|
|
||||||
|
fun convertToChatItem(message: Message, context: Context, avatar: String? = null): ChatItem? {
|
||||||
|
val timestamp = message.createTime
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.timeInMillis = timestamp
|
||||||
|
|
||||||
|
var faceAvatar = avatar
|
||||||
|
if (faceAvatar == null) {
|
||||||
|
faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}"
|
||||||
|
}
|
||||||
|
|
||||||
|
when (message.contentType) {
|
||||||
|
MESSAGE_TYPE_IMAGE -> {
|
||||||
|
val pictureElem = message.pictureElem
|
||||||
|
if (pictureElem != null && !pictureElem.sourcePicture.url.isNullOrEmpty()) {
|
||||||
|
return ChatItem(
|
||||||
|
message = "Image",
|
||||||
|
avatar = faceAvatar,
|
||||||
|
time = calendar.time.formatChatTime(context),
|
||||||
|
userId = message.sendID,
|
||||||
|
nickname = message.senderNickname ?: "",
|
||||||
|
timestamp = timestamp,
|
||||||
|
imageList = listOfNotNull(
|
||||||
|
PictureInfo(
|
||||||
|
url = pictureElem.sourcePicture.url ?: "",
|
||||||
|
width = pictureElem.sourcePicture.width,
|
||||||
|
height = pictureElem.sourcePicture.height,
|
||||||
|
size = pictureElem.sourcePicture.size
|
||||||
|
)
|
||||||
|
).toMutableList(),
|
||||||
|
messageType = MESSAGE_TYPE_IMAGE,
|
||||||
|
textDisplay = "Image",
|
||||||
|
msgId = message.clientMsgID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
MESSAGE_TYPE_TEXT -> {
|
||||||
|
return ChatItem(
|
||||||
|
message = message.textElem?.content ?: "Unsupported message type",
|
||||||
|
avatar = faceAvatar,
|
||||||
|
time = calendar.time.formatChatTime(context),
|
||||||
|
userId = message.sendID,
|
||||||
|
nickname = message.senderNickname ?: "",
|
||||||
|
timestamp = timestamp,
|
||||||
|
imageList = emptyList<PictureInfo>().toMutableList(),
|
||||||
|
messageType = MESSAGE_TYPE_TEXT,
|
||||||
|
textDisplay = message.textElem?.content ?: "Unsupported message type",
|
||||||
|
msgId = message.clientMsgID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenIM 图片信息数据类
|
||||||
|
data class PictureInfo(
|
||||||
|
val url: String,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val size: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class ChatNotification(
|
||||||
|
@SerializedName("userId")
|
||||||
|
val userId: Int,
|
||||||
|
@SerializedName("userTrtcId")
|
||||||
|
val userTrtcId: String,
|
||||||
|
@SerializedName("targetUserId")
|
||||||
|
val targetUserId: Int,
|
||||||
|
@SerializedName("targetTrtcId")
|
||||||
|
val targetTrtcId: String,
|
||||||
|
@SerializedName("strategy")
|
||||||
|
val strategy: String
|
||||||
|
)
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package com.aiosman.riderpro.entity
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.aiosman.riderpro.data.CommentRemoteDataSource
|
import com.aiosman.ravenow.data.CommentRemoteDataSource
|
||||||
import com.aiosman.riderpro.data.NoticePost
|
import com.aiosman.ravenow.data.NoticePost
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
data class CommentEntity(
|
data class CommentEntity(
|
||||||
19
app/src/main/java/com/aiosman/ravenow/entity/Common.kt
Normal file
19
app/src/main/java/com/aiosman/ravenow/entity/Common.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class ReportReasons(
|
||||||
|
@SerializedName("id") var id: Int,
|
||||||
|
@SerializedName("text") var text: Map<String, String>
|
||||||
|
) {
|
||||||
|
fun getReasonText(context:Context): String? {
|
||||||
|
val language = context.resources.configuration.locale.language
|
||||||
|
val langMapping = mapOf(
|
||||||
|
"zh" to "zh",
|
||||||
|
"en" to "en"
|
||||||
|
)
|
||||||
|
val useLang = langMapping[language] ?: "en"
|
||||||
|
return text[useLang] ?: text["en"]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/src/main/java/com/aiosman/ravenow/entity/Group.kt
Normal file
16
app/src/main/java/com/aiosman/ravenow/entity/Group.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
data class GroupMember(
|
||||||
|
val userId: String,
|
||||||
|
val nickname: String,
|
||||||
|
val avatar: String,
|
||||||
|
val isOwner: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GroupInfo(
|
||||||
|
val groupId: String,
|
||||||
|
val groupName: String,
|
||||||
|
val groupAvatar: String,
|
||||||
|
val memberCount: Int,
|
||||||
|
val isCreator: Boolean = false,
|
||||||
|
)
|
||||||
109
app/src/main/java/com/aiosman/ravenow/entity/Loader.kt
Normal file
109
app/src/main/java/com/aiosman/ravenow/entity/Loader.kt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
|
||||||
|
abstract class DataLoader<T,ET> {
|
||||||
|
var list: MutableList<T> = mutableListOf()
|
||||||
|
var page by mutableStateOf(1)
|
||||||
|
var total by mutableStateOf(0)
|
||||||
|
var pageSize by mutableStateOf(10)
|
||||||
|
var hasNext by mutableStateOf(true)
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
var onListChanged: ((List<T>) -> Unit)? = null
|
||||||
|
var onError: ((String) -> Unit)? = null
|
||||||
|
private var firstLoad = true
|
||||||
|
|
||||||
|
|
||||||
|
abstract suspend fun fetchData(
|
||||||
|
page: Int, pageSize: Int, extra: ET
|
||||||
|
): ListContainer<T>
|
||||||
|
|
||||||
|
suspend fun loadData(
|
||||||
|
extra: ET
|
||||||
|
) {
|
||||||
|
Log.d("DataLoader", "loadData开始 - firstLoad: $firstLoad")
|
||||||
|
if (!firstLoad) {
|
||||||
|
Log.d("DataLoader", "loadData跳过 - 非首次加载")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Log.d("DataLoader", "loadData跳过 - 正在加载中")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
firstLoad = false
|
||||||
|
isLoading = true
|
||||||
|
error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.d("DataLoader", "调用fetchData - page: $page, pageSize: $pageSize")
|
||||||
|
val result = fetchData(page, pageSize, extra)
|
||||||
|
list = result.list.toMutableList()
|
||||||
|
this.page = page
|
||||||
|
this.total = result.total
|
||||||
|
this.pageSize = pageSize
|
||||||
|
this.hasNext = result.list.size == pageSize
|
||||||
|
Log.d("DataLoader", "loadData完成 - 数据量: ${list.size}, total: $total, hasNext: $hasNext")
|
||||||
|
onListChanged?.invoke(list)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("DataLoader", "loadData失败", e)
|
||||||
|
error = e.message ?: "加载数据时发生未知错误"
|
||||||
|
firstLoad = true // 重置firstLoad状态,允许重试
|
||||||
|
onError?.invoke(error!!)
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadMore(extra: ET) {
|
||||||
|
if (firstLoad) {
|
||||||
|
Log.d("DataLoader", "loadMore跳过 - firstLoad为true")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!hasNext) {
|
||||||
|
Log.d("DataLoader", "loadMore跳过 - hasNext为false")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Log.d("DataLoader", "loadMore跳过 - 正在加载中")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.d("DataLoader", "开始loadMore - 当前页: $page, 当前数据量: ${list.size}")
|
||||||
|
val result = fetchData(page + 1, pageSize, extra)
|
||||||
|
list.addAll(result.list)
|
||||||
|
page += 1
|
||||||
|
hasNext = result.list.size == pageSize
|
||||||
|
Log.d("DataLoader", "loadMore完成 - 新页: $page, 新数据量: ${list.size}, 本次获取: ${result.list.size}, hasNext: $hasNext")
|
||||||
|
onListChanged?.invoke(list)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("DataLoader", "loadMore失败", e)
|
||||||
|
error = e.message ?: "加载更多数据时发生未知错误"
|
||||||
|
onError?.invoke(error!!)
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
list.clear()
|
||||||
|
page = 1
|
||||||
|
total = 0
|
||||||
|
pageSize = 10
|
||||||
|
hasNext = true
|
||||||
|
firstLoad = true
|
||||||
|
isLoading = false
|
||||||
|
error = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.aiosman.riderpro.entity
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.aiosman.riderpro.data.ListContainer
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
import com.aiosman.riderpro.data.MomentService
|
import com.aiosman.ravenow.data.MomentService
|
||||||
import com.aiosman.riderpro.data.ServiceException
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
import com.aiosman.riderpro.data.UploadImage
|
import com.aiosman.ravenow.data.UploadImage
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.AgentMomentRequestBody
|
||||||
import com.aiosman.riderpro.data.parseErrorResponse
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.riderpro.entity.MomentEntity
|
import com.aiosman.ravenow.data.parseErrorResponse
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
@@ -27,6 +27,7 @@ class MomentPagingSource(
|
|||||||
private val timelineId: Int? = null,
|
private val timelineId: Int? = null,
|
||||||
private val contentSearch: String? = null,
|
private val contentSearch: String? = null,
|
||||||
private val trend: Boolean? = false,
|
private val trend: Boolean? = false,
|
||||||
|
private val explore: Boolean? = false,
|
||||||
private val favoriteUserId: Int? = null
|
private val favoriteUserId: Int? = null
|
||||||
) : PagingSource<Int, MomentEntity>() {
|
) : PagingSource<Int, MomentEntity>() {
|
||||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
|
||||||
@@ -38,6 +39,7 @@ class MomentPagingSource(
|
|||||||
timelineId = timelineId,
|
timelineId = timelineId,
|
||||||
contentSearch = contentSearch,
|
contentSearch = contentSearch,
|
||||||
trend = trend,
|
trend = trend,
|
||||||
|
explore = explore,
|
||||||
favoriteUserId = favoriteUserId
|
favoriteUserId = favoriteUserId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ class MomentRemoteDataSource(
|
|||||||
timelineId: Int?,
|
timelineId: Int?,
|
||||||
contentSearch: String?,
|
contentSearch: String?,
|
||||||
trend: Boolean?,
|
trend: Boolean?,
|
||||||
|
explore: Boolean?,
|
||||||
favoriteUserId: Int?
|
favoriteUserId: Int?
|
||||||
): ListContainer<MomentEntity> {
|
): ListContainer<MomentEntity> {
|
||||||
return momentService.getMoments(
|
return momentService.getMoments(
|
||||||
@@ -74,6 +77,7 @@ class MomentRemoteDataSource(
|
|||||||
timelineId = timelineId,
|
timelineId = timelineId,
|
||||||
contentSearch = contentSearch,
|
contentSearch = contentSearch,
|
||||||
trend = trend,
|
trend = trend,
|
||||||
|
explore = explore,
|
||||||
favoriteUserId = favoriteUserId
|
favoriteUserId = favoriteUserId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -88,7 +92,8 @@ class MomentServiceImpl() : MomentService {
|
|||||||
timelineId: Int?,
|
timelineId: Int?,
|
||||||
contentSearch: String?,
|
contentSearch: String?,
|
||||||
trend: Boolean?,
|
trend: Boolean?,
|
||||||
favoriteUserId: Int?
|
explore: Boolean?,
|
||||||
|
favoriteUserId: Int?,
|
||||||
): ListContainer<MomentEntity> {
|
): ListContainer<MomentEntity> {
|
||||||
return momentBackend.fetchMomentItems(
|
return momentBackend.fetchMomentItems(
|
||||||
pageNumber = pageNumber,
|
pageNumber = pageNumber,
|
||||||
@@ -96,7 +101,8 @@ class MomentServiceImpl() : MomentService {
|
|||||||
timelineId = timelineId,
|
timelineId = timelineId,
|
||||||
contentSearch = contentSearch,
|
contentSearch = contentSearch,
|
||||||
trend = trend,
|
trend = trend,
|
||||||
favoriteUserId = favoriteUserId
|
favoriteUserId = favoriteUserId,
|
||||||
|
explore = explore
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +128,10 @@ class MomentServiceImpl() : MomentService {
|
|||||||
return momentBackend.createMoment(content, authorId, images, relPostId)
|
return momentBackend.createMoment(content, authorId, images, relPostId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun agentMoment(content: String): String {
|
||||||
|
return momentBackend.agentMoment(content)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun favoriteMoment(id: Int) {
|
override suspend fun favoriteMoment(id: Int) {
|
||||||
momentBackend.favoriteMoment(id)
|
momentBackend.favoriteMoment(id)
|
||||||
}
|
}
|
||||||
@@ -144,6 +154,7 @@ class MomentBackend {
|
|||||||
timelineId: Int?,
|
timelineId: Int?,
|
||||||
contentSearch: String?,
|
contentSearch: String?,
|
||||||
trend: Boolean?,
|
trend: Boolean?,
|
||||||
|
explore: Boolean?,
|
||||||
favoriteUserId: Int? = null
|
favoriteUserId: Int? = null
|
||||||
): ListContainer<MomentEntity> {
|
): ListContainer<MomentEntity> {
|
||||||
val resp = ApiClient.api.getPosts(
|
val resp = ApiClient.api.getPosts(
|
||||||
@@ -153,7 +164,8 @@ class MomentBackend {
|
|||||||
authorId = author,
|
authorId = author,
|
||||||
contentSearch = contentSearch,
|
contentSearch = contentSearch,
|
||||||
trend = if (trend == true) "true" else "",
|
trend = if (trend == true) "true" else "",
|
||||||
favouriteUserId = favoriteUserId
|
favouriteUserId = favoriteUserId,
|
||||||
|
explore = if (explore == true) "true" else ""
|
||||||
)
|
)
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get moments")
|
val body = resp.body() ?: throw ServiceException("Failed to get moments")
|
||||||
return ListContainer(
|
return ListContainer(
|
||||||
@@ -205,6 +217,17 @@ class MomentBackend {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun agentMoment(
|
||||||
|
content: String,
|
||||||
|
): String {
|
||||||
|
val textContent = content.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
|
val sessionId = ""
|
||||||
|
val response = ApiClient.api.agentMoment(AgentMomentRequestBody(generateText = content, sessionId =sessionId ))
|
||||||
|
val body = response.body()?.data ?: throw ServiceException("Failed to agent moment")
|
||||||
|
return body.toString()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun favoriteMoment(id: Int) {
|
suspend fun favoriteMoment(id: Int) {
|
||||||
ApiClient.api.favoritePost(id)
|
ApiClient.api.favoritePost(id)
|
||||||
}
|
}
|
||||||
@@ -276,5 +299,108 @@ data class MomentEntity(
|
|||||||
// 关联动态
|
// 关联动态
|
||||||
var relMoment: MomentEntity? = null,
|
var relMoment: MomentEntity? = null,
|
||||||
// 是否收藏
|
// 是否收藏
|
||||||
var isFavorite: Boolean = false
|
var isFavorite: Boolean = false,
|
||||||
|
// 是否为新闻
|
||||||
|
val isNews: Boolean? = null,
|
||||||
|
// 新闻标题
|
||||||
|
val newsTitle: String? = null,
|
||||||
|
// 新闻链接
|
||||||
|
val newsUrl: String? = null,
|
||||||
|
// 新闻来源
|
||||||
|
val newsSource: String? = null,
|
||||||
|
// 新闻分类
|
||||||
|
val newsCategory: String? = null,
|
||||||
|
// 新闻语言
|
||||||
|
val newsLanguage: String? = null,
|
||||||
|
// 新闻内容
|
||||||
|
val newsContent: String? = null,
|
||||||
|
// 是否有完整文本
|
||||||
|
val hasFullText: Boolean? = null,
|
||||||
|
// 摘要
|
||||||
|
val summary: String? = null,
|
||||||
|
// 发布时间
|
||||||
|
val publishedAt: String? = null,
|
||||||
|
// 图片是否已缓存
|
||||||
|
val imageCached: Boolean? = null
|
||||||
)
|
)
|
||||||
|
class MomentLoaderExtraArgs(
|
||||||
|
val explore: Boolean? = false,
|
||||||
|
val timelineId: Int? = null,
|
||||||
|
val authorId : Int? = null,
|
||||||
|
val newsOnly: Boolean? = false,
|
||||||
|
val trend: Boolean? = false
|
||||||
|
)
|
||||||
|
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||||
|
override suspend fun fetchData(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
extra: MomentLoaderExtraArgs
|
||||||
|
): ListContainer<MomentEntity> {
|
||||||
|
val result = ApiClient.api.getPosts(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
explore = if (extra.explore == true) "true" else "",
|
||||||
|
timelineId = extra.timelineId,
|
||||||
|
authorId = extra.authorId,
|
||||||
|
newsFilter = if (extra.newsOnly == true) "news_only" else "",
|
||||||
|
trend = if (extra.trend == true) "1" else ""
|
||||||
|
|
||||||
|
)
|
||||||
|
val data = result.body()?.let {
|
||||||
|
ListContainer(
|
||||||
|
list = it.list.map { it.toMomentItem() },
|
||||||
|
total = it.total,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (data == null) {
|
||||||
|
throw ServiceException("Failed to get moments")
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMomentLike(id: Int,isLike:Boolean) {
|
||||||
|
this.list = this.list.map { momentItem ->
|
||||||
|
if (momentItem.id == id) {
|
||||||
|
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike)
|
||||||
|
} else {
|
||||||
|
momentItem
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
|
||||||
|
this.list = this.list.map { momentItem ->
|
||||||
|
if (momentItem.id == id) {
|
||||||
|
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
|
||||||
|
} else {
|
||||||
|
momentItem
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMoment(id: Int) {
|
||||||
|
this.list = this.list.filter { it.id != id }.toMutableList()
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMoment(moment: MomentEntity) {
|
||||||
|
this.list.add(0, moment)
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFollowStatus(authorId:Int,isFollow:Boolean) {
|
||||||
|
this.list = this.list.map { momentItem ->
|
||||||
|
if (momentItem.authorId == authorId) {
|
||||||
|
momentItem.copy(followStatus = isFollow)
|
||||||
|
} else {
|
||||||
|
momentItem
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
85
app/src/main/java/com/aiosman/ravenow/entity/Room.kt
Normal file
85
app/src/main/java/com/aiosman/ravenow/entity/Room.kt
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群聊房间
|
||||||
|
*/
|
||||||
|
|
||||||
|
data class RoomEntity(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val trtcRoomId: String,
|
||||||
|
val trtcType: String,
|
||||||
|
val cover: String,
|
||||||
|
val avatar: String,
|
||||||
|
val recommendBanner: String,
|
||||||
|
val isRecommended: Boolean,
|
||||||
|
val allowInHot: Boolean,
|
||||||
|
val creator: CreatorEntity,
|
||||||
|
val userCount: Int,
|
||||||
|
val maxMemberLimit: Int,
|
||||||
|
val canJoin: Boolean,
|
||||||
|
val canJoinCode: Int,
|
||||||
|
val users: List<UsersEntity>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorEntity(
|
||||||
|
val id: Int,
|
||||||
|
val userId: String,
|
||||||
|
val trtcUserId: String,
|
||||||
|
val profile: ProfileEntity
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UsersEntity(
|
||||||
|
val id: Int,
|
||||||
|
val userId: String,
|
||||||
|
val profile: ProfileEntity
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ProfileEntity(
|
||||||
|
val id: Int,
|
||||||
|
val username: String,
|
||||||
|
val nickname: String,
|
||||||
|
val avatar: String,
|
||||||
|
val banner: String,
|
||||||
|
val bio: String,
|
||||||
|
val trtcUserId: String,
|
||||||
|
val chatAIId: String,
|
||||||
|
val aiAccount: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
|
||||||
|
override suspend fun fetchData(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
extra: AgentLoaderExtraArgs
|
||||||
|
): ListContainer<AgentEntity> {
|
||||||
|
val result = ApiClient.api.getAgent(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
|
||||||
|
)
|
||||||
|
val data = result.body()?.let {
|
||||||
|
ListContainer(
|
||||||
|
list = it.data.list.map { it.toAgentEntity()},
|
||||||
|
total = it.data.total,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (data == null) {
|
||||||
|
throw ServiceException("Failed to get agent")
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.aiosman.riderpro.entity
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.aiosman.riderpro.data.UserService
|
import com.aiosman.ravenow.data.UserService
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
data class FollowChangeEvent(
|
||||||
|
val userId: Int,
|
||||||
|
val isFollow: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
|
||||||
|
data class MomentAddEvent(
|
||||||
|
val moment:MomentEntity
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
data class MomentFavouriteChangeEvent(
|
||||||
|
val postId: Int,
|
||||||
|
val isFavourite: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
data class MomentLikeChangeEvent(
|
||||||
|
val postId: Int,
|
||||||
|
val likeCount: Int?,
|
||||||
|
val isLike: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
data class MomentRemoveEvent(
|
||||||
|
val postId: Int
|
||||||
|
)
|
||||||
9
app/src/main/java/com/aiosman/ravenow/exp/Bitmap.kt
Normal file
9
app/src/main/java/com/aiosman/ravenow/exp/Bitmap.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package com.aiosman.ravenow.exp
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
|
||||||
|
fun Bitmap.rotate(degree: Int): Bitmap {
|
||||||
|
val matrix = android.graphics.Matrix()
|
||||||
|
matrix.postRotate(degree.toFloat())
|
||||||
|
return Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true)
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.aiosman.riderpro.exp
|
package com.aiosman.ravenow.exp
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.icu.text.SimpleDateFormat
|
import android.icu.text.SimpleDateFormat
|
||||||
import android.icu.util.Calendar
|
import android.icu.util.Calendar
|
||||||
import androidx.compose.ui.res.stringResource
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.riderpro.R
|
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.exp
|
package com.aiosman.ravenow.exp
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.exp
|
package com.aiosman.ravenow.exp
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
342
app/src/main/java/com/aiosman/ravenow/im/OpenIMManager.kt
Normal file
342
app/src/main/java/com/aiosman/ravenow/im/OpenIMManager.kt
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
package com.aiosman.ravenow.im
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import io.openim.android.sdk.OpenIMClient
|
||||||
|
import io.openim.android.sdk.listener.*
|
||||||
|
import io.openim.android.sdk.models.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM SDK 管理器
|
||||||
|
* 负责 OpenIM SDK 的初始化和各种监听器的设置
|
||||||
|
*/
|
||||||
|
object OpenIMManager {
|
||||||
|
|
||||||
|
private const val TAG = "OpenIMManager"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 OpenIM SDK
|
||||||
|
* @param context Android上下文
|
||||||
|
* @param initConfig SDK初始化配置
|
||||||
|
*/
|
||||||
|
fun initSDK(context: Context, initConfig: InitConfig) {
|
||||||
|
try {
|
||||||
|
OpenIMClient.getInstance().initSDK(
|
||||||
|
context.applicationContext as Application,
|
||||||
|
initConfig,
|
||||||
|
object : OnConnListener {
|
||||||
|
override fun onConnectFailed(code: Int, error: String?) {
|
||||||
|
Log.e(TAG, "连接服务器失败: code=$code, error=$error")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnectSuccess() {
|
||||||
|
//连接服务器成功
|
||||||
|
Log.d(TAG, "连接服务器成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnecting() {
|
||||||
|
//连接服务器中...
|
||||||
|
Log.d(TAG, "连接服务器中...")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKickedOffline() {
|
||||||
|
//当前用户被踢下线
|
||||||
|
Log.w(TAG, "当前用户被踢下线")
|
||||||
|
// 可以在这里处理用户被踢下线的逻辑,比如跳转到登录页面
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUserTokenExpired() {
|
||||||
|
//登录票据已经过期
|
||||||
|
Log.w(TAG, "登录票据已经过期")
|
||||||
|
// 可以在这里处理token过期的逻辑,比如重新登录
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 初始化完成后设置各种监听器
|
||||||
|
initListeners()
|
||||||
|
|
||||||
|
Log.d(TAG, "OpenIM SDK 初始化成功")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "OpenIM SDK 初始化失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化所有监听器
|
||||||
|
*/
|
||||||
|
private fun initListeners() {
|
||||||
|
setUserListener()
|
||||||
|
setMessageListener()
|
||||||
|
setFriendshipListener()
|
||||||
|
setConversationListener()
|
||||||
|
setGroupListener()
|
||||||
|
setSignalingListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户信息监听器
|
||||||
|
*/
|
||||||
|
private fun setUserListener() {
|
||||||
|
OpenIMClient.getInstance().userInfoManager.setOnUserListener(object : OnUserListener {
|
||||||
|
override fun onSelfInfoUpdated(userInfo: UserInfo?) {
|
||||||
|
Log.d(TAG, "用户信息更新: ${userInfo?.nickname}")
|
||||||
|
// 处理用户信息更新
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置消息监听器
|
||||||
|
*/
|
||||||
|
private fun setMessageListener() {
|
||||||
|
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(object : OnAdvanceMsgListener {
|
||||||
|
override fun onRecvNewMessage(msg: Message?) {
|
||||||
|
Log.d(TAG, "收到新消息: ${msg?.toString()}")
|
||||||
|
// 处理新消息
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvC2CReadReceipt(list: List<C2CReadReceiptInfo>?) {
|
||||||
|
Log.d(TAG, "收到C2C已读回执,数量: ${list?.size}")
|
||||||
|
// 处理C2C已读回执
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvGroupMessageReadReceipt(groupMessageReceipt: GroupMessageReceipt?) {
|
||||||
|
Log.d(TAG, "收到群组消息已读回执")
|
||||||
|
// 处理群组消息已读回执
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvMessageRevokedV2(info: RevokedInfo?) {
|
||||||
|
Log.d(TAG, "消息被撤回: ${info?.clientMsgID}")
|
||||||
|
// 处理消息撤回
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvMessageExtensionsChanged(msgID: String?, list: List<KeyValue>?) {
|
||||||
|
Log.d(TAG, "消息扩展信息变更: $msgID")
|
||||||
|
// 处理消息扩展信息变更
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvMessageExtensionsDeleted(msgID: String?, list: List<String>?) {
|
||||||
|
Log.d(TAG, "消息扩展信息删除: $msgID")
|
||||||
|
// 处理消息扩展信息删除
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvMessageExtensionsAdded(msgID: String?, list: List<KeyValue>?) {
|
||||||
|
Log.d(TAG, "消息扩展信息添加: $msgID")
|
||||||
|
// 处理消息扩展信息添加
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMsgDeleted(message: Message?) {
|
||||||
|
Log.d(TAG, "消息被删除: ${message?.clientMsgID}")
|
||||||
|
// 处理消息删除
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvOfflineNewMessage(msg: List<Message>?) {
|
||||||
|
Log.d(TAG, "收到离线新消息,数量: ${msg?.size}")
|
||||||
|
// 处理离线新消息
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRecvOnlineOnlyMessage(s: String?) {
|
||||||
|
Log.d(TAG, "收到仅在线消息: $s")
|
||||||
|
// 处理仅在线消息
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置好友关系监听器
|
||||||
|
*/
|
||||||
|
private fun setFriendshipListener() {
|
||||||
|
OpenIMClient.getInstance().friendshipManager.setOnFriendshipListener(object : OnFriendshipListener {
|
||||||
|
override fun onFriendApplicationAdded(friendApplication: FriendApplicationInfo?) {
|
||||||
|
Log.d(TAG, "收到好友申请")
|
||||||
|
// 处理好友申请
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFriendApplicationDeleted(friendApplication: FriendApplicationInfo?) {
|
||||||
|
Log.d(TAG, "好友申请被删除")
|
||||||
|
// 处理好友申请删除
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFriendApplicationAccepted(friendApplication: FriendApplicationInfo?) {
|
||||||
|
Log.d(TAG, "好友申请被接受")
|
||||||
|
// 处理好友申请接受
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFriendApplicationRejected(friendApplication: FriendApplicationInfo?) {
|
||||||
|
Log.d(TAG, "好友申请被拒绝")
|
||||||
|
// 处理好友申请拒绝
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFriendAdded(friendInfo: FriendInfo?) {
|
||||||
|
Log.d(TAG, "添加好友: ${friendInfo?.nickname}")
|
||||||
|
// 处理添加好友
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFriendDeleted(friendInfo: FriendInfo?) {
|
||||||
|
Log.d(TAG, "删除好友: ${friendInfo?.nickname}")
|
||||||
|
// 处理删除好友
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFriendInfoChanged(friendInfo: FriendInfo?) {
|
||||||
|
Log.d(TAG, "好友信息变更: ${friendInfo?.nickname}")
|
||||||
|
// 处理好友信息变更
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBlacklistAdded(blackInfo: BlacklistInfo) {
|
||||||
|
Log.d(TAG, "添加黑名单")
|
||||||
|
// 处理添加黑名单
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBlacklistDeleted(blackInfo: BlacklistInfo) {
|
||||||
|
Log.d(TAG, "移除黑名单")
|
||||||
|
// 处理移除黑名单
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置会话监听器
|
||||||
|
*/
|
||||||
|
private fun setConversationListener() {
|
||||||
|
OpenIMClient.getInstance().conversationManager.setOnConversationListener(object : OnConversationListener {
|
||||||
|
override fun onConversationChanged(conversationList: MutableList<ConversationInfo>?) {
|
||||||
|
Log.d(TAG, "会话发生变化,数量: ${conversationList?.size}")
|
||||||
|
// 处理会话变化
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewConversation(conversationList: MutableList<ConversationInfo>?) {
|
||||||
|
Log.d(TAG, "新增会话,数量: ${conversationList?.size}")
|
||||||
|
// 处理新增会话
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSyncServerFailed(reinstalled:Boolean) {
|
||||||
|
Log.e(TAG, "同步服务器失败")
|
||||||
|
// 处理同步失败
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSyncServerFinish(reinstalled:Boolean) {
|
||||||
|
Log.d(TAG, "同步服务器完成")
|
||||||
|
// 处理同步完成
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSyncServerStart(reinstalled:Boolean) {
|
||||||
|
Log.d(TAG, "开始同步服务器")
|
||||||
|
// 处理开始同步
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTotalUnreadMessageCountChanged(totalUnreadCount: Int) {
|
||||||
|
Log.d(TAG, "总未读消息数变化: $totalUnreadCount")
|
||||||
|
// 处理总未读数变化
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置群组监听器
|
||||||
|
*/
|
||||||
|
private fun setGroupListener() {
|
||||||
|
OpenIMClient.getInstance().groupManager.setOnGroupListener(object : OnGroupListener {
|
||||||
|
override fun onGroupApplicationAdded(groupApplication: GroupApplicationInfo?) {
|
||||||
|
Log.d(TAG, "收到入群申请")
|
||||||
|
// 处理入群申请
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGroupApplicationDeleted(groupApplication: GroupApplicationInfo?) {
|
||||||
|
Log.d(TAG, "入群申请被删除")
|
||||||
|
// 处理入群申请删除
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGroupApplicationAccepted(groupApplication: GroupApplicationInfo?) {
|
||||||
|
Log.d(TAG, "入群申请被接受")
|
||||||
|
// 处理入群申请接受
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGroupApplicationRejected(groupApplication: GroupApplicationInfo?) {
|
||||||
|
Log.d(TAG, "入群申请被拒绝")
|
||||||
|
// 处理入群申请拒绝
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGroupInfoChanged(groupInfo: GroupInfo?) {
|
||||||
|
Log.d(TAG, "群信息变更: ${groupInfo?.groupName}")
|
||||||
|
// 处理群信息变更
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGroupMemberAdded(groupMemberInfo: GroupMembersInfo?) {
|
||||||
|
Log.d(TAG, "群成员加入")
|
||||||
|
// 处理群成员加入
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGroupMemberDeleted(groupMemberInfo: GroupMembersInfo?) {
|
||||||
|
Log.d(TAG, "群成员退出")
|
||||||
|
// 处理群成员退出
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGroupMemberInfoChanged(groupMemberInfo: GroupMembersInfo?) {
|
||||||
|
Log.d(TAG, "群成员信息变更")
|
||||||
|
// 处理群成员信息变更
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onJoinedGroupAdded(groupInfo: GroupInfo?) {
|
||||||
|
Log.d(TAG, "加入新群: ${groupInfo?.groupName}")
|
||||||
|
// 处理加入新群
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onJoinedGroupDeleted(groupInfo: GroupInfo?) {
|
||||||
|
Log.d(TAG, "退出群聊: ${groupInfo?.groupName}")
|
||||||
|
// 处理退出群聊
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置信令监听器
|
||||||
|
*/
|
||||||
|
private fun setSignalingListener() {
|
||||||
|
// OpenIMClient.getInstance(). .setSignalingListener(object : OnSignalingListener {
|
||||||
|
// override fun onInvitationReceived(signalInvitationInfo: SignalInvitationInfo?) {
|
||||||
|
// Log.d(TAG, "收到信令邀请")
|
||||||
|
// // 处理信令邀请
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onInvitationCancelled(signalInvitationInfo: SignalInvitationInfo?) {
|
||||||
|
// Log.d(TAG, "信令邀请被取消")
|
||||||
|
// // 处理信令邀请取消
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onInvitationTimeout(signalInvitationInfo: SignalInvitationInfo?) {
|
||||||
|
// Log.d(TAG, "信令邀请超时")
|
||||||
|
// // 处理信令邀请超时
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onInviteeAccepted(signalInvitationInfo: SignalInvitationInfo?) {
|
||||||
|
// Log.d(TAG, "信令邀请被接受")
|
||||||
|
// // 处理信令邀请接受
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onInviteeRejected(signalInvitationInfo: SignalInvitationInfo?) {
|
||||||
|
// Log.d(TAG, "信令邀请被拒绝")
|
||||||
|
// // 处理信令邀请拒绝
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取SDK数据库存储目录
|
||||||
|
* @param context Android上下文
|
||||||
|
* @return 存储目录路径
|
||||||
|
*/
|
||||||
|
fun getStorageDir(context: Context): String {
|
||||||
|
// 使用应用的内部存储目录下的im_sdk文件夹
|
||||||
|
val storageDir = context.filesDir.resolve("im_sdk")
|
||||||
|
|
||||||
|
// 如果目录不存在,创建它
|
||||||
|
if (!storageDir.exists()) {
|
||||||
|
storageDir.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
return storageDir.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.model
|
package com.aiosman.ravenow.model
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.model
|
package com.aiosman.ravenow.model
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
41
app/src/main/java/com/aiosman/ravenow/model/UpdateInfo.kt
Normal file
41
app/src/main/java/com/aiosman/ravenow/model/UpdateInfo.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package com.aiosman.ravenow.model
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
data class UpdateInfo(
|
||||||
|
val versionCode: Int,
|
||||||
|
val versionName: String,
|
||||||
|
val updateContent: String,
|
||||||
|
val downloadUrl: String,
|
||||||
|
val forceUpdate: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
class ApkInstallReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
Log.d("ApkInstallReceiver", "onReceive() called") // 添加日志输出
|
||||||
|
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE == intent.action) {
|
||||||
|
Log.d("ApkInstallReceiver", "Download complete") // 添加日志输出
|
||||||
|
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||||
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
|
// 方案二:通过 DownloadManager 的 API 获取 Uri
|
||||||
|
val uri = downloadManager.getUriForDownloadedFile(downloadId)
|
||||||
|
if (uri != null) {
|
||||||
|
installApk(context, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installApk(context: Context, uri: Uri) {
|
||||||
|
Log.d("ApkInstallReceiver", "installApk() called with: context = $context, uri = $uri") // 添加日志输出
|
||||||
|
val installIntent = Intent(Intent.ACTION_VIEW)
|
||||||
|
installIntent.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
installIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
context.startActivity(installIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro
|
package com.aiosman.ravenow
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@@ -12,6 +12,7 @@ object AppStore {
|
|||||||
private const val PREFS_NAME = "app_prefs_$STORE_VERSION"
|
private const val PREFS_NAME = "app_prefs_$STORE_VERSION"
|
||||||
var token: String? = null
|
var token: String? = null
|
||||||
var rememberMe: Boolean = false
|
var rememberMe: Boolean = false
|
||||||
|
var isGuest: Boolean = false
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
lateinit var googleSignInOptions: GoogleSignInOptions
|
lateinit var googleSignInOptions: GoogleSignInOptions
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
@@ -23,6 +24,12 @@ object AppStore {
|
|||||||
.requestEmail()
|
.requestEmail()
|
||||||
.build()
|
.build()
|
||||||
googleSignInOptions = gso
|
googleSignInOptions = gso
|
||||||
|
// apply dark mode
|
||||||
|
if (sharedPreferences.getBoolean("darkMode", false)) {
|
||||||
|
AppState.darkMode = true
|
||||||
|
AppState.appTheme = DarkThemeColors()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveData() {
|
suspend fun saveData() {
|
||||||
@@ -30,6 +37,7 @@ object AppStore {
|
|||||||
sharedPreferences.edit().apply {
|
sharedPreferences.edit().apply {
|
||||||
putString("token", token)
|
putString("token", token)
|
||||||
putBoolean("rememberMe", rememberMe)
|
putBoolean("rememberMe", rememberMe)
|
||||||
|
putBoolean("isGuest", isGuest)
|
||||||
}.apply()
|
}.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,5 +45,14 @@ object AppStore {
|
|||||||
// shared preferences
|
// shared preferences
|
||||||
token = sharedPreferences.getString("token", null)
|
token = sharedPreferences.getString("token", null)
|
||||||
rememberMe = sharedPreferences.getBoolean("rememberMe", false)
|
rememberMe = sharedPreferences.getBoolean("rememberMe", false)
|
||||||
|
isGuest = sharedPreferences.getBoolean("isGuest", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveDarkMode(darkMode: Boolean) {
|
||||||
|
sharedPreferences.edit().apply {
|
||||||
|
putBoolean("darkMode", darkMode)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.test
|
package com.aiosman.ravenow.test
|
||||||
|
|
||||||
data class StreetPosition(
|
data class StreetPosition(
|
||||||
val name:String,
|
val name:String,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.ui
|
package com.aiosman.ravenow.ui
|
||||||
|
|
||||||
import ChangePasswordScreen
|
import ChangePasswordScreen
|
||||||
import ImageViewer
|
import ImageViewer
|
||||||
@@ -6,8 +6,13 @@ import ModificationListScreen
|
|||||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
import androidx.compose.animation.SharedTransitionLayout
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
@@ -24,37 +29,48 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.aiosman.riderpro.LocalAnimatedContentScope
|
import com.aiosman.ravenow.LocalAnimatedContentScope
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.riderpro.LocalSharedTransitionScope
|
import com.aiosman.ravenow.LocalSharedTransitionScope
|
||||||
import com.aiosman.riderpro.ui.account.AccountEditScreen2
|
import com.aiosman.ravenow.ui.about.AboutScreen
|
||||||
import com.aiosman.riderpro.ui.account.ResetPasswordScreen
|
import com.aiosman.ravenow.ui.account.AccountEditScreen2
|
||||||
import com.aiosman.riderpro.ui.chat.ChatScreen
|
import com.aiosman.ravenow.ui.account.AccountSetting
|
||||||
import com.aiosman.riderpro.ui.comment.CommentsScreen
|
import com.aiosman.ravenow.ui.account.RemoveAccountScreen
|
||||||
import com.aiosman.riderpro.ui.comment.notice.CommentNoticeScreen
|
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
|
||||||
import com.aiosman.riderpro.ui.crop.ImageCropScreen
|
import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
||||||
import com.aiosman.riderpro.ui.favourite.FavouriteListPage
|
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
|
||||||
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeScreen
|
import com.aiosman.ravenow.ui.agent.CreateAgentV2Screen
|
||||||
import com.aiosman.riderpro.ui.follower.FollowerListScreen
|
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
||||||
import com.aiosman.riderpro.ui.follower.FollowerNoticeScreen
|
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
||||||
import com.aiosman.riderpro.ui.follower.FollowingListScreen
|
import com.aiosman.ravenow.ui.chat.ChatScreen
|
||||||
import com.aiosman.riderpro.ui.gallery.OfficialGalleryScreen
|
import com.aiosman.ravenow.ui.chat.GroupChatScreen
|
||||||
import com.aiosman.riderpro.ui.gallery.OfficialPhotographerScreen
|
import com.aiosman.ravenow.ui.comment.CommentsScreen
|
||||||
import com.aiosman.riderpro.ui.gallery.ProfileTimelineScreen
|
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
||||||
import com.aiosman.riderpro.ui.index.IndexScreen
|
import com.aiosman.ravenow.ui.crop.ImageCropScreen
|
||||||
import com.aiosman.riderpro.ui.index.tabs.message.NotificationsScreen
|
import com.aiosman.ravenow.ui.favourite.FavouriteListPage
|
||||||
import com.aiosman.riderpro.ui.index.tabs.search.SearchScreen
|
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen
|
||||||
import com.aiosman.riderpro.ui.like.LikeNoticeScreen
|
import com.aiosman.ravenow.ui.follower.FollowerListScreen
|
||||||
import com.aiosman.riderpro.ui.location.LocationDetailScreen
|
import com.aiosman.ravenow.ui.follower.FollowerNoticeScreen
|
||||||
import com.aiosman.riderpro.ui.login.EmailSignupScreen
|
import com.aiosman.ravenow.ui.follower.FollowingListScreen
|
||||||
import com.aiosman.riderpro.ui.login.LoginPage
|
import com.aiosman.ravenow.ui.gallery.OfficialGalleryScreen
|
||||||
import com.aiosman.riderpro.ui.login.SignupScreen
|
import com.aiosman.ravenow.ui.gallery.OfficialPhotographerScreen
|
||||||
import com.aiosman.riderpro.ui.login.UserAuthScreen
|
import com.aiosman.ravenow.ui.gallery.ProfileTimelineScreen
|
||||||
import com.aiosman.riderpro.ui.modification.EditModificationScreen
|
import com.aiosman.ravenow.ui.group.GroupChatInfoScreen
|
||||||
import com.aiosman.riderpro.ui.post.NewPostImageGridScreen
|
import com.aiosman.ravenow.ui.index.IndexScreen
|
||||||
import com.aiosman.riderpro.ui.post.NewPostScreen
|
import com.aiosman.ravenow.ui.index.tabs.message.NotificationsScreen
|
||||||
import com.aiosman.riderpro.ui.post.PostScreen
|
import com.aiosman.ravenow.ui.index.tabs.search.SearchScreen
|
||||||
import com.aiosman.riderpro.ui.profile.AccountProfileV2
|
import com.aiosman.ravenow.ui.like.LikeNoticeScreen
|
||||||
|
import com.aiosman.ravenow.ui.location.LocationDetailScreen
|
||||||
|
import com.aiosman.ravenow.ui.login.EmailSignupScreen
|
||||||
|
import com.aiosman.ravenow.ui.login.LoginPage
|
||||||
|
import com.aiosman.ravenow.ui.login.SignupScreen
|
||||||
|
import com.aiosman.ravenow.ui.login.UserAuthScreen
|
||||||
|
import com.aiosman.ravenow.ui.modification.EditModificationScreen
|
||||||
|
import com.aiosman.ravenow.ui.post.NewPostImageGridScreen
|
||||||
|
import com.aiosman.ravenow.ui.post.NewPostScreen
|
||||||
|
import com.aiosman.ravenow.ui.post.PostScreen
|
||||||
|
import com.aiosman.ravenow.ui.profile.AccountProfileV2
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
|
||||||
|
|
||||||
sealed class NavigationRoute(
|
sealed class NavigationRoute(
|
||||||
val route: String,
|
val route: String,
|
||||||
@@ -73,7 +89,7 @@ sealed class NavigationRoute(
|
|||||||
data object NewPost : NavigationRoute("NewPost")
|
data object NewPost : NavigationRoute("NewPost")
|
||||||
data object EditModification : NavigationRoute("EditModification")
|
data object EditModification : NavigationRoute("EditModification")
|
||||||
data object Login : NavigationRoute("Login")
|
data object Login : NavigationRoute("Login")
|
||||||
data object AccountProfile : NavigationRoute("AccountProfile/{id}")
|
data object AccountProfile : NavigationRoute("AccountProfile/{id}?isAiAccount={isAiAccount}")
|
||||||
data object SignUp : NavigationRoute("SignUp")
|
data object SignUp : NavigationRoute("SignUp")
|
||||||
data object UserAuth : NavigationRoute("UserAuth")
|
data object UserAuth : NavigationRoute("UserAuth")
|
||||||
data object EmailSignUp : NavigationRoute("EmailSignUp")
|
data object EmailSignUp : NavigationRoute("EmailSignUp")
|
||||||
@@ -88,8 +104,18 @@ sealed class NavigationRoute(
|
|||||||
data object ResetPassword : NavigationRoute("ResetPassword")
|
data object ResetPassword : NavigationRoute("ResetPassword")
|
||||||
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 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")
|
||||||
|
data object AgentImageCrop : NavigationRoute("AgentImageCrop")
|
||||||
|
data object AccountSetting : NavigationRoute("AccountSetting")
|
||||||
|
data object AboutScreen : NavigationRoute("AboutScreen")
|
||||||
|
data object AddAgent : NavigationRoute("AddAgent")
|
||||||
|
data object CreateGroupChat : NavigationRoute("CreateGroupChat")
|
||||||
|
data object GroupInfo : NavigationRoute("GroupInfo/{id}")
|
||||||
|
data object VipSelPage : NavigationRoute("VipSelPage")
|
||||||
|
data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -146,10 +172,35 @@ fun NavigationController(
|
|||||||
navArgument("highlightCommentId") { type = NavType.IntType },
|
navArgument("highlightCommentId") { type = NavType.IntType },
|
||||||
navArgument("initImagePagerIndex") { type = NavType.IntType }
|
navArgument("initImagePagerIndex") { type = NavType.IntType }
|
||||||
),
|
),
|
||||||
|
enterTransition = {
|
||||||
|
// iOS push: new screen slides in from the right
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { fullWidth -> fullWidth },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
// iOS push: previous screen shifts slightly left (parallax)
|
||||||
|
slideOutHorizontally(
|
||||||
|
targetOffsetX = { fullWidth -> -fullWidth / 3 },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
// iOS pop: previous screen slides back from slight left offset
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { fullWidth -> -fullWidth / 3 },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
// iOS pop: current screen slides out to the right
|
||||||
|
slideOutHorizontally(
|
||||||
|
targetOffsetX = { fullWidth -> fullWidth },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
}
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalAnimatedContentScope provides this,
|
|
||||||
) {
|
|
||||||
val id = backStackEntry.arguments?.getString("id")
|
val id = backStackEntry.arguments?.getString("id")
|
||||||
val highlightCommentId =
|
val highlightCommentId =
|
||||||
backStackEntry.arguments?.getInt("highlightCommentId")?.let {
|
backStackEntry.arguments?.getInt("highlightCommentId")?.let {
|
||||||
@@ -162,7 +213,7 @@ fun NavigationController(
|
|||||||
initImagePagerIndex = initIndex
|
initImagePagerIndex = initIndex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
composable(route = NavigationRoute.ModificationList.route,
|
composable(route = NavigationRoute.ModificationList.route,
|
||||||
enterTransition = {
|
enterTransition = {
|
||||||
fadeIn(animationSpec = tween(durationMillis = 0))
|
fadeIn(animationSpec = tween(durationMillis = 0))
|
||||||
@@ -237,12 +288,48 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.AccountProfile.route,
|
route = NavigationRoute.AccountProfile.route,
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
arguments = listOf(
|
||||||
|
navArgument("id") { type = NavType.StringType },
|
||||||
|
navArgument("isAiAccount") {
|
||||||
|
type = NavType.BoolType
|
||||||
|
defaultValue = false
|
||||||
|
}
|
||||||
|
),
|
||||||
|
enterTransition = {
|
||||||
|
// iOS push: new screen slides in from the right
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { fullWidth -> fullWidth },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
// iOS push: previous screen shifts slightly left (parallax)
|
||||||
|
slideOutHorizontally(
|
||||||
|
targetOffsetX = { fullWidth -> -fullWidth / 3 },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
// iOS pop: previous screen slides back from slight left offset
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { fullWidth -> -fullWidth / 3 },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
// iOS pop: current screen slides out to the right
|
||||||
|
slideOutHorizontally(
|
||||||
|
targetOffsetX = { fullWidth -> fullWidth },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAnimatedContentScope provides this,
|
LocalAnimatedContentScope provides this,
|
||||||
) {
|
) {
|
||||||
AccountProfileV2(it.arguments?.getString("id")!!)
|
val id = it.arguments?.getString("id")!!
|
||||||
|
val isAiAccount = it.arguments?.getBoolean("isAiAccount") ?: false
|
||||||
|
AccountProfileV2(id, isAiAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
@@ -281,24 +368,58 @@ fun NavigationController(
|
|||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.AccountEdit.route,
|
route = NavigationRoute.AccountEdit.route,
|
||||||
enterTransition = {
|
enterTransition = {
|
||||||
fadeIn(animationSpec = tween(durationMillis = 0))
|
// iOS风格:从底部向上滑入
|
||||||
|
slideInVertically(
|
||||||
|
initialOffsetY = { fullHeight -> fullHeight },
|
||||||
|
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
|
||||||
|
) + fadeIn(
|
||||||
|
animationSpec = tween(durationMillis = 300)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
exitTransition = {
|
exitTransition = {
|
||||||
fadeOut(animationSpec = tween(durationMillis = 0))
|
// iOS风格:向底部滑出
|
||||||
|
slideOutVertically(
|
||||||
|
targetOffsetY = { fullHeight -> fullHeight },
|
||||||
|
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
|
||||||
|
) + fadeOut(
|
||||||
|
animationSpec = tween(durationMillis = 300)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
// 返回时从底部滑入
|
||||||
|
slideInVertically(
|
||||||
|
initialOffsetY = { fullHeight -> fullHeight },
|
||||||
|
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
|
||||||
|
) + fadeIn(
|
||||||
|
animationSpec = tween(durationMillis = 300)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
// 返回时向底部滑出
|
||||||
|
slideOutVertically(
|
||||||
|
targetOffsetY = { fullHeight -> fullHeight },
|
||||||
|
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
|
||||||
|
) + fadeOut(
|
||||||
|
animationSpec = tween(durationMillis = 300)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
AccountEditScreen2()
|
AccountEditScreen2()
|
||||||
}
|
}
|
||||||
composable(route = NavigationRoute.ImageViewer.route) {
|
composable(route = NavigationRoute.ImageViewer.route) {
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalAnimatedContentScope provides this,
|
|
||||||
) {
|
|
||||||
ImageViewer()
|
ImageViewer()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(route = NavigationRoute.ChangePasswordScreen.route) {
|
composable(route = NavigationRoute.ChangePasswordScreen.route) {
|
||||||
ChangePasswordScreen()
|
ChangePasswordScreen()
|
||||||
}
|
}
|
||||||
|
composable(route = NavigationRoute.RemoveAccountScreen.route) {
|
||||||
|
RemoveAccountScreen()
|
||||||
|
}
|
||||||
|
composable(route = NavigationRoute.VipSelPage.route) {
|
||||||
|
VipSelPage()
|
||||||
|
}
|
||||||
composable(route = NavigationRoute.FavouritesScreen.route) {
|
composable(route = NavigationRoute.FavouritesScreen.route) {
|
||||||
FavouriteNoticeScreen()
|
FavouriteNoticeScreen()
|
||||||
}
|
}
|
||||||
@@ -356,6 +477,36 @@ fun NavigationController(
|
|||||||
ChatScreen(it.arguments?.getString("id")!!)
|
ChatScreen(it.arguments?.getString("id")!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.ChatAi.route,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
ChatAiScreen(it.arguments?.getString("id")!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.ChatGroup.route,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType },
|
||||||
|
navArgument("name") { type = NavType.StringType },
|
||||||
|
navArgument("avatar") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
val encodedId = it.arguments?.getString("id")
|
||||||
|
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
||||||
|
val name = it.arguments?.getString("name")
|
||||||
|
val avatar = it.arguments?.getString("avatar")
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
GroupChatScreen(decodedId?:"",name?:"",avatar?:"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
composable(route = NavigationRoute.CommentNoticeScreen.route) {
|
composable(route = NavigationRoute.CommentNoticeScreen.route) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAnimatedContentScope provides this,
|
LocalAnimatedContentScope provides this,
|
||||||
@@ -370,6 +521,77 @@ fun NavigationController(
|
|||||||
ImageCropScreen()
|
ImageCropScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
composable(route = NavigationRoute.AgentImageCrop.route) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
AgentImageCropScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable(route = NavigationRoute.AccountSetting.route) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
AccountSetting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable(route = NavigationRoute.AboutScreen.route) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
AboutScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.AddAgent.route,
|
||||||
|
) {
|
||||||
|
CreateAgentV2Screen()
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.CreateGroupChat.route,
|
||||||
|
enterTransition = {
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { fullWidth -> fullWidth },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
slideOutHorizontally(
|
||||||
|
targetOffsetX = { fullWidth -> -fullWidth / 3 },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { fullWidth -> -fullWidth / 3 },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
slideOutHorizontally(
|
||||||
|
targetOffsetX = { fullWidth -> fullWidth },
|
||||||
|
animationSpec = tween(durationMillis = 280)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CreateGroupChatScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.GroupInfo.route,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
val encodedId = it.arguments?.getString("id")
|
||||||
|
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
GroupChatInfoScreen(decodedId?:"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -416,5 +638,44 @@ fun NavHostController.navigateToChat(id: String) {
|
|||||||
navigate(
|
navigate(
|
||||||
route = NavigationRoute.Chat.route
|
route = NavigationRoute.Chat.route
|
||||||
.replace("{id}", id)
|
.replace("{id}", id)
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToChatAi(id: String) {
|
||||||
|
navigate(
|
||||||
|
route = NavigationRoute.ChatAi.route
|
||||||
|
.replace("{id}", id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToGroupChat(id: String,name:String,avatar:String) {
|
||||||
|
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
|
||||||
|
val encodedName = java.net.URLEncoder.encode(name, "UTF-8")
|
||||||
|
val encodedAvator = java.net.URLEncoder.encode(avatar, "UTF-8")
|
||||||
|
navigate(
|
||||||
|
route = NavigationRoute.ChatGroup.route
|
||||||
|
.replace("{id}", encodedId)
|
||||||
|
.replace("{name}", encodedName)
|
||||||
|
.replace("{avatar}", encodedAvator)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToGroupInfo(id: String) {
|
||||||
|
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
route = NavigationRoute.GroupInfo.route
|
||||||
|
.replace("{id}", encodedId)
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun NavHostController.goTo(
|
||||||
|
route: NavigationRoute
|
||||||
|
) {
|
||||||
|
navigate(route.route)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.aiosman.ravenow.ui.about
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.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.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.ui.comment.NoticeScreenHeader
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AboutScreen() {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val versionText = context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(appColors.background),
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
NoticeScreenHeader(
|
||||||
|
title = stringResource(R.string.about_rave_now),
|
||||||
|
moreIcon = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
// app icon
|
||||||
|
Box {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
|
||||||
|
contentDescription = "app icon",
|
||||||
|
modifier = Modifier.size(80.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
// app name
|
||||||
|
Text(
|
||||||
|
text = "Rave Now".uppercase(),
|
||||||
|
fontSize = 24.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
// app version
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.version_text, versionText),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = appColors.secondaryText,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.UploadImage
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
|
import com.aiosman.ravenow.utils.TrtcHelper
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object AccountEditViewModel : ViewModel() {
|
||||||
|
var name by mutableStateOf("")
|
||||||
|
var bio by mutableStateOf("")
|
||||||
|
var imageUrl by mutableStateOf<Uri?>(null)
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
||||||
|
var isUpdating by mutableStateOf(false)
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
suspend fun reloadProfile(updateTrtcProfile:Boolean = false) {
|
||||||
|
Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料")
|
||||||
|
isLoading = true
|
||||||
|
try {
|
||||||
|
Log.d("AccountEditViewModel", "reloadProfile: 调用API获取用户资料")
|
||||||
|
accountService.getMyAccountProfile().let {
|
||||||
|
Log.d("AccountEditViewModel", "reloadProfile: 成功获取用户资料 - nickName: ${it.nickName}")
|
||||||
|
profile = it
|
||||||
|
name = it.nickName
|
||||||
|
bio = it.bio
|
||||||
|
// 清除之前裁剪的图片
|
||||||
|
croppedBitmap = null
|
||||||
|
if (updateTrtcProfile) {
|
||||||
|
TrtcHelper.updateTrtcProfile(
|
||||||
|
it.nickName,
|
||||||
|
it.rawAvatar
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 处理异常,避免UI消失
|
||||||
|
Log.e("AccountEditViewModel", "reloadProfile: 加载用户资料失败", e)
|
||||||
|
e.printStackTrace()
|
||||||
|
// 如果是首次加载失败,至少保持之前的profile不变
|
||||||
|
// 这样UI不会突然消失
|
||||||
|
} finally {
|
||||||
|
Log.d("AccountEditViewModel", "reloadProfile: 加载完成,isLoading设为false")
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetToOriginalData() {
|
||||||
|
profile?.let {
|
||||||
|
name = it.nickName
|
||||||
|
bio = it.bio
|
||||||
|
// 清除之前裁剪的图片
|
||||||
|
croppedBitmap = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun updateUserProfile(context: Context) {
|
||||||
|
val newAvatar = croppedBitmap?.let {
|
||||||
|
val file = File(context.cacheDir, "avatar.jpg")
|
||||||
|
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||||
|
UploadImage(file, "avatar.jpg", "", "jpg")
|
||||||
|
}
|
||||||
|
// 去除换行符,确保昵称和个人简介不包含换行
|
||||||
|
val cleanName = name.trim().replace("\n", "").replace("\r", "")
|
||||||
|
val cleanBio = bio.trim().replace("\n", "").replace("\r", "")
|
||||||
|
|
||||||
|
val newName = if (cleanName == profile?.nickName) null else cleanName
|
||||||
|
accountService.updateProfile(
|
||||||
|
avatar = newAvatar,
|
||||||
|
banner = null,
|
||||||
|
nickName = newName,
|
||||||
|
bio = cleanBio
|
||||||
|
)
|
||||||
|
// 刷新用户资料
|
||||||
|
reloadProfile()
|
||||||
|
// 刷新个人资料页面的用户资料
|
||||||
|
MyProfileViewModel.loadUserProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置ViewModel状态
|
||||||
|
* 用于用户登出或切换账号时清理数据
|
||||||
|
*/
|
||||||
|
fun ResetModel() {
|
||||||
|
Log.d("AccountEditViewModel", "ResetModel: 重置ViewModel状态")
|
||||||
|
profile = null
|
||||||
|
name = ""
|
||||||
|
bio = ""
|
||||||
|
imageUrl = null
|
||||||
|
croppedBitmap = null
|
||||||
|
isUpdating = false
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.Messaging
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.index.NavItem
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AccountSetting() {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(appColors.background),
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
NoticeScreenHeader(
|
||||||
|
title = stringResource(R.string.account_and_security),
|
||||||
|
moreIcon = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(start = 24.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
NavItem(
|
||||||
|
iconRes = R.mipmap.rider_pro_change_password,
|
||||||
|
label = stringResource(R.string.change_password),
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割线
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(appColors.divider)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
NavItem(
|
||||||
|
iconRes = R.drawable.rider_pro_moment_delete,
|
||||||
|
label = stringResource(R.string.remove_account),
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
navController.navigate(NavigationRoute.RemoveAccountScreen.route)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(appColors.divider)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.aiosman.riderpro.ui.account
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
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.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -19,27 +20,26 @@ 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.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.riderpro.ConstVars
|
import com.aiosman.ravenow.ConstVars
|
||||||
import com.aiosman.riderpro.ErrorCode
|
import com.aiosman.ravenow.data.api.ErrorCode
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.riderpro.data.AccountService
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.riderpro.data.DictService
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.riderpro.data.DictServiceImpl
|
import com.aiosman.ravenow.data.DictService
|
||||||
import com.aiosman.riderpro.data.ServiceException
|
import com.aiosman.ravenow.data.DictServiceImpl
|
||||||
import com.aiosman.riderpro.getErrorMessageCode
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
import com.aiosman.riderpro.ui.composables.ActionButton
|
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.riderpro.ui.composables.TextInputField
|
import com.aiosman.ravenow.ui.composables.TextInputField
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ fun ResetPasswordScreen() {
|
|||||||
var usernameError by remember { mutableStateOf<String?>(null) }
|
var usernameError by remember { mutableStateOf<String?>(null) }
|
||||||
var countDown by remember { mutableStateOf<Int?>(null) }
|
var countDown by remember { mutableStateOf<Int?>(null) }
|
||||||
var countDownMax by remember { mutableStateOf(60) }
|
var countDownMax by remember { mutableStateOf(60) }
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
fun validate(): Boolean {
|
fun validate(): Boolean {
|
||||||
if (username.isEmpty()) {
|
if (username.isEmpty()) {
|
||||||
usernameError = context.getString(R.string.text_error_email_required)
|
usernameError = context.getString(R.string.text_error_email_required)
|
||||||
@@ -68,7 +69,7 @@ fun ResetPasswordScreen() {
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
try {
|
try {
|
||||||
dictService.getDictByKey(ConstVars.DIC_KEY_RESET_EMAIL_INTERVAL).let {
|
dictService.getDictByKey(ConstVars.DIC_KEY_RESET_EMAIL_INTERVAL).let {
|
||||||
countDownMax = it.value.toInt()
|
countDownMax = it.value as? Int ?: 60
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
countDownMax = 60
|
countDownMax = 60
|
||||||
@@ -111,7 +112,7 @@ fun ResetPasswordScreen() {
|
|||||||
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize().background(color = appColors.background)
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
Box(
|
Box(
|
||||||
@@ -148,7 +149,7 @@ fun ResetPasswordScreen() {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.reset_mail_send_success),
|
text = stringResource(R.string.reset_mail_send_success),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color(0xFF333333),
|
color = appColors.text,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
),
|
),
|
||||||
@@ -158,7 +159,7 @@ fun ResetPasswordScreen() {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.reset_mail_send_failed),
|
text = stringResource(R.string.reset_mail_send_failed),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color(0xFF333333),
|
color = appColors.text,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
),
|
),
|
||||||
@@ -177,8 +178,8 @@ fun ResetPasswordScreen() {
|
|||||||
} else {
|
} else {
|
||||||
stringResource(R.string.recover)
|
stringResource(R.string.recover)
|
||||||
},
|
},
|
||||||
backgroundColor = Color(0xffda3832),
|
backgroundColor = appColors.main,
|
||||||
color = Color.White,
|
color = appColors.mainText,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
enabled = countDown == null,
|
enabled = countDown == null,
|
||||||
@@ -194,7 +195,7 @@ fun ResetPasswordScreen() {
|
|||||||
text = stringResource(R.string.back_upper),
|
text = stringResource(R.string.back_upper),
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
) {
|
) {
|
||||||
navController.popBackStack()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import android.widget.Toast
|
||||||
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
|
||||||
@@ -15,16 +16,23 @@ 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.Color
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.riderpro.data.AccountService
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.riderpro.ui.composables.ActionButton
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.data.api.ErrorCode
|
||||||
import com.aiosman.riderpro.ui.composables.TextInputField
|
import com.aiosman.ravenow.data.api.showToast
|
||||||
|
import com.aiosman.ravenow.data.api.toErrorMessage
|
||||||
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.composables.TextInputField
|
||||||
|
import com.aiosman.ravenow.utils.PasswordValidator
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,34 +56,37 @@ class ChangePasswordViewModel {
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangePasswordScreen() {
|
fun ChangePasswordScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
val viewModel = remember { ChangePasswordViewModel() }
|
val viewModel = remember { ChangePasswordViewModel() }
|
||||||
var currentPassword by remember { mutableStateOf("") }
|
var currentPassword by remember { mutableStateOf("") }
|
||||||
var newPassword by remember { mutableStateOf("") }
|
var newPassword by remember { mutableStateOf("") }
|
||||||
var confirmPassword by remember { mutableStateOf("") }
|
var confirmPassword by remember { mutableStateOf("") }
|
||||||
var errorMessage by remember { mutableStateOf("") }
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
var oldPasswordError by remember { mutableStateOf<String?>(null) }
|
var oldPasswordError by remember { mutableStateOf<String?>(null) }
|
||||||
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
||||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
fun validate(): Boolean {
|
fun validate(): Boolean {
|
||||||
oldPasswordError =
|
// 使用通用密码校验器校验当前密码
|
||||||
if (currentPassword.isEmpty()) "Please enter your current password" else null
|
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
|
||||||
passwordError = when {
|
oldPasswordError = if (!currentPasswordValidation.isValid) currentPasswordValidation.errorMessage else null
|
||||||
newPassword.length < 8 -> "Password must be at least 8 characters long"
|
|
||||||
!newPassword.any { it.isDigit() } -> "Password must contain at least one digit"
|
// 使用通用密码校验器校验新密码
|
||||||
!newPassword.any { it.isUpperCase() } -> "Password must contain at least one uppercase letter"
|
val newPasswordValidation = PasswordValidator.validatePassword(newPassword, context)
|
||||||
!newPassword.any { it.isLowerCase() } -> "Password must contain at least one lowercase letter"
|
passwordError = if (!newPasswordValidation.isValid) newPasswordValidation.errorMessage else null
|
||||||
else -> null
|
|
||||||
}
|
// 使用通用密码确认校验器
|
||||||
confirmPasswordError =
|
val confirmPasswordValidation = PasswordValidator.validatePasswordConfirmation(newPassword, confirmPassword, context)
|
||||||
if (newPassword != confirmPassword) "Passwords do not match" else null
|
confirmPasswordError = if (!confirmPasswordValidation.isValid) confirmPasswordValidation.errorMessage else null
|
||||||
|
|
||||||
return passwordError == null && confirmPasswordError == null && oldPasswordError == null
|
return passwordError == null && confirmPasswordError == null && oldPasswordError == null
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.White),
|
.background(AppColors.background),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
@@ -83,7 +94,7 @@ fun ChangePasswordScreen() {
|
|||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(
|
NoticeScreenHeader(
|
||||||
title = "Change password",
|
title = stringResource(R.string.change_password),
|
||||||
moreIcon = false
|
moreIcon = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,8 +110,8 @@ fun ChangePasswordScreen() {
|
|||||||
text = currentPassword,
|
text = currentPassword,
|
||||||
onValueChange = { currentPassword = it },
|
onValueChange = { currentPassword = it },
|
||||||
password = true,
|
password = true,
|
||||||
label = "Current password",
|
label = stringResource(R.string.current_password),
|
||||||
hint = "Enter your current password",
|
hint = stringResource(R.string.current_password_tip5),
|
||||||
error = oldPasswordError
|
error = oldPasswordError
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
@@ -108,8 +119,8 @@ fun ChangePasswordScreen() {
|
|||||||
text = newPassword,
|
text = newPassword,
|
||||||
onValueChange = { newPassword = it },
|
onValueChange = { newPassword = it },
|
||||||
password = true,
|
password = true,
|
||||||
label = "New password",
|
label = stringResource(R.string.new_password),
|
||||||
hint = "Enter your new password",
|
hint = stringResource(R.string.new_password),
|
||||||
error = passwordError
|
error = passwordError
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
@@ -117,25 +128,31 @@ fun ChangePasswordScreen() {
|
|||||||
text = confirmPassword,
|
text = confirmPassword,
|
||||||
onValueChange = { confirmPassword = it },
|
onValueChange = { confirmPassword = it },
|
||||||
password = true,
|
password = true,
|
||||||
label = "Confirm new password",
|
label = stringResource(R.string.confirm_new_password_tip1),
|
||||||
hint = "Enter your new password again",
|
hint = stringResource(R.string.new_password_tip1),
|
||||||
error = confirmPasswordError
|
error = confirmPasswordError
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(50.dp))
|
Spacer(modifier = Modifier.height(50.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(345.dp)
|
.width(345.dp),
|
||||||
.height(48.dp),
|
text = stringResource(R.string.lets_ride_upper),
|
||||||
text = "Let's Ride",
|
|
||||||
backgroundImage = R.mipmap.rider_pro_signup_red_bg
|
|
||||||
) {
|
) {
|
||||||
if (validate()) {
|
if (validate()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
viewModel.changePassword(currentPassword, newPassword)
|
viewModel.changePassword(currentPassword, newPassword)
|
||||||
navController.popBackStack()
|
|
||||||
|
navController.navigateUp()
|
||||||
|
} catch (e: ServiceException) {
|
||||||
|
when (e.errorType) {
|
||||||
|
ErrorCode.IncorrectOldPassword ->
|
||||||
|
oldPasswordError = e.errorType.toErrorMessage(context)
|
||||||
|
else ->
|
||||||
|
e.errorType.showToast(context)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errorMessage = e.message ?: "An error occurred"
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.ui.account
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -34,14 +34,14 @@ import androidx.compose.ui.Modifier
|
|||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.riderpro.data.AccountService
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.riderpro.data.UploadImage
|
import com.aiosman.ravenow.data.UploadImage
|
||||||
import com.aiosman.riderpro.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile
|
import com.aiosman.ravenow.ui.post.NewPostViewModel.uriToFile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,7 +82,8 @@ fun AccountEditScreen() {
|
|||||||
var newAvatar: UploadImage? = null
|
var newAvatar: UploadImage? = null
|
||||||
cursor?.use { cur ->
|
cursor?.use { cur ->
|
||||||
if (cur.moveToFirst()) {
|
if (cur.moveToFirst()) {
|
||||||
val displayName = cur.getString(cur.getColumnIndex("_display_name"))
|
val columnIndex = cur.getColumnIndex("_display_name")
|
||||||
|
val displayName = if (columnIndex >= 0) cur.getString(columnIndex) else "unknown"
|
||||||
val extension = displayName.substringAfterLast(".")
|
val extension = displayName.substringAfterLast(".")
|
||||||
Log.d("NewPost", "File name: $displayName, extension: $extension")
|
Log.d("NewPost", "File name: $displayName, extension: $extension")
|
||||||
// read as file
|
// read as file
|
||||||
@@ -98,7 +99,8 @@ fun AccountEditScreen() {
|
|||||||
var newBanner: UploadImage? = null
|
var newBanner: UploadImage? = null
|
||||||
cursor?.use { cur ->
|
cursor?.use { cur ->
|
||||||
if (cur.moveToFirst()) {
|
if (cur.moveToFirst()) {
|
||||||
val displayName = cur.getString(cur.getColumnIndex("_display_name"))
|
val columnIndex = cur.getColumnIndex("_display_name")
|
||||||
|
val displayName = if (columnIndex >= 0) cur.getString(columnIndex) else "unknown"
|
||||||
val extension = displayName.substringAfterLast(".")
|
val extension = displayName.substringAfterLast(".")
|
||||||
Log.d("NewPost", "File name: $displayName, extension: $extension")
|
Log.d("NewPost", "File name: $displayName, extension: $extension")
|
||||||
// read as file
|
// read as file
|
||||||
261
app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt
Normal file
261
app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.composables.form.FormTextInput
|
||||||
|
import com.aiosman.ravenow.ui.composables.debouncedClickable
|
||||||
|
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑用户资料界面
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AccountEditScreen2() {
|
||||||
|
val model = AccountEditViewModel
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
var usernameError by remember { mutableStateOf<String?>(null) }
|
||||||
|
var bioError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
// 防抖导航器
|
||||||
|
val debouncedNavigation = rememberDebouncedNavigation()
|
||||||
|
fun onNicknameChange(value: String) {
|
||||||
|
// 去除换行符,确保昵称不包含换行
|
||||||
|
val cleanValue = value.replace("\n", "").replace("\r", "")
|
||||||
|
model.name = cleanValue
|
||||||
|
usernameError = when {
|
||||||
|
cleanValue.trim().isEmpty() -> "昵称不能为空"
|
||||||
|
cleanValue.length < 3 -> "昵称长度不能小于3"
|
||||||
|
cleanValue.length > 20 -> "昵称长度不能大于20"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
fun onBioChange(value: String) {
|
||||||
|
// 去除换行符,确保个人简介不包含换行
|
||||||
|
val cleanValue = value.replace("\n", "").replace("\r", "")
|
||||||
|
model.bio = cleanValue
|
||||||
|
bioError = when {
|
||||||
|
cleanValue.length > 100 -> "个人简介长度不能大于100"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validate(): Boolean {
|
||||||
|
return usernameError == null && bioError == null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为游客模式
|
||||||
|
if (AppStore.isGuest) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// 游客模式不允许编辑资料,返回上一页(防抖)
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 游客模式时不渲染任何内容
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// 每次进入编辑页面时都重新加载当前用户的资料
|
||||||
|
// 确保显示的是当前登录用户的信息,而不是之前用户的缓存数据
|
||||||
|
model.reloadProfile()
|
||||||
|
}
|
||||||
|
StatusBarMaskLayout(
|
||||||
|
modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp),
|
||||||
|
darkIcons = !AppState.darkMode,
|
||||||
|
maskBoxBackgroundColor = appColors.background
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color = appColors.background),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
//StatusBarSpacer()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 0.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
NoticeScreenHeader(
|
||||||
|
title = stringResource(R.string.edit_profile),
|
||||||
|
moreIcon = false
|
||||||
|
) {
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.debouncedClickable(
|
||||||
|
enabled = validate() && !model.isUpdating,
|
||||||
|
debounceTime = 1000L
|
||||||
|
) {
|
||||||
|
if (validate() && !model.isUpdating) {
|
||||||
|
model.viewModelScope.launch {
|
||||||
|
model.isUpdating = true
|
||||||
|
model.updateUserProfile(context)
|
||||||
|
model.viewModelScope.launch(Dispatchers.Main) {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
model.isUpdating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = "保存",
|
||||||
|
tint = if (validate() && !model.isUpdating) appColors.text else appColors.nonActiveText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(44.dp))
|
||||||
|
|
||||||
|
// 显示内容或加载状态
|
||||||
|
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
|
||||||
|
when {
|
||||||
|
model.profile != null -> {
|
||||||
|
Log.d("AccountEditScreen2", "显示用户资料内容")
|
||||||
|
// 有数据时显示内容
|
||||||
|
val it = model.profile!!
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(88.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
model.croppedBitmap ?: it.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(88.dp)
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(88.dp)
|
||||||
|
),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(appColors.main)
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.debouncedClickable(
|
||||||
|
debounceTime = 800L
|
||||||
|
) {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.ImageCrop.route)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = "Add",
|
||||||
|
tint = Color.White,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(58.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
FormTextInput(
|
||||||
|
value = model.name,
|
||||||
|
label = stringResource(R.string.nickname),
|
||||||
|
hint = "Input nickname",
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
error = usernameError
|
||||||
|
) { value ->
|
||||||
|
onNicknameChange(value)
|
||||||
|
}
|
||||||
|
FormTextInput(
|
||||||
|
value = model.bio,
|
||||||
|
label = stringResource(R.string.bio),
|
||||||
|
hint = "Input bio",
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
error = bioError
|
||||||
|
) { value ->
|
||||||
|
onBioChange(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.isLoading -> {
|
||||||
|
Log.d("AccountEditScreen2", "显示加载指示器")
|
||||||
|
// 加载中状态
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.CircularProgressIndicator(
|
||||||
|
color = appColors.main
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d("AccountEditScreen2", "显示错误信息 - 没有数据且不在加载中")
|
||||||
|
// 没有数据且不在加载中,显示错误信息
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "加载用户资料失败,请重试",
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
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.AppState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.Messaging
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.data.api.ErrorCode
|
||||||
|
import com.aiosman.ravenow.data.api.showToast
|
||||||
|
import com.aiosman.ravenow.data.api.toErrorMessage
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.composables.TextInputField
|
||||||
|
import com.aiosman.ravenow.utils.PasswordValidator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RemoveAccountScreen() {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
var inputPassword by remember { mutableStateOf("") }
|
||||||
|
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
fun removeAccount(password: String) {
|
||||||
|
// 使用通用密码校验器
|
||||||
|
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
||||||
|
if (!passwordValidation.isValid) {
|
||||||
|
passwordError = passwordValidation.errorMessage
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val accountService = AccountServiceImpl()
|
||||||
|
accountService.removeAccount(password)
|
||||||
|
Messaging.unregisterDevice(context)
|
||||||
|
AppStore.apply {
|
||||||
|
token = null
|
||||||
|
rememberMe = false
|
||||||
|
saveData()
|
||||||
|
}
|
||||||
|
//返回到登录页面
|
||||||
|
navController.navigate(NavigationRoute.Login.route) {
|
||||||
|
popUpTo(NavigationRoute.Login.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//重置AppState
|
||||||
|
AppState.ReloadAppState(context)
|
||||||
|
Toast.makeText(context, "Account has been deleted", Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: ServiceException) {
|
||||||
|
passwordError = "Incorrect password"
|
||||||
|
// e.errorType.showToast(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Toast.makeText(context, "An error occurred. Please try again.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(appColors.background),
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
NoticeScreenHeader(
|
||||||
|
title = stringResource(R.string.remove_account),
|
||||||
|
moreIcon = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
|
.background(appColors.background),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 32.dp, top = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.remove_account_desc),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextInputField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = inputPassword,
|
||||||
|
onValueChange = {
|
||||||
|
inputPassword = it
|
||||||
|
if (passwordError != null) {
|
||||||
|
passwordError = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
password = true,
|
||||||
|
hint = stringResource(R.string.remove_account_password_hint),
|
||||||
|
error = passwordError
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
ActionButton(
|
||||||
|
text = stringResource(R.string.remove_account),
|
||||||
|
fullWidth = true,
|
||||||
|
enabled = true,
|
||||||
|
click = {
|
||||||
|
removeAccount(inputPassword)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
296
app/src/main/java/com/aiosman/ravenow/ui/agent/AddAgent.kt
Normal file
296
app/src/main/java/com/aiosman/ravenow/ui/agent/AddAgent.kt
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
package com.aiosman.ravenow.ui.agent
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
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.activity.compose.BackHandler
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.account.AccountEditViewModel
|
||||||
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
import com.aiosman.ravenow.ui.comment.ScreenHeader
|
||||||
|
import com.aiosman.ravenow.ui.comment.ScreenHeader2
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.composables.form.FormTextInput
|
||||||
|
import com.aiosman.ravenow.ui.composables.form.FormTextInput2
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加智能体界面
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AddAgentScreen() {
|
||||||
|
val model = AddAgentViewModel
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
var agnetNameError by remember { mutableStateOf<String?>(null) }
|
||||||
|
var agnetDescError by remember { mutableStateOf<String?>(null) }
|
||||||
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun onNameChange(value: String) {
|
||||||
|
model.name = value.trim()
|
||||||
|
agnetNameError = when {
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
fun onDescChange(value: String) {
|
||||||
|
model.desc = value.trim()
|
||||||
|
agnetDescError = when {
|
||||||
|
value.length > 100 -> "简介长度不能大于100"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validate(): Boolean {
|
||||||
|
return agnetNameError == null && agnetDescError == null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 处理系统返回键
|
||||||
|
BackHandler {
|
||||||
|
// 如果不是在选择头像过程中,则清空数据
|
||||||
|
if (!model.isSelectingAvatar) {
|
||||||
|
model.clearData()
|
||||||
|
}
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面进入时重置头像选择状态
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
model.isSelectingAvatar = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color = appColors.decentBackground),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
|
.background(color = appColors.decentBackground)
|
||||||
|
) {
|
||||||
|
// 自定义header,控制返回按钮行为
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "返回",
|
||||||
|
modifier = Modifier.size(24.dp).clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 与BackHandler保持一致的逻辑
|
||||||
|
if (!model.isSelectingAvatar) {
|
||||||
|
model.clearData()
|
||||||
|
}
|
||||||
|
navController.navigateUp()
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.agent_add),
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
// 提交创建智能体的逻辑可以在这里实现
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = "Add",
|
||||||
|
tint = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(44.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(88.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
model.croppedBitmap,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(88.dp)
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(88.dp)
|
||||||
|
),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
placeholderRes = R.mipmap.rider_pro_agent_avatar
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(appColors.main)
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.noRippleClickable {
|
||||||
|
// 设置正在选择头像的标志
|
||||||
|
model.isSelectingAvatar = true
|
||||||
|
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = "Add",
|
||||||
|
tint = Color.White,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(58.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
FormTextInput(
|
||||||
|
value = model.name,
|
||||||
|
label = stringResource(R.string.agent_name),
|
||||||
|
hint = stringResource(R.string.agent_name_hint),
|
||||||
|
background = appColors.inputBackground2,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { value ->
|
||||||
|
onNameChange(value)
|
||||||
|
}
|
||||||
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
FormTextInput2(
|
||||||
|
value = model.desc,
|
||||||
|
label = stringResource(R.string.agent_desc),
|
||||||
|
hint = stringResource(R.string.agent_desc_hint),
|
||||||
|
background = appColors.inputBackground2,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { value ->
|
||||||
|
onDescChange(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(58.dp))
|
||||||
|
|
||||||
|
// 错误信息显示
|
||||||
|
errorMessage?.let { error ->
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = Color.Red,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(345.dp)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFFEE2A33),
|
||||||
|
Color(0xFFD80264),
|
||||||
|
Color(0xFF8468BC)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
),
|
||||||
|
color = Color.White,
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
text = stringResource(R.string.agent_create),
|
||||||
|
isLoading = model.isUpdating,
|
||||||
|
loadingText = stringResource(R.string.agent_createing),
|
||||||
|
enabled = !model.isUpdating && validate()
|
||||||
|
) {
|
||||||
|
// 验证输入
|
||||||
|
val validationError = model.validate()
|
||||||
|
if (validationError != null) {
|
||||||
|
// 显示验证错误
|
||||||
|
errorMessage = validationError
|
||||||
|
return@ActionButton
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除之前的错误信息
|
||||||
|
errorMessage = null
|
||||||
|
|
||||||
|
// 调用创建智能体API
|
||||||
|
model.viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val result = model.createAgent(context)
|
||||||
|
if (result != null) {
|
||||||
|
// 创建成功,清空数据并关闭页面
|
||||||
|
model.clearData()
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 显示错误信息
|
||||||
|
errorMessage = "创建智能体失败: ${e.message}"
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.aiosman.ravenow.ui.agent
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.UploadImage
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
|
import com.aiosman.ravenow.entity.createAgent
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
|
import com.aiosman.ravenow.utils.TrtcHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object AddAgentViewModel : ViewModel() {
|
||||||
|
var name by mutableStateOf("")
|
||||||
|
var desc by mutableStateOf("")
|
||||||
|
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
||||||
|
var isUpdating by mutableStateOf(false)
|
||||||
|
var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像
|
||||||
|
var hasExitedPage by mutableStateOf(false) // 标记是否已经完全退出页面
|
||||||
|
|
||||||
|
// 保存AI生成的输入文本,避免页面重建时丢失
|
||||||
|
var generateInputText by mutableStateOf("")
|
||||||
|
|
||||||
|
suspend fun updateAgentAvatar(context: Context) {
|
||||||
|
croppedBitmap?.let {
|
||||||
|
val file = File(context.cacheDir, "agent_avatar.jpg")
|
||||||
|
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||||
|
// 这里可以上传图片到服务器,暂时先保存到本地
|
||||||
|
// UploadImage(file, "agent_avatar.jpg", "", "jpg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createAgent(context: Context): AgentEntity? {
|
||||||
|
try {
|
||||||
|
isUpdating = true
|
||||||
|
|
||||||
|
// 准备头像文件
|
||||||
|
val avatarFile = if (croppedBitmap != null) {
|
||||||
|
val file = File(context.cacheDir, "agent_avatar.jpg")
|
||||||
|
croppedBitmap!!.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||||
|
UploadImage(file, "agent_avatar.jpg", "", "jpg")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API创建智能体
|
||||||
|
val result = createAgent(
|
||||||
|
title = name,
|
||||||
|
desc = desc,
|
||||||
|
avatar = avatarFile
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
isUpdating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun validate(): String? {
|
||||||
|
return when {
|
||||||
|
name.isEmpty() -> "智能体名称不能为空"
|
||||||
|
name.length < 2 -> "智能体名称长度不能少于2个字符"
|
||||||
|
name.length > 20 -> "智能体名称长度不能超过20个字符"
|
||||||
|
desc.isEmpty() -> "智能体描述不能为空"
|
||||||
|
desc.length > 100 -> "智能体描述长度不能超过100个字符"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有页面数据
|
||||||
|
*/
|
||||||
|
fun clearData() {
|
||||||
|
name = ""
|
||||||
|
desc = ""
|
||||||
|
croppedBitmap = null
|
||||||
|
isUpdating = false
|
||||||
|
isSelectingAvatar = false
|
||||||
|
hasExitedPage = false
|
||||||
|
generateInputText = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package com.aiosman.ravenow.ui.agent
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
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.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.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import com.image.cropview.CropType
|
||||||
|
import com.image.cropview.EdgeType
|
||||||
|
import com.image.cropview.ImageCrop
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专门用于智能体头像裁剪的页面
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AgentImageCropScreen() {
|
||||||
|
var imageCrop by remember { mutableStateOf<ImageCrop?>(null) }
|
||||||
|
var croppedBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
var imageWidthInDp by remember { mutableStateOf(0) }
|
||||||
|
var imageHeightInDp by remember { mutableStateOf(0) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
val imagePickLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri: Uri? ->
|
||||||
|
uri?.let {
|
||||||
|
val bitmap = uriToBitmap(context = context, uri = it)
|
||||||
|
if (bitmap != null) {
|
||||||
|
val aspectRatio = bitmap.height.toFloat() / bitmap.width.toFloat()
|
||||||
|
imageHeightInDp = (imageWidthInDp.toFloat() * aspectRatio).toInt()
|
||||||
|
imageCrop = ImageCrop(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uri == null) {
|
||||||
|
// 用户取消选择图片,重置标志
|
||||||
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
systemUiController.setStatusBarColor(darkIcons = false, color = Color.Black)
|
||||||
|
imagePickLauncher.launch("image/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
imageCrop = null
|
||||||
|
val isDarkMode = AppState.darkMode
|
||||||
|
systemUiController.setStatusBarColor(
|
||||||
|
darkIcons = !isDarkMode,
|
||||||
|
color = if(isDarkMode)Color.Black else Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.background(Color.Black).fillMaxSize()
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
|
||||||
|
// 头部工具栏
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
// 用户取消头像选择,重置标志
|
||||||
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(Color.White)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
// 确认按钮
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (croppedBitmap != null) Color.Green else Color.White,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
if (croppedBitmap != null) {
|
||||||
|
// 如果已经有裁剪结果,直接返回
|
||||||
|
AddAgentViewModel.croppedBitmap = croppedBitmap
|
||||||
|
// 重置头像选择标志
|
||||||
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
|
AddAgentViewModel.viewModelScope.launch {
|
||||||
|
AddAgentViewModel.updateAgentAvatar(context)
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 进行裁剪
|
||||||
|
imageCrop?.let {
|
||||||
|
val bitmap = it.onCrop()
|
||||||
|
croppedBitmap = bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 裁剪预览区域
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(24.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(imageHeightInDp.dp)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
with(density) {
|
||||||
|
imageWidthInDp = coordinates.size.width.toDp().value.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
imageCrop?.ImageCropView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
guideLineColor = Color.White,
|
||||||
|
guideLineWidth = 2.dp,
|
||||||
|
edgeCircleSize = 5.dp,
|
||||||
|
cropType = CropType.SQUARE,
|
||||||
|
edgeType = EdgeType.CIRCULAR
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 裁剪结果预览
|
||||||
|
croppedBitmap?.let { bitmap ->
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
Color.Black.copy(alpha = 0.8f),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "✅ 裁剪完成",
|
||||||
|
color = Color.Green,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
bitmap,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.Gray.copy(alpha = 0.3f), CircleShape),
|
||||||
|
contentDescription = "智能体头像预览",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "点击 ✓ 确认使用此头像",
|
||||||
|
color = Color.White.copy(alpha = 0.8f),
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||||
|
BitmapFactory.decodeStream(inputStream)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
648
app/src/main/java/com/aiosman/ravenow/ui/agent/CreateAgentV2.kt
Normal file
648
app/src/main/java/com/aiosman/ravenow/ui/agent/CreateAgentV2.kt
Normal file
@@ -0,0 +1,648 @@
|
|||||||
|
package com.aiosman.ravenow.ui.agent
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingDots(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
dotColor: Color = Color.Gray
|
||||||
|
) {
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "loading_dots")
|
||||||
|
|
||||||
|
val animationValues = (0..2).map { index ->
|
||||||
|
infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 600,
|
||||||
|
easing = EaseInOut,
|
||||||
|
delayMillis = index * 200
|
||||||
|
),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "dot_$index"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
animationValues.forEach { animValue ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(6.dp)
|
||||||
|
.offset(y = (-8 * animValue.value).dp)
|
||||||
|
.background(
|
||||||
|
color = dotColor.copy(alpha = 0.5f + 0.5f * animValue.value),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CreateAgentV2Screen(
|
||||||
|
viewModel: CreateAgentV2ViewModel = remember { CreateAgentV2ViewModel() }
|
||||||
|
) {
|
||||||
|
// 页面进入时的状态管理
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// 总是先同步状态
|
||||||
|
viewModel.syncStateOnResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面退出时的处理
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
// 页面退出时,标记为已退出(除非是在选择头像)
|
||||||
|
if (!viewModel.isSelectingAvatar) {
|
||||||
|
viewModel.markPageExited()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// 获取当前用户名,如果没有则使用默认值
|
||||||
|
val userName = AppState.profile?.nickName ?: "用户"
|
||||||
|
|
||||||
|
// 渐变边框旋转动画
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "gradient_rotation")
|
||||||
|
val rotationAngle by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 360f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 16000,
|
||||||
|
easing = LinearEasing
|
||||||
|
),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
),
|
||||||
|
label = "rotation_angle"
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(appColors.background)
|
||||||
|
) {
|
||||||
|
// 状态栏占位
|
||||||
|
StatusBarSpacer()
|
||||||
|
|
||||||
|
// 顶部标题栏
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 返回按钮
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "返回",
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// 标题 - 左对齐
|
||||||
|
Text(
|
||||||
|
text = "创建AI",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.W700,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主要内容区域 - 可滚动
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.Start
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
|
// AI头像图标
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(
|
||||||
|
color = appColors.inputBackground,
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_create_head_logo),
|
||||||
|
contentDescription = "AI头像",
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 问候语
|
||||||
|
Text(
|
||||||
|
text = "$userName 你好呀!今天想创建什么?",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = appColors.text,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 描述性文字
|
||||||
|
Text(
|
||||||
|
text = "只需要一句话,你的专属AI在这里诞生",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = appColors.secondaryText,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
|
// 根据模式显示不同的UI
|
||||||
|
if (!viewModel.isManualMode) {
|
||||||
|
// AI生成模式 - 渐变边框输入框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.shadow(
|
||||||
|
elevation = 8.dp,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
ambientColor = Color(0xFF6246ff).copy(alpha = 0.4f),
|
||||||
|
spotColor = Color(0xFFd80264).copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 渐变边框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF6246ff), // 紫色
|
||||||
|
Color(0xFFd80264), // 红色
|
||||||
|
Color(0xFF6246ff), // 紫色
|
||||||
|
Color(0xFFd80264) // 红色
|
||||||
|
),
|
||||||
|
start = Offset(
|
||||||
|
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
|
||||||
|
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
|
||||||
|
),
|
||||||
|
end = Offset(
|
||||||
|
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
|
||||||
|
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
.padding(1.5.dp) // 边框宽度
|
||||||
|
) {
|
||||||
|
// 内部输入框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = appColors.background,
|
||||||
|
shape = RoundedCornerShape(15.dp)
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
TextField(
|
||||||
|
value = viewModel.inputText,
|
||||||
|
onValueChange = {
|
||||||
|
if (!viewModel.isGenerating) {
|
||||||
|
viewModel.updateInputText(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "一个会写诗的AI,一个会懂你笑点的AI",
|
||||||
|
color = appColors.inputHint,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
focusedTextColor = appColors.text,
|
||||||
|
unfocusedTextColor = appColors.text,
|
||||||
|
disabledTextColor = appColors.inputHint,
|
||||||
|
cursorColor = if (viewModel.isGenerating) Color.Transparent else appColors.main,
|
||||||
|
focusedPlaceholderColor = appColors.inputHint,
|
||||||
|
unfocusedPlaceholderColor = appColors.inputHint,
|
||||||
|
disabledPlaceholderColor = appColors.inputHint.copy(alpha = 0.5f)
|
||||||
|
),
|
||||||
|
enabled = !viewModel.isGenerating,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 100.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// AI美化按钮
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (!viewModel.isGenerating) {
|
||||||
|
viewModel.generateAgentInfo()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = viewModel.canGenerate() && !viewModel.isGenerating,
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = if (viewModel.isGenerating) appColors.inputHint else Color(0xFF7c46ed),
|
||||||
|
disabledContentColor = appColors.inputHint
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_create_agent_generate),
|
||||||
|
contentDescription = "AI美化图标",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color(0xFF7c46ed))
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (viewModel.isGenerating) "生成中..." else "AI美化",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// AI生成中的loading状态
|
||||||
|
if (viewModel.isGenerating) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
LoadingDots(
|
||||||
|
dotColor = appColors.main
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "正在为你构思",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = appColors.secondaryText,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动创造AI按钮 - 只在非生成状态下显示
|
||||||
|
if (!viewModel.isGenerating) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
if (!viewModel.isGenerating) {
|
||||||
|
viewModel.enableManualMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !viewModel.isGenerating,
|
||||||
|
modifier = Modifier.height(40.dp),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
border = BorderStroke(1.dp, appColors.inputHint.copy(alpha = 0.3f)),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = appColors.secondaryText,
|
||||||
|
disabledContentColor = appColors.inputHint
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "编辑图标",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = appColors.secondaryText
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "手动创造AI",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 手动模式 - "一句话创造AI"按钮
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
ambientColor = Color(0xFF6246ff).copy(alpha = 0.3f),
|
||||||
|
spotColor = Color(0xFFd80264).copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 渐变边框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF6246ff), // 紫色
|
||||||
|
Color(0xFFd80264), // 红色
|
||||||
|
Color(0xFF6246ff), // 紫色
|
||||||
|
Color(0xFFd80264) // 红色
|
||||||
|
),
|
||||||
|
start = Offset(
|
||||||
|
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
|
||||||
|
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
|
||||||
|
),
|
||||||
|
end = Offset(
|
||||||
|
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
|
||||||
|
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.padding(1.dp) // 边框宽度
|
||||||
|
) {
|
||||||
|
// 内部按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = appColors.background,
|
||||||
|
shape = RoundedCornerShape(11.dp)
|
||||||
|
)
|
||||||
|
.noRippleClickable {
|
||||||
|
viewModel.disableManualMode()
|
||||||
|
}
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "编辑图标",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = appColors.secondaryText
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "一句话创造AI",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = appColors.secondaryText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成结果显示区域
|
||||||
|
if (viewModel.hasGeneratedResult()) {
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 头像选择组件
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
viewModel.setSelectingAvatar(true)
|
||||||
|
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (viewModel.croppedBitmap != null) {
|
||||||
|
// 有头像时显示头像
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
viewModel.croppedBitmap,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentDescription = "AI头像",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
placeholderRes = R.mipmap.rider_pro_agent_avatar,
|
||||||
|
showShimmer = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 没有头像时显示渐变背景和编辑图标
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
0f to Color(0xFF7c45ed),
|
||||||
|
0.24f to Color(0xFF7c68ef),
|
||||||
|
1f to Color(0xFF7bd8f8)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "选择头像",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 标题输入框
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "名称",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = appColors.text,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = viewModel.agentTitle,
|
||||||
|
onValueChange = { viewModel.updateAgentTitle(it) },
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = appColors.inputBackground,
|
||||||
|
unfocusedContainerColor = appColors.inputBackground,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
focusedTextColor = appColors.text,
|
||||||
|
unfocusedTextColor = appColors.text
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 描述输入框
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "描述",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = appColors.text,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = viewModel.agentDescription,
|
||||||
|
onValueChange = { viewModel.updateAgentDescription(it) },
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = appColors.inputBackground,
|
||||||
|
unfocusedContainerColor = appColors.inputBackground,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
focusedTextColor = appColors.text,
|
||||||
|
unfocusedTextColor = appColors.text
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 120.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 错误信息显示
|
||||||
|
viewModel.errorMessage?.let { error ->
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = Color.Red,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建AI按钮
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7bd8f8),
|
||||||
|
Color(0xFF7c68ef),
|
||||||
|
Color(0xFF7c45ed)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
),
|
||||||
|
color = Color.White,
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
text = "好的,就它了",
|
||||||
|
isLoading = viewModel.isCreating,
|
||||||
|
loadingText = "创建中...",
|
||||||
|
enabled = viewModel.canCreate()
|
||||||
|
) {
|
||||||
|
viewModel.createAgent(context) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package com.aiosman.ravenow.ui.agent
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.data.api.GenerateAgentInfoRequestBody
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class CreateAgentV2ViewModel : ViewModel() {
|
||||||
|
// UI状态
|
||||||
|
var inputText by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
var agentTitle by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
var agentDescription by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isGenerating by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isCreating by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isManualMode by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
// 临时保存的生成结果,用于在生成过程中暂时隐藏当前结果
|
||||||
|
private var tempAgentTitle by mutableStateOf("")
|
||||||
|
private var tempAgentDescription by mutableStateOf("")
|
||||||
|
|
||||||
|
// AddAgentViewModel实例,用于头像和创建逻辑
|
||||||
|
private val addAgentViewModel = AddAgentViewModel
|
||||||
|
|
||||||
|
// 获取头像相关状态
|
||||||
|
val croppedBitmap get() = addAgentViewModel.croppedBitmap
|
||||||
|
val isSelectingAvatar get() = addAgentViewModel.isSelectingAvatar
|
||||||
|
|
||||||
|
init {
|
||||||
|
// 初始化时检查是否需要恢复状态
|
||||||
|
if (addAgentViewModel.hasExitedPage) {
|
||||||
|
// 如果之前已经完全退出页面,清空所有数据
|
||||||
|
addAgentViewModel.clearData()
|
||||||
|
} else {
|
||||||
|
// 否则恢复已有状态(包括从头像选择回来的情况)
|
||||||
|
if (addAgentViewModel.name.isNotEmpty()) {
|
||||||
|
agentTitle = addAgentViewModel.name
|
||||||
|
}
|
||||||
|
if (addAgentViewModel.desc.isNotEmpty()) {
|
||||||
|
agentDescription = addAgentViewModel.desc
|
||||||
|
}
|
||||||
|
// 恢复输入文本
|
||||||
|
if (addAgentViewModel.generateInputText.isNotEmpty()) {
|
||||||
|
inputText = addAgentViewModel.generateInputText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateInputText(text: String) {
|
||||||
|
inputText = text
|
||||||
|
addAgentViewModel.generateInputText = text // 同时保存到AddAgentViewModel
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAgentTitle(title: String) {
|
||||||
|
agentTitle = title
|
||||||
|
syncToAddAgentViewModel()
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAgentDescription(description: String) {
|
||||||
|
agentDescription = description
|
||||||
|
syncToAddAgentViewModel()
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearError() {
|
||||||
|
errorMessage = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncToAddAgentViewModel() {
|
||||||
|
addAgentViewModel.name = agentTitle
|
||||||
|
addAgentViewModel.desc = agentDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectingAvatar(isSelecting: Boolean) {
|
||||||
|
addAgentViewModel.isSelectingAvatar = isSelecting
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markPageExited() {
|
||||||
|
addAgentViewModel.hasExitedPage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncStateOnResume() {
|
||||||
|
// 如果之前在选择头像,现在回来了,重置选择状态
|
||||||
|
if (addAgentViewModel.isSelectingAvatar) {
|
||||||
|
addAgentViewModel.isSelectingAvatar = false
|
||||||
|
// 从头像选择页面回来,恢复文本状态
|
||||||
|
if (addAgentViewModel.name.isNotEmpty()) {
|
||||||
|
agentTitle = addAgentViewModel.name
|
||||||
|
}
|
||||||
|
if (addAgentViewModel.desc.isNotEmpty()) {
|
||||||
|
agentDescription = addAgentViewModel.desc
|
||||||
|
}
|
||||||
|
if (addAgentViewModel.generateInputText.isNotEmpty()) {
|
||||||
|
inputText = addAgentViewModel.generateInputText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableManualMode() {
|
||||||
|
isManualMode = true
|
||||||
|
// 手动模式下,如果没有现有内容,初始化为空
|
||||||
|
if (agentTitle.isEmpty() && agentDescription.isEmpty()) {
|
||||||
|
agentTitle = ""
|
||||||
|
agentDescription = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableManualMode() {
|
||||||
|
isManualMode = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateAgentInfo() {
|
||||||
|
if (inputText.isBlank() || isGenerating) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isGenerating = true
|
||||||
|
clearError()
|
||||||
|
|
||||||
|
// 开始生成时,暂存当前结果并清空显示
|
||||||
|
tempAgentTitle = agentTitle
|
||||||
|
tempAgentDescription = agentDescription
|
||||||
|
agentTitle = ""
|
||||||
|
agentDescription = ""
|
||||||
|
|
||||||
|
val response = ApiClient.longTimeoutApi.generateAgentInfo(
|
||||||
|
GenerateAgentInfoRequestBody(inputText)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val data = response.body()?.data
|
||||||
|
data?.let {
|
||||||
|
// 成功时,使用新结果
|
||||||
|
agentTitle = it.title
|
||||||
|
agentDescription = it.description
|
||||||
|
syncToAddAgentViewModel()
|
||||||
|
// 清空临时保存
|
||||||
|
tempAgentTitle = ""
|
||||||
|
tempAgentDescription = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 失败时,恢复之前的结果
|
||||||
|
agentTitle = tempAgentTitle
|
||||||
|
agentDescription = tempAgentDescription
|
||||||
|
tempAgentTitle = ""
|
||||||
|
tempAgentDescription = ""
|
||||||
|
errorMessage = "生成失败,请重试"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 异常时,恢复之前的结果
|
||||||
|
agentTitle = tempAgentTitle
|
||||||
|
agentDescription = tempAgentDescription
|
||||||
|
tempAgentTitle = ""
|
||||||
|
tempAgentDescription = ""
|
||||||
|
errorMessage = "网络错误: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isGenerating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createAgent(context: Context, onSuccess: () -> Unit) {
|
||||||
|
if (isCreating) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isCreating = true
|
||||||
|
clearError()
|
||||||
|
|
||||||
|
// 验证输入
|
||||||
|
val validationError = addAgentViewModel.validate()
|
||||||
|
if (validationError != null) {
|
||||||
|
errorMessage = validationError
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用创建智能体API
|
||||||
|
val result = addAgentViewModel.createAgent(context)
|
||||||
|
if (result != null) {
|
||||||
|
// 创建成功,清空数据
|
||||||
|
clearData()
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
errorMessage = "创建失败,请重试"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = "创建智能体失败: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isCreating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearData() {
|
||||||
|
inputText = ""
|
||||||
|
agentTitle = ""
|
||||||
|
agentDescription = ""
|
||||||
|
errorMessage = null
|
||||||
|
isGenerating = false
|
||||||
|
isCreating = false
|
||||||
|
addAgentViewModel.clearData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以创建
|
||||||
|
fun canCreate(): Boolean {
|
||||||
|
return !isCreating && agentTitle.isNotBlank() && agentDescription.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以生成
|
||||||
|
fun canGenerate(): Boolean {
|
||||||
|
return !isGenerating && inputText.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有生成结果或处于手动模式
|
||||||
|
fun hasGeneratedResult(): Boolean {
|
||||||
|
return agentTitle.isNotEmpty() || agentDescription.isNotEmpty() || isManualMode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.UserService
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import io.openim.android.sdk.OpenIMClient
|
||||||
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
|
import io.openim.android.sdk.enums.ViewType
|
||||||
|
import io.openim.android.sdk.listener.OnAdvanceMsgListener
|
||||||
|
import io.openim.android.sdk.listener.OnBase
|
||||||
|
import io.openim.android.sdk.listener.OnMsgSendCallback
|
||||||
|
import io.openim.android.sdk.models.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天ViewModel基类,包含所有聊天功能的通用实现
|
||||||
|
* 子类需要实现抽象方法来处理特定的聊天类型(单聊/群聊)
|
||||||
|
*/
|
||||||
|
abstract class BaseChatViewModel : ViewModel() {
|
||||||
|
|
||||||
|
// 通用状态属性
|
||||||
|
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
|
||||||
|
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
var hasMore by mutableStateOf(true)
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var lastMessage: Message? = null
|
||||||
|
val showTimestampMap = mutableMapOf<String, Boolean>()
|
||||||
|
var goToNew by mutableStateOf(false)
|
||||||
|
var conversationID: String = "" // 会话ID,通过getOneConversation初始化
|
||||||
|
|
||||||
|
// 通用服务
|
||||||
|
val userService: UserService = UserServiceImpl()
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
var textMessageListener: OnAdvanceMsgListener? = null
|
||||||
|
|
||||||
|
val fetchHistorySize = 20
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化方法,子类需要实现具体的初始化逻辑
|
||||||
|
*/
|
||||||
|
abstract fun init(context: Context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志标签,子类需要实现
|
||||||
|
*/
|
||||||
|
abstract fun getLogTag(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话参数,子类需要实现
|
||||||
|
* @return Triple(targetId, conversationType, isSingleChat)
|
||||||
|
*/
|
||||||
|
abstract fun getConversationParams(): Triple<String, Int, Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理接收到的新消息,子类可以重写以添加特定逻辑
|
||||||
|
*/
|
||||||
|
open fun handleNewMessage(message: Message, context: Context): Boolean {
|
||||||
|
return false // 默认不处理,子类重写
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取发送消息时的接收者ID,子类需要实现
|
||||||
|
*/
|
||||||
|
abstract fun getReceiverInfo(): Pair<String?, String?> // (recvID, groupID)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息成功后的额外处理,子类可以重写
|
||||||
|
*/
|
||||||
|
open fun onMessageSentSuccess(message: String, sentMessage: Message?) {
|
||||||
|
// 默认无额外处理,子类可以重写
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话信息并初始化conversationID
|
||||||
|
*/
|
||||||
|
fun getOneConversation(onSuccess: (() -> Unit)? = null) {
|
||||||
|
val (targetId, conversationType, isSingleChat) = getConversationParams()
|
||||||
|
|
||||||
|
OpenIMClient.getInstance().conversationManager.getOneConversation(
|
||||||
|
object : OnBase<ConversationInfo> {
|
||||||
|
override fun onError(code: Int, error: String) {
|
||||||
|
Log.e(getLogTag(), "getOneConversation error: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: ConversationInfo) {
|
||||||
|
conversationID = data.conversationID
|
||||||
|
Log.d(getLogTag(), "获取会话信息成功,conversationID: $conversationID")
|
||||||
|
onSuccess?.invoke()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
targetId,
|
||||||
|
conversationType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册消息监听器
|
||||||
|
*/
|
||||||
|
fun RegistListener(context: Context) {
|
||||||
|
// 检查 OpenIM 是否已登录
|
||||||
|
if (!com.aiosman.ravenow.AppState.enableChat) {
|
||||||
|
Log.w(getLogTag(), "OpenIM 未登录,跳过注册消息监听器")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
textMessageListener = object : OnAdvanceMsgListener {
|
||||||
|
override fun onRecvNewMessage(msg: Message?) {
|
||||||
|
msg?.let { message ->
|
||||||
|
if (handleNewMessage(message, context)) {
|
||||||
|
val chatItem = ChatItem.convertToChatItem(message, context, avatar = getMessageAvatar(message))
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
Log.i(getLogTag(), "收到来自 ${message.sendID} 的消息,更新聊天列表")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息头像,子类可以重写
|
||||||
|
*/
|
||||||
|
open fun getMessageAvatar(message: Message): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消注册消息监听器
|
||||||
|
*/
|
||||||
|
fun UnRegistListener() {
|
||||||
|
textMessageListener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除未读消息
|
||||||
|
*/
|
||||||
|
fun clearUnRead() {
|
||||||
|
if (conversationID.isEmpty()) {
|
||||||
|
Log.w(getLogTag(), "conversationID为空,无法清除未读消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
|
||||||
|
conversationID,
|
||||||
|
object : OnBase<String> {
|
||||||
|
override fun onSuccess(data: String?) {
|
||||||
|
Log.i("openim", "清除未读消息成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.i("openim", "清除未读消息失败, code:$code, error:$error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载更多历史消息
|
||||||
|
*/
|
||||||
|
fun onLoadMore(context: Context) {
|
||||||
|
if (!hasMore || isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadHistoryMessages(context, isLoadMore = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送文本消息
|
||||||
|
*/
|
||||||
|
fun sendMessage(message: String, context: Context) {
|
||||||
|
// 检查 OpenIM 是否已登录
|
||||||
|
if (!com.aiosman.ravenow.AppState.enableChat) {
|
||||||
|
Log.w(getLogTag(), "OpenIM 未登录,无法发送消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
|
||||||
|
val (recvID, groupID) = getReceiverInfo()
|
||||||
|
|
||||||
|
OpenIMClient.getInstance().messageManager.sendMessage(
|
||||||
|
object : OnMsgSendCallback {
|
||||||
|
override fun onProgress(progress: Long) {
|
||||||
|
// 发送进度
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.e(getLogTag(), "发送消息失败: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: Message?) {
|
||||||
|
Log.d(getLogTag(), "发送消息成功")
|
||||||
|
onMessageSentSuccess(message, data)
|
||||||
|
data?.let { sentMessage ->
|
||||||
|
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textMessage,
|
||||||
|
recvID,
|
||||||
|
groupID,
|
||||||
|
OfflinePushInfo()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送图片消息
|
||||||
|
*/
|
||||||
|
fun sendImageMessage(imageUri: Uri, context: Context) {
|
||||||
|
val tempFile = createTempFile(context, imageUri)
|
||||||
|
val imagePath = tempFile?.path
|
||||||
|
if (imagePath != null) {
|
||||||
|
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
|
||||||
|
val (recvID, groupID) = getReceiverInfo()
|
||||||
|
|
||||||
|
OpenIMClient.getInstance().messageManager.sendMessage(
|
||||||
|
object : OnMsgSendCallback {
|
||||||
|
override fun onProgress(progress: Long) {
|
||||||
|
Log.d(getLogTag(), "发送图片消息进度: $progress")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.e(getLogTag(), "发送图片消息失败: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: Message?) {
|
||||||
|
Log.d(getLogTag(), "发送图片消息成功")
|
||||||
|
data?.let { sentMessage ->
|
||||||
|
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageMessage,
|
||||||
|
recvID,
|
||||||
|
groupID,
|
||||||
|
OfflinePushInfo()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建临时文件
|
||||||
|
*/
|
||||||
|
fun createTempFile(context: Context, uri: Uri): File? {
|
||||||
|
return try {
|
||||||
|
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||||
|
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||||
|
cursor?.use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||||
|
val filePath = it.getString(columnIndex)
|
||||||
|
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||||
|
val mimeType = context.contentResolver.getType(uri)
|
||||||
|
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
|
||||||
|
val tempFile =
|
||||||
|
File.createTempFile("temp_image", ".$extension", context.cacheDir)
|
||||||
|
val outputStream = FileOutputStream(tempFile)
|
||||||
|
|
||||||
|
inputStream?.use { input ->
|
||||||
|
outputStream.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempFile
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取历史消息
|
||||||
|
*/
|
||||||
|
fun fetchHistoryMessage(context: Context) {
|
||||||
|
loadHistoryMessages(context, isLoadMore = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载历史消息的通用方法
|
||||||
|
* @param context 上下文
|
||||||
|
* @param isLoadMore 是否是加载更多(true:追加到现有数据,false:替换现有数据)
|
||||||
|
*/
|
||||||
|
private fun loadHistoryMessages(context: Context, isLoadMore: Boolean) {
|
||||||
|
if (conversationID.isEmpty()) {
|
||||||
|
Log.w(getLogTag(), "conversationID为空,无法${if (isLoadMore) "加载更多" else "获取"}历史消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
isLoading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
|
||||||
|
object : OnBase<AdvancedMessage> {
|
||||||
|
override fun onSuccess(data: AdvancedMessage?) {
|
||||||
|
val messages = data?.messageList ?: emptyList()
|
||||||
|
val newChatItems = messages.mapNotNull {
|
||||||
|
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))
|
||||||
|
}.reversed() // 反转顺序,使最新消息在前面
|
||||||
|
|
||||||
|
// 根据是否是加载更多来决定数据处理方式
|
||||||
|
chatData = if (isLoadMore) {
|
||||||
|
chatData + newChatItems // 追加到现有数据
|
||||||
|
} else {
|
||||||
|
newChatItems // 替换现有数据
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.size < fetchHistorySize) {
|
||||||
|
hasMore = false
|
||||||
|
}
|
||||||
|
lastMessage = messages.firstOrNull()
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(getLogTag(), "${if (isLoadMore) "加载更多" else "获取"}历史消息成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, error: String?) {
|
||||||
|
Log.e(getLogTag(), "${if (isLoadMore) "加载更多" else "获取"}历史消息失败: $error")
|
||||||
|
if (isLoadMore) {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
conversationID,
|
||||||
|
if (isLoadMore) lastMessage else null, // 首次加载不传lastMessage
|
||||||
|
fetchHistorySize,
|
||||||
|
ViewType.History
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显示的聊天列表
|
||||||
|
*/
|
||||||
|
fun getDisplayChatList(): List<ChatItem> {
|
||||||
|
val list = chatData
|
||||||
|
// 更新每条消息的时间戳显示状态
|
||||||
|
for (item in list) {
|
||||||
|
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
669
app/src/main/java/com/aiosman/ravenow/ui/chat/ChatAiScreen.kt
Normal file
669
app/src/main/java/com/aiosman/ravenow/ui/chat/ChatAiScreen.kt
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
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.layout.widthIn
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
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.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||||
|
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
import io.openim.android.sdk.enums.MessageType
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiScreen(userId: String) {
|
||||||
|
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalNavController.current.context
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
var goToNewCount by remember { mutableStateOf(0) }
|
||||||
|
val viewModel = viewModel<ChatAiViewModel>(
|
||||||
|
key = "ChatAiViewModel_$userId",
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return ChatAiViewModel(userId) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
var isLoadingMore by remember { mutableStateOf(false) } // Add a state for loading
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init(context = context)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
viewModel.UnRegistListener()
|
||||||
|
viewModel.clearUnRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val navigationBarHeight = with(LocalDensity.current) {
|
||||||
|
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||||
|
}
|
||||||
|
var inBottom by remember { mutableStateOf(true) }
|
||||||
|
// 监听滚动状态,触发加载更多
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||||
|
.collect { index ->
|
||||||
|
Log.d("ChatScreen", "lastVisibleItemIndex: ${index}")
|
||||||
|
if (index == listState.layoutInfo.totalItemsCount - 1) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.onLoadMore(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 监听滚动状态,触发滚动到底部
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index }
|
||||||
|
.collect { index ->
|
||||||
|
inBottom = index == 0
|
||||||
|
if (index == 0) {
|
||||||
|
goToNewCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 监听是否需要滚动到最新消息
|
||||||
|
LaunchedEffect(viewModel.goToNew) {
|
||||||
|
if (viewModel.goToNew) {
|
||||||
|
if (inBottom) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
} else {
|
||||||
|
goToNewCount++
|
||||||
|
}
|
||||||
|
viewModel.goToNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp, horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(
|
||||||
|
AppColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = viewModel.userProfile?.avatar ?: "",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(RoundedCornerShape(40.dp)),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = viewModel.userProfile?.nickName ?: "",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W700
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Box {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
isMenuExpanded = true
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(
|
||||||
|
AppColors.text)
|
||||||
|
)
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = isMenuExpanded,
|
||||||
|
onDismissRequest = {
|
||||||
|
isMenuExpanded = false
|
||||||
|
},
|
||||||
|
menuItems = listOf(
|
||||||
|
MenuItem(
|
||||||
|
title = if (viewModel.notificationStrategy == "mute") "Unmute" else "Mute",
|
||||||
|
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
||||||
|
) {
|
||||||
|
|
||||||
|
isMenuExpanded = false
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
|
viewModel.viewModelScope.launch {
|
||||||
|
if (viewModel.notificationStrategy == "mute") {
|
||||||
|
viewModel.updateNotificationStrategy("active")
|
||||||
|
} else {
|
||||||
|
viewModel.updateNotificationStrategy("mute")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(
|
||||||
|
AppColors.decentBackground)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
ChatAiInput(
|
||||||
|
onSendImage = {
|
||||||
|
it?.let {
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
|
viewModel.sendImageMessage(it, context)
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
viewModel.sendMessage(it, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
reverseLayout = true,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
|
||||||
|
items(chatList.size, key = { index -> chatList[index].msgId + UUID.randomUUID().toString()}) { index ->
|
||||||
|
val item = chatList[index]
|
||||||
|
if (item.showTimeDivider) {
|
||||||
|
val calendar = java.util.Calendar.getInstance()
|
||||||
|
calendar.timeInMillis = item.timestamp
|
||||||
|
Text(
|
||||||
|
text = calendar.time.formatChatTime(context), // Format the timestamp
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatAiItem(item = item, viewModel.myProfile?.trtcUserId!!)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
// item {
|
||||||
|
// Spacer(modifier = Modifier.height(72.dp))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
if (goToNewCount > 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(bottom = 16.dp, end = 16.dp)
|
||||||
|
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
coroutineScope.launch {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${goToNewCount} New Message",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiSelfItem(item: ChatItem) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = androidx.compose.ui.Alignment.End,
|
||||||
|
) {
|
||||||
|
/* Text(
|
||||||
|
text = item.nickname,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(
|
||||||
|
min = 20.dp,
|
||||||
|
max = (if (item.messageType == MessageType.TEXT) 250.dp else 150.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.background(Color(0xFF6246FF))
|
||||||
|
.padding(
|
||||||
|
vertical = (if (item.messageType == MessageType.TEXT) 8.dp else 0.dp),
|
||||||
|
horizontal = (if (item.messageType == MessageType.TEXT) 16.dp else 0.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
) {
|
||||||
|
when (item.messageType) {
|
||||||
|
MessageType.TEXT -> {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType.PICTURE -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.imageList[1].url,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "不支持的消息类型",
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.avatar,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiOtherItem(item: ChatItem) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.avatar,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(
|
||||||
|
min = 20.dp,
|
||||||
|
max = (if (item.messageType == MessageType.TEXT) 250.dp else 150.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.bubbleBackground)
|
||||||
|
.padding(
|
||||||
|
vertical = (if (item.messageType == MessageType.TEXT) 8.dp else 0.dp),
|
||||||
|
horizontal = (if (item.messageType == MessageType.TEXT) 16.dp else 0.dp)
|
||||||
|
)
|
||||||
|
.padding(bottom = (if (item.messageType == MessageType.TEXT) 3.dp else 0.dp))
|
||||||
|
) {
|
||||||
|
when (item.messageType) {
|
||||||
|
MessageType.TEXT -> {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType.PICTURE -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.imageList[1].url,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "Unsupported message type",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiItem(item: ChatItem, currentUserId: String) {
|
||||||
|
val isCurrentUser = item.userId == currentUserId
|
||||||
|
if (isCurrentUser) {
|
||||||
|
ChatAiSelfItem(item)
|
||||||
|
} else {
|
||||||
|
ChatAiOtherItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiInput(
|
||||||
|
onSendImage: (Uri?) -> Unit = {},
|
||||||
|
onSend: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigationBarHeight = with(LocalDensity.current) {
|
||||||
|
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||||
|
}
|
||||||
|
var keyboardController by remember { mutableStateOf<SoftwareKeyboardController?>(null) }
|
||||||
|
var isKeyboardOpen by remember { mutableStateOf(false) }
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val inputBarHeight by animateDpAsState(
|
||||||
|
targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp),
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = androidx.compose.animation.core.LinearEasing
|
||||||
|
), label = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(isKeyboardOpen) {
|
||||||
|
inputBarHeight
|
||||||
|
}
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val windowInsets = WindowInsets.ime
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val currentDensity by rememberUpdatedState(density)
|
||||||
|
|
||||||
|
LaunchedEffect(windowInsets.getBottom(currentDensity)) {
|
||||||
|
if (windowInsets.getBottom(currentDensity) <= 0) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imagePickUpLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = it.data?.data
|
||||||
|
onSendImage(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box( modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.background(appColors.decentBackground)
|
||||||
|
.padding(start = 16.dp, end = 8.dp, top = 2.dp, bottom = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = {
|
||||||
|
text = it
|
||||||
|
},
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(appColors.text),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
isKeyboardOpen = focusState.isFocused
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
keyboardController = softwareKeyboardController
|
||||||
|
awaitFirstDown().also {
|
||||||
|
keyboardController?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Crossfade(
|
||||||
|
targetState = text.isNotEmpty(), animationSpec = tween(500),
|
||||||
|
label = ""
|
||||||
|
) { isNotEmpty ->
|
||||||
|
val alpha by animateFloatAsState(
|
||||||
|
targetValue = if (isNotEmpty) 1f else 0.5f,
|
||||||
|
animationSpec = tween(300)
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.alpha(alpha)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
|
onSend(text)
|
||||||
|
text = ""
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupMessagesByTime(chatList: List<ChatItem>, viewModel: ChatAiViewModel): List<ChatItem> {
|
||||||
|
for (i in chatList.indices) { // Iterate in normal order
|
||||||
|
if (i == 0) {
|
||||||
|
viewModel.showTimestampMap[chatList[i].msgId] = false
|
||||||
|
chatList[i].showTimeDivider = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val currentMessage = chatList[i]
|
||||||
|
val timeDiff = currentMessage.timestamp - chatList[i - 1].timestamp
|
||||||
|
// 时间间隔大于 3 分钟,显示时间戳
|
||||||
|
if (-timeDiff > 30 * 60 * 1000) {
|
||||||
|
viewModel.showTimestampMap[currentMessage.msgId] = true
|
||||||
|
currentMessage.showTimeDivider = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chatList
|
||||||
|
}
|
||||||
108
app/src/main/java/com/aiosman/ravenow/ui/chat/ChatAiViewModel.kt
Normal file
108
app/src/main/java/com/aiosman/ravenow/ui/chat/ChatAiViewModel.kt
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.ChatState
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
|
import io.openim.android.sdk.models.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
class ChatAiViewModel(
|
||||||
|
val userId: String,
|
||||||
|
) : BaseChatViewModel() {
|
||||||
|
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
var chatNotification by mutableStateOf<ChatNotification?>(null)
|
||||||
|
override fun init(context: Context) {
|
||||||
|
// 获取用户信息
|
||||||
|
viewModelScope.launch {
|
||||||
|
val resp = userService.getUserProfile(userId)
|
||||||
|
userProfile = resp
|
||||||
|
myProfile = accountService.getMyAccountProfile()
|
||||||
|
|
||||||
|
RegistListener(context)
|
||||||
|
|
||||||
|
// 获取会话信息,然后加载历史消息
|
||||||
|
getOneConversation {
|
||||||
|
fetchHistoryMessage(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取通知信息
|
||||||
|
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
|
||||||
|
chatNotification = notiStrategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||||
|
return Triple(userProfile?.trtcUserId ?: userId, ConversationType.SINGLE_CHAT, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogTag(): String {
|
||||||
|
return "ChatAiViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleNewMessage(message: Message, context: Context): Boolean {
|
||||||
|
// 只处理当前聊天对象的消息
|
||||||
|
val currentChatUserId = userProfile?.trtcUserId
|
||||||
|
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
|
||||||
|
|
||||||
|
if (currentChatUserId != null && currentUserId != null) {
|
||||||
|
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
|
||||||
|
return (message.sendID == currentChatUserId || message.sendID == currentUserId) &&
|
||||||
|
message.sendID != currentUserId
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReceiverInfo(): Pair<String?, String?> {
|
||||||
|
return Pair(userProfile?.trtcUserId, null) // (recvID, groupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMessageAvatar(message: Message): String? {
|
||||||
|
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
||||||
|
myProfile?.avatar
|
||||||
|
} else {
|
||||||
|
userProfile?.avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageSentSuccess(message: String, sentMessage: Message?) {
|
||||||
|
// AI聊天特有的处理逻辑
|
||||||
|
sendChatAiMessage(myProfile?.trtcUserId!!, userProfile?.trtcUserId!!, message)
|
||||||
|
createGroup2ChatAi(userProfile?.trtcUserId!!, "ai_group")
|
||||||
|
}
|
||||||
|
fun createGroup2ChatAi(
|
||||||
|
trtcUserId: String,
|
||||||
|
groupName: String,
|
||||||
|
) {
|
||||||
|
// OpenIM 不支持会话分组功能,这里可以留空或者使用其他方式实现
|
||||||
|
Log.d("ChatAiViewModel", "OpenIM 不支持会话分组功能")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendChatAiMessage(
|
||||||
|
fromTrtcUserId: String,
|
||||||
|
toTrtcUserId: String,
|
||||||
|
message: String,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(fromTrtcUserId = fromTrtcUserId,toTrtcUserId = toTrtcUserId,message = message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateNotificationStrategy(strategy: String) {
|
||||||
|
userProfile?.let {
|
||||||
|
val result = ChatState.updateChatNotification(it.id, strategy)
|
||||||
|
chatNotification = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationStrategy get() = chatNotification?.strategy ?: "default"
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.ui.chat
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -8,10 +8,10 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
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.awaitFirstDown
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -50,10 +50,13 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -70,18 +73,20 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.riderpro.entity.ChatItem
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.riderpro.exp.formatChatTime
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
import com.aiosman.riderpro.ui.composables.DropdownMenu
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.riderpro.ui.composables.MenuItem
|
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.tencent.imsdk.v2.V2TIMMessage
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
import io.openim.android.sdk.enums.MessageType
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -89,6 +94,7 @@ fun ChatScreen(userId: String) {
|
|||||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
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
|
||||||
var goToNewCount by remember { mutableStateOf(0) }
|
var goToNewCount by remember { mutableStateOf(0) }
|
||||||
val viewModel = viewModel<ChatViewModel>(
|
val viewModel = viewModel<ChatViewModel>(
|
||||||
key = "ChatViewModel_$userId",
|
key = "ChatViewModel_$userId",
|
||||||
@@ -159,7 +165,7 @@ fun ChatScreen(userId: String) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color.White)
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
Row(
|
Row(
|
||||||
@@ -172,23 +178,36 @@ fun ChatScreen(userId: String) {
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.rider_pro_back_icon),
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(28.dp)
|
.size(24.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.popBackStack()
|
navController.navigateUp()
|
||||||
},
|
},
|
||||||
contentDescription = null
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(
|
||||||
|
AppColors.text)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = viewModel.userProfile?.avatar ?: "",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(RoundedCornerShape(32.dp)),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = viewModel.userProfile?.nickName ?: "",
|
text = viewModel.userProfile?.nickName ?: "",
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f)
|
||||||
|
,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.Black,
|
color = AppColors.text,
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Box {
|
Box {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
@@ -197,7 +216,9 @@ fun ChatScreen(userId: String) {
|
|||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
isMenuExpanded = true
|
isMenuExpanded = true
|
||||||
},
|
},
|
||||||
contentDescription = null
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(
|
||||||
|
AppColors.text)
|
||||||
)
|
)
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = isMenuExpanded,
|
expanded = isMenuExpanded,
|
||||||
@@ -206,11 +227,12 @@ fun ChatScreen(userId: String) {
|
|||||||
},
|
},
|
||||||
menuItems = listOf(
|
menuItems = listOf(
|
||||||
MenuItem(
|
MenuItem(
|
||||||
title = if (viewModel.notificationStrategy == "mute") "Unmute" else "Mute",
|
title = if (viewModel.notificationStrategy == "mute") "取消静音" else "静音",
|
||||||
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
isMenuExpanded = false
|
isMenuExpanded = false
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
viewModel.viewModelScope.launch {
|
viewModel.viewModelScope.launch {
|
||||||
if (viewModel.notificationStrategy == "mute") {
|
if (viewModel.notificationStrategy == "mute") {
|
||||||
viewModel.updateNotificationStrategy("active")
|
viewModel.updateNotificationStrategy("active")
|
||||||
@@ -218,6 +240,9 @@ fun ChatScreen(userId: String) {
|
|||||||
viewModel.updateNotificationStrategy("mute")
|
viewModel.updateNotificationStrategy("mute")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -236,13 +261,17 @@ fun ChatScreen(userId: String) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(1.dp)
|
.height(1.dp)
|
||||||
.background(Color(0xfff7f7f7))
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
ChatInput(
|
ChatInput(
|
||||||
onSendImage = {
|
onSendImage = {
|
||||||
it?.let {
|
it?.let {
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
viewModel.sendImageMessage(it, context)
|
viewModel.sendImageMessage(it, context)
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -254,18 +283,17 @@ fun ChatScreen(userId: String) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color(0xfff7f7f7))
|
.background(AppColors.background)
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize(),
|
|
||||||
reverseLayout = true,
|
reverseLayout = true,
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
) {
|
||||||
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
|
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
|
||||||
items(chatList.size, key = { index -> chatList[index].msgId }) { index ->
|
items(chatList.size, key = { index -> chatList[index].msgId + UUID.randomUUID().toString()}) { index ->
|
||||||
val item = chatList[index]
|
val item = chatList[index]
|
||||||
if (item.showTimeDivider) {
|
if (item.showTimeDivider) {
|
||||||
val calendar = java.util.Calendar.getInstance()
|
val calendar = java.util.Calendar.getInstance()
|
||||||
@@ -273,7 +301,7 @@ fun ChatScreen(userId: String) {
|
|||||||
Text(
|
Text(
|
||||||
text = calendar.time.formatChatTime(context), // Format the timestamp
|
text = calendar.time.formatChatTime(context), // Format the timestamp
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.Gray,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
),
|
),
|
||||||
@@ -298,7 +326,7 @@ fun ChatScreen(userId: String) {
|
|||||||
.padding(bottom = 16.dp, end = 16.dp)
|
.padding(bottom = 16.dp, end = 16.dp)
|
||||||
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
|
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.background(Color.White)
|
.background(AppColors.background)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
@@ -310,7 +338,7 @@ fun ChatScreen(userId: String) {
|
|||||||
Text(
|
Text(
|
||||||
text = "${goToNewCount} New Message",
|
text = "${goToNewCount} New Message",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.Black,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -324,6 +352,7 @@ fun ChatScreen(userId: String) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatSelfItem(item: ChatItem) {
|
fun ChatSelfItem(item: ChatItem) {
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -337,33 +366,42 @@ fun ChatSelfItem(item: ChatItem) {
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = androidx.compose.ui.Alignment.End,
|
horizontalAlignment = androidx.compose.ui.Alignment.End,
|
||||||
) {
|
) {
|
||||||
|
/* Text(
|
||||||
|
text = item.nickname,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
*/
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(
|
.widthIn(
|
||||||
min = 20.dp,
|
min = 20.dp,
|
||||||
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
|
max = (if (item.messageType == MessageType.TEXT) 250.dp else 150.dp)
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(Color(0xFF000000))
|
.background(Color(0xFF6246FF))
|
||||||
.padding(
|
.padding(
|
||||||
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
|
vertical = (if (item.messageType == MessageType.TEXT) 8.dp else 0.dp),
|
||||||
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
|
horizontal = (if (item.messageType == MessageType.TEXT) 16.dp else 0.dp)
|
||||||
)
|
)
|
||||||
.padding(bottom = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
|
|
||||||
) {
|
) {
|
||||||
when (item.messageType) {
|
when (item.messageType) {
|
||||||
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
|
MessageType.TEXT -> {
|
||||||
Text(
|
Text(
|
||||||
text = item.message,
|
text = item.message,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontSize = 16.sp,
|
fontSize = 14.sp,
|
||||||
),
|
),
|
||||||
textAlign = TextAlign.Start
|
textAlign = TextAlign.Start
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
|
MessageType.PICTURE -> {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = item.imageList[1].url,
|
imageUrl = item.imageList[1].url,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -373,34 +411,36 @@ fun ChatSelfItem(item: ChatItem) {
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Text(
|
Text(
|
||||||
text = "Unsupported message type",
|
text = "不支持的消息类型",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontSize = 16.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
/*Spacer(modifier = Modifier.width(12.dp))
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(24.dp)
|
||||||
.clip(RoundedCornerShape(40.dp))
|
.clip(RoundedCornerShape(24.dp))
|
||||||
) {
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = item.avatar,
|
imageUrl = item.avatar,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentDescription = "avatar"
|
contentDescription = "avatar"
|
||||||
)
|
)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatOtherItem(item: ChatItem) {
|
fun ChatOtherItem(item: ChatItem) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -427,29 +467,29 @@ fun ChatOtherItem(item: ChatItem) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(
|
.widthIn(
|
||||||
min = 20.dp,
|
min = 20.dp,
|
||||||
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
|
max = (if (item.messageType == MessageType.TEXT) 250.dp else 150.dp)
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(Color(0xffFFFFFF))
|
.background(AppColors.background)
|
||||||
.padding(
|
.padding(
|
||||||
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
|
vertical = (if (item.messageType == MessageType.TEXT) 8.dp else 0.dp),
|
||||||
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
|
horizontal = (if (item.messageType == MessageType.TEXT) 16.dp else 0.dp)
|
||||||
)
|
)
|
||||||
.padding(bottom = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
|
.padding(bottom = (if (item.messageType == MessageType.TEXT) 3.dp else 0.dp))
|
||||||
) {
|
) {
|
||||||
when (item.messageType) {
|
when (item.messageType) {
|
||||||
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
|
MessageType.TEXT -> {
|
||||||
Text(
|
Text(
|
||||||
text = item.message,
|
text = item.message,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.Black,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
),
|
),
|
||||||
textAlign = TextAlign.Start
|
textAlign = TextAlign.Start
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
|
MessageType.PICTURE -> {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = item.imageList[1].url,
|
imageUrl = item.imageList[1].url,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -461,7 +501,7 @@ fun ChatOtherItem(item: ChatItem) {
|
|||||||
Text(
|
Text(
|
||||||
text = "Unsupported message type",
|
text = "Unsupported message type",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.White,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -490,12 +530,14 @@ fun ChatInput(
|
|||||||
onSendImage: (Uri?) -> Unit = {},
|
onSendImage: (Uri?) -> Unit = {},
|
||||||
onSend: (String) -> Unit = {},
|
onSend: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val navigationBarHeight = with(LocalDensity.current) {
|
val navigationBarHeight = with(LocalDensity.current) {
|
||||||
WindowInsets.navigationBars.getBottom(this).toDp()
|
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||||
}
|
}
|
||||||
var keyboardController by remember { mutableStateOf<SoftwareKeyboardController?>(null) }
|
var keyboardController by remember { mutableStateOf<SoftwareKeyboardController?>(null) }
|
||||||
var isKeyboardOpen by remember { mutableStateOf(false) }
|
var isKeyboardOpen by remember { mutableStateOf(false) }
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
val inputBarHeight by animateDpAsState(
|
val inputBarHeight by animateDpAsState(
|
||||||
targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp),
|
targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp),
|
||||||
animationSpec = tween(
|
animationSpec = tween(
|
||||||
@@ -528,20 +570,21 @@ fun ChatInput(
|
|||||||
onSendImage(uri)
|
onSendImage(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Box( modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.padding(bottom = inputBarHeight)
|
.background(appColors.decentBackground)
|
||||||
|
.padding(start = 16.dp, end = 8.dp, top = 2.dp, bottom = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.background(Color(0xffe5e5e5))
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
contentAlignment = androidx.compose.ui.Alignment.CenterStart,
|
|
||||||
) {
|
) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = text,
|
value = text,
|
||||||
@@ -549,9 +592,11 @@ fun ChatInput(
|
|||||||
text = it
|
text = it
|
||||||
},
|
},
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
color = Color.Black,
|
color = appColors.text,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
),
|
),
|
||||||
|
cursorBrush = SolidColor(appColors.text),
|
||||||
|
singleLine = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
@@ -574,13 +619,14 @@ fun ChatInput(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Icon(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_camera),
|
painter = painterResource(R.mipmap.rider_pro_im_image),
|
||||||
contentDescription = "Emoji",
|
contentDescription = "Image",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
imagePickUpLauncher.launch(
|
imagePickUpLauncher.launch(
|
||||||
Intent.createChooser(
|
Intent.createChooser(
|
||||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
@@ -589,29 +635,41 @@ fun ChatInput(
|
|||||||
"Select Image"
|
"Select Image"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tint = Color(0xffe0e0e0)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Crossfade(
|
Crossfade(
|
||||||
targetState = text.isNotEmpty(), animationSpec = tween(500),
|
targetState = text.isNotEmpty(), animationSpec = tween(500),
|
||||||
label = ""
|
label = ""
|
||||||
) { isNotEmpty ->
|
) { isNotEmpty ->
|
||||||
Icon(
|
val alpha by animateFloatAsState(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_video_share),
|
targetValue = if (isNotEmpty) 1f else 0.5f,
|
||||||
contentDescription = "Emoji",
|
animationSpec = tween(300)
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(32.dp)
|
.size(24.dp)
|
||||||
|
.alpha(alpha)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
onSend(text)
|
onSend(text)
|
||||||
text = ""
|
text = ""
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tint = if (isNotEmpty) Color.Red else Color(0xffe0e0e0)
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun groupMessagesByTime(chatList: List<ChatItem>, viewModel: ChatViewModel): List<ChatItem> {
|
fun groupMessagesByTime(chatList: List<ChatItem>, viewModel: ChatViewModel): List<ChatItem> {
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.ChatState
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
|
import io.openim.android.sdk.models.Message
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
class ChatViewModel(
|
||||||
|
val userId: String,
|
||||||
|
) : BaseChatViewModel() {
|
||||||
|
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
var chatNotification by mutableStateOf<ChatNotification?>(null)
|
||||||
|
override fun init(context: Context) {
|
||||||
|
// 获取用户信息
|
||||||
|
viewModelScope.launch {
|
||||||
|
val resp = userService.getUserProfile(userId)
|
||||||
|
userProfile = resp
|
||||||
|
myProfile = accountService.getMyAccountProfile()
|
||||||
|
|
||||||
|
RegistListener(context)
|
||||||
|
|
||||||
|
// 获取会话信息,然后加载历史消息
|
||||||
|
getOneConversation {
|
||||||
|
fetchHistoryMessage(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取通知信息
|
||||||
|
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
|
||||||
|
chatNotification = notiStrategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||||
|
return Triple(userProfile?.trtcUserId ?: userId, ConversationType.SINGLE_CHAT, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogTag(): String {
|
||||||
|
return "ChatViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleNewMessage(message: Message, context: Context): Boolean {
|
||||||
|
// 只处理当前聊天对象的消息
|
||||||
|
val currentChatUserId = userProfile?.trtcUserId
|
||||||
|
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
|
||||||
|
|
||||||
|
if (currentChatUserId != null && currentUserId != null) {
|
||||||
|
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
|
||||||
|
return (message.sendID == currentChatUserId || message.sendID == currentUserId) &&
|
||||||
|
message.sendID != currentUserId
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReceiverInfo(): Pair<String?, String?> {
|
||||||
|
return Pair(userProfile?.trtcUserId, null) // (recvID, groupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMessageAvatar(message: Message): String? {
|
||||||
|
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
||||||
|
myProfile?.avatar
|
||||||
|
} else {
|
||||||
|
userProfile?.avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateNotificationStrategy(strategy: String) {
|
||||||
|
userProfile?.let {
|
||||||
|
val result = ChatState.updateChatNotification(it.id, strategy)
|
||||||
|
chatNotification = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationStrategy get() = chatNotification?.strategy ?: "default"
|
||||||
|
}
|
||||||
716
app/src/main/java/com/aiosman/ravenow/ui/chat/GroupChatScreen.kt
Normal file
716
app/src/main/java/com/aiosman/ravenow/ui/chat/GroupChatScreen.kt
Normal file
@@ -0,0 +1,716 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
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.layout.widthIn
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
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 androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||||
|
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.ui.navigateToGroupInfo
|
||||||
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
// 临时兼容层 - TODO: 完成 OpenIM 迁移后删除
|
||||||
|
import io.openim.android.sdk.enums.MessageType
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
||||||
|
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalNavController.current.context
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
var goToNewCount by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
val viewModel = viewModel<GroupChatViewModel>(
|
||||||
|
key = "GroupChatViewModel_$groupId",
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return GroupChatViewModel(groupId,name,avatar) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init(context = context)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
viewModel.UnRegistListener()
|
||||||
|
viewModel.clearUnRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var inBottom by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||||
|
.collect { index ->
|
||||||
|
if (index == listState.layoutInfo.totalItemsCount - 1) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.onLoadMore(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index }
|
||||||
|
.collect { index ->
|
||||||
|
inBottom = index == 0
|
||||||
|
if (index == 0) {
|
||||||
|
goToNewCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.goToNew) {
|
||||||
|
if (viewModel.goToNew) {
|
||||||
|
if (inBottom) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
} else {
|
||||||
|
goToNewCount++
|
||||||
|
}
|
||||||
|
viewModel.goToNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp, horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
if (viewModel.groupAvatar.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = viewModel.groupAvatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToGroupInfo(groupId)
|
||||||
|
},
|
||||||
|
contentDescription = "群聊头像"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToGroupInfo(groupId)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.groupName,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W700
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow =TextOverflow.Ellipsis,
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = viewModel.groupName,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow =TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToGroupInfo(groupId)
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
GroupChatInput(
|
||||||
|
onSendImage = { uri ->
|
||||||
|
uri?.let {
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
|
viewModel.sendImageMessage(it, context)
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { message ->
|
||||||
|
viewModel.sendMessage(message, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
reverseLayout = true,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
|
||||||
|
items(chatList.size, key = { index -> chatList[index].msgId + UUID.randomUUID().toString()}) { index ->
|
||||||
|
val item = chatList[index]
|
||||||
|
Column {
|
||||||
|
if (item.showTimeDivider) {
|
||||||
|
val calendar = java.util.Calendar.getInstance()
|
||||||
|
calendar.timeInMillis = item.timestamp
|
||||||
|
Text(
|
||||||
|
text = calendar.time.formatChatTime(context),
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 获取上一个item的userId,用于判断是否显示头像和昵称
|
||||||
|
val previousItem = if (index < chatList.size - 1) chatList[index + 1] else null
|
||||||
|
val showAvatarAndNickname = previousItem?.userId != item.userId
|
||||||
|
GroupChatItem(
|
||||||
|
item = item,
|
||||||
|
currentUserId = viewModel.myProfile?.trtcUserId!!,
|
||||||
|
showAvatarAndNickname = showAvatarAndNickname
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goToNewCount > 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(bottom = 16.dp, end = 16.dp)
|
||||||
|
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
coroutineScope.launch {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${goToNewCount} 条新消息",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatSelfItem(item: ChatItem) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = androidx.compose.ui.Alignment.End,
|
||||||
|
) {
|
||||||
|
/* Text(
|
||||||
|
text = item.nickname,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(
|
||||||
|
min = 20.dp,
|
||||||
|
max = (if (item.messageType == MessageType.TEXT) 250.dp else 150.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.background(Color(0xFF6246FF))
|
||||||
|
.padding(
|
||||||
|
vertical = (if (item.messageType == MessageType.TEXT) 8.dp else 0.dp),
|
||||||
|
horizontal = (if (item.messageType == MessageType.TEXT) 16.dp else 0.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
) {
|
||||||
|
when (item.messageType) {
|
||||||
|
MessageType.TEXT -> {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType.PICTURE -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.imageList[1].url,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "不支持的消息类型",
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.avatar,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatOtherItem(item: ChatItem, showAvatarAndNickname: Boolean = true) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (showAvatarAndNickname) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.avatar.replace("storage/avatars/", "/avatar/"),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
} else {
|
||||||
|
// 当不显示头像时,添加左边距以保持消息对齐
|
||||||
|
Spacer(modifier = Modifier.width(36.dp))
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(
|
||||||
|
min = 20.dp,
|
||||||
|
max = (if (item.messageType == MessageType.TEXT) 250.dp else 150.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.background(AppColors.bubbleBackground)
|
||||||
|
.padding(
|
||||||
|
vertical = (if (item.messageType == MessageType.TEXT) 8.dp else 0.dp),
|
||||||
|
horizontal = (if (item.messageType == MessageType.TEXT) 16.dp else 0.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
when (item.messageType) {
|
||||||
|
MessageType.TEXT -> {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType.PICTURE -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.imageList[1].url,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "不支持的消息类型",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAvatarAndNickname) {
|
||||||
|
Text(
|
||||||
|
text = item.nickname,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatItem(item: ChatItem, currentUserId: String, showAvatarAndNickname: Boolean = true) {
|
||||||
|
val isCurrentUser = item.userId == currentUserId
|
||||||
|
|
||||||
|
// 管理员消息显示特殊布局
|
||||||
|
if (item.userId == "administrator") {
|
||||||
|
GroupChatAdminItem(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据是否是当前用户显示不同样式
|
||||||
|
when (item.userId) {
|
||||||
|
currentUserId -> GroupChatSelfItem(item)
|
||||||
|
else -> GroupChatOtherItem(item, showAvatarAndNickname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatAdminItem(item: ChatItem) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 50.dp, vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
maxLines = Int.MAX_VALUE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatInput(
|
||||||
|
onSendImage: (Uri?) -> Unit = {},
|
||||||
|
onSend: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigationBarHeight = with(LocalDensity.current) {
|
||||||
|
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||||
|
}
|
||||||
|
var keyboardController by remember { mutableStateOf<SoftwareKeyboardController?>(null) }
|
||||||
|
var isKeyboardOpen by remember { mutableStateOf(false) }
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val inputBarHeight by animateDpAsState(
|
||||||
|
targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp),
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = androidx.compose.animation.core.LinearEasing
|
||||||
|
), label = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(isKeyboardOpen) {
|
||||||
|
inputBarHeight
|
||||||
|
}
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val windowInsets = WindowInsets.ime
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val currentDensity by rememberUpdatedState(density)
|
||||||
|
|
||||||
|
LaunchedEffect(windowInsets.getBottom(currentDensity)) {
|
||||||
|
if (windowInsets.getBottom(currentDensity) <= 0) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imagePickUpLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = it.data?.data
|
||||||
|
onSendImage(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box( modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.background(appColors.decentBackground)
|
||||||
|
.padding(start = 16.dp, end = 8.dp, top = 2.dp, bottom = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = {
|
||||||
|
text = it
|
||||||
|
},
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(appColors.text),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
isKeyboardOpen = focusState.isFocused
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
keyboardController = softwareKeyboardController
|
||||||
|
awaitFirstDown().also {
|
||||||
|
keyboardController?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Crossfade(
|
||||||
|
targetState = text.isNotEmpty(), animationSpec = tween(500),
|
||||||
|
label = ""
|
||||||
|
) { isNotEmpty ->
|
||||||
|
val alpha by animateFloatAsState(
|
||||||
|
targetValue = if (isNotEmpty) 1f else 0.5f,
|
||||||
|
animationSpec = tween(300)
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.alpha(alpha)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||||
|
onSend(text)
|
||||||
|
text = ""
|
||||||
|
} else {
|
||||||
|
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupMessagesByTime(chatList: List<ChatItem>, viewModel: GroupChatViewModel): List<ChatItem> {
|
||||||
|
for (i in chatList.indices) {
|
||||||
|
if (i == 0) {
|
||||||
|
viewModel.showTimestampMap[chatList[i].msgId] = false
|
||||||
|
chatList[i].showTimeDivider = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val currentMessage = chatList[i]
|
||||||
|
val timeDiff = currentMessage.timestamp - chatList[i - 1].timestamp
|
||||||
|
if (-timeDiff > 10 * 60 * 1000) {
|
||||||
|
viewModel.showTimestampMap[currentMessage.msgId] = true
|
||||||
|
currentMessage.showTimeDivider = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chatList
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
|
||||||
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
|
import io.openim.android.sdk.models.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class GroupChatViewModel(
|
||||||
|
val groupId: String,
|
||||||
|
val name: String,
|
||||||
|
val avatar: String,
|
||||||
|
) : BaseChatViewModel() {
|
||||||
|
var groupInfo by mutableStateOf<GroupInfo?>(null)
|
||||||
|
|
||||||
|
// 群聊特有属性
|
||||||
|
var memberCount by mutableStateOf(0)
|
||||||
|
var groupName by mutableStateOf("")
|
||||||
|
var groupAvatar by mutableStateOf("")
|
||||||
|
|
||||||
|
data class GroupInfo(
|
||||||
|
val groupId: String,
|
||||||
|
val groupName: String,
|
||||||
|
val groupAvatar: String,
|
||||||
|
val memberCount: Int,
|
||||||
|
val ownerId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun init(context: Context) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
getGroupInfo()
|
||||||
|
myProfile = accountService.getMyAccountProfile()
|
||||||
|
RegistListener(context)
|
||||||
|
|
||||||
|
// 获取会话信息,然后加载历史消息
|
||||||
|
getOneConversation {
|
||||||
|
fetchHistoryMessage(context)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("GroupChatViewModel", "初始化失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getGroupInfo() {
|
||||||
|
// 简化群组信息获取,使用默认信息
|
||||||
|
groupInfo = GroupInfo(
|
||||||
|
groupId = groupId,
|
||||||
|
groupName = name,
|
||||||
|
groupAvatar = avatar,
|
||||||
|
memberCount = 0,
|
||||||
|
ownerId = ""
|
||||||
|
)
|
||||||
|
groupName = groupInfo?.groupName ?: ""
|
||||||
|
groupAvatar = groupInfo?.groupAvatar ?: ""
|
||||||
|
memberCount = groupInfo?.memberCount ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||||
|
// 根据群组类型决定ConversationType,这里假设是普通群聊
|
||||||
|
return Triple(groupId, ConversationType.GROUP_CHAT, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogTag(): String {
|
||||||
|
return "GroupChatViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleNewMessage(message: Message, context: Context): Boolean {
|
||||||
|
// 检查是否是当前群聊的消息
|
||||||
|
return message.groupID == groupId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReceiverInfo(): Pair<String?, String?> {
|
||||||
|
return Pair(null, groupId) // (recvID, groupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMessageAvatar(message: Message): String? {
|
||||||
|
// 群聊中,如果是自己发送的消息显示自己的头像,否则为null(由ChatItem处理)
|
||||||
|
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
|
||||||
|
myProfile?.avatar
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageSentSuccess(message: String, sentMessage: Message?) {
|
||||||
|
// 群聊特有的处理逻辑
|
||||||
|
sendChatAiMessage(message = message, trtcGroupId = groupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun sendChatAiMessage(
|
||||||
|
trtcGroupId: String,
|
||||||
|
message: String,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(trtcGroupId = trtcGroupId,message = message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.ui.comment
|
package com.aiosman.ravenow.ui.comment
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -40,13 +40,14 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.riderpro.entity.CommentEntity
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.aiosman.riderpro.ui.post.CommentContent
|
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
||||||
import com.aiosman.riderpro.ui.post.CommentMenuModal
|
import com.aiosman.ravenow.ui.post.CommentContent
|
||||||
import com.aiosman.riderpro.ui.post.CommentsViewModel
|
import com.aiosman.ravenow.ui.post.CommentMenuModal
|
||||||
import com.aiosman.riderpro.ui.post.OrderSelectionComponent
|
import com.aiosman.ravenow.ui.post.CommentsViewModel
|
||||||
|
import com.aiosman.ravenow.ui.post.OrderSelectionComponent
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,6 +80,7 @@ fun CommentModalContent(
|
|||||||
onCommentAdded: () -> Unit = {},
|
onCommentAdded: () -> Unit = {},
|
||||||
onDismiss: () -> Unit = {}
|
onDismiss: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
val model = viewModel<CommentModalViewModel>(
|
val model = viewModel<CommentModalViewModel>(
|
||||||
key = "CommentModalViewModel_$postId",
|
key = "CommentModalViewModel_$postId",
|
||||||
factory = object : ViewModelProvider.Factory {
|
factory = object : ViewModelProvider.Factory {
|
||||||
@@ -113,7 +115,7 @@ fun CommentModalContent(
|
|||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showCommentMenu = false
|
showCommentMenu = false
|
||||||
},
|
},
|
||||||
containerColor = Color.White,
|
containerColor = AppColors.background,
|
||||||
sheetState = rememberModalBottomSheetState(
|
sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
),
|
),
|
||||||
@@ -142,24 +144,8 @@ fun CommentModalContent(
|
|||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
|
|
||||||
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.comment),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
color = Color(0xFFF7F7F7)
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -170,7 +156,8 @@ fun CommentModalContent(
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.comment_count, commentCount),
|
text = stringResource(id = R.string.comment_count, commentCount),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = Color(0xff666666)
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AppColors.nonActiveText
|
||||||
)
|
)
|
||||||
OrderSelectionComponent {
|
OrderSelectionComponent {
|
||||||
commentViewModel.order = it
|
commentViewModel.order = it
|
||||||
@@ -205,7 +192,7 @@ fun CommentModalContent(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0xfff7f7f7))
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
EditCommentBottomModal(replyComment) {
|
EditCommentBottomModal(replyComment) {
|
||||||
commentViewModel.viewModelScope.launch {
|
commentViewModel.viewModelScope.launch {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.ui.comment
|
package com.aiosman.ravenow.ui.comment
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -13,6 +13,7 @@ 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
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -20,15 +21,18 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||||
|
import com.aiosman.ravenow.ui.composables.BottomNavigationPlaceholder
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
@@ -67,24 +71,26 @@ fun NoticeScreenHeader(
|
|||||||
rightIcon: @Composable (() -> Unit)? = null
|
rightIcon: @Composable (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val nav = LocalNavController.current
|
val nav = LocalNavController.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
//返回tab
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
painter = painterResource(id = R.drawable.rider_pro_back_icon,),
|
||||||
contentDescription = title,
|
contentDescription = title,
|
||||||
modifier = Modifier.size(16.dp).clickable(
|
modifier = Modifier.size(24.dp).clickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
nav.popBackStack()
|
nav.navigateUp()
|
||||||
}
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(12.dp))
|
|
||||||
Text(title, fontWeight = FontWeight.W800, fontSize = 17.sp)
|
|
||||||
if (moreIcon) {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
if (moreIcon) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||||
contentDescription = "More",
|
contentDescription = "More",
|
||||||
@@ -92,10 +98,109 @@ fun NoticeScreenHeader(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (rightIcon != null) {
|
if (rightIcon != null) {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
rightIcon()
|
rightIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
title, fontWeight = FontWeight.W800,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScreenHeader(
|
||||||
|
title:String,
|
||||||
|
moreIcon: Boolean = true,
|
||||||
|
rightIcon: @Composable (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val nav = LocalNavController.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_close,),
|
||||||
|
contentDescription = title,
|
||||||
|
modifier = Modifier.size(24.dp).clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
nav.navigateUp()
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
Text(title,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
color = AppColors.text)
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
if (moreIcon) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||||
|
contentDescription = "More",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (rightIcon != null) {
|
||||||
|
//rightIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScreenHeader2(
|
||||||
|
title:String,
|
||||||
|
moreIcon: Boolean = true,
|
||||||
|
rightIcon: @Composable (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val nav = LocalNavController.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon,),
|
||||||
|
contentDescription = title,
|
||||||
|
modifier = Modifier.size(24.dp).clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
nav.navigateUp()
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
Text(title,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
color = AppColors.text)
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
if (moreIcon) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||||
|
contentDescription = "More",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (rightIcon != null) {
|
||||||
|
//rightIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.ui.comment.notice
|
package com.aiosman.ravenow.ui.comment.notice
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -21,9 +21,10 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
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.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -33,16 +34,18 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.riderpro.entity.CommentEntity
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.riderpro.exp.timeAgo
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.riderpro.ui.NavigationRoute
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
import com.aiosman.ravenow.exp.timeAgo
|
||||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.riderpro.ui.navigateToPost
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -62,8 +65,10 @@ fun CommentNoticeScreen() {
|
|||||||
var dataFlow = viewModel.commentItemsFlow
|
var dataFlow = viewModel.commentItemsFlow
|
||||||
var comments = dataFlow.collectAsLazyPagingItems()
|
var comments = dataFlow.collectAsLazyPagingItems()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().background(color = Color(0xFFFFFFFF))
|
modifier = Modifier.fillMaxSize().background(color = AppColors.background)
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
Box(
|
Box(
|
||||||
@@ -73,7 +78,41 @@ fun CommentNoticeScreen() {
|
|||||||
) {
|
) {
|
||||||
NoticeScreenHeader(stringResource(R.string.comment), moreIcon = false)
|
NoticeScreenHeader(stringResource(R.string.comment), moreIcon = false)
|
||||||
}
|
}
|
||||||
|
if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 149.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(
|
||||||
|
id =if(AppState.darkMode) R.mipmap.qst_pl_qs_as_img
|
||||||
|
else R.mipmap.qst_pl_qs_img),
|
||||||
|
contentDescription = "No Comment",
|
||||||
|
modifier = Modifier.size(181.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
Text(
|
||||||
|
text = "等一位旅人~",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "去发布动态,让更多人参与对话",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize().padding(horizontal = 16.dp)
|
.fillMaxSize().padding(horizontal = 16.dp)
|
||||||
@@ -109,7 +148,7 @@ fun CommentNoticeScreen() {
|
|||||||
) {
|
) {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
modifier = Modifier.width(160.dp),
|
modifier = Modifier.width(160.dp),
|
||||||
color = Color(0xFFDA3832)
|
color = AppColors.main
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,6 +167,7 @@ fun CommentNoticeScreen() {
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Load comment error, click to retry",
|
text = "Load comment error, click to retry",
|
||||||
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +178,7 @@ fun CommentNoticeScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -148,6 +188,8 @@ fun CommentNoticeItem(
|
|||||||
) {
|
) {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
|
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
@@ -183,7 +225,8 @@ fun CommentNoticeItem(
|
|||||||
Text(
|
Text(
|
||||||
text = commentItem.name,
|
text = commentItem.name,
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
modifier = Modifier
|
modifier = Modifier,
|
||||||
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Row {
|
Row {
|
||||||
@@ -195,7 +238,7 @@ fun CommentNoticeItem(
|
|||||||
text = text,
|
text = text,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = Color(0x99000000),
|
color = AppColors.secondaryText,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
@@ -203,7 +246,7 @@ fun CommentNoticeItem(
|
|||||||
Text(
|
Text(
|
||||||
text = commentItem.date.timeAgo(context),
|
text = commentItem.date.timeAgo(context),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = Color(0x66000000)
|
color = AppColors.secondaryText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +271,7 @@ fun CommentNoticeItem(
|
|||||||
if (commentItem.unread) {
|
if (commentItem.unread) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(Color(0xFFE53935), CircleShape)
|
.background(AppColors.main, CircleShape)
|
||||||
.size(12.dp)
|
.size(12.dp)
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aiosman.riderpro.ui.comment.notice
|
package com.aiosman.ravenow.ui.comment.notice
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -11,15 +11,15 @@ import androidx.paging.PagingConfig
|
|||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
import com.aiosman.riderpro.data.AccountService
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.riderpro.data.CommentRemoteDataSource
|
import com.aiosman.ravenow.data.CommentRemoteDataSource
|
||||||
import com.aiosman.riderpro.data.CommentService
|
import com.aiosman.ravenow.data.CommentService
|
||||||
import com.aiosman.riderpro.data.CommentServiceImpl
|
import com.aiosman.ravenow.data.CommentServiceImpl
|
||||||
import com.aiosman.riderpro.data.UserService
|
import com.aiosman.ravenow.data.UserService
|
||||||
import com.aiosman.riderpro.data.UserServiceImpl
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
import com.aiosman.riderpro.entity.CommentEntity
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.aiosman.riderpro.entity.CommentPagingSource
|
import com.aiosman.ravenow.entity.CommentPagingSource
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
package com.aiosman.riderpro.ui.composables
|
package com.aiosman.ravenow.ui.composables
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
//import androidx.compose.foundation.layout.ColumnScopeInstance.weight
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
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.ColumnScopeInstance.weight
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
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.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -24,46 +20,51 @@ import androidx.compose.ui.draw.clip
|
|||||||
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
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
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.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ActionButton(
|
fun ActionButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String,
|
text: String,
|
||||||
color: Color = Color.Black,
|
color: Color? = null,
|
||||||
@DrawableRes backgroundImage: Int? = null,
|
backgroundColor: Color? = null,
|
||||||
backgroundColor: Color = Color(0xfff0f0f0),
|
|
||||||
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),
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
loadingTextColor: Color = Color.White,
|
loadingTextColor: Color? = null,
|
||||||
loadingText: String = "Loading",
|
loadingText: String = "Loading",
|
||||||
loadingBackgroundColor: Color = Color(0xFFD95757),
|
loadingBackgroundColor: Color? = null,
|
||||||
disabledBackgroundColor: Color = Color(0xFFD0D0D0),
|
disabledBackgroundColor: Color? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
fullWidth:Boolean = false,
|
fullWidth: Boolean = false,
|
||||||
|
roundCorner: Float = 24f,
|
||||||
|
fontSize: TextUnit = 17.sp,
|
||||||
|
fontWeight: FontWeight = FontWeight.W900,
|
||||||
click: () -> Unit = {}
|
click: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
val animatedBackgroundColor by animateColorAsState(
|
val animatedBackgroundColor by animateColorAsState(
|
||||||
targetValue = run{
|
targetValue = run {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
loadingBackgroundColor
|
loadingBackgroundColor ?: AppColors.loadingMain
|
||||||
} else {
|
} else {
|
||||||
backgroundColor
|
backgroundColor ?: AppColors.basicMain
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
disabledBackgroundColor
|
disabledBackgroundColor ?: AppColors.disabledBackground
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
animationSpec = tween(300), label = ""
|
animationSpec = tween(300), label = ""
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(RoundedCornerShape(24.dp))
|
.clip(RoundedCornerShape(roundCorner.dp))
|
||||||
.background(animatedBackgroundColor)
|
.background(animatedBackgroundColor)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
if (enabled && !isLoading) {
|
if (enabled && !isLoading) {
|
||||||
@@ -79,9 +80,9 @@ fun ActionButton(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.let {
|
.let {
|
||||||
if(fullWidth){
|
if (fullWidth) {
|
||||||
it.fillMaxWidth()
|
it.fillMaxWidth()
|
||||||
}else{
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -91,27 +92,26 @@ fun ActionButton(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.align(Alignment.CenterStart)){
|
Box(modifier = Modifier.align(Alignment.CenterStart)) {
|
||||||
leading?.invoke()
|
leading?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
fontSize = 17.sp,
|
fontSize = fontSize,
|
||||||
color = color,
|
color = color ?: AppColors.text,
|
||||||
fontWeight = FontWeight.W900,
|
fontWeight = fontWeight,
|
||||||
textAlign = if (expandText) TextAlign.Center else TextAlign.Start
|
textAlign = if (expandText) TextAlign.Center else TextAlign.Start
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.let {
|
.let {
|
||||||
if(fullWidth){
|
if (fullWidth) {
|
||||||
it.fillMaxWidth()
|
it.fillMaxWidth()
|
||||||
}else{
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,13 +124,13 @@ fun ActionButton(
|
|||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
color = Color.White
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
loadingText,
|
loadingText,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = loadingTextColor
|
color = loadingTextColor ?: AppColors.loadingText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
119
app/src/main/java/com/aiosman/ravenow/ui/composables/Agent.kt
Normal file
119
app/src/main/java/com/aiosman/ravenow/ui/composables/Agent.kt
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package com.aiosman.ravenow.ui.composables
|
||||||
|
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.exp.timeAgo
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AgentCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
agentEntity: AgentEntity,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
onAvatarClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(start = 0.dp, end = 0.dp, top = 16.dp, bottom = 8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onClick ()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(40.dp))
|
||||||
|
.noRippleClickable {
|
||||||
|
onAvatarClick()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
agentEntity.avatar,
|
||||||
|
contentDescription = agentEntity.openId,
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 12.dp, end = 12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(22.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
text = agentEntity.title,
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.W700)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(21.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier,
|
||||||
|
text = agentEntity.desc,
|
||||||
|
color = AppColors.text,
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
//MomentPostLocation(momentEntity.location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
package com.aiosman.riderpro.ui.composables
|
package com.aiosman.ravenow.ui.composables
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.animation.SizeTransform
|
import androidx.compose.animation.SizeTransform
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.animation.with
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 24) {
|
fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 24) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = count,
|
targetState = count,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
@@ -36,6 +36,6 @@ fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 2
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { targetCount ->
|
) { targetCount ->
|
||||||
Text(text = "$targetCount", modifier = modifier, fontSize = fontSize.sp)
|
Text(text = "$targetCount", modifier = modifier, fontSize = fontSize.sp, color = AppColors.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user