186 Commits

Author SHA1 Message Date
d8df67bae5 动态模块新增推荐Tab,UI优化及调整
- 新增推荐Tab,采用垂直滑动样式,展示推荐动态内容。
- 推荐Tab支持预加载周围图片,提升滑动体验,并增加loading和错误状态指示。
- 优化评论弹窗UI,移除自动聚焦,调整背景色和输入框样式。
- 动态Tab样式调整,使用下划线指示当前选中Tab。
- 调整MomentLoaderExtraArgs,增加trend参数用于推荐动态加载。
- 新增字符串资源 `index_recommend`。
2025-09-23 10:58:50 +08:00
742410223c Merge pull request #29 from Zhong202501/main
手动创建AI界面调整
2025-09-23 10:37:15 +08:00
bd5aff7564 手动创建AI界面调整 2025-09-22 17:57:39 +08:00
b43c1585c4 Merge pull request #28 from Zhong202501/main
创建AI界面UI兼容;动态页面调整
2025-09-22 10:48:57 +08:00
cb582393f1 手动创造AI界面;调整输入框点击区域;创建AI时的三点彩色动画 2025-09-19 18:45:10 +08:00
a200d00587 UI调整 2025-09-18 18:19:19 +08:00
6d2133545f 动态详情页面评论调整 2025-09-18 18:16:54 +08:00
2aad126010 创建AI界面UI兼容;动态页面调整 2025-09-17 18:41:15 +08:00
b7b777d2d0 Merge pull request #26 from Zhong202501/new
首页底部导航栏图标;创建AI界面
2025-09-17 11:03:17 +08:00
e804c8be0c Merge pull request #20 from Kevinlinpr/new-bottom-create-button
Feat: Add Create Bottom Sheet and icons
2025-09-17 10:44:34 +08:00
228a74695e Merge pull request #22 from Zhong202501/main
添加Category接口
2025-09-17 10:39:22 +08:00
41a51b85da 首页底部导航栏图标;创建AI界面 2025-09-16 18:18:36 +08:00
e74e8615a5 Agent卡片组件UI;Agent聊天界面输入框显示问题 2025-09-15 14:06:05 +08:00
349d39daf2 修复BUG:我的界面右上角图标会跟随背景图一起向上滑走 2025-09-12 18:24:49 +08:00
eca85c8377 Feat: Add Create Bottom Sheet and icons
- Implemented a new `CreateBottomSheet` Composable to provide users with options to create AI, Group Chat, or Moment.
- Added new drawable resources for the create options: `ic_create_ai.xml`, `ic_create_group_chat.xml`, `ic_create_monent.xml`, and `ic_create_close.xml`.
- Integrated the `CreateBottomSheet` into the `IndexScreen`. Clicking the "+" button now opens this bottom sheet instead of directly navigating to new post creation.
- Updated `IndexViewModel` to manage the visibility state of the `CreateBottomSheet`.
- Added string resources for the Create Bottom Sheet in English, Chinese, and Japanese.
- Ensured proper navigation and tourist mode checks for each create option.
- Implemented graceful dismissal of the bottom sheet with animations.
2025-09-12 17:21:29 +08:00
8154a0ddc4 Category接口;Agent卡片组件背景颜色 2025-09-11 18:14:54 +08:00
f8be622ba6 Merge pull request #17 from Zhong202501/main
首页Agent卡片组件
2025-09-10 19:22:52 +08:00
f3c841779b Merge pull request #18 from Kevinlinpr/ll
修复一些未处理异常,切换到测试服务器
2025-09-10 19:19:38 +08:00
57e4614ce8 修复一些未处理异常,切换到测试服务器 2025-09-10 18:34:36 +08:00
922d6e72d6 首页Agent卡片组件 2025-09-10 18:02:58 +08:00
c41c097d41 处理最新消息显示 2025-09-10 14:03:27 +08:00
5218ca7046 重构IM viewmodel代码 2025-09-10 11:57:05 +08:00
ce6ee7bf82 imsdk 调通 2025-09-09 19:05:07 +08:00
e00deb5661 merge conflict 2025-09-09 17:57:28 +08:00
95d6522a54 fix im connect error 2025-09-09 17:53:52 +08:00
d231f3678c 首页UI 2025-09-09 16:18:35 +08:00
cd35562244 标签页调整 2025-09-09 14:41:37 +08:00
21cb512237 启动图标;动态界面调整 2025-09-08 18:06:39 +08:00
0aa3069efe 调整im 登陆 2025-09-08 16:02:46 +08:00
b79073b295 优化 firebase 报错 2025-09-08 15:42:55 +08:00
1a41cb7aef 初步替换IM接口 2025-09-08 15:13:17 +08:00
1d632fb757 openim sdk 依赖及初始化 2025-09-08 12:10:38 +08:00
ef04450696 移除二进制文件 2025-09-05 16:54:24 +08:00
ef6fb6348f Merge branch 'main' of github.com-qq:Kevinlinpr/rider-pro-android-app 2025-09-05 16:52:10 +08:00
e61fb2ad79 修复无网崩溃 2025-09-05 16:51:27 +08:00
b4ed311978 增加加载异常处理 2025-09-05 16:42:34 +08:00
20ff6df3bf 暗黑模式下输入框为全白问题 2025-09-05 16:10:40 +08:00
18a0bb8494 动态详情页交互功能增加游客登录判断 2025-09-05 15:21:09 +08:00
9dceb99a98 群聊创建失败提示弹窗问题 2025-09-05 14:17:09 +08:00
9f14b35847 防抖调整 2025-09-04 18:28:11 +08:00
9b7349a761 暗黑模式下创建群聊界面不显示置灰状态的创建群聊按钮问题;输入框光标颜色 2025-09-04 14:23:15 +08:00
23f3baf238 Merge remote-tracking branch 'upstream/main' 2025-09-03 18:39:42 +08:00
1f1101e260 动态输入评论UI调整 2025-09-03 18:38:11 +08:00
ca9f4a372b Merge remote-tracking branch 'origin/main' 2025-09-03 18:07:59 +08:00
d93373d8fa Refactor: Add debounce for navigation and optimize comments loading
- Implemented debounced navigation to prevent multiple rapid navigations.
- Replaced Pager-based comment loading with a simpler list-based approach for improved performance and reduced complexity.
- Added loading and error states for comment fetching.
- Introduced `debouncedClickable` modifier for handling click events with debounce.
- Updated image viewer to use simple navigation arrows instead of HorizontalPager for better user experience.
- Added a new string resource for password length error.
2025-09-03 18:07:44 +08:00
ae7254163a Refactor: Add debounce for navigation and optimize comments loading
- Implemented debounced navigation to prevent multiple rapid navigations.
- Replaced Pager-based comment loading with a simpler list-based approach for improved performance and reduced complexity.
- Added loading and error states for comment fetching.
- Introduced `debouncedClickable` modifier for handling click events with debounce.
- Updated image viewer to use simple navigation arrows instead of HorizontalPager for better user experience.
- Added a new string resource for password length error.
2025-09-03 18:02:39 +08:00
e49e509c38 暗黑模式缺省图;UI调整 2025-09-03 17:37:52 +08:00
d703b5ae05 Refactor: Add debounce for navigation and optimize comments loading
- Implemented debounced navigation to prevent multiple rapid navigations.
- Replaced Pager-based comment loading with a simpler list-based approach for improved performance and reduced complexity.
- Added loading and error states for comment fetching.
- Introduced `debouncedClickable` modifier for handling click events with debounce.
- Updated image viewer to use simple navigation arrows instead of HorizontalPager for better user experience.
- Added a new string resource for password length error.
2025-09-03 16:03:57 +08:00
824be5fad8 图片添加加载效果
- 为AsyncImage添加了Shimmer加载效果
- 优化了热门动态的加载逻辑
- 统一了ViewModel的重置方法名
2025-09-03 14:59:47 +08:00
79547de2db 账户编辑、评论点赞功能优化和UI调整
**功能优化:**

*   **账户编辑:**
    *   昵称和个人简介输入时自动去除换行符。
    *   修复了进入编辑页面时可能未正确加载或重置用户资料的问题。
    *   保存资料时,确保昵称和个人简介中的换行符被移除。
    *   清除裁剪的头像图片,避免重复使用。
*   **评论点赞/取消点赞:**
    *   引入乐观更新策略,提升用户体验,点赞/取消点赞操作会立即反映在UI上,然后进行后台API调用。
    *   增加防抖机制,防止用户快速重复点击点赞/取消点赞按钮导致多次API请求。

**UI调整:**

*   **账户编辑页面:**
    *   页面切换动画调整为iOS风格的底部滑入滑出效果。
2025-09-03 14:21:13 +08:00
16f95782f8 优化AI页面资源管理和数据加载
- AgentViewModel:
    - 新增 `refreshAgentData` 方法用于刷新推荐Agent数据。
    - 新增 `ensureDataLoaded` 方法用于检查推荐Agent数据是否为空,如果为空则重新加载。
- ResourceCleanupManager:
    - `cleanupPageResources` 方法新增 `preserveRecommendedData` 参数,用于控制是否保留推荐数据。
    - 新增 `cleanupAiPageCompletely` 方法,用于完全清理AI页面资源(包括推荐数据),主要用于登出等场景。
- Agent.kt:
    - 在页面进入时调用 `viewModel.ensureDataLoaded()` 确保推荐Agent数据已加载。
    - 页面退出时不再清理推荐Agent数据,只在完全登出或需要彻底清理时才清理。

此更改旨在:
- 避免不必要的推荐Agent数据重复加载。
- 优化AI页面退出时的资源清理逻辑,仅在需要时才清理推荐数据。
2025-09-03 11:49:53 +08:00
3aa0172580 Api地址调整 2025-09-03 11:42:59 +08:00
a950504d6f 修改头像昵称获取方式,修改接口地址 2025-09-03 11:41:28 +08:00
288728f142 修复添加Agent时取消选择头像后数据被清空的问题
- 引入`isSelectingAvatar`状态来标记是否正在选择头像。
- 当用户从图片裁剪页面返回或取消选择时,不再清空`AddAgentViewModel`中的数据。
- 仅当用户从`AddAgent`页面返回(并且不是在选择头像的过程中)时,才清空已填写的数据。
- 调整了`AddAgent`页面的返回逻辑,以确保在选择头像过程中返回时数据得以保留。
2025-09-03 11:05:12 +08:00
db85deea96 群聊仅自己可见开关调整 2025-09-02 18:28:37 +08:00
7882ab8f19 评论区UI调整 2025-09-02 17:38:56 +08:00
2b0d33fdbe 上架图-缺省图调整 2025-09-02 16:32:59 +08:00
0ac3312fea 群聊信息UI调整 2025-09-01 18:52:50 +08:00
2f61850c33 群聊信息;更改密码UI调整 2025-09-01 18:17:50 +08:00
6c743a6cac VIP页面UI调整 2025-09-01 18:01:43 +08:00
c202f0d280 UI调整 2025-09-01 17:54:38 +08:00
c20b6ba682 动态tab栏UI调整
- 移除不安全的HttpClient,使用安全的HttpClient
- 动态tab栏非激活状态颜色调整
2025-09-01 17:46:00 +08:00
ab43f154f5 Refactor: 优化智能体操作交互
- 个人主页智能体列表项增加长按操作,用于弹出操作菜单。
- 实现智能体操作菜单弹窗,提供删除等操作选项。
- 增加删除智能体确认对话框,防止误操作。
- 移除账号页面密码输入框重构为通用组件。
2025-09-01 17:14:22 +08:00
9c7c87722b 切换评论排列方式时屏幕下方会出现红线调整 2025-09-01 16:51:12 +08:00
337bda46a2 Ai列表的游客模式优化 2025-09-01 16:14:53 +08:00
2e176605f7 Merge remote-tracking branch 'origin/main' 2025-09-01 16:03:58 +08:00
6721d715e7 关注、粉丝列表点击关注按钮增加游客登录判断 2025-09-01 16:03:12 +08:00
b9fac2c1ee 暗黑模式下更换头像后上方白边调整 2025-09-01 16:02:05 +08:00
118a6cf986 优化DropdownMenu UI
- 修复聊天页静音的显示文案问题
- 优化DropdownMenu的UI
2025-09-01 15:50:10 +08:00
36d322ef8c Reset ViewModels on logout/account switch
This commit introduces a `ResetModel()` function to `AgentViewModel` and `MineAgentViewModel` to clear their state.

This function is now called in `ResourceCleanupManager` and `AppState` during logout or when switching accounts to ensure that data from the previous session is not retained.

Additionally, the search and group name input fields in `CreateGroupChatScreen` are now single-line.
2025-09-01 15:36:22 +08:00
5c12982908 Refactor: Use common password validator
Refactors password validation logic to use a common `PasswordValidator` utility. This ensures consistent password validation rules across different parts of the application, including:

- User login
- Email sign-up
- Password change
- Account removal

Also adds a string resource for password too long error message.
2025-09-01 15:10:14 +08:00
4d319351b7 朋友圈刷新方式调整 2025-09-01 15:02:39 +08:00
76bcb1e7fc Limit user bio display to one line
Ensures that user bios in the user item component are truncated to a single line with an ellipsis if they exceed the available width. This applies to both users with existing bios and those with the "No bio here." placeholder.
2025-09-01 14:34:03 +08:00
83cff9d56c Enhance AI Agent Profile Interaction
This commit introduces several enhancements to how AI agent profiles are displayed and interacted with:

**Profile Display:**
- **AI Account Distinction:** Profile pages now differentiate between regular user accounts and AI agent accounts.
    - AI agent profiles will not display the "Agents" tab in their profile.
    - The profile header height is adjusted for AI accounts.
- **Navigation Parameter:** An `isAiAccount` boolean parameter is added to the `AccountProfile` navigation route to indicate if the profile being viewed belongs to an AI.

**Interaction & Navigation:**
- **Avatar Click Navigation:**
    - Clicking an AI agent's avatar in various lists (Hot Agents, My Agents, User Agents Row, User Agents List) now navigates to the agent's dedicated profile page.
    - When navigating to an agent's profile from an agent list, `isAiAccount` is set to `true`.
- **Chat Initiation:** Clicking the chat button on AI agent cards in the "Agent" tab (both Hot and My Agents) now correctly initiates a chat with the respective AI.
- **ViewModel Updates:**
    - `AgentViewModel`, `MineAgentViewModel`, and `HotAgentViewModel` now include a `goToProfile` function to handle navigation to agent profiles, correctly passing the `isAiAccount` flag.

**Code Refinements:**
- Click handlers for agent avatars and chat buttons are now wrapped with `DebounceUtils.simpleDebounceClick` to prevent multiple rapid clicks.
- The `UserContentPageIndicator` now conditionally hides the "Agent" tab based on the `isAiAccount` status.
- `UserAgentsRow` and `UserAgentsList` now accept an `onAvatarClick` callback for navigating to agent profiles.
- `AgentItem` (used in `UserAgentsRow`) and `UserAgentCard` (used in `UserAgentsList`) now handle avatar clicks.
- The general `Agent` composable (used in `AiPostComposable`) now also supports an `onAvatarClick` callback.
2025-09-01 14:17:44 +08:00
484d641554 Enhance AI Agent Profile Interaction
This commit introduces several enhancements to how AI agent profiles are displayed and interacted with:

**Profile Display:**
- **AI Account Distinction:** Profile pages now differentiate between regular user accounts and AI agent accounts.
    - AI agent profiles will not display the "Agents" tab in their profile.
    - The profile header height is adjusted for AI accounts.
- **Navigation Parameter:** An `isAiAccount` boolean parameter is added to the `AccountProfile` navigation route to indicate if the profile being viewed belongs to an AI.

**Interaction & Navigation:**
- **Avatar Click Navigation:**
    - Clicking an AI agent's avatar in various lists (Hot Agents, My Agents, User Agents Row, User Agents List) now navigates to the agent's dedicated profile page.
    - When navigating to an agent's profile from an agent list, `isAiAccount` is set to `true`.
- **Chat Initiation:** Clicking the chat button on AI agent cards in the "Agent" tab (both Hot and My Agents) now correctly initiates a chat with the respective AI.
- **ViewModel Updates:**
    - `AgentViewModel`, `MineAgentViewModel`, and `HotAgentViewModel` now include a `goToProfile` function to handle navigation to agent profiles, correctly passing the `isAiAccount` flag.

**Code Refinements:**
- Click handlers for agent avatars and chat buttons are now wrapped with `DebounceUtils.simpleDebounceClick` to prevent multiple rapid clicks.
- The `UserContentPageIndicator` now conditionally hides the "Agent" tab based on the `isAiAccount` status.
- `UserAgentsRow` and `UserAgentsList` now accept an `onAvatarClick` callback for navigating to agent profiles.
- `AgentItem` (used in `UserAgentsRow`) and `UserAgentCard` (used in `UserAgentsList`) now handle avatar clicks.
- The general `Agent` composable (used in `AiPostComposable`) now also supports an `onAvatarClick` callback.
2025-09-01 01:10:35 +08:00
12b3c15083 允许在群聊列表中跳转到群聊信息页面 2025-09-01 00:02:20 +08:00
cb253d3276 完善个人信息编辑
- 修复了从相册选择图片时,如果文件名不包含扩展名导致的崩溃问题。
- 在编辑个人信息页面增加了“修改密码”的入口。
- 修复了新建Agent时,保存按钮状态更新不及时的问题。
- 优化了新建Agent时,对异常的处理。
2025-08-31 23:59:05 +08:00
2907e7f9a6 Refactor: 优化关注列表和代码逻辑
本次提交主要包含以下更改:

- **关注/粉丝列表逻辑修正**:修复了在 `FollowerListViewModel` 和 `FollowingListViewModel` 中 `followerId` 和 `followingId` 属性赋值错误的问题,确保正确加载关注和粉丝列表。
- **分页大小调整**:将 `BaseFollowModel` 中分页加载的 `pageSize` 从 5 调整为 20,以提高用户体验。
- **个人资料页智能体展示**:
    - 在 `AccountProfileV2` 和 `AccountProfileViewModel` 中增加了对用户智能体列表的加载和展示逻辑。
    - 修复了 `MyProfileViewModel` 中加载当前用户智能体时 `authorId` 传递错误的问题。
    - 优化了 `UserAgentsRow` 和 `UserAgentsViewModel` 的逻辑,确保正确加载和显示用户智能体,并在用户切换时清理旧数据。
- **代码清理**:移除了部分冗余的 `println` 调试信息。
- **Agent创建流程优化**:在 `AddAgentViewModel` 和 `AgentImageCropScreen` 中,确保在已有裁剪图片时直接使用,避免重复裁剪。
2025-08-31 23:50:52 +08:00
281169cfcb UI优化 2025-08-31 23:26:13 +08:00
ba7eeeca90 增加智能体头像裁剪页面
- 新增 `AgentImageCropScreen.kt` 用于智能体头像的裁剪和预览。
- 调整导航逻辑,添加智能体时跳转到新的 `AgentImageCrop` 路由。
- 原有的 `ImageCropScreen` 专门用于处理个人资料头像的裁剪。
- `AddAgentViewModel` 中移除 `isFromAddAgent` 标志,通过不同的导航路由区分头像裁剪来源。
2025-08-31 23:18:33 +08:00
00824ff7b4 Refactor: 优化个人主页和账户主页帖子加载逻辑
- 统一MyProfileViewModel和AccountProfileViewModel中的帖子加载逻辑,使用一致的pageSize。
- 在ProfileWrap和AccountProfileV2中传递正确的postCount。
- 在ProfileV3中改进了加载更多帖子的触发条件,确保在有更多数据时才触发加载。
- 修复了注册页面勾选协议和促销选项后,错误状态未清除的问题。
- 为DataLoader和ProfileV3中的滚动加载逻辑添加了详细日志,方便调试。
2025-08-31 23:04:52 +08:00
3777a76c44 群聊头像支持点击跳转到群信息页面 2025-08-31 22:24:53 +08:00
40ccd70e80 Add VIP selection page and related data models
This commit introduces a new VIP selection page (`VipSelPage.kt`) that allows users to choose between Premium and Standard membership plans.

Key changes include:

*   **New VIP Selection UI:**
    *   `VipSelPage.kt`: Implements the UI for selecting VIP plans, displaying prices, and benefits.
    *   `SelfProfileAction.kt`: Updated to include a "Rave Premium" button alongside "Edit Profile".
*   **Data Models for Membership:**
    *   `MembershipModels.kt`: Defines data classes for membership configuration (`MembershipConfigData`, `ConfigData`, `Member`, `Benefit`, `Good`), price models (`VipPriceModel`), page data models (`VipPageDataModel`), and request bodies (`ValidateProductRequestBody`, `ValidateData`).
    *   `VipModelMapper`: Provides functions to transform backend data into UI-friendly models for price and benefit display.
*   **API Integration:**
    *   `RiderProAPI.kt`: Added new endpoints `getMembershipConfig` to fetch membership details and `validateAndroidProduct` for product validation.
*   **Navigation:**
    *   `Navi.kt`: Added `VipSelPage` to the navigation routes.
    *   `ProfileV3.kt`: The "Rave Premium" button in the self profile action now navigates to the `VipSelPage`.
*   **Theming:**
    *   `Colors.kt`: Added new color definitions for premium buttons, VIP benefit highlighting, and price card states (selected/unselected).
*   **Assets:**
    *   `ic_member.webp`: New icon for the "Rave Premium" button.
2025-08-31 22:17:20 +08:00
5759d4ec95 导航切换动画调整
将默认的淡入淡出动画效果替换为更接近iOS风格的侧滑动画,提升页面切换的流畅度和视觉体验。

**具体变更:**

*   **页面进入:** 新页面从右侧滑入。
*   **页面退出:** 当前页面向右侧滑出,前一页面从左侧轻微偏移处滑回。
*   **动画时长:** 统一设置为280毫秒。

**影响范围:**

*   图片详情页 (`ImagePagerScreen`)
*   创建群聊页 (`CreateGroupChatScreen`)

**其他优化:**

*   **创建群聊页UI调整:**
    *   群聊名称输入框样式统一,采用圆角灰色背景。
    *   底部创建按钮适配导航栏高度。
    *   列表区域自适应填满剩余空间,防止内容被遮挡。
    *   选择成员列表项固定高度,避免选中状态变化时布局跳动。
    *   为头像和选择框添加默认图和占位图。
*   **ImageLoader优化:**
    *   实现全局共享的 `ImageLoader` 实例,避免重复创建,提高内存缓存利用率。
*   **列表性能优化:**
    *   为好友列表和AI助手列表的 `items` 添加 `key`,提升列表项更新效率。
*   **资源清理调整:**
    *   移除了在离开首页和动态页时全量清理资源的操作,以避免返回时列表重置或不必要的重新加载。
*   **ProfileV3页代码清理:**
    *   移除未使用的导入。
2025-08-31 21:13:06 +08:00
21200910c1 Refactor: 个人主页UI及智能体列表功能调整
- **个人主页UI调整:**
    - "编辑个人资料"按钮样式调整为与"私信"按钮一致。
    - "关注"按钮样式调整为渐变色,"已关注"状态下显示灰色边框。
    - "私信"按钮样式调整为灰色背景。
    - "帖子"、"粉丝"、"关注"文案调整。
    - 个人主页背景色适配深色模式。
    - 个人主页内容切换Tab图标化,并添加下划线指示器。
- **智能体列表功能:**
    - 新增`UserAgentsList.kt`用于展示用户的智能体列表。
    - 在个人主页中集成智能体列表展示(新的Tab页)。
    - 调整`UserAgentsRow`组件,使其能够加载并展示当前用户或其他用户的智能体,并添加"更多"按钮。
    - `MyProfileViewModel`中增加对智能体数据的加载和管理。
    - `UserAgentsViewModel`调整为可以加载指定用户或当前用户的智能体数据。
- **其他:**
    - `Colors.kt`中新增`profileBackground`颜色定义。
    - `Agent.kt`中调整了`getAgent`方法返回类型。
2025-08-31 20:23:28 +08:00
a79628026c 首页UI调整 2025-08-29 18:39:59 +08:00
7c0d35ec8c 首页搜索调整 2025-08-28 18:58:22 +08:00
da5fdcbd57 删除账户UI调整 2025-08-28 14:30:47 +08:00
weber
fdf8c1fa5a 资源清理管理 2025-08-27 18:32:51 +08:00
weber
2a7d310be5 新增网络工具,修改bug 2025-08-27 16:37:53 +08:00
weber
52e571da01 用户信息调整 2025-08-27 11:39:50 +08:00
5d4a95bf07 UI调整 2025-08-26 18:42:33 +08:00
f2ab55d545 Merge upstream/main and resolve conflicts 2025-08-25 18:48:56 +08:00
weber
df75c710e5 UI调整 2025-08-25 18:35:06 +08:00
weber
77033854f0 UI调整 2025-08-25 10:49:00 +08:00
Weber
4be63f3428 Merge pull request #16 from Zhong202501/main
粉丝 关注 评论UI调整
2025-08-25 10:09:57 +08:00
52d32a8510 粉丝 关注 评论UI调整 2025-08-22 18:52:41 +08:00
bd2d291164 Merge upstream/main and resolve conflicts 2025-08-22 10:41:30 +08:00
weber
9fb79b3881 聊天室和群聊信息调整 2025-08-21 19:04:59 +08:00
weber
5ee1897739 消息列表和聊天时调整 2025-08-21 17:08:18 +08:00
Weber
edcab76fdb Merge pull request #15 from Zhong202501/zhong
UI调整
2025-08-21 16:51:49 +08:00
ed9dd9ab3e first push 2025-08-21 16:49:24 +08:00
a9066bd473 test push 2025-08-21 16:43:30 +08:00
3e9353f07b UI调整 2025-08-21 16:36:54 +08:00
weber
8f8c2ff2e9 UI调整,群聊开发 2025-08-20 19:19:14 +08:00
weber
791b24b2fb UI调整,群聊天室开发 2025-08-18 19:02:11 +08:00
weber
2de8127882 首页UI调整 2025-08-15 18:56:38 +08:00
weber
112efa74e7 首页UI调整 2025-08-14 19:04:20 +08:00
weber
d8c091b19b 页面样式调整 2025-08-14 15:39:21 +08:00
weber
cd3fc03524 修正会话分组列表数据 2025-08-13 19:04:50 +08:00
weber
bc7a897cec 预留首页-探索,完善群组功能 2025-08-13 18:57:03 +08:00
weber
2d518cbd68 自定义NavigationItem,新增群组创建页面 2025-08-12 19:06:56 +08:00
weber
697af504b7 会话分组及聊天室实现 2025-08-11 18:21:22 +08:00
weber
54ca1d3f1c Ai和用户类型分组验证 2025-08-08 18:53:10 +08:00
1bb0adeb90 添加标签组件并更新导航栏Add按钮样式
- 新增可复用的`TabItem`和`TabSpacer`组件,用于实现标签页切换效果。
- 在消息列表和AI Agent页面中,使用新的`TabItem`和`TabSpacer`组件替换原有的标签页实现,简化代码并统一风格。
- 更新底部导航栏Add按钮的图标和交互行为:
    - 使用新的`ic_nav_add.xml`图标。
    - Add按钮只显示图标,不显示文字标签。
    - Add按钮图标放大。
- 在`Colors.kt`中为`AppThemeData`添加新的颜色属性,以支持新标签组件的自定义主题。
2025-08-08 09:57:42 +08:00
weber
f6a796e2bc 智能体会话开发 2025-08-07 19:03:05 +08:00
weber
38759eb3e4 处理下标越界和防抖 2025-08-06 18:47:24 +08:00
weber
b837c704e5 我的智能体 2025-08-06 18:14:36 +08:00
weber
a944bd0fa3 智能体列表,全部 2025-08-06 15:58:17 +08:00
weber
993604bfc1 我的智能体 2025-08-05 17:01:23 +08:00
dc9c013383 更新消息和AI代理页面的文本颜色,注释掉部分消息列表初始化逻辑 2025-08-05 16:51:17 +08:00
91c34a0acf Merge remote-tracking branch 'origin/main' 2025-08-05 16:31:46 +08:00
a2254e503e 适配夜间模式 2025-08-05 16:31:28 +08:00
weber
daea7824af create agent 2025-08-05 16:25:06 +08:00
0f5d3d7960 限制帖子图片数量为9张 2025-08-05 16:20:30 +08:00
6433d4a23c 修改颜色配置 2025-08-05 16:00:36 +08:00
873001ce28 在 FavouriteListViewModel 中增加 EventBus 监听,以便在动态取消收藏时刷新列表。 2025-08-05 15:47:30 +08:00
7ea75a4755 区分动态发布者和其他用户在动态菜单中的操作权限 2025-08-05 15:12:29 +08:00
a80711a475 统一重置密码界面背景色和错误文本颜色 2025-08-05 15:03:05 +08:00
weber
3e544844bb 添加新的表单文本输入组件 FormTextInput2,包含错误提示和动态显示功能;新增图标和图片资源。 2025-08-05 14:24:15 +08:00
weber
29d2bb753f 实现了 Agent 和 Profile 数据类,添加了 AddAgent 界面 2025-07-31 15:37:17 +08:00
weber
6ec732b996 20250723-Test Push 2025-07-23 19:07:29 +08:00
9f93e6dc14 更新代码 2024-12-07 17:14:45 +08:00
50fb1874e7 增加动态举报 2024-12-06 22:08:57 +08:00
76e7bbb84a 添加Chat处理逻辑 2024-12-06 10:57:31 +08:00
cc7a65e016 更新Google登录 2024-12-01 21:27:39 +08:00
784427cfba 更新密码校验规则 2024-12-01 15:29:03 +08:00
c54d5c914a 更新关注逻辑 2024-12-01 15:13:47 +08:00
79fccda1aa 更新动态加载逻辑 2024-12-01 09:40:13 +08:00
6c19f83cfb 更新个人资料同时更新聊天个人信息 2024-11-30 22:24:28 +08:00
3a68a51f3c 更新聊天头像逻辑 2024-11-30 08:50:50 +08:00
324e04881e 修改推送配置 2024-11-28 18:03:24 +08:00
5cacfbdd95 Rave Now 版本 VERSION 1.0.000.19 2024-11-24 21:14:41 +08:00
9fac012d11 移除https错误忽略 2024-11-24 19:58:53 +08:00
ca5629043b Rave Now 版本 VERSION 1.0.000.18 2024-11-21 06:18:16 +08:00
b697082732 Rave Now 版本 新签名 2024-11-17 20:54:32 +08:00
a243abb8a2 Rave Now 版本1.0.000.17 2024-11-17 20:41:11 +08:00
e75b181818 Rave Now 版本1.0.000.17 2024-11-17 20:35:42 +08:00
4cdb7f28b3 完全替换为Rave Now,只保留了Google Play的密钥还有一些图片映射还有旧字样。 2024-11-17 20:31:18 +08:00
074244c0f8 改包名com.aiosman.ravenow 2024-11-17 20:07:42 +08:00
914cfca6be 更新软件Logo 2024-11-15 20:04:15 +08:00
e62e83c566 Merge remote-tracking branch 'refs/remotes/origin/main' 2024-11-14 22:41:23 +08:00
5b2613cacc 修正firebase analytics 2024-11-14 17:49:26 +08:00
ee88708860 1.0.000.16 2024-11-05 04:37:03 +08:00
34ce648102 fix #12 修正删除动态后,移除我的页面中的item 2024-11-03 16:51:23 +08:00
7152bfd9c1 fix #9 修正多次点击的白屏,添加修改用户资料防抖 2024-11-03 12:48:37 +08:00
d5e210b080 更新代码 2024-11-03 09:18:28 +08:00
3cef90c887 更新代码 2024-11-03 09:17:46 +08:00
d49e037f51 fix #7 新增了修改密码时错误的旧密码提示 2024-11-03 09:15:19 +08:00
7ed3c51118 fix #6 修正了创建新的评论后,评论区重复显示评论的问题 2024-11-03 08:20:48 +08:00
560f4655df fix #5
修正了按钮高度问题
2024-11-03 00:32:58 +08:00
720bafe787 添加com.google.ar.core 2024-10-30 01:11:50 +08:00
068ca693ea 1.0.000.15 2024-10-29 05:30:54 +08:00
d6e2db7edb 删除post中的following按钮 2024-10-29 05:30:01 +08:00
5b3d663a73 修正动态页面错误 2024-10-27 11:31:27 +08:00
be18a60b2b 修正个人主页阴影错误 2024-10-26 23:05:31 +08:00
e1ba8709de 禁止在主页进行unfollow操作 2024-10-26 21:59:37 +08:00
e8ddd67219 屏蔽关注动态页面的关注按钮 2024-10-26 19:18:50 +08:00
2012668361 动态主题切换 2024-10-26 19:05:52 +08:00
01fb092e83 feat: Implement moment follow/unfollow feature
This commit implements the follow/unfollow feature for moments in the Search, Timeline, and Explore tabs.

It includes:

- Adding a follow button to moment cards and implementing follow/unfollow logic in the respective ViewModels.
- Updating the UI to reflect the follow status changes in the moment list.
- Handling follow/unfollow API requests.
2024-10-26 17:44:51 +08:00
fe2bd2f382 feat: 实现探索动态功能
在动态列表中新增探索动态功能,允许用户浏览和发现新的动态内容。

具体修改包括:

- 在API接口中添加`explore`参数,用于请求探索动态数据。
- 修改MomentService,添加`explore`参数,用于获取探索动态数据。
- 更新MomentViewModel,使用`explore`参数请求探索动态数据。
- 修改MomentPagingSource,使用`explore`参数请求探索动态数据。
- 修改MomentCard,使用`explore`参数显示探索动态数据。
2024-10-26 17:17:29 +08:00
5c1cca6a69 修改更新提示的颜色 2024-10-26 16:48:45 +08:00
9838439ff1 补充提交 2024-10-26 16:14:47 +08:00
994e493035 版本更新提示 2024-10-26 16:06:59 +08:00
fd2f1cfc1d 版本1.0.000.15 2024-10-26 06:54:56 +08:00
8dca3e8de1 暗黑模式按钮、更新弹框文案 2024-10-26 06:53:47 +08:00
e38c36aa2c 软件内版本检测更新 2024-10-26 06:02:30 +08:00
ed66f15be7 修复小细节,home图标和消息通知 2024-10-26 02:51:23 +08:00
d3518b1a82 首页新增推荐 2024-10-25 22:01:58 +08:00
5e65f217bb 底部导航Mask颜色使用AppColor 2024-10-25 03:33:21 +08:00
c183c2fb9d TextField图标文字对齐(password) 2024-10-25 02:49:04 +08:00
684078515f 提取应用颜色 2024-10-24 23:53:51 +08:00
9cf6a338ea 提取App颜色 2024-10-24 17:22:05 +08:00
9e55043a17 新消息时自动刷新 2024-10-24 15:00:11 +08:00
fde7a0ad95 问题修正 2024-10-23 21:09:59 +08:00
dad032e233 问题修正 2024-10-23 20:32:32 +08:00
597 changed files with 28748 additions and 8051 deletions

2
.idea/.name generated
View File

@@ -1 +1 @@
RiderPro
RaveNow

2
.idea/compiler.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-08-11T15:57:44.196893Z">
<DropdownSelection timestamp="2025-09-17T06:25:35.585100400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/kevinlinpr/.android/avd/Pixel_8_Pro_API_34.avd" />
<DeviceId pluginId="Default" identifier="serial=192.168.0.216:5555;connection=698a7727" />
</handle>
</Target>
</DropdownSelection>

1
.idea/gradle.xml generated
View File

@@ -4,6 +4,7 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">

View File

@@ -65,6 +65,10 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</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">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
<option name="version" value="1.9.10" />
</component>
</project>

3
.idea/misc.xml generated
View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<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" />
</component>
<component name="ProjectType">

View File

@@ -5,8 +5,12 @@
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<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.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>
</option>
</component>

176
MIGRATION_GUIDE.md Normal file
View 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. 清理所有相关的临时注释

View File

@@ -7,15 +7,15 @@ plugins {
}
android {
namespace = "com.aiosman.riderpro"
namespace = "com.aiosman.ravenow"
compileSdk = 34
defaultConfig {
applicationId = "com.aiosman.riderpro"
applicationId = "com.aiosman.ravenow"
minSdk = 24
targetSdk = 34
versionCode = 1000014
versionName = "1.0.000.14"
versionCode = 1000019
versionName = "1.0.000.19"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -24,7 +24,7 @@ android {
addManifestPlaceholders(
mapOf(
"JPUSH_PKGNAME " to applicationId!!,
"JPUSH_APPKEY" to "ad805ee9f2760376f4f47178",
"JPUSH_APPKEY" to "cbd968cae60346065e03f9d7",
"JPUSH_CHANNEL" to "developer-default",
)
@@ -32,6 +32,9 @@ android {
}
buildTypes {
debug {
isDebuggable = true
}
release {
isMinifyEnabled = false
proguardFiles(
@@ -49,9 +52,10 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
kotlinCompilerExtensionVersion = "1.5.3"
}
packaging {
resources {
@@ -93,27 +97,35 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.androidx.animation)
implementation("io.coil-kt:coil-compose:2.7.0")
implementation("io.coil-kt:coil:2.7.0")
implementation("com.google.android.gms:play-services-auth:21.2.0")
implementation("io.github.serpro69:kotlin-faker:2.0.0-rc.5")
implementation("androidx.compose.material:material:1.6.8")
implementation("net.engawapg.lib:zoomable:1.6.1")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("androidx.credentials:credentials:1.2.2")
implementation("androidx.credentials:credentials-play-services-auth:1.2.2")
implementation("com.auth0.android:jwtdecode:2.0.2")
implementation(libs.coil.compose)
implementation(libs.coil)
implementation(libs.play.services.auth)
implementation(libs.kotlin.faker)
implementation(libs.androidx.material)
implementation(libs.zoomable)
implementation(libs.retrofit)
implementation(libs.converter.gson)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.play.services.auth)
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 依赖
}

View File

@@ -9,7 +9,7 @@
"client_info": {
"mobilesdk_app_id": "1:987156664714:android:2c29c11b9cd8be78b9f873",
"android_client_info": {
"package_name": "com.aiosman.riderpro"
"package_name": "com.aiosman.ravenow"
}
},
"oauth_client": [],

View File

@@ -20,4 +20,23 @@
# hide the original source file name.
#-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 *;
}

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.aiosman.riderpro", appContext.packageName)
assertEquals("com.aiosman.ravenow", appContext.packageName)
}
}

View File

@@ -4,18 +4,20 @@
xmlns:tools="http://schemas.android.com/tools">
<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.READ_PHONE_STATE" />
<application
android:name=".RaveNowApplication"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/rider_pro_logo_red"
android:icon="@mipmap/invalid_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:theme="@style/Theme.RiderPro"
android:theme="@style/Theme.RaveNow"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<meta-data
@@ -34,11 +36,20 @@
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
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
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.App.Starting"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -52,6 +63,13 @@
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<receiver
android:name=".model.ApkInstallReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<service
android:name=".MyFirebaseMessagingService"
@@ -70,7 +88,7 @@
</intent-filter>
</service>
<service
android:name=".TrtcService"
android:name=".OpenIMService"
android:exported="false" />
<receiver
@@ -79,14 +97,14 @@
android:exported="false">
<intent-filter>
<action android:name="cn.jpush.android.intent.RECEIVER_MESSAGE" />
<category android:name="com.aiosman.riderpro" />
<category android:name="com.aiosman.ravenow" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.aiosman.riderpro.fileprovider"
android:authorities="com.aiosman.ravenow.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data

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

View File

@@ -1,8 +1,8 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import com.aiosman.riderpro.data.ChatService
import com.aiosman.riderpro.data.ChatServiceImpl
import com.aiosman.riderpro.entity.ChatNotification
import com.aiosman.ravenow.data.ChatService
import com.aiosman.ravenow.data.ChatServiceImpl
import com.aiosman.ravenow.entity.ChatNotification
/**
* 保存一些关于聊天的状态

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

View File

@@ -0,0 +1,76 @@
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环境
}
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)
}
}

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import android.content.Context
import android.content.Intent

View File

@@ -1,6 +1,5 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import android.content.Context
import cn.jpush.android.service.JCommonService
class JpushService : JCommonService() {

View File

@@ -1,10 +1,11 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
@@ -14,9 +15,12 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
@@ -24,15 +28,18 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.navigation.NavHostController
import cn.jiguang.api.utils.JCollectionAuth
import cn.jpush.android.api.JPushInterface
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.ui.Navigation
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.navigateToPost
import com.aiosman.riderpro.ui.post.NewPostViewModel
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.ui.Navigation
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.dialogs.CheckUpdateDialog
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.analytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -42,6 +49,7 @@ class MainActivity : ComponentActivity() {
// Firebase Analytics
private lateinit var analytics: FirebaseAnalytics
private val scope = CoroutineScope(Dispatchers.Main)
val context = this
// 请求通知权限
private val requestPermissionLauncher = registerForActivityResult(
@@ -67,8 +75,14 @@ class MainActivity : ComponentActivity() {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(Build.VERSION_CODES.P)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置屏幕方向为竖屏
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
// 监听应用生命周期
ProcessLifecycleOwner.get().lifecycle.addObserver(MainActivityLifecycleObserver())
// 创建通知渠道
@@ -78,7 +92,7 @@ class MainActivity : ComponentActivity() {
// 初始化 Places SDK
// 初始化 Firebase Analytics
// analytics = Firebase.analytics
analytics = Firebase.analytics
// 请求通知权限
askNotificationPermission()
// 加载一些本地化的配置
@@ -91,84 +105,92 @@ class MainActivity : ComponentActivity() {
JPushInterface.init(this)
if (AppState.darkMode) {
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
}
enableEdgeToEdge()
// 初始化腾讯云通信 SDK
scope.launch {
// 检查是否有登录态
val isAccountValidate = getAccount()
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)
startDestination = NavigationRoute.Index.route
}
setContent {
Navigation(startDestination) { navController ->
// 处理带有 postId 的通知点击
val postId = intent.getStringExtra("POST_ID")
var commentId = intent.getStringExtra("COMMENT_ID")
val action = intent.getStringExtra("ACTION")
if (action == "newFollow") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "followCount") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "TRTC_NEW_MESSAGE") {
val userService:UserService = UserServiceImpl()
val sender = intent.getStringExtra("SENDER")
sender?.let {
scope.launch {
try {
val profile = userService.getUserProfileByTrtcUserId(it)
navController.navigate(NavigationRoute.Chat.route.replace(
"{id}",
profile.id.toString()
))
}catch (e:Exception){
e.printStackTrace()
CompositionLocalProvider(
LocalAppTheme provides AppState.appTheme
) {
CheckUpdateDialog()
Navigation(startDestination) { navController ->
// 处理带有 postId 的通知点击
val postId = intent.getStringExtra("POST_ID")
var commentId = intent.getStringExtra("COMMENT_ID")
val action = intent.getStringExtra("ACTION")
if (action == "newFollow") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "followCount") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "TRTC_NEW_MESSAGE") {
val userService:UserService = UserServiceImpl()
val sender = intent.getStringExtra("SENDER")
sender?.let {
scope.launch {
try {
val profile = userService.getUserProfileByTrtcUserId(it,0)
navController.navigate(NavigationRoute.Chat.route.replace(
"{id}",
profile.id.toString()
))
}catch (e:Exception){
e.printStackTrace()
}
}
}
return@Navigation
}
return@Navigation
}
if (commentId == null) {
commentId = "0"
}
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
navController.navigateToPost(
id = postId.toInt(),
highlightCommentId = commentId.toInt(),
initImagePagerIndex = 0
)
}
// 处理分享过来的图片
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
val imageUris: List<Uri>? = if (intent.action == Intent.ACTION_SEND) {
listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM)!!)
} else {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
if (commentId == null) {
commentId = "0"
}
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
navController.navigate(NavigationRoute.NewPost.route)
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
navController.navigateToPost(
id = postId.toInt(),
highlightCommentId = commentId.toInt(),
initImagePagerIndex = 0
)
}
// 处理分享过来的图片
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
val imageUris: List<Uri>? = if (intent.action == Intent.ACTION_SEND) {
listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM)!!)
} else {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
}
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
navController.navigate(NavigationRoute.NewPost.route)
}
}
}
}
}
}
/**
* 请求通知权限
*/
@@ -218,3 +240,6 @@ val LocalAnimatedContentScope = compositionLocalOf<AnimatedContentScope> {
}
val LocalAppTheme = compositionLocalOf<AppThemeData> {
error("AppThemeData not provided")
}

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner

View File

@@ -1,10 +1,10 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import android.content.Context
import android.util.Log
import cn.jpush.android.api.JPushInterface
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.CoroutineScope

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import android.Manifest
import android.app.PendingIntent
@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import androidx.compose.material.Icon
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -31,7 +30,7 @@ fun showLikeNotification(context: Context, title: String, message: String, postI
)
val notificationBuilder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.rider_pro_favoriate)
.setSmallIcon(R.drawable.rider_pro_favourite)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)

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

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

View File

@@ -1,25 +1,30 @@
package com.aiosman.riderpro.data
package com.aiosman.ravenow.data
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.data.api.AppConfig
import com.aiosman.riderpro.data.api.CaptchaInfo
import com.aiosman.riderpro.data.api.ChangePasswordRequestBody
import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody
import com.aiosman.riderpro.data.api.LoginUserRequestBody
import com.aiosman.riderpro.data.api.RegisterMessageChannelRequestBody
import com.aiosman.riderpro.data.api.RegisterRequestBody
import com.aiosman.riderpro.data.api.ResetPasswordRequestBody
import com.aiosman.riderpro.data.api.TrtcSignResponseBody
import com.aiosman.riderpro.data.api.UnRegisterMessageChannelRequestBody
import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody
import com.aiosman.riderpro.data.api.UpdateUserLangRequestBody
import com.aiosman.riderpro.entity.AccountFavouriteEntity
import com.aiosman.riderpro.entity.AccountLikeEntity
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.entity.NoticeCommentEntity
import com.aiosman.riderpro.entity.NoticePostEntity
import com.aiosman.riderpro.entity.NoticeUserEntity
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.AppConfig
import com.aiosman.ravenow.data.api.CaptchaInfo
import com.aiosman.ravenow.data.api.ChangePasswordRequestBody
import com.aiosman.ravenow.data.api.GoogleRegisterRequestBody
import com.aiosman.ravenow.data.api.GuestLoginRequestBody
import com.aiosman.ravenow.data.api.LoginUserRequestBody
import com.aiosman.ravenow.data.api.RegisterMessageChannelRequestBody
import com.aiosman.ravenow.data.api.RegisterRequestBody
import com.aiosman.ravenow.data.api.RemoveAccountRequestBody
import com.aiosman.ravenow.data.api.ResetPasswordRequestBody
import com.aiosman.ravenow.data.api.TrtcSignResponseBody
import com.aiosman.ravenow.data.api.UnRegisterMessageChannelRequestBody
import com.aiosman.ravenow.data.api.UpdateNoticeRequestBody
import com.aiosman.ravenow.data.DataContainer
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.api.UpdateUserLangRequestBody
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 okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
@@ -52,6 +57,13 @@ data class AccountProfile(
val banner: String?,
// trtcUserId
val trtcUserId: String,
val openImToken: String?,
// aiAccount true:ai false:普通用户
val aiAccount: Boolean,
val chatAIId: String,
) {
/**
* 转换为Entity
@@ -62,7 +74,8 @@ data class AccountProfile(
followerCount = followerCount,
followingCount = followingCount,
nickName = nickname,
avatar = "${ApiClient.BASE_SERVER}$avatar",
avatar =
"${ApiClient.BASE_SERVER}$avatar",
bio = bio,
country = "Worldwide",
isFollowing = isFollowing,
@@ -72,7 +85,11 @@ data class AccountProfile(
}
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
/**
* 游客登录
* @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 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 {
override suspend fun getMyAccountProfile(): AccountProfileEntity {
// 如果已有缓存,直接返回缓存结果
AppState.profile?.let { return it }
// 第一次调用,获取数据并缓存
val resp = ApiClient.api.getMyAccount()
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 {
@@ -423,12 +472,31 @@ class AccountServiceImpl : AccountService {
override suspend fun loginUserWithGoogle(googleId: String): UserAuth {
val resp = ApiClient.api.login(LoginUserRequestBody(googleId = googleId))
val body = resp.body() ?: throw ServiceException("Failed to login")
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) {
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")
}
}
@@ -472,7 +540,13 @@ class AccountServiceImpl : AccountService {
}
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> {
@@ -543,4 +617,29 @@ class AccountServiceImpl : AccountService {
val body = resp.body() ?: throw ServiceException("Failed to get app config")
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)
}
}

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

View File

@@ -1,10 +1,10 @@
package com.aiosman.riderpro.data
package com.aiosman.ravenow.data
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.data.api.CaptchaRequestBody
import com.aiosman.riderpro.data.api.CaptchaResponseBody
import com.aiosman.riderpro.data.api.CheckLoginCaptchaRequestBody
import com.aiosman.riderpro.data.api.GenerateLoginCaptchaRequestBody
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CaptchaRequestBody
import com.aiosman.ravenow.data.api.CaptchaResponseBody
import com.aiosman.ravenow.data.api.CheckLoginCaptchaRequestBody
import com.aiosman.ravenow.data.api.GenerateLoginCaptchaRequestBody
interface CaptchaService {

View File

@@ -1,8 +1,8 @@
package com.aiosman.riderpro.data
package com.aiosman.ravenow.data
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.data.api.UpdateChatNotificationRequestBody
import com.aiosman.riderpro.entity.ChatNotification
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.UpdateChatNotificationRequestBody
import com.aiosman.ravenow.entity.ChatNotification
interface ChatService {
suspend fun getChatNotifications(

View File

@@ -1,8 +1,8 @@
package com.aiosman.riderpro.data
package com.aiosman.ravenow.data
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.data.api.CommentRequestBody
import com.aiosman.riderpro.entity.CommentEntity
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CommentRequestBody
import com.aiosman.ravenow.entity.CommentEntity
import com.google.gson.annotations.SerializedName
/**

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

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.data
package com.aiosman.ravenow.data
/**
* 通用接口返回数据

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

View File

@@ -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.annotations.SerializedName
import okhttp3.ResponseBody
@@ -13,6 +15,7 @@ class ServiceException(
val data: Any? = null,
val error: String? = null,
val name: String? = null,
val errorType: ErrorCode = ErrorCode.UNKNOWN
) : Exception(
message
)
@@ -30,7 +33,8 @@ data class ApiErrorResponse(
message = error ?: name ?: "",
code = code,
error = error,
name = name
name = name,
errorType = (code ?: 0).toErrorCode()
)
}
}

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.data
package com.aiosman.ravenow.data
import com.google.gson.annotations.SerializedName

View File

@@ -1,9 +1,9 @@
package com.aiosman.riderpro.data
package com.aiosman.ravenow.data
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.entity.MomentImageEntity
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentImageEntity
import com.google.gson.annotations.SerializedName
import java.io.File
@@ -121,6 +121,7 @@ interface MomentService {
* @param timelineId 用户时间线ID,指定用户 ID 的时间线
* @param contentSearch 内容搜索,过滤条件
* @param trend 是否趋势动态
* @param explore 是否探索动态
* @return 动态列表
*/
suspend fun getMoments(
@@ -129,6 +130,7 @@ interface MomentService {
timelineId: Int? = null,
contentSearch: String? = null,
trend: Boolean? = false,
explore: Boolean? = false,
favoriteUserId: Int? = null
): ListContainer<MomentEntity>
@@ -146,6 +148,10 @@ interface MomentService {
relPostId: Int? = null
): MomentEntity
suspend fun agentMoment(
content: String,
): String
/**
* 收藏动态
* @param id 动态ID

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

View File

@@ -1,11 +1,12 @@
package com.aiosman.riderpro.data
package com.aiosman.ravenow.data
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AccountProfileEntity
data class UserAuth(
val id: Int,
val token: String? = null
val token: String? = null,
val isGuest: Boolean = false
)
/**
@@ -48,7 +49,22 @@ interface UserService {
followingId: Int? = null
): 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 {
val resp = ApiClient.api.getAccountProfileByTrtcUserId(id)
override suspend fun getUserProfileByTrtcUserId(id: String,includeAI: Int): AccountProfileEntity {
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")
return body.data.toAccountProfileEntity()
}

View File

@@ -1,9 +1,9 @@
package com.aiosman.riderpro.data.api
package com.aiosman.ravenow.data.api
import android.icu.text.SimpleDateFormat
import android.icu.util.TimeZone
import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.ConstVars
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ConstVars
import com.auth0.android.jwt.JWT
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
@@ -11,54 +11,19 @@ import okhttp3.OkHttpClient
import okhttp3.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.security.cert.CertificateException
import java.util.Date
import java.util.Locale
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
fun getUnsafeOkHttpClient(
fun getSafeOkHttpClient(
authInterceptor: AuthInterceptor? = null
): OkHttpClient {
return try {
// 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
) {
return OkHttpClient.Builder()
.apply {
authInterceptor?.let {
addInterceptor(it)
}
@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 {
authInterceptor?.let {
addInterceptor(it)
}
}
.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
.build()
}
class AuthInterceptor() : Interceptor {
@@ -81,6 +46,7 @@ class AuthInterceptor() : Interceptor {
}
requestBuilder.addHeader("Authorization", "Bearer ${AppStore.token}")
requestBuilder.addHeader("DEVICE-OS", "Android")
val response = chain.proceed(requestBuilder.build())
return response
@@ -90,9 +56,9 @@ class AuthInterceptor() : Interceptor {
val client = Retrofit.Builder()
.baseUrl(ApiClient.RETROFIT_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(getUnsafeOkHttpClient())
.client(getSafeOkHttpClient())
.build()
.create(RiderProAPI::class.java)
.create(RaveNowAPI::class.java)
val resp = client.refreshToken(AppStore.token ?: "")
val newToken = resp.body()?.token
@@ -104,12 +70,12 @@ class AuthInterceptor() : Interceptor {
}
object ApiClient {
const val BASE_SERVER = ConstVars.BASE_SERVER
const val BASE_API_URL = "${BASE_SERVER}/api/v1"
const val RETROFIT_URL = "${BASE_API_URL}/"
val BASE_SERVER = ConstVars.BASE_SERVER
val BASE_API_URL = "${BASE_SERVER}/api/v1"
val RETROFIT_URL = "${BASE_API_URL}/"
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
private val okHttpClient: OkHttpClient by lazy {
getUnsafeOkHttpClient(authInterceptor = AuthInterceptor())
getSafeOkHttpClient(authInterceptor = AuthInterceptor())
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
@@ -118,8 +84,8 @@ object ApiClient {
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api: RiderProAPI by lazy {
retrofit.create(RiderProAPI::class.java)
val api: RaveNowAPI by lazy {
retrofit.create(RaveNowAPI::class.java)
}
fun formatTime(date: Date): String {

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

View File

@@ -0,0 +1,822 @@
package com.aiosman.ravenow.data.api
import com.aiosman.ravenow.data.AccountFavourite
import com.aiosman.ravenow.data.AccountFollow
import com.aiosman.ravenow.data.AccountLike
import com.aiosman.ravenow.data.AccountNotice
import com.aiosman.ravenow.data.AccountProfile
import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.Comment
import com.aiosman.ravenow.data.DataContainer
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 okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
data class RegisterRequestBody(
@SerializedName("username")
val username: String,
@SerializedName("password")
val password: String
)
data class AgentMomentRequestBody(
@SerializedName("generateText")
val generateText: String,
@SerializedName("sessionId")
val sessionId: 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? = null,
@SerializedName("roomId")
val roomId: Int? = null,
)
data class LoginUserRequestBody(
@SerializedName("username")
val username: String? = null,
@SerializedName("password")
val password: String? = null,
@SerializedName("googleId")
val googleId: String? = null,
@SerializedName("captcha")
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(
@SerializedName("idToken")
val idToken: String
)
data class AuthResult(
@SerializedName("code")
val code: Int,
@SerializedName("expire")
val expire: String,
@SerializedName("token")
val token: String
)
data class ValidateTokenResult(
@SerializedName("id")
val id: Int,
)
data class CommentRequestBody(
@SerializedName("content")
val content: String,
@SerializedName("parentCommentId")
val parentCommentId: Int? = null,
@SerializedName("replyUserId")
val replyUserId: Int? = null,
@SerializedName("replyCommentId")
val replyCommentId: Int? = null,
)
data class ChangePasswordRequestBody(
@SerializedName("currentPassword")
val oldPassword: String = "",
@SerializedName("newPassword")
val newPassword: String = ""
)
data class UpdateNoticeRequestBody(
@SerializedName("lastLookLikeTime")
val lastLookLikeTime: String? = null,
@SerializedName("lastLookFollowTime")
val lastLookFollowTime: String? = null,
@SerializedName("lastLookFavoriteTime")
val lastLookFavouriteTime: String? = null
)
data class RegisterMessageChannelRequestBody(
@SerializedName("client")
val client: String,
@SerializedName("identifier")
val identifier: String,
)
data class UnRegisterMessageChannelRequestBody(
@SerializedName("client")
val client: String,
@SerializedName("identifier")
val identifier: String,
)
data class ResetPasswordRequestBody(
@SerializedName("username")
val username: String,
)
data class UpdateUserLangRequestBody(
@SerializedName("language")
val lang: String,
@SerializedName("timeOffset")
val timeOffset: Int,
@SerializedName("timezone")
val timezone: String,
)
data class TrtcSignResponseBody(
@SerializedName("sig")
val sig: String,
@SerializedName("userId")
val userId: String,
)
data class AppConfig(
@SerializedName("trtcAppId")
val trtcAppId: Int,
)
data class DictItem(
@SerializedName("key")
val key: String,
@SerializedName("value")
val value: Any,
@SerializedName("desc")
val desc: String,
)
data class CaptchaRequestBody(
@SerializedName("source")
val source: String,
)
data class CaptchaResponseBody(
@SerializedName("id")
val id: Int,
@SerializedName("thumb_base64")
val thumbBase64: String,
@SerializedName("master_base64")
val masterBase64: String,
@SerializedName("count")
val count: Int,
)
data class CheckLoginCaptchaRequestBody(
@SerializedName("username")
val username: String,
)
data class GenerateLoginCaptchaRequestBody(
@SerializedName("username")
val username: String,
)
data class DotPosition(
@SerializedName("index")
val index: Int,
@SerializedName("x")
val x: Int,
@SerializedName("y")
val y: Int,
)
data class CaptchaInfo(
@SerializedName("id")
val id: Int,
@SerializedName("dot")
val dot: List<DotPosition>
)
data class UpdateChatNotificationRequestBody(
@SerializedName("targetUserId")
val targetUserId: Int,
@SerializedName("strategy")
val strategy: String,
)
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,
)
// API 错误响应(用于加入房间等接口的错误处理)
data class ApiErrorResponse(
@SerializedName("err")
val error: String,
@SerializedName("success")
val success: Boolean
)
// 群聊中的用户信息
data class GroupChatUser(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("userSessionId")
val userSessionId: String,
@SerializedName("sessions")
val sessions: Any?, // 根据实际需要可以定义具体类型
@SerializedName("prompts")
val prompts: Any?, // 根据实际需要可以定义具体类型
@SerializedName("isAgent")
val isAgent: Boolean
)
// 智能体角色信息
data class GroupChatPrompt(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("Title")
val title: String,
@SerializedName("Desc")
val desc: String,
@SerializedName("Value")
val value: String,
@SerializedName("Enable")
val enable: Boolean,
@SerializedName("UserSessions")
val userSessions: Any?, // 根据实际需要可以定义具体类型
@SerializedName("Avatar")
val avatar: String,
@SerializedName("AuthorId")
val authorId: Int?,
@SerializedName("Author")
val author: Any?, // 根据实际需要可以定义具体类型
@SerializedName("TokenCount")
val tokenCount: Int,
@SerializedName("OpenId")
val openId: String,
@SerializedName("Public")
val public: Boolean,
@SerializedName("BreakMode")
val breakMode: Boolean,
@SerializedName("DocNamespace")
val docNamespace: String,
@SerializedName("UseRag")
val useRag: Boolean,
@SerializedName("RagThreshold")
val ragThreshold: Double,
@SerializedName("WorkflowId")
val workflowId: Int?,
@SerializedName("Workflow")
val workflow: Any?, // 根据实际需要可以定义具体类型
@SerializedName("WorkflowInputs")
val workflowInputs: Any?, // 根据实际需要可以定义具体类型
@SerializedName("Source")
val source: String,
@SerializedName("categories")
val categories: Any? // 根据实际需要可以定义具体类型
)
// 群聊详细信息响应
data class GroupChatResponse(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("name")
val name: String,
@SerializedName("description")
val description: String,
@SerializedName("creatorId")
val creatorId: Int,
@SerializedName("creator")
val creator: Any?, // 根据实际需要可以定义具体类型
@SerializedName("trtcRoomId")
val trtcRoomId: String,
@SerializedName("trtcType")
val trtcType: String,
@SerializedName("cover")
val cover: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("recommendBanner")
val recommendBanner: String,
@SerializedName("isRecommended")
val isRecommended: Boolean,
@SerializedName("allowInHot")
val allowInHot: Boolean,
@SerializedName("users")
val users: List<GroupChatUser>,
@SerializedName("prompts")
val prompts: List<GroupChatPrompt>,
@SerializedName("source")
val source: String
)
class CategoryTemplateTranslation(
@SerializedName("name")
val name: String,
@SerializedName("description")
val description: String,
)
data class CategoryTemplate(
@SerializedName("id")
val id: Int,
@SerializedName("name")
val name: String,
@SerializedName("description")
val description: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("parentId")
val parentId: Int?,
@SerializedName("parent")
val parent: CategoryTemplate?,
@SerializedName("children")
val children: List<CategoryTemplate>?,
@SerializedName("sort")
val sort: Int,
@SerializedName("isActive")
val isActive: Boolean,
@SerializedName("promptCount")
val promptCount: Int?,
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String,
@SerializedName("translations")
val translations: Map<String, CategoryTemplateTranslation>?
) {
/**
* 获取本地化名称,优先使用当前语言的翻译,如果没有则使用默认名称
*/
fun getLocalizedName(): String {
// 这里可以根据需要添加国际化逻辑
// 目前直接返回默认名称
return name
}
}
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")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@POST("login")
suspend fun login(@Body body: LoginUserRequestBody): Response<AuthResult>
@POST("guest/login")
suspend fun guestLogin(@Body body: GuestLoginRequestBody): Response<AuthResult>
@GET("auth/token")
suspend fun checkToken(): Response<ValidateTokenResult>
@GET("auth/refresh_token")
suspend fun refreshToken(
@Query("token") token: String
): Response<AuthResult>
@GET("posts")
suspend fun getPosts(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("timelineId") timelineId: Int? = null,
@Query("authorId") authorId: Int? = null,
@Query("contentSearch") contentSearch: String? = null,
@Query("postUser") postUser: Int? = null,
@Query("trend") trend: String? = null,
@Query("favouriteUserId") favouriteUserId: Int? = null,
@Query("explore") explore: String? = null,
): Response<ListContainer<Moment>>
@Multipart
@POST("posts")
suspend fun createPost(
@Part image: List<MultipartBody.Part>,
@Part("textContent") textContent: RequestBody,
): Response<DataContainer<Moment>>
@GET("post/{id}")
suspend fun getPost(
@Path("id") id: Int
): Response<DataContainer<Moment>>
@POST("post/{id}/like")
suspend fun likePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/dislike")
suspend fun dislikePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/favorite")
suspend fun favoritePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/unfavorite")
suspend fun unfavoritePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/comment")
suspend fun createComment(
@Path("id") id: Int,
@Body body: CommentRequestBody
): Response<DataContainer<Comment>>
@POST("comment/{id}/like")
suspend fun likeComment(
@Path("id") id: Int
): Response<Unit>
@POST("comment/{id}/dislike")
suspend fun dislikeComment(
@Path("id") id: Int
): Response<Unit>
@POST("comment/{id}/read")
suspend fun updateReadStatus(
@Path("id") id: Int
): Response<Unit>
@GET("comments")
suspend fun getComments(
@Query("page") page: Int = 1,
@Query("postId") postId: Int? = null,
@Query("pageSize") pageSize: Int = 20,
@Query("postUser") postUser: Int? = null,
@Query("selfNotice") selfNotice: Int? = 0,
@Query("order") order: String? = null,
@Query("parentCommentId") parentCommentId: Int? = null,
): Response<ListContainer<Comment>>
@GET("account/my")
suspend fun getMyAccount(): Response<DataContainer<AccountProfile>>
@Multipart
@PATCH("account/my/profile")
suspend fun updateProfile(
@Part avatar: MultipartBody.Part?,
@Part banner: MultipartBody.Part?,
@Part("nickname") nickname: RequestBody?,
@Part("bio") bio: RequestBody?,
): Response<Unit>
@POST("account/my/password")
suspend fun changePassword(
@Body body: ChangePasswordRequestBody
): Response<Unit>
@GET("account/my/notice/like")
suspend fun getMyLikeNotices(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
): Response<ListContainer<AccountLike>>
@GET("account/my/notice/follow")
suspend fun getMyFollowNotices(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
): Response<ListContainer<AccountFollow>>
@GET("account/my/notice/favourite")
suspend fun getMyFavouriteNotices(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
): Response<ListContainer<AccountFavourite>>
@GET("account/my/notice")
suspend fun getMyNoticeInfo(): Response<DataContainer<AccountNotice>>
@POST("account/my/notice")
suspend fun updateNoticeInfo(
@Body body: UpdateNoticeRequestBody
): Response<Unit>
@POST("account/my/messaging")
suspend fun registerMessageChannel(
@Body body: RegisterMessageChannelRequestBody
): Response<Unit>
@POST("account/my/messaging/unregister")
suspend fun unRegisterMessageChannel(
@Body body: UnRegisterMessageChannelRequestBody
): Response<Unit>
@GET("profile/{id}")
suspend fun getAccountProfileById(
@Path("id") id: Int
): Response<DataContainer<AccountProfile>>
@GET("profile/trtc/{id}")
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
): Response<DataContainer<AccountProfile>>
@POST("user/{id}/follow")
suspend fun followUser(
@Path("id") id: Int
): Response<Unit>
@POST("user/{id}/unfollow")
suspend fun unfollowUser(
@Path("id") id: Int
): Response<Unit>
@GET("users")
suspend fun getUsers(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("nickname") search: String? = null,
@Query("followerId") followerId: Int? = null,
@Query("followingId") followingId: Int? = null,
@Query("includeAI") includeAI: Boolean? = false,
@Query("chatSessionIdNotNull") chatSessionIdNotNull: Boolean? = true,
): Response<ListContainer<AccountProfile>>
@POST("register/google")
suspend fun registerWithGoogle(@Body body: GoogleRegisterRequestBody): Response<AuthResult>
@DELETE("post/{id}")
suspend fun deletePost(
@Path("id") id: Int
): Response<Unit>
@DELETE("comment/{id}")
suspend fun deleteComment(
@Path("id") id: Int
): Response<Unit>
@POST("account/my/password/reset")
suspend fun resetPassword(
@Body body: ResetPasswordRequestBody
): Response<Unit>
@GET("comment/{id}")
suspend fun getComment(
@Path("id") id: Int
): Response<DataContainer<Comment>>
@PATCH("account/my/extra")
suspend fun updateUserExtra(
@Body body: UpdateUserLangRequestBody
): Response<Unit>
@GET("account/my/chat/sign")
suspend fun getChatSign(): Response<DataContainer<TrtcSignResponseBody>>
@GET("app/info")
suspend fun getAppConfig(): Response<DataContainer<AppConfig>>
@GET("dict")
suspend fun getDict(
@Query("key") key: String
): Response<DataContainer<DictItem>>
@GET("dicts")
suspend fun getDicts(
@Query("keys") keys: String
): Response<ListContainer<DictItem>>
@POST("captcha/generate")
suspend fun generateCaptcha(
@Body body: CaptchaRequestBody
): Response<DataContainer<CaptchaResponseBody>>
@POST("login/needCaptcha")
suspend fun checkLoginCaptcha(
@Body body: CheckLoginCaptchaRequestBody
): Response<DataContainer<Boolean>>
@POST("captcha/login/generate")
suspend fun generateLoginCaptcha(
@Body body: GenerateLoginCaptchaRequestBody
): Response<DataContainer<CaptchaResponseBody>>
@GET("chat/notification")
suspend fun getChatNotification(
@Query("targetTrtcId") targetTrtcId: String
): Response<DataContainer<ChatNotification>>
@POST("chat/notification")
suspend fun updateChatNotification(
@Body body: UpdateChatNotificationRequestBody
): 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: Int = 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? = null,
@Query("roomId") roomId: Int? = null
): Response<DataContainer<GroupChatResponse>>
@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>>
@GET("outside/categories")
suspend fun getCategories(
@Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null,
@Query("parentId") parentId: Int? = null,
@Query("isActive") isActive: Boolean? = null,
@Query("name") name: String? = null,
@Query("withChildren") withChildren: Boolean? = null,
@Query("withParent") withParent: Boolean? = null,
@Query("withCount") withCount: Boolean? = null,
@Query("hideEmpty") hideEmpty: Boolean? = null
): Response<ListContainer<CategoryTemplate>>
@GET("outside/categories/tree")
suspend fun getCategoryTree(
@Query("withCount") withCount: Boolean? = null,
@Query("hideEmpty") hideEmpty: Boolean? = null
): Response<DataContainer<List<CategoryTemplate>>>
@GET("outside/categories/{id}")
suspend fun getCategoryById(
@Path("id") id: Int
): Response<DataContainer<CategoryTemplate>>
}

View File

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

View File

@@ -1,11 +1,11 @@
package com.aiosman.riderpro.entity
package com.aiosman.ravenow.entity
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.riderpro.data.AccountFollow
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.Image
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.ravenow.data.AccountFollow
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.Image
import com.aiosman.ravenow.data.api.ApiClient
import java.io.IOException
import java.util.Date
@@ -61,6 +61,12 @@ data class AccountProfileEntity(
val banner: String?,
// trtcUserId
val trtcUserId: String,
val chatToken: String?,
val aiAccount: Boolean,
val rawAvatar: String,
val chatAIId: String,
)
/**

View 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>? {
// 如果是游客模式且获取我的AgentauthorId为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> {
// 如果是游客模式且获取我的AgentauthorId为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
)
}
}
}

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

View File

@@ -1,10 +1,9 @@
package com.aiosman.riderpro.entity
package com.aiosman.ravenow.entity
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.riderpro.data.CommentRemoteDataSource
import com.aiosman.riderpro.data.NoticePost
import java.io.IOException
import com.aiosman.ravenow.data.CommentRemoteDataSource
import com.aiosman.ravenow.data.NoticePost
import java.util.Date
data class CommentEntity(

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

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

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

View File

@@ -1,15 +1,15 @@
package com.aiosman.riderpro.entity
package com.aiosman.ravenow.entity
import androidx.annotation.DrawableRes
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.riderpro.data.ListContainer
import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.ServiceException
import com.aiosman.riderpro.data.UploadImage
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.data.parseErrorResponse
import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.api.AgentMomentRequestBody
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.parseErrorResponse
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -27,6 +27,7 @@ class MomentPagingSource(
private val timelineId: Int? = null,
private val contentSearch: String? = null,
private val trend: Boolean? = false,
private val explore: Boolean? = false,
private val favoriteUserId: Int? = null
) : PagingSource<Int, MomentEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
@@ -38,6 +39,7 @@ class MomentPagingSource(
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend,
explore = explore,
favoriteUserId = favoriteUserId
)
@@ -66,6 +68,7 @@ class MomentRemoteDataSource(
timelineId: Int?,
contentSearch: String?,
trend: Boolean?,
explore: Boolean?,
favoriteUserId: Int?
): ListContainer<MomentEntity> {
return momentService.getMoments(
@@ -74,6 +77,7 @@ class MomentRemoteDataSource(
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend,
explore = explore,
favoriteUserId = favoriteUserId
)
}
@@ -88,7 +92,8 @@ class MomentServiceImpl() : MomentService {
timelineId: Int?,
contentSearch: String?,
trend: Boolean?,
favoriteUserId: Int?
explore: Boolean?,
favoriteUserId: Int?,
): ListContainer<MomentEntity> {
return momentBackend.fetchMomentItems(
pageNumber = pageNumber,
@@ -96,7 +101,8 @@ class MomentServiceImpl() : MomentService {
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend,
favoriteUserId = favoriteUserId
favoriteUserId = favoriteUserId,
explore = explore
)
}
@@ -122,6 +128,10 @@ class MomentServiceImpl() : MomentService {
return momentBackend.createMoment(content, authorId, images, relPostId)
}
override suspend fun agentMoment(content: String): String {
return momentBackend.agentMoment(content)
}
override suspend fun favoriteMoment(id: Int) {
momentBackend.favoriteMoment(id)
}
@@ -144,6 +154,7 @@ class MomentBackend {
timelineId: Int?,
contentSearch: String?,
trend: Boolean?,
explore: Boolean?,
favoriteUserId: Int? = null
): ListContainer<MomentEntity> {
val resp = ApiClient.api.getPosts(
@@ -153,7 +164,8 @@ class MomentBackend {
authorId = author,
contentSearch = contentSearch,
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")
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) {
ApiClient.api.favoritePost(id)
}
@@ -277,4 +300,80 @@ data class MomentEntity(
var relMoment: MomentEntity? = null,
// 是否收藏
var isFavorite: Boolean = false
)
)
class MomentLoaderExtraArgs(
val explore: Boolean? = false,
val timelineId: Int? = null,
val authorId : Int? = null
)
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
)
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)
}
}

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

View File

@@ -1,8 +1,8 @@
package com.aiosman.riderpro.entity
package com.aiosman.ravenow.entity
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.riderpro.data.UserService
import com.aiosman.ravenow.data.UserService
import java.io.IOException
/**

View File

@@ -0,0 +1,6 @@
package com.aiosman.ravenow.event
data class FollowChangeEvent(
val userId: Int,
val isFollow: Boolean
)

View File

@@ -0,0 +1,7 @@
package com.aiosman.ravenow.event
import com.aiosman.ravenow.entity.MomentEntity
data class MomentAddEvent(
val moment:MomentEntity
)

View File

@@ -0,0 +1,6 @@
package com.aiosman.ravenow.event
data class MomentFavouriteChangeEvent(
val postId: Int,
val isFavourite: Boolean
)

View File

@@ -0,0 +1,7 @@
package com.aiosman.ravenow.event
data class MomentLikeChangeEvent(
val postId: Int,
val likeCount: Int?,
val isLike: Boolean
)

View File

@@ -0,0 +1,5 @@
package com.aiosman.ravenow.event
data class MomentRemoveEvent(
val postId: Int
)

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

View File

@@ -1,11 +1,9 @@
package com.aiosman.riderpro.exp
package com.aiosman.ravenow.exp
import android.content.Context
import android.icu.text.SimpleDateFormat
import android.icu.util.Calendar
import androidx.compose.ui.res.stringResource
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.ravenow.R
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.exp
package com.aiosman.ravenow.exp
import android.annotation.SuppressLint
import android.app.Activity

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.exp
package com.aiosman.ravenow.exp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

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

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.model
package com.aiosman.ravenow.model
import androidx.annotation.DrawableRes

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.model
package com.aiosman.ravenow.model
import androidx.paging.PagingSource
import androidx.paging.PagingState

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

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro
package com.aiosman.ravenow
import android.content.Context
import android.content.SharedPreferences
@@ -12,6 +12,7 @@ object AppStore {
private const val PREFS_NAME = "app_prefs_$STORE_VERSION"
var token: String? = null
var rememberMe: Boolean = false
var isGuest: Boolean = false
private lateinit var sharedPreferences: SharedPreferences
lateinit var googleSignInOptions: GoogleSignInOptions
fun init(context: Context) {
@@ -23,6 +24,12 @@ object AppStore {
.requestEmail()
.build()
googleSignInOptions = gso
// apply dark mode
if (sharedPreferences.getBoolean("darkMode", false)) {
AppState.darkMode = true
AppState.appTheme = DarkThemeColors()
}
}
suspend fun saveData() {
@@ -30,6 +37,7 @@ object AppStore {
sharedPreferences.edit().apply {
putString("token", token)
putBoolean("rememberMe", rememberMe)
putBoolean("isGuest", isGuest)
}.apply()
}
@@ -37,5 +45,14 @@ object AppStore {
// shared preferences
token = sharedPreferences.getString("token", null)
rememberMe = sharedPreferences.getBoolean("rememberMe", false)
isGuest = sharedPreferences.getBoolean("isGuest", false)
}
fun saveDarkMode(darkMode: Boolean) {
sharedPreferences.edit().apply {
putBoolean("darkMode", darkMode)
}.apply()
}
}

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.test
package com.aiosman.ravenow.test
data class StreetPosition(
val name:String,

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.ui
package com.aiosman.ravenow.ui
import ChangePasswordScreen
import ImageViewer
@@ -6,8 +6,13 @@ import ModificationListScreen
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.fadeIn
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.WindowInsets
import androidx.compose.foundation.layout.navigationBars
@@ -24,37 +29,47 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.ui.account.AccountEditScreen2
import com.aiosman.riderpro.ui.account.ResetPasswordScreen
import com.aiosman.riderpro.ui.chat.ChatScreen
import com.aiosman.riderpro.ui.comment.CommentsScreen
import com.aiosman.riderpro.ui.comment.notice.CommentNoticeScreen
import com.aiosman.riderpro.ui.crop.ImageCropScreen
import com.aiosman.riderpro.ui.favourite.FavouriteListPage
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeScreen
import com.aiosman.riderpro.ui.follower.FollowerListScreen
import com.aiosman.riderpro.ui.follower.FollowerNoticeScreen
import com.aiosman.riderpro.ui.follower.FollowingListScreen
import com.aiosman.riderpro.ui.gallery.OfficialGalleryScreen
import com.aiosman.riderpro.ui.gallery.OfficialPhotographerScreen
import com.aiosman.riderpro.ui.gallery.ProfileTimelineScreen
import com.aiosman.riderpro.ui.index.IndexScreen
import com.aiosman.riderpro.ui.index.tabs.message.NotificationsScreen
import com.aiosman.riderpro.ui.index.tabs.search.SearchScreen
import com.aiosman.riderpro.ui.like.LikeNoticeScreen
import com.aiosman.riderpro.ui.location.LocationDetailScreen
import com.aiosman.riderpro.ui.login.EmailSignupScreen
import com.aiosman.riderpro.ui.login.LoginPage
import com.aiosman.riderpro.ui.login.SignupScreen
import com.aiosman.riderpro.ui.login.UserAuthScreen
import com.aiosman.riderpro.ui.modification.EditModificationScreen
import com.aiosman.riderpro.ui.post.NewPostImageGridScreen
import com.aiosman.riderpro.ui.post.NewPostScreen
import com.aiosman.riderpro.ui.post.PostScreen
import com.aiosman.riderpro.ui.profile.AccountProfileV2
import com.aiosman.ravenow.LocalAnimatedContentScope
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.LocalSharedTransitionScope
import com.aiosman.ravenow.ui.about.AboutScreen
import com.aiosman.ravenow.ui.account.AccountEditScreen2
import com.aiosman.ravenow.ui.account.AccountSetting
import com.aiosman.ravenow.ui.account.RemoveAccountScreen
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatScreen
import com.aiosman.ravenow.ui.chat.GroupChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
import com.aiosman.ravenow.ui.crop.ImageCropScreen
import com.aiosman.ravenow.ui.favourite.FavouriteListPage
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen
import com.aiosman.ravenow.ui.follower.FollowerListScreen
import com.aiosman.ravenow.ui.follower.FollowerNoticeScreen
import com.aiosman.ravenow.ui.follower.FollowingListScreen
import com.aiosman.ravenow.ui.gallery.OfficialGalleryScreen
import com.aiosman.ravenow.ui.gallery.OfficialPhotographerScreen
import com.aiosman.ravenow.ui.gallery.ProfileTimelineScreen
import com.aiosman.ravenow.ui.group.GroupChatInfoScreen
import com.aiosman.ravenow.ui.index.IndexScreen
import com.aiosman.ravenow.ui.index.tabs.message.NotificationsScreen
import com.aiosman.ravenow.ui.index.tabs.search.SearchScreen
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(
val route: String,
@@ -73,7 +88,7 @@ sealed class NavigationRoute(
data object NewPost : NavigationRoute("NewPost")
data object EditModification : NavigationRoute("EditModification")
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 UserAuth : NavigationRoute("UserAuth")
data object EmailSignUp : NavigationRoute("EmailSignUp")
@@ -88,8 +103,18 @@ sealed class NavigationRoute(
data object ResetPassword : NavigationRoute("ResetPassword")
data object FavouriteList : NavigationRoute("FavouriteList")
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 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,23 +171,48 @@ fun NavigationController(
navArgument("highlightCommentId") { type = NavType.IntType },
navArgument("initImagePagerIndex") { type = NavType.IntType }
),
) { backStackEntry ->
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
val id = backStackEntry.arguments?.getString("id")
val highlightCommentId =
backStackEntry.arguments?.getInt("highlightCommentId")?.let {
if (it == 0) null else it
}
val initIndex = backStackEntry.arguments?.getInt("initImagePagerIndex")
PostScreen(
id!!,
highlightCommentId,
initImagePagerIndex = initIndex
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 ->
val id = backStackEntry.arguments?.getString("id")
val highlightCommentId =
backStackEntry.arguments?.getInt("highlightCommentId")?.let {
if (it == 0) null else it
}
val initIndex = backStackEntry.arguments?.getInt("initImagePagerIndex")
PostScreen(
id!!,
highlightCommentId,
initImagePagerIndex = initIndex
)
}
composable(route = NavigationRoute.ModificationList.route,
enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 0))
@@ -237,12 +287,48 @@ fun NavigationController(
}
composable(
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(
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(
@@ -281,24 +367,58 @@ fun NavigationController(
composable(
route = NavigationRoute.AccountEdit.route,
enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 0))
// iOS风格从底部向上滑入
slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
) + fadeIn(
animationSpec = tween(durationMillis = 300)
)
},
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()
}
composable(route = NavigationRoute.ImageViewer.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
ImageViewer()
}
ImageViewer()
}
composable(route = NavigationRoute.ChangePasswordScreen.route) {
ChangePasswordScreen()
}
composable(route = NavigationRoute.RemoveAccountScreen.route) {
RemoveAccountScreen()
}
composable(route = NavigationRoute.VipSelPage.route) {
VipSelPage()
}
composable(route = NavigationRoute.FavouritesScreen.route) {
FavouriteNoticeScreen()
}
@@ -353,9 +473,39 @@ fun NavigationController(
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
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) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
@@ -370,6 +520,77 @@ fun NavigationController(
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,
) {
AddAgentScreen()
}
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 +637,44 @@ fun NavHostController.navigateToChat(id: String) {
navigate(
route = NavigationRoute.Chat.route
.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)
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package com.aiosman.riderpro.ui.account
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.PaddingValues
@@ -19,27 +20,26 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.riderpro.ConstVars
import com.aiosman.riderpro.ErrorCode
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.DictService
import com.aiosman.riderpro.data.DictServiceImpl
import com.aiosman.riderpro.data.ServiceException
import com.aiosman.riderpro.getErrorMessageCode
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.ActionButton
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.composables.TextInputField
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.api.ErrorCode
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
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.data.ServiceException
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 kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -56,6 +56,7 @@ fun ResetPasswordScreen() {
var usernameError by remember { mutableStateOf<String?>(null) }
var countDown by remember { mutableStateOf<Int?>(null) }
var countDownMax by remember { mutableStateOf(60) }
val appColors = LocalAppTheme.current
fun validate(): Boolean {
if (username.isEmpty()) {
usernameError = context.getString(R.string.text_error_email_required)
@@ -68,7 +69,7 @@ fun ResetPasswordScreen() {
LaunchedEffect(Unit) {
try {
dictService.getDictByKey(ConstVars.DIC_KEY_RESET_EMAIL_INTERVAL).let {
countDownMax = it.value.toInt()
countDownMax = it.value as? Int ?: 60
}
} catch (e: Exception) {
countDownMax = 60
@@ -111,7 +112,7 @@ fun ResetPasswordScreen() {
Column(
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize().background(color = appColors.background)
) {
StatusBarSpacer()
Box(
@@ -148,7 +149,7 @@ fun ResetPasswordScreen() {
Text(
text = stringResource(R.string.reset_mail_send_success),
style = TextStyle(
color = Color(0xFF333333),
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
),
@@ -158,7 +159,7 @@ fun ResetPasswordScreen() {
Text(
text = stringResource(R.string.reset_mail_send_failed),
style = TextStyle(
color = Color(0xFF333333),
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
),
@@ -177,8 +178,8 @@ fun ResetPasswordScreen() {
} else {
stringResource(R.string.recover)
},
backgroundColor = Color(0xffda3832),
color = Color.White,
backgroundColor = appColors.main,
color = appColors.mainText,
isLoading = isLoading,
contentPadding = PaddingValues(0.dp),
enabled = countDown == null,
@@ -194,7 +195,7 @@ fun ResetPasswordScreen() {
text = stringResource(R.string.back_upper),
contentPadding = PaddingValues(0.dp),
) {
navController.popBackStack()
navController.navigateUp()
}
}

View File

@@ -1,3 +1,4 @@
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -15,16 +16,23 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.ActionButton
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.composables.TextInputField
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.AccountService
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.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
/**
@@ -48,34 +56,37 @@ class ChangePasswordViewModel {
*/
@Composable
fun ChangePasswordScreen() {
val context = LocalContext.current
val viewModel = remember { ChangePasswordViewModel() }
var currentPassword by remember { mutableStateOf("") }
var newPassword by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
var oldPasswordError by remember { mutableStateOf<String?>(null) }
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var passwordError by remember { mutableStateOf<String?>(null) }
val AppColors = LocalAppTheme.current
fun validate(): Boolean {
oldPasswordError =
if (currentPassword.isEmpty()) "Please enter your current password" else null
passwordError = when {
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"
!newPassword.any { it.isLowerCase() } -> "Password must contain at least one lowercase letter"
else -> null
}
confirmPasswordError =
if (newPassword != confirmPassword) "Passwords do not match" else null
// 使用通用密码校验器校验当前密码
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
oldPasswordError = if (!currentPasswordValidation.isValid) currentPasswordValidation.errorMessage else null
// 使用通用密码校验器校验新密码
val newPasswordValidation = PasswordValidator.validatePassword(newPassword, context)
passwordError = if (!newPasswordValidation.isValid) newPasswordValidation.errorMessage else null
// 使用通用密码确认校验器
val confirmPasswordValidation = PasswordValidator.validatePasswordConfirmation(newPassword, confirmPassword, context)
confirmPasswordError = if (!confirmPasswordValidation.isValid) confirmPasswordValidation.errorMessage else null
return passwordError == null && confirmPasswordError == null && oldPasswordError == null
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
.background(AppColors.background),
horizontalAlignment = Alignment.CenterHorizontally
) {
StatusBarSpacer()
@@ -83,7 +94,7 @@ fun ChangePasswordScreen() {
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) {
NoticeScreenHeader(
title = "Change password",
title = stringResource(R.string.change_password),
moreIcon = false
)
@@ -99,8 +110,8 @@ fun ChangePasswordScreen() {
text = currentPassword,
onValueChange = { currentPassword = it },
password = true,
label = "Current password",
hint = "Enter your current password",
label = stringResource(R.string.current_password),
hint = stringResource(R.string.current_password_tip5),
error = oldPasswordError
)
Spacer(modifier = Modifier.height(4.dp))
@@ -108,8 +119,8 @@ fun ChangePasswordScreen() {
text = newPassword,
onValueChange = { newPassword = it },
password = true,
label = "New password",
hint = "Enter your new password",
label = stringResource(R.string.new_password),
hint = stringResource(R.string.new_password),
error = passwordError
)
Spacer(modifier = Modifier.height(4.dp))
@@ -117,25 +128,31 @@ fun ChangePasswordScreen() {
text = confirmPassword,
onValueChange = { confirmPassword = it },
password = true,
label = "Confirm new password",
hint = "Enter your new password again",
label = stringResource(R.string.confirm_new_password_tip1),
hint = stringResource(R.string.new_password_tip1),
error = confirmPasswordError
)
Spacer(modifier = Modifier.height(50.dp))
ActionButton(
modifier = Modifier
.width(345.dp)
.height(48.dp),
text = "Let's Ride",
backgroundImage = R.mipmap.rider_pro_signup_red_bg
.width(345.dp),
text = stringResource(R.string.lets_ride_upper),
) {
if (validate()) {
scope.launch {
try {
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) {
errorMessage = e.message ?: "An error occurred"
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.ui.account
package com.aiosman.ravenow.ui.account
import android.app.Activity
import android.content.Intent
@@ -34,14 +34,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.UploadImage
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile
import com.aiosman.ravenow.LocalNavController
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.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.post.NewPostViewModel.uriToFile
import kotlinx.coroutines.launch
/**
@@ -82,7 +82,8 @@ fun AccountEditScreen() {
var newAvatar: UploadImage? = null
cursor?.use { cur ->
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(".")
Log.d("NewPost", "File name: $displayName, extension: $extension")
// read as file
@@ -98,7 +99,8 @@ fun AccountEditScreen() {
var newBanner: UploadImage? = null
cursor?.use { cur ->
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(".")
Log.d("NewPost", "File name: $displayName, extension: $extension")
// read as file

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

View File

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

View File

@@ -0,0 +1,641 @@
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.focus.FocusRequester
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.platform.LocalSoftwareKeyboardController
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
import androidx.compose.foundation.border
import androidx.compose.ui.draw.shadow
import com.aiosman.ravenow.AppState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.StartOffset
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.offset
/**
* 添加智能体界面
*/
@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) }
var isProcessing by remember { mutableStateOf(false) }
var showWaveAnimation by remember { mutableStateOf(false) }
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
) {
var showManualCreation by remember { mutableStateOf(false) }
StatusBarSpacer()
Box(
modifier = Modifier.padding(horizontal = 14.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),
fontSize = 17.sp,
color = appColors.text
)
}
}
Spacer(modifier = Modifier.height(1.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(horizontal = 20.dp),
) {
Image(
painter = painterResource(id = R.mipmap.group_copy),
contentDescription = "",
modifier = Modifier
.size(48.dp)
.clip(
RoundedCornerShape(48.dp)
),
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier.fillMaxWidth()
.padding(start = 20.dp)
) {
Text(
text = "${AppState.profile?.nickName ?: "User"} 你好呀!今天想创造什么?",
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
Spacer(modifier = Modifier.height(8.dp))
Column(
modifier = Modifier.fillMaxWidth()
.padding(start = 20.dp)
) {
if (!showManualCreation) {
Text(
text = "只需要一句话你的专属AI将在这里诞生。",
fontSize = 14.sp,
color = LocalAppTheme.current.text.copy(alpha = 0.6f),
)
}
}
Spacer(modifier = Modifier.height(24.dp))
if (!showManualCreation) {
Column(
modifier = Modifier
.padding(horizontal = 20.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(95.dp)
.shadow(
elevation = 10.dp,
shape = RoundedCornerShape(10.dp),
spotColor = Color(0x33F563FF),
ambientColor = Color(0x99F563FF),
clip = false
)
.background(
brush = Brush.linearGradient(
listOf(
Color(0xFF6246FF),
Color(0xFF7C45ED)
)
),
shape = RoundedCornerShape(10.dp)
)
.padding(0.5.dp)
.background(
color = appColors.inputBackground2,
shape = RoundedCornerShape(10.dp)
)
) {
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
model.viewModelScope.launch {
focusRequester.requestFocus()
keyboardController?.show()
}
}
)
FormTextInput2(
value = model.desc,
hint = "一个会写诗的AI一个懂你笑点的AI...",
background = appColors.inputBackground2,
focusRequester = focusRequester,
modifier = Modifier
.fillMaxWidth()
.height(95.dp)
) { value ->
onDescChange(value)
}
Row(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 12.dp, bottom = 12.dp)
.noRippleClickable {
if (!isProcessing && model.desc.isNotEmpty()) {
isProcessing = true
model.viewModelScope.launch {
try {
//AI美化功能待实现
} catch (e: Exception) {
e.printStackTrace()
} finally {
isProcessing = false
}
}
}
},
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_info_magic),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = Color.Unspecified
)
Spacer(modifier = Modifier.width(5.dp))
Text(
text = "AI美化",
color = Color(0xFF6246FF),
fontSize = 14.sp
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
if (showWaveAnimation) {
Row(
modifier = Modifier
.align(Alignment.Start)
.padding(start = 20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.size(18.dp)
) {
val infiniteTransition = rememberInfiniteTransition()
val dot1Translation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = -12f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(0)
)
)
val dot2Translation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = -12f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(333)
)
)
val dot3Translation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = -12f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(666)
)
)
// 三个彩色圆点
Box(
modifier = Modifier
.size(6.dp)
.align(Alignment.BottomStart)
.offset(y = dot1Translation.dp)
.background(Color(0xFFFFD400), CircleShape)
)
Box(
modifier = Modifier
.size(6.dp)
.align(Alignment.BottomCenter)
.offset(y = dot2Translation.dp)
.background(Color(0xFF2F80FF), CircleShape)
)
Box(
modifier = Modifier
.size(6.dp)
.align(Alignment.BottomEnd)
.offset(y = dot3Translation.dp)
.background(Color(0xFF27C84D), CircleShape)
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "正在为你构思",
color = Color.Black.copy(alpha = 0.6f),
fontSize = 14.sp
)
}
} else {
Box(
modifier = Modifier
.align(Alignment.Start)
.padding(start = 20.dp)
.width(136.dp)
.height(40.dp)
.border(
width = 1.dp,
color = Color(0x33858B98),
shape = RoundedCornerShape(12.dp)
)
.background(
color = appColors.background,
shape = RoundedCornerShape(12.dp),
)
.noRippleClickable {
showManualCreation = true
}
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 18.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "手动创造Ai",
color = Color.Black,
fontWeight = FontWeight.W600,
fontSize = 14.sp
)
}
}
}
}else {
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.align(Alignment.Start)
) {
// 添加新的一句话创造AI按钮
Box(
modifier = Modifier
.align(Alignment.Start)
.width(140.dp)
.height(40.dp)
.shadow(
elevation = 10.dp,
shape = RoundedCornerShape(10.dp),
spotColor = Color(0x33F563FF),
ambientColor = Color(0x99F563FF),
clip = false
)
.background(
brush = Brush.linearGradient(
listOf(
Color(0xFF6246FF),
Color(0xFF7C45ED)
)
),
shape = RoundedCornerShape(10.dp)
)
.padding(0.5.dp)
.background(
color = appColors.background,
shape = RoundedCornerShape(10.dp),
)
.noRippleClickable {
showManualCreation = false
}
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_info_magic),
contentDescription = null,
tint = Color.Black,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "一句话创造AI",
color = Color.Black,
fontWeight = FontWeight.W600,
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "头像",
fontSize = 12.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x777c45ed),
Color(0x777c68ef),
Color(0x557bd8f8)
)
)
)
.align(Alignment.Start)
.noRippleClickable {
// 设置正在选择头像的标志
model.isSelectingAvatar = true
navController.navigate(NavigationRoute.AgentImageCrop.route)
},
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = "Edit",
tint = Color.White,
modifier = Modifier.size(20.dp),
)
}
}
Spacer(modifier = Modifier.height(18.dp))
// 原版两个输入框
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
) {
Text(
text = stringResource(R.string.agent_name),
fontSize = 12.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput(
value = model.name,
hint = "给它取个名字,让它成为独一无二的你",
background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(),
) { value ->
onNameChange(value)
}
Text(
text = stringResource(R.string.agent_desc),
fontSize = 12.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput2(
value = model.desc,
hint = stringResource(R.string.agent_desc_hint),
background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(),
) { value ->
onDescChange(value)
}
}
}
// 错误信息显示
Spacer(modifier = Modifier.weight(1f))
Box(modifier = Modifier.fillMaxWidth()) {
errorMessage?.let { error ->
Text(
text = error,
color = Color.Red,
modifier = Modifier
.padding(bottom = 20.dp)
.align(Alignment.Center),
fontSize = 14.sp
)
}
}
ActionButton(
modifier = Modifier
.width(345.dp)
.padding(bottom = 40.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x777c45ed),
Color(0x777c68ef),
Color(0x557bd8f8)
)
),
shape = RoundedCornerShape(24.dp)
),
color = Color.White,
backgroundColor = Color.Transparent,
text = stringResource(R.string.create_confirm),
isLoading = model.isUpdating,
enabled = !model.isUpdating && validate()
) {
// 验证输入
val validationError = model.validate()
if (validationError != null) {
// 显示验证错误
errorMessage = validationError
model.viewModelScope.launch {
kotlinx.coroutines.delay(3000)
errorMessage = null
}
return@ActionButton
}
// 清除之前的错误信息
errorMessage = null
// 显示波动动画
showWaveAnimation = true
// 调用创建智能体API
model.viewModelScope.launch {
try {
val result = model.createAgent(context)
if (result != null) {
// 创建成功,清空数据并关闭页面
model.clearData()
navController.popBackStack()
}
} catch (e: Exception) {
// 隐藏波动动画
showWaveAnimation = false
// 显示错误信息
errorMessage = "创建智能体失败: ${e.message}"
e.printStackTrace()
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
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) // 标记是否正在选择头像
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
}
}

View File

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

View File

@@ -0,0 +1,377 @@
package com.aiosman.ravenow.ui.chat
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem
import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.enums.ViewType
import io.openim.android.sdk.listener.OnAdvanceMsgListener
import io.openim.android.sdk.listener.OnBase
import io.openim.android.sdk.listener.OnMsgSendCallback
import io.openim.android.sdk.models.*
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
/**
* 聊天ViewModel基类包含所有聊天功能的通用实现
* 子类需要实现抽象方法来处理特定的聊天类型(单聊/群聊)
*/
abstract class BaseChatViewModel : ViewModel() {
// 通用状态属性
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
var hasMore by mutableStateOf(true)
var isLoading by mutableStateOf(false)
var lastMessage: Message? = null
val showTimestampMap = mutableMapOf<String, Boolean>()
var goToNew by mutableStateOf(false)
var conversationID: String = "" // 会话ID通过getOneConversation初始化
// 通用服务
val userService: UserService = UserServiceImpl()
val accountService: AccountService = AccountServiceImpl()
var textMessageListener: OnAdvanceMsgListener? = null
val fetchHistorySize = 20
/**
* 初始化方法,子类需要实现具体的初始化逻辑
*/
abstract fun init(context: Context)
/**
* 获取日志标签,子类需要实现
*/
abstract fun getLogTag(): String
/**
* 获取会话参数,子类需要实现
* @return Triple(targetId, conversationType, isSingleChat)
*/
abstract fun getConversationParams(): Triple<String, Int, Boolean>
/**
* 处理接收到的新消息,子类可以重写以添加特定逻辑
*/
open fun handleNewMessage(message: Message, context: Context): Boolean {
return false // 默认不处理,子类重写
}
/**
* 获取发送消息时的接收者ID子类需要实现
*/
abstract fun getReceiverInfo(): Pair<String?, String?> // (recvID, groupID)
/**
* 发送消息成功后的额外处理,子类可以重写
*/
open fun onMessageSentSuccess(message: String, sentMessage: Message?) {
// 默认无额外处理,子类可以重写
}
/**
* 获取会话信息并初始化conversationID
*/
fun getOneConversation(onSuccess: (() -> Unit)? = null) {
val (targetId, conversationType, isSingleChat) = getConversationParams()
OpenIMClient.getInstance().conversationManager.getOneConversation(
object : OnBase<ConversationInfo> {
override fun onError(code: Int, error: String) {
Log.e(getLogTag(), "getOneConversation error: $error")
}
override fun onSuccess(data: ConversationInfo) {
conversationID = data.conversationID
Log.d(getLogTag(), "获取会话信息成功conversationID: $conversationID")
onSuccess?.invoke()
}
},
targetId,
conversationType
)
}
/**
* 注册消息监听器
*/
fun RegistListener(context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
Log.w(getLogTag(), "OpenIM 未登录,跳过注册消息监听器")
return
}
textMessageListener = object : OnAdvanceMsgListener {
override fun onRecvNewMessage(msg: Message?) {
msg?.let { message ->
if (handleNewMessage(message, context)) {
val chatItem = ChatItem.convertToChatItem(message, context, avatar = getMessageAvatar(message))
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
Log.i(getLogTag(), "收到来自 ${message.sendID} 的消息,更新聊天列表")
}
}
}
}
}
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
}
/**
* 获取消息头像,子类可以重写
*/
open fun getMessageAvatar(message: Message): String? {
return null
}
/**
* 取消注册消息监听器
*/
fun UnRegistListener() {
textMessageListener = null
}
/**
* 清除未读消息
*/
fun clearUnRead() {
if (conversationID.isEmpty()) {
Log.w(getLogTag(), "conversationID为空无法清除未读消息")
return
}
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
conversationID,
object : OnBase<String> {
override fun onSuccess(data: String?) {
Log.i("openim", "清除未读消息成功")
}
override fun onError(code: Int, error: String?) {
Log.i("openim", "清除未读消息失败, code:$code, error:$error")
}
}
)
}
/**
* 加载更多历史消息
*/
fun onLoadMore(context: Context) {
if (!hasMore || isLoading) {
return
}
loadHistoryMessages(context, isLoadMore = true)
}
/**
* 发送文本消息
*/
fun sendMessage(message: String, context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
Log.w(getLogTag(), "OpenIM 未登录,无法发送消息")
return
}
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
val (recvID, groupID) = getReceiverInfo()
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
// 发送进度
}
override fun onError(code: Int, error: String?) {
Log.e(getLogTag(), "发送消息失败: $error")
}
override fun onSuccess(data: Message?) {
Log.d(getLogTag(), "发送消息成功")
onMessageSentSuccess(message, data)
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
textMessage,
recvID,
groupID,
OfflinePushInfo()
)
}
/**
* 发送图片消息
*/
fun sendImageMessage(imageUri: Uri, context: Context) {
val tempFile = createTempFile(context, imageUri)
val imagePath = tempFile?.path
if (imagePath != null) {
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
val (recvID, groupID) = getReceiverInfo()
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
Log.d(getLogTag(), "发送图片消息进度: $progress")
}
override fun onError(code: Int, error: String?) {
Log.e(getLogTag(), "发送图片消息失败: $error")
}
override fun onSuccess(data: Message?) {
Log.d(getLogTag(), "发送图片消息成功")
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
imageMessage,
recvID,
groupID,
OfflinePushInfo()
)
}
}
/**
* 创建临时文件
*/
fun createTempFile(context: Context, uri: Uri): File? {
return try {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = context.contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val filePath = it.getString(columnIndex)
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
val mimeType = context.contentResolver.getType(uri)
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
val tempFile =
File.createTempFile("temp_image", ".$extension", context.cacheDir)
val outputStream = FileOutputStream(tempFile)
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
tempFile
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 获取历史消息
*/
fun fetchHistoryMessage(context: Context) {
loadHistoryMessages(context, isLoadMore = false)
}
/**
* 加载历史消息的通用方法
* @param context 上下文
* @param isLoadMore 是否是加载更多true追加到现有数据false替换现有数据
*/
private fun loadHistoryMessages(context: Context, isLoadMore: Boolean) {
if (conversationID.isEmpty()) {
Log.w(getLogTag(), "conversationID为空无法${if (isLoadMore) "加载更多" else "获取"}历史消息")
return
}
if (isLoadMore) {
isLoading = true
}
viewModelScope.launch {
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
val newChatItems = messages.mapNotNull {
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))
}.reversed() // 反转顺序,使最新消息在前面
// 根据是否是加载更多来决定数据处理方式
chatData = if (isLoadMore) {
chatData + newChatItems // 追加到现有数据
} else {
newChatItems // 替换现有数据
}
if (messages.size < fetchHistorySize) {
hasMore = false
}
lastMessage = messages.firstOrNull()
if (isLoadMore) {
isLoading = false
}
Log.d(getLogTag(), "${if (isLoadMore) "加载更多" else "获取"}历史消息成功")
}
override fun onError(code: Int, error: String?) {
Log.e(getLogTag(), "${if (isLoadMore) "加载更多" else "获取"}历史消息失败: $error")
if (isLoadMore) {
isLoading = false
}
}
},
conversationID,
if (isLoadMore) lastMessage else null, // 首次加载不传lastMessage
fetchHistorySize,
ViewType.History
)
}
}
/**
* 获取显示的聊天列表
*/
fun getDisplayChatList(): List<ChatItem> {
val list = chatData
// 更新每条消息的时间戳显示状态
for (item in list) {
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
}
return list
}
}

View File

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

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

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.ui.chat
package com.aiosman.ravenow.ui.chat
import android.app.Activity
import android.content.Intent
@@ -8,10 +8,10 @@ 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.border
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -50,10 +50,13 @@ 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
@@ -70,18 +73,20 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.ChatItem
import com.aiosman.riderpro.exp.formatChatTime
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.DropdownMenu
import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.tencent.imsdk.v2.V2TIMMessage
import kotlinx.coroutines.flow.collectLatest
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
@@ -89,6 +94,7 @@ fun ChatScreen(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<ChatViewModel>(
key = "ChatViewModel_$userId",
@@ -159,7 +165,7 @@ fun ChatScreen(userId: String) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.background(AppColors.background)
) {
StatusBarSpacer()
Row(
@@ -172,23 +178,36 @@ fun ChatScreen(userId: String) {
Image(
painter = painterResource(R.drawable.rider_pro_back_icon),
modifier = Modifier
.size(28.dp)
.size(24.dp)
.noRippleClickable {
navController.popBackStack()
navController.navigateUp()
},
contentDescription = null
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(32.dp)),
contentDescription = "avatar"
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = viewModel.userProfile?.nickName ?: "",
modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f)
,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
style = TextStyle(
color = Color.Black,
color = AppColors.text,
fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
)
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(8.dp))
Box {
Image(
painter = painterResource(R.drawable.rider_pro_more_horizon),
@@ -197,7 +216,9 @@ fun ChatScreen(userId: String) {
.noRippleClickable {
isMenuExpanded = true
},
contentDescription = null
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
)
DropdownMenu(
expanded = isMenuExpanded,
@@ -206,17 +227,21 @@ fun ChatScreen(userId: String) {
},
menuItems = listOf(
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,
) {
isMenuExpanded = false
viewModel.viewModelScope.launch {
if (viewModel.notificationStrategy == "mute") {
viewModel.updateNotificationStrategy("active")
} else {
viewModel.updateNotificationStrategy("mute")
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()
}
}
),
@@ -236,13 +261,17 @@ fun ChatScreen(userId: String) {
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0xfff7f7f7))
)
Spacer(modifier = Modifier.height(8.dp))
ChatInput(
onSendImage = {
it?.let {
viewModel.sendImageMessage(it, context)
if (NetworkUtils.isNetworkAvailable(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(
modifier = Modifier
.fillMaxSize()
.background(Color(0xfff7f7f7))
.background(AppColors.background)
.padding(paddingValues)
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
reverseLayout = true,
verticalArrangement = Arrangement.Top
) {
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]
if (item.showTimeDivider) {
val calendar = java.util.Calendar.getInstance()
@@ -273,7 +301,7 @@ fun ChatScreen(userId: String) {
Text(
text = calendar.time.formatChatTime(context), // Format the timestamp
style = TextStyle(
color = Color.Gray,
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center
),
@@ -298,7 +326,7 @@ fun ChatScreen(userId: String) {
.padding(bottom = 16.dp, end = 16.dp)
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.background(AppColors.background)
.padding(8.dp)
.noRippleClickable {
coroutineScope.launch {
@@ -310,7 +338,7 @@ fun ChatScreen(userId: String) {
Text(
text = "${goToNewCount} New Message",
style = TextStyle(
color = Color.Black,
color = AppColors.text,
fontSize = 16.sp,
),
)
@@ -324,6 +352,7 @@ fun ChatScreen(userId: String) {
@Composable
fun ChatSelfItem(item: ChatItem) {
val context = LocalContext.current
Column(
modifier = Modifier
@@ -337,33 +366,42 @@ fun ChatSelfItem(item: ChatItem) {
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 == 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))
.background(Color(0xFF000000))
.clip(RoundedCornerShape(20.dp))
.background(Color(0xFF6246FF))
.padding(
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
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 == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
) {
when (item.messageType) {
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
MessageType.TEXT -> {
Text(
text = item.message,
style = TextStyle(
color = Color.White,
fontSize = 16.sp,
fontSize = 14.sp,
),
textAlign = TextAlign.Start
)
}
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
MessageType.PICTURE -> {
CustomAsyncImage(
imageUrl = item.imageList[1].url,
modifier = Modifier.fillMaxSize(),
@@ -373,34 +411,36 @@ fun ChatSelfItem(item: ChatItem) {
else -> {
Text(
text = "Unsupported message type",
text = "不支持的消息类型",
style = TextStyle(
color = Color.White,
fontSize = 16.sp,
fontSize = 14.sp,
)
)
}
}
}
}
Spacer(modifier = Modifier.width(12.dp))
/*Spacer(modifier = Modifier.width(12.dp))
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(40.dp))
.size(24.dp)
.clip(RoundedCornerShape(24.dp))
) {
CustomAsyncImage(
imageUrl = item.avatar,
modifier = Modifier.fillMaxSize(),
contentDescription = "avatar"
)
}
}*/
}
}
}
@Composable
fun ChatOtherItem(item: ChatItem) {
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier
.fillMaxWidth()
@@ -427,29 +467,29 @@ fun ChatOtherItem(item: ChatItem) {
modifier = Modifier
.widthIn(
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))
.background(Color(0xffFFFFFF))
.background(AppColors.background)
.padding(
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
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 == 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) {
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
MessageType.TEXT -> {
Text(
text = item.message,
style = TextStyle(
color = Color.Black,
color = AppColors.text,
fontSize = 16.sp,
),
textAlign = TextAlign.Start
)
}
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
MessageType.PICTURE -> {
CustomAsyncImage(
imageUrl = item.imageList[1].url,
modifier = Modifier.fillMaxSize(),
@@ -461,7 +501,7 @@ fun ChatOtherItem(item: ChatItem) {
Text(
text = "Unsupported message type",
style = TextStyle(
color = Color.White,
color = AppColors.text,
fontSize = 16.sp,
)
)
@@ -490,12 +530,14 @@ fun ChatInput(
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(
@@ -528,88 +570,104 @@ fun ChatInput(
onSendImage(uri)
}
}
Box( modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = inputBarHeight)
) {
Box(
Row(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(16.dp))
.background(Color(0xffe5e5e5))
.padding(horizontal = 16.dp),
contentAlignment = androidx.compose.ui.Alignment.CenterStart,
.fillMaxWidth()
.clip(RoundedCornerShape(20.dp))
.background(appColors.decentBackground)
.padding(start = 16.dp, end = 8.dp, top = 2.dp, bottom = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
BasicTextField(
value = text,
onValueChange = {
text = it
},
textStyle = TextStyle(
color = Color.Black,
fontSize = 16.sp
),
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.onFocusChanged { focusState ->
isKeyboardOpen = focusState.isFocused
}
.pointerInput(Unit) {
awaitPointerEventScope {
keyboardController = softwareKeyboardController
awaitFirstDown().also {
keyboardController?.show()
.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))
Image(
painter = painterResource(R.mipmap.rider_pro_im_image),
contentDescription = "Image",
modifier = Modifier
.size(30.dp)
.noRippleClickable {
if (NetworkUtils.isNetworkAvailable(context)) {
imagePickUpLauncher.launch(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
},
"Select Image"
)
)
} else {
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
}
},
)
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()
}
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
keyboardController?.hide()
}
)
)
}
Spacer(modifier = Modifier.width(16.dp))
Icon(
painter = painterResource(id = R.drawable.rider_pro_camera),
contentDescription = "Emoji",
modifier = Modifier
.size(30.dp)
.noRippleClickable {
imagePickUpLauncher.launch(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
},
"Select Image"
)
)
},
tint = Color(0xffe0e0e0)
)
Spacer(modifier = Modifier.width(8.dp))
Crossfade(
targetState = text.isNotEmpty(), animationSpec = tween(500),
label = ""
) { isNotEmpty ->
Icon(
painter = painterResource(id = R.drawable.rider_pro_video_share),
contentDescription = "Emoji",
modifier = Modifier
.size(32.dp)
.noRippleClickable {
if (text.isNotEmpty()) {
onSend(text)
text = ""
}
},
tint = if (isNotEmpty) Color.Red else Color(0xffe0e0e0)
contentDescription = null,
)
}
}
}
}

View File

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

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

View File

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

View File

@@ -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.layout.Arrangement
@@ -40,13 +40,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.CommentEntity
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal
import com.aiosman.riderpro.ui.post.CommentContent
import com.aiosman.riderpro.ui.post.CommentMenuModal
import com.aiosman.riderpro.ui.post.CommentsViewModel
import com.aiosman.riderpro.ui.post.OrderSelectionComponent
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
import com.aiosman.ravenow.ui.post.CommentContent
import com.aiosman.ravenow.ui.post.CommentMenuModal
import com.aiosman.ravenow.ui.post.CommentsViewModel
import com.aiosman.ravenow.ui.post.OrderSelectionComponent
import kotlinx.coroutines.launch
/**

View File

@@ -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.background
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -20,15 +21,18 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
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.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.composables.BottomNavigationPlaceholder
@Preview
@Composable
@@ -67,33 +71,134 @@ fun NoticeScreenHeader(
rightIcon: @Composable (() -> Unit)? = null
) {
val nav = LocalNavController.current
val AppColors = LocalAppTheme.current
Box(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
//返回tab
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.weight(1f))
if (moreIcon) {
Image(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "More",
modifier = Modifier.size(24.dp)
)
}
if (rightIcon != null) {
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_back_icon),
painter = painterResource(id = R.drawable.rider_pro_close,),
contentDescription = title,
modifier = Modifier.size(16.dp).clickable(
modifier = Modifier.size(24.dp).clickable(
indication = null,
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)
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)
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))
rightIcon()
Image(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "More",
modifier = Modifier
.size(24.dp),
)
}
if (rightIcon != null) {
//rightIcon()
}
}
}

View File

@@ -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.layout.Box
@@ -21,9 +21,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -33,16 +34,18 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.CommentEntity
import com.aiosman.riderpro.exp.timeAgo
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.navigateToPost
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.exp.timeAgo
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.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import kotlinx.coroutines.launch
@Composable
@@ -62,8 +65,10 @@ fun CommentNoticeScreen() {
var dataFlow = viewModel.commentItemsFlow
var comments = dataFlow.collectAsLazyPagingItems()
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier.fillMaxSize().background(color = Color(0xFFFFFFFF))
modifier = Modifier.fillMaxSize().background(color = AppColors.background)
) {
StatusBarSpacer()
Box(
@@ -73,72 +78,107 @@ fun CommentNoticeScreen() {
) {
NoticeScreenHeader(stringResource(R.string.comment), moreIcon = false)
}
LazyColumn(
modifier = Modifier
.fillMaxSize().padding(horizontal = 16.dp)
) {
items(comments.itemCount) { index ->
comments[index]?.let { comment ->
CommentNoticeItem(comment) {
viewModel.updateReadStatus(comment.id)
viewModel.viewModelScope.launch {
var highlightCommentId = comment.id
comment.parentCommentId?.let {
highlightCommentId = it
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(
modifier = Modifier
.fillMaxSize().padding(horizontal = 16.dp)
) {
items(comments.itemCount) { index ->
comments[index]?.let { comment ->
CommentNoticeItem(comment) {
viewModel.updateReadStatus(comment.id)
viewModel.viewModelScope.launch {
var highlightCommentId = comment.id
comment.parentCommentId?.let {
highlightCommentId = it
}
navController.navigateToPost(
id = comment.post!!.id,
highlightCommentId = highlightCommentId,
initImagePagerIndex = 0
)
}
navController.navigateToPost(
id = comment.post!!.id,
highlightCommentId = highlightCommentId,
initImagePagerIndex = 0
)
}
}
}
}
// handle load error
when {
comments.loadState.append is LoadState.Loading -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
LinearProgressIndicator(
modifier = Modifier.width(160.dp),
color = Color(0xFFDA3832)
)
// handle load error
when {
comments.loadState.append is LoadState.Loading -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
LinearProgressIndicator(
modifier = Modifier.width(160.dp),
color = AppColors.main
)
}
}
}
}
comments.loadState.append is LoadState.Error -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.noRippleClickable {
comments.retry()
},
contentAlignment = Alignment.Center
) {
Text(
text = "Load comment error, click to retry",
)
comments.loadState.append is LoadState.Error -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.noRippleClickable {
comments.retry()
},
contentAlignment = Alignment.Center
) {
Text(
text = "Load comment error, click to retry",
color = AppColors.text
)
}
}
}
}
}
item {
Spacer(modifier = Modifier.height(72.dp))
item {
Spacer(modifier = Modifier.height(72.dp))
}
}
}
}
}
@Composable
@@ -148,6 +188,8 @@ fun CommentNoticeItem(
) {
val navController = LocalNavController.current
val context = LocalContext.current
val AppColors = LocalAppTheme.current
Row(
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
) {
@@ -183,7 +225,8 @@ fun CommentNoticeItem(
Text(
text = commentItem.name,
fontSize = 18.sp,
modifier = Modifier
modifier = Modifier,
color = AppColors.text
)
Spacer(modifier = Modifier.height(4.dp))
Row {
@@ -195,7 +238,7 @@ fun CommentNoticeItem(
text = text,
fontSize = 14.sp,
maxLines = 1,
color = Color(0x99000000),
color = AppColors.secondaryText,
modifier = Modifier.weight(1f),
overflow = TextOverflow.Ellipsis
)
@@ -203,7 +246,7 @@ fun CommentNoticeItem(
Text(
text = commentItem.date.timeAgo(context),
fontSize = 14.sp,
color = Color(0x66000000)
color = AppColors.secondaryText,
)
}
@@ -228,7 +271,7 @@ fun CommentNoticeItem(
if (commentItem.unread) {
Box(
modifier = Modifier
.background(Color(0xFFE53935), CircleShape)
.background(AppColors.main, CircleShape)
.size(12.dp)
.align(Alignment.TopEnd)
)

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.ui.comment.notice
package com.aiosman.ravenow.ui.comment.notice
import android.content.Context
import androidx.compose.runtime.getValue
@@ -11,15 +11,15 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.CommentRemoteDataSource
import com.aiosman.riderpro.data.CommentService
import com.aiosman.riderpro.data.CommentServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.CommentEntity
import com.aiosman.riderpro.entity.CommentPagingSource
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.CommentRemoteDataSource
import com.aiosman.ravenow.data.CommentService
import com.aiosman.ravenow.data.CommentServiceImpl
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.entity.CommentPagingSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

View File

@@ -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.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
//import androidx.compose.foundation.layout.ColumnScopeInstance.weight
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
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
fun ActionButton(
modifier: Modifier = Modifier,
text: String,
color: Color = Color.Black,
@DrawableRes backgroundImage: Int? = null,
backgroundColor: Color = Color(0xfff0f0f0),
color: Color? = null,
backgroundColor: Color? = null,
leading: @Composable (() -> Unit)? = null,
expandText: Boolean = false,
contentPadding: PaddingValues = PaddingValues(vertical = 16.dp),
isLoading: Boolean = false,
loadingTextColor: Color = Color.White,
loadingTextColor: Color? = null,
loadingText: String = "Loading",
loadingBackgroundColor: Color = Color(0xFFD95757),
disabledBackgroundColor: Color = Color(0xFFD0D0D0),
loadingBackgroundColor: Color? = null,
disabledBackgroundColor: Color? = null,
enabled: Boolean = true,
fullWidth:Boolean = false,
fullWidth: Boolean = false,
roundCorner: Float = 24f,
fontSize: TextUnit = 17.sp,
fontWeight: FontWeight = FontWeight.W900,
click: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val animatedBackgroundColor by animateColorAsState(
targetValue = run{
targetValue = run {
if (enabled) {
if (isLoading) {
loadingBackgroundColor
loadingBackgroundColor ?: AppColors.loadingMain
} else {
backgroundColor
backgroundColor ?: AppColors.basicMain
}
} else {
disabledBackgroundColor
disabledBackgroundColor ?: AppColors.disabledBackground
}
},
animationSpec = tween(300), label = ""
)
Box(
modifier = modifier
.clip(RoundedCornerShape(24.dp))
.clip(RoundedCornerShape(roundCorner.dp))
.background(animatedBackgroundColor)
.noRippleClickable {
if (enabled && !isLoading) {
@@ -79,9 +80,9 @@ fun ActionButton(
modifier = Modifier
.align(Alignment.Center)
.let {
if(fullWidth){
if (fullWidth) {
it.fillMaxWidth()
}else{
} else {
it
}
},
@@ -91,27 +92,26 @@ fun ActionButton(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
Box(modifier = Modifier.align(Alignment.CenterStart)){
Box(modifier = Modifier.align(Alignment.CenterStart)) {
leading?.invoke()
}
}
Text(
text,
fontSize = 17.sp,
color = color,
fontWeight = FontWeight.W900,
fontSize = fontSize,
color = color ?: AppColors.text,
fontWeight = fontWeight,
textAlign = if (expandText) TextAlign.Center else TextAlign.Start
)
}
}else{
} else {
Box(
modifier = Modifier
.let {
if(fullWidth){
if (fullWidth) {
it.fillMaxWidth()
}else{
} else {
it
}
}
@@ -124,13 +124,13 @@ fun ActionButton(
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White
color = AppColors.text
)
Text(
loadingText,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = loadingTextColor
color = loadingTextColor ?: AppColors.loadingText,
)
}
}

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

View File

@@ -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.ExperimentalAnimationApi
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.animation.with
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 24) {
val AppColors = LocalAppTheme.current
AnimatedContent(
targetState = count,
transitionSpec = {
@@ -36,6 +36,6 @@ fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 2
)
}
) { targetCount ->
Text(text = "$targetCount", modifier = modifier, fontSize = fontSize.sp)
Text(text = "$targetCount", modifier = modifier, fontSize = fontSize.sp, color = AppColors.text)
}
}

View File

@@ -1,22 +1,20 @@
package com.aiosman.riderpro.ui.composables
package com.aiosman.ravenow.ui.composables
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@Composable
@@ -25,6 +23,7 @@ fun AnimatedFavouriteIcon(
isFavourite: Boolean = false,
onClick: (() -> Unit)? = null
) {
val AppColors = LocalAppTheme.current
val animatableRotation = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
suspend fun shake() {
@@ -65,6 +64,7 @@ fun AnimatedFavouriteIcon(
modifier = modifier.graphicsLayer {
rotationZ = animatableRotation.value
},
colorFilter = ColorFilter.tint(AppColors.text)
)
}
}

View File

@@ -1,24 +1,20 @@
package com.aiosman.riderpro.ui.composables
package com.aiosman.ravenow.ui.composables
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
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.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@Composable
@@ -27,6 +23,8 @@ fun AnimatedLikeIcon(
liked: Boolean = false,
onClick: (() -> Unit)? = null
) {
val AppColors = LocalAppTheme.current
val animatableRotation = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
suspend fun shake() {
@@ -65,6 +63,7 @@ fun AnimatedLikeIcon(
modifier = modifier.graphicsLayer {
rotationZ = animatableRotation.value
},
colorFilter = if (!liked) ColorFilter.tint(AppColors.text) else null
)
}
}

View File

@@ -1,23 +1,18 @@
package com.aiosman.riderpro.ui.composables
package com.aiosman.ravenow.ui.composables
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import androidx.compose.foundation.layout.BoxWithConstraints
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.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.aiosman.riderpro.utils.BlurHashDecoder
import com.aiosman.riderpro.utils.Utils.getImageLoader
import com.aiosman.ravenow.utils.BlurHashDecoder
import com.aiosman.ravenow.utils.Utils.getImageLoader
const val DEFAULT_HASHED_BITMAP_WIDTH = 4
const val DEFAULT_HASHED_BITMAP_HEIGHT = 3

View File

@@ -1,4 +1,4 @@
package com.aiosman.riderpro.ui.composables
package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box

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