diff --git a/.gitignore b/.gitignore index 8f97012..9d51fc1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,13 @@ /components/ .selftest/ data/ + +# mobile-next:官方基线构建产物与本机路径,不入库 +/mobile-next/build/ +/mobile-next/.dart_tool/ +/mobile-next/android/.gradle/ +/mobile-next/android/local.properties +/mobile-next/android/app/proguard/ +/mobile-next/**/.flutter-plugins +/mobile-next/**/.flutter-plugins-dependencies + diff --git a/docs/mobile-next-baseline-import.md b/docs/mobile-next-baseline-import.md new file mode 100644 index 0000000..49e6f4f --- /dev/null +++ b/docs/mobile-next-baseline-import.md @@ -0,0 +1,115 @@ +# mobile-next 固定基线导入与构建记录(B-128 第1阶段) + +## 上游来源 + +- 项目:OpenIM 官方 Flutter 样板工程 `openim-flutter-demo` +- 仓库:`https://github.com/OpenIMSDK/openim-flutter-demo.git` +- 固定基线:tag `3.8.3-patch.3`,提交 `b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f`("Updated IM SDK",2025-03-07) +- 校验:`git ls-remote --tags` 解析该 tag 的 `^{}` 即指向 `b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f` + +## 导入方式 + +- 并行目录 `mobile-next/`,与现网 `mobile/` 并存,未覆盖、未改动 `mobile/`。 +- 以 `git clone --no-checkout` 拉取上游后 `git checkout 3.8.3-patch.3`,将整棵工作树复制入 `mobile-next/`(剔除 `.git` 与 `.DS_Store`)。 +- 导入后与原 tag 树做 `git diff --no-index` 比对,除 `.git`/`.DS_Store` 外**零差异**。 +- 未迁入公司功能、未换品牌、未调整 SDK 版本,源码保持上游原样。 +- 平台工程内生成的 `android/local.properties`(sdk.dir / flutter.sdk 指向本机)不入库(见 `.gitignore`)。 + +## 许可证位置 + +- 上游仓库根目录 `LICENSE`(已随导入进入 `mobile-next/LICENSE`)。 +- 子包 `openim_common/LICENSE`、`openim_live/LICENSE`、`local_plugin/flutter_openim_live_alert/LICENSE`。 +- 各 pub.dev 依赖许可证见各自包目录,依赖清单以 `mobile-next/pubspec.yaml` 与 `mobile-next/pubspec.lock` 为准。 + +## 锁定工具链版本 + +| 组件 | 版本 | +| --- | --- | +| Flutter | 3.24.5 stable(revision `dec2ee5c1f`,engine `a18df97ca5`) | +| Dart | 3.5.4(随 Flutter 3.24.5 分发) | +| JDK | OpenJDK 17.0.20+8(Temurin) | +| Gradle | 7.6.3(`android/gradle/wrapper/gradle-wrapper.properties` 锁定) | +| AGP | 7.3.1(`android/build.gradle` buildscript 声明) | +| Kotlin | 1.9.23(`android/build.gradle` `ext.kotlin_version`) | +| Android SDK | compileSdk 34,minSdk 24,targetSdk 33,build-tools 36.0.0 | +| Dart 依赖锁 | `mobile-next/pubspec.lock`(上游自带,原样保留) | +| OpenIM SDK | `flutter_openim_sdk 3.8.3+3`(上游 pubspec 锁定) | + +注意:`pubspec.lock` 为上游原样内容;执行 `flutter pub get` 后仅将 `flutter_openim_sdk` 由 lock 中记录的 `3.8.3+2` 对齐到 `pubspec.yaml` 声明的 `3.8.3+3`(上游 lock 与 pubspec 不一致,为官方产物)。 + +## 可复现构建命令 + +前置环境变量: + +``` +set JAVA_HOME= +set ANDROID_HOME= +set PATH=%JAVA_HOME%\bin;\bin;%PATH% +``` + +`mobile-next/android/local.properties` 需含: + +``` +sdk.dir= +flutter.sdk= +``` + +首次执行依赖解析(会写 `.dart_tool`,不产生源码改动): + +``` +flutter pub get +``` + +### Debug 构建 + +``` +flutter build apk --debug +``` + +产物:`build/app/outputs/flutter-apk/app-debug.apk`(约 110 MB)。 + +### Release 构建 + +``` +flutter build apk --release +``` + +产物:`build/app/outputs/flutter-apk/app-release.apk`(约 51.3 MB,签名使用上游自带 keystore `android/app/openim`,alias/password 均为 `openim`)。 + +## 构建结果(2026-08-19,本机实测) + +| 构建 | 结果 | 产物 | 大小 | SHA256 | +| --- | --- | --- | --- | --- | +| debug | 成功 | `app-debug.apk` | 109,930,873 B | `55789791CE8226573C0030A026E5C89C44AD6E154884F24F7329357FCA7E0464` | +| release | 成功 | `app-release.apk` | 53,742,479 B | `46D5900FC735FE0880994385691E62A00589040C4D9FCD4DFA69E003763E7780`(sha1 `7191307bc54624c7b3cfdf13ed2378a749b19ec2`) | + +APK 元数据(aapt badging):package `io.openim.flutter.demo`,versionName `3.8.3`,versionCode `180`,minSdk 24 / targetSdk 33 / compileSdk 34。 + +### 构建期警告(不影响产物) + +- Flutter Gradle 插件以 `apply script` 方式应用,报 "deprecated, will be removed in a future release"(上游工程原生写法,非本任务改动)。 +- 部分依赖 Kotlin metadata 版本(1.8.x)高于工程 Kotlin(1.9.23 预期 1.6.0/1.8.0)的警告,属于上游依赖组合特性,debug/release 均通过。 + +## 失败证据 + +本阶段 debug 与 release 均构建成功,无失败步骤。若后续步骤出现构建失败,按完成标准须明确到具体依赖或构建步骤并留存日志,不再继续后续兼容迁移。 + +## 回退方法 + +本任务为独立提交,仅新增 `mobile-next/` 与本文档,未触碰 `mobile/`、`pc-client/`、`account-service/` 及现网服务。 + +回退命令: + +``` +git revert cbfced2 +``` + +或直接删除并行目录(不影响现网): + +``` +git rm -r mobile-next +``` + +## 备注 + +- 本目录仅证明「官方固定基线可稳定构建」,不含任何公司定制逻辑,尚未接入公司账号/品牌,属后续阶段工作。 diff --git a/mobile-next/.gitignore b/mobile-next/.gitignore new file mode 100644 index 0000000..7d198be --- /dev/null +++ b/mobile-next/.gitignore @@ -0,0 +1,4 @@ +/.flutter-plugins +/.flutter-plugins-dependencies +/.packages +/.dart_tool/ diff --git a/mobile-next/CONFIGKEY.md b/mobile-next/CONFIGKEY.md new file mode 100644 index 0000000..24736e9 --- /dev/null +++ b/mobile-next/CONFIGKEY.md @@ -0,0 +1,88 @@ + +# Common Function Configuration Guide + +- [Offline push](#offlinepush) +- [Map](#map) + +## Offlinepush + +Currently using the integrated solution. + +### Client configuration + +#### 1. Use Getui (https://getui.com/) in mainland China + +###### Configure iOS and Android in the integration guide of Getui + +**iOS platform configuration:** +According to [its documentation](https://docs.getui.com/getui/mobile/ios/overview/), make the corresponding iOS configuration. Then find the following files in the code and modify the corresponding iOS side key: + +- **[push_controller.dart](openim_common/lib/src/controller/push_controller.dart)** + +```dart + const appID = 'your-app-id'; + const appKey = 'your-app-key'; + const appSecret = 'your-app-secret'; +``` + +**Android platform configuration:** +According to [its documentation](https://docs.getui.com/getui/mobile/android/overview/), make corresponding Android configurations, and pay attention to [multi-vendor](https://docs.getui.com/getui/mobile/vendor/vendor_open/) configurations. Then modify the following file contents: + +- **[build.gradle](android/app/build.gradle)** + +```gradle + manifestPlaceholders = [ + GETUI_APPID : "", + XIAOMI_APP_ID : "", + XIAOMI_APP_KEY : "", + MEIZU_APP_ID : "", + MEIZU_APP_KEY : "", + HUAWEI_APP_ID : "", + OPPO_APP_KEY : "", + OPPO_APP_SECRET: "", + VIVO_APP_ID : "", + VIVO_APP_KEY : "", + HONOR_APP_ID : "", + ] +``` + +#### 2. Use [FCM (Firebase Cloud Messaging)](https://firebase.google.com/docs/cloud-messaging) in overseas regions + +According to the integration guide of [FCM](https://firebase.google.com/docs/cloud-messaging), replace the following files: + +- **[google-services.json](android/app/google-services.json)** (Android platform) +- **[GoogleService-Info.plist](ios/Runner/GoogleService-Info.plist)** (iOS platform) +- **[firebase_options.dart](openim_common/lib/src/controller/firebase_options.dart)** (Firebase configuration in Dart project) + +### Offline push banner settings + +Currently, the SDK is designed to directly control the display content of the push banner by the client. When sending a message, set the input parameter [offlinePushInfo](https://github.com/openimsdk/openim-flutter-demo/blob/cc72b6d7ca5f70ca07885857beecec512f904f8c/lib/pages/chat/chat_logic.dart#L543): + +```dart + final offlinePushInfo = OfflinePushInfo( + title: "Fill in the title", + desc: "Fill in the description, such as the message content", + iOSBadgeCount: true, + ); + // If you do not customize offlinePushInfo, the title defaults to the app name, and the desc defaults to "You received a new message" +``` + +According to actual needs, you can enable the offline push function after completing the corresponding client and server configurations. + +--- + +## Map + +### Configuration Guide + +Need to configure the corresponding AMap Key. Please refer to [AMap Document](https://lbs.amap.com/) for details. The code in the project needs to modify the following Key: + +- **[webKey](https://github.com/openimsdk/openim-flutter-demo/blob/5720a10a31a0a9bc5319775f9f4da83d6996dbfe/openim_common/lib/src/config.dart#L49)** +- **[webServerKey](https://github.com/openimsdk/openim-flutter-demo/blob/5720a10a31a0a9bc5319775f9f4da83d6996dbfe/openim_common/lib/src/config.dart#L50)** + +```dart + static const webKey = 'webKey'; + static const webServerKey = 'webServerKey'; +``` + +Once the configuration is complete, you can enable the map function. \ No newline at end of file diff --git a/mobile-next/CONFIGKEY.zh-CN.md b/mobile-next/CONFIGKEY.zh-CN.md new file mode 100644 index 0000000..70ab0ca --- /dev/null +++ b/mobile-next/CONFIGKEY.zh-CN.md @@ -0,0 +1,88 @@ + +# 常见功能配置指南 + +- [离线推送功能](#离线推送功能) +- [地图功能](#地图功能) + +## 离线推送功能 + +目前使用的是集成方案。 + +### 客户端配置 + +#### 1. 中国大陆地区使用个推([Getui](https://getui.com/)) + +###### 在[Getui](https://getui.com/)的集成指南,配置iOS和Android。 + +**iOS 平台配置:** +根据[其文档](https://docs.getui.com/getui/mobile/ios/overview/)做好相应的iOS配置。然后在代码中找到以下文件并修改对应的 iOS侧Key: + +- **[push_controller.dart](openim_common/lib/src/controller/push_controller.dart)** + +```dart + const appID = 'your-app-id'; + const appKey = 'your-app-key'; + const appSecret = 'your-app-secret'; +``` + +**Android 平台配置:** +根据[其文档](https://docs.getui.com/getui/mobile/android/overview/)做好相应的Android配置,注意[多厂商](https://docs.getui.com/getui/mobile/vendor/vendor_open/)配置。然后修改以下文件内容: + +- **[build.gradle](android/app/build.gradle)** + +```gradle + manifestPlaceholders = [ + GETUI_APPID : "", + XIAOMI_APP_ID : "", + XIAOMI_APP_KEY : "", + MEIZU_APP_ID : "", + MEIZU_APP_KEY : "", + HUAWEI_APP_ID : "", + OPPO_APP_KEY : "", + OPPO_APP_SECRET: "", + VIVO_APP_ID : "", + VIVO_APP_KEY : "", + HONOR_APP_ID : "", + ] +``` + +#### 2. 海外地区使用 [FCM(Firebase Cloud Messaging)](https://firebase.google.com/docs/cloud-messaging) + +根据 [FCM](https://firebase.google.com/docs/cloud-messaging) 的集成指南,替换以下文件: + +- **[google-services.json](android/app/google-services.json)**(Android 平台) +- **[GoogleService-Info.plist](ios/Runner/GoogleService-Info.plist)**(iOS 平台) +- **[firebase_options.dart](openim_common/lib/src/controller/firebase_options.dart)**(Dart 项目中的 Firebase 配置) + +### 离线推送横幅设置 + +目前SDK的设计是直接由客户端控制推送横幅的展示内容。发送消息时,设置入参[offlinePushInfo](https://github.com/openimsdk/openim-flutter-demo/blob/cc72b6d7ca5f70ca07885857beecec512f904f8c/lib/pages/chat/chat_logic.dart#L543): + +```dart + final offlinePushInfo = OfflinePushInfo( + title: "填写标题", + desc: "填写描述信息,例如消息内容", + iOSBadgeCount: true, + ); + // 如果不自定义offlinePushInfo,则title默认为app名称,desc默认为为“你收到了一条新消息” +``` + +根据实际需求,完成对应的客户端和服务端配置后即可启用离线推送功能。 + +--- + +## 地图功能 + +### 配置指南 + +需要配置对应的 AMap Key。具体请参考 [AMap 文档](https://lbs.amap.com/),工程中的代码需要修改以下 Key: + +- **[webKey](https://github.com/openimsdk/openim-flutter-demo/blob/5720a10a31a0a9bc5319775f9f4da83d6996dbfe/openim_common/lib/src/config.dart#L49)** +- **[webServerKey](https://github.com/openimsdk/openim-flutter-demo/blob/5720a10a31a0a9bc5319775f9f4da83d6996dbfe/openim_common/lib/src/config.dart#L50)** + +```dart + static const webKey = 'webKey'; + static const webServerKey = 'webServerKey'; +``` + +完成配置后即可启用地图功能。 diff --git a/mobile-next/LICENSE b/mobile-next/LICENSE new file mode 100644 index 0000000..72dfe81 --- /dev/null +++ b/mobile-next/LICENSE @@ -0,0 +1,45 @@ +OpenIM Open Source License +This repository is licensed under the GNU Affero General Public License version 3 (AGPL-3.0) and is subject to the following additional terms: +1. Commercial Use +1.1 Prohibition of Commercial Use +This repository and its contents may not be used for any commercial purposes. If you wish to use OpenIM for commercial purposes, you must obtain a commercial license from the producer. +1.2 Commercial License Application +For commercial licensing, please contact contact@openim.io. Commercial licenses will be tailored to your specific needs and use cases to ensure that you use OpenIM legally and compliantly. +1.3 License After Commercial Authorization +If you have paid the relevant commercial licensing fees to the producer, you may use OpenIM in accordance with the terms of the commercial license without adhering to the provisions of AGPL-3.0. The commercial license grants you the rights to use, modify, and distribute OpenIM under specific conditions detailed in the commercial licensing agreement. +2. LOGO and Copyright Information +2.1 Protection of LOGO and Copyright Information +During the use of this repository, you may not remove, modify, or hide the LOGO or copyright information within the OpenIM console or applications. +2.2 LOGO Definition +All unified text or icons bearing the words OpenIM or OpenIMSDK within the user interfaces of this repository are collectively referred to as the LOGO. This includes, but is not limited to, textual and iconographic identifiers in software interfaces, documentation, and promotional materials. +3. Contributor Agreement +As a contributor, you agree to the following terms: +3.1 Right to Adjust the Agreement +The producer reserves the right to adjust the strictness or leniency of the open-source agreement as necessary to ensure that the agreement aligns with the development and needs of the project. +3.2 Commercial Use of Code +The code you contribute may be used for commercial purposes, including but not limited to cloud business operations. By contributing, you agree that the producer has the right to use your contributions for commercial purposes without additional compensation or authorization. +4. Other Terms +4.1 Compliance with AGPL-3.0 +Except for the specific conditions mentioned above, all other rights and restrictions are governed by the GNU Affero General Public License version 3 (AGPL-3.0). For detailed information about the AGPL-3.0 license, please refer to the GNU AGPL-3.0 License. +4.2 Patent Protection +The interactive design of this product is protected by design patents. Any unauthorized copying, modification, or distribution of the interactive design constitutes infringement and will be subject to legal action. +5. Disclaimer +This software is provided "as is," without any form of express or implied warranty, including but not limited to warranties of merchantability, fitness for a particular purpose, or non-infringement. Users assume all risks associated with the use of this software. +6. Contact Information +For any licensing-related questions or to obtain a commercial license, please contact contact@openim.io. + +GNU Affero General Public License Version 3 (AGPL-3.0) +© 2024 OpenIMSDK + +This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . diff --git a/mobile-next/README.md b/mobile-next/README.md new file mode 100644 index 0000000..c67895b --- /dev/null +++ b/mobile-next/README.md @@ -0,0 +1,257 @@ +

+ + + +

+ +# OpenIM Flutter 💬💻 + +

+ OpenIM Docs + • + OpenIM Server + • + openim-sdk-flutter + • + openim-sdk-core +

+ +OpenIM provides an open-source Instant Messaging (IM) SDK for developers, serving as an alternative solution to cloud services like Twilio and Sendbird. With OpenIM, developers can build secure and reliable IM applications similar to WeChat, Zoom, and Slack. + +This repository is based on the open-source version of the OpenIM SDK, offering an Flutter IM application. You can use this application as a reference implementation of the OpenIM SDK. + +

+ Preview + + Preview +

+ +## License :page_facing_up: + +This repository is licensed under the GNU Affero General Public License Version 3 (AGPL-3.0) and is subject to additional terms. **Commercial use is prohibited**. For more details, see [here](./LICENSE). + +## Development Environment + +Before you start development, ensure that the following software is installed on your system: + +- **Operating System**: macOS 14.6 or later +- **Flutter**: Version 3.24.5 ([Installation Guide](https://docs.flutter.dev/get-started/install)), XCode: 15.4, Android Studio: Koala | 2024.1.1 Patch 1 +- **Git**: For version control + +Additionally, make sure you have [deployed](https://docs.openim.io/zh-Hans/guides/gettingStarted/dockerCompose) the latest version of the OpenIM Server. After deployment, you can compile the project and connect it to your server for testing. + +## Supported Platforms + +This application supports the following platforms: + +| Platform | Version | Status | +| ------------- | --------------------- | ------ | +| **iOS** | 13.0 and above | ✅ | +| **Android** | minSdkVersion 24 | ✅ | + +### Notes + +- **Flutter**: Ensure your version meets the requirements to avoid dependency issues. + +## Quick Start + +Follow the steps below to set up your local development environment: + +1. Clone the repository: + + ```bash + git clone https://github.com/openimsdk/openim-flutter-demo.git + cd openim-flutter-demo + ``` + +2. Install dependencies + + ```bash + flutter clean + flutter pub get + ``` + +3. Modify the configuration + + > If you have not changed the default server ports, update only the [_host](https://github.com/openimsdk/open-im-flutter-demo/blob/a309f25fdbc143e49d5ca852171ce57970871c85/openim_common/lib/src/config.dart#L59) to your server IP. + + ```dart + static const _host = "your-server-ip or your-domain"; + ``` + +4. Run the application using `flutter run` from the terminal or through your IDE. + +5. Start developing and testing! 🎉 + +## Audio/Video Calls + +The open-source version supports one-to-one audio and video calls. You need to first deploy and configure the [server](https://github.com/openimsdk/chat/blob/main/HOW_TO_SETUP_LIVEKIT_SERVER.md). For multi-party audio/video calls or video conferencing, please contact us at [contact@openim.io](mailto:contact@openim.io). + +## Build 🚀 + +> This project allows building separate iOS and Android applications with some differences in the process. + + - iOS: + ```bash + flutter build ipa + ``` + - Android: + ```bash + flutter build apk + ``` + +1. The build artifacts will be located in the `build` directory. + +## Features + +### Description + +| Feature Module | Feature | Status | +| -------------------------- | -------------------------------------------------------------------------------- | ------ | +| **Account Features** | Phone number registration \ Email registration \ Verification code login | ✅ | +| | View \ Edit personal information | ✅ | +| | Multi-language settings | ✅ | +| | Change password \ Forgot password | ✅ | +| **Friend Features** | Find \ Apply \ Search \ Add \ Delete friends | ✅ | +| | Accept \ Reject friend requests | ✅ | +| | Friend notes | ✅ | +| | Allow friend requests or not | ✅ | +| | Friend list \ Friend data real-time syncing | ✅ | +| **Blocklist** | Restrict messages | ✅ | +| | Real-time syncing of blocklist | ✅ | +| | Add \ Remove from blocklist | ✅ | +| **Group Features** | Create \ Dismiss groups | ✅ | +| | Apply to join \ Invite to join \ Leave group \ Remove members | ✅ | +| | Group name / Avatar changes / Group data updates (notifications, real-time sync) | ✅ | +| | Invite members to group | ✅ | +| | Transfer group ownership | ✅ | +| | Group owner or admin approve join requests | ✅ | +| | Search group members | ✅ | +| **Message Features** | Offline messages | ✅ | +| | Roaming messages | ✅ | +| | Multi-end messages | ✅ | +| | Message history | ✅ | +| | Message deletion | ✅ | +| | Clear messages | ✅ | +| | Copy messages | ✅ | +| | Typing indicator in single chat | ✅ | +| | Do Not Disturb for new messages | ✅ | +| | Clear chat history | ✅ | +| | New members can view group chat history | ✅ | +| | New message reminders | ✅ | +| | Text messages | ✅ | +| | Image messages | ✅ | +| | Video messages | ✅ | +| | Emoji messages | ✅ | +| | File messages | ✅ | +| | Voice messages | ✅ | +| | Contact card messages | ✅ | +| | Location messages | ✅ | +| | Custom messages | ✅ | +| **Conversation** | Pin conversation | ✅ | +| | Mark conversation as read | ✅ | +| | Mute conversation | ✅ | +| **REST API** | Authentication management | ✅ | +| | User management | ✅ | +| | Relationship chain management | ✅ | +| | Group management | ✅ | +| | Conversation management | ✅ | +| | Message management | ✅ | +| **Webhook** | Group callbacks | ✅ | +| | Message callbacks | ✅ | +| | Push callbacks | ✅ | +| | Relationship callbacks | ✅ | +| | User callbacks | ✅ | +| **Capacity & Performance** | 10,000 friends | ✅ | +| | 100,000-member supergroup | ✅ | +| | Second-level syncing | ✅ | +| | Cluster deployment | ✅ | +| | Multi-device kick-out strategy | | +| **Online Status** | No mutual kick-out across all platforms | ✅ | +| | Each platform can only log in with one device | ✅ | +| | PC, Mobile, Pad, Web, Mini Program each can log in with one device | ✅ | +| | PC not mutually kicked, only one device total for other platforms | ✅ | +| **Audio/Video Call** | One-to-one audio and video calls | ✅ | +| **File Storage** | Supports private Minio deployment | ✅ | +| | Supports public cloud services COS, OSS, Kodo, S3 | ✅ | +| **Push** | Real-time online message push | ✅ | +| | Offline message push, supports Getui, Firebase | ✅ | + +For more advanced features, audio/video calls, or video conferences, please contact us at [contact@openim.io](mailto:contact@openim.io). + +## Join Our Community :busts_in_silhouette: + +- 🚀 [Join our Slack community](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +- :eyes: [Join our WeChat group](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) + +## FAQ + +##### 1. Does it support multiple languages? + +A: Support, follow the system language by default + +##### 2. Which platforms are supported? + +A: The demo currently supports android and ios. + +##### 3. The debug of the android installation package can run, but the release starts with a white screen? + +A: The release package of flutter is obfuscated by default. You can use the command: flutter build release --no -shrink. If this command is invalid, you can do the following + +Add the following configuration to the release configuration configured in android/app/build.gradle + +``` +release { + minifyEnabled false + useProguard false + shrinkResources false +} +``` + +##### 4. What should I do if the code must be confused? + +A: Add the following rules to the obfuscation rules + +``` +-keep class io.openim.**{*;} +-keep class open_im_sdk.**{*;} +-keep class open_im_sdk_callback.**{*;} +``` + +##### 5. The android installation package cannot be installed on the emulator? + +A: Because the Demo has removed some cpu architectures, if you want to run it on the emulator, please do the following: + +Add in android/build.gradle configuration + +``` +ndk { + abiFilters "armeabi-v7a", "x86" +} +``` + +##### 6, ios run/build release package error + +A: Please set the CPU architecture to arm64, and then operate as follows + +- flutter clean +- flutter pub get +- cd ios +- rm -f Podfile.lock +- rm -rf Pods +- pod install +- Run Archive after connecting to the real device + +![ios cpu](https://user-images.githubusercontent.com/7018230/155913400-6231329a-aee9-4082-8d24-a25baad55261.png) + +##### 7. What is the minimum version number for ios to run? + +A: 13.0 + +##### 8. Why is the map not working? + +A: [Doc](CONFIGKEY.md) + +##### 9. Why is offline push not working? + +A: [Doc](CONFIGKEY.md) diff --git a/mobile-next/README.zh-CN.md b/mobile-next/README.zh-CN.md new file mode 100644 index 0000000..8df7e64 --- /dev/null +++ b/mobile-next/README.zh-CN.md @@ -0,0 +1,253 @@ +

+ + + +

+ +# OpenIM Flutter 💬💻 + +

+ OpenIM Docs + • + OpenIM Server + • + openim-sdk-flutter + • + openim-sdk-core +

+OpenIM 为开发者提供开源即时通讯 SDK,作为 Twilio、Sendbird 等云服务的替代方案。借助 OpenIM,开发者可以构建安全可靠的即时通讯应用,如 WeChat、Zoom、Slack 等。 + +本仓库基于开源版 OpenIM SDK 开发,提供了一款基于 Flutter 的即时通讯应用。您可以使用此应用程序作为 OpenIM SDK 的参考实现。 + +

+ 预览图 + + 预览图 +

+ +## 授权许可 :page_facing_up: + +本仓库采用 GNU Affero 通用公共许可证第 3 版 (AGPL-3.0) 进行许可,并受以下附加条款的约束。**不允许用于商业用途**。详情请参阅 [此处](./LICENSE)。 + +## 开发环境 + +在开始开发之前,请确保您的系统已安装以下软件: + +- **操作系统**:macOS 14.6 或更高版本 +- **Flutter**:版本 3.24.5(根据官网步骤进行[安装](https://docs.flutter.cn/get-started/install))XCode: 15.4, Android Studio: Koala | 2024.1.1 Patch 1 +- **Git**:用于代码版本控制 + +同时,您需要确保已经[部署](https://docs.openim.io/zh-Hans/guides/gettingStarted/dockerCompose)了最新版本的 OpenIM Server。接下来,您可以编译项目并连接自己的服务端进行测试。 + +## 运行环境 + +本应用支持以下操作系统版本: + +| 操作系统 | 版本 | 状态 | +| --------------- | ----------------- | ---- | +| **iOS** | 13.0 及以上 | ✅ | +| **Android** | minSdkVersion 24 | ✅ | + +### 说明 + +- **Flutter**:确保您的版本符合要求,以避免依赖问题。 + +## 快速开始 + +按照以下步骤设置本地开发环境: + +1. 拉取代码 + +```bash + git clone https://github.com/openimsdk/openim-flutter-demo.git + cd openim-flutter-demo +``` + +2. 安装依赖 + +```bash + flutter clean + flutter pub get +``` + +3. 修改配置 + + > 如果没有修改过服务端默认端口,则只需要修改[_host](https://github.com/openimsdk/open-im-flutter-demo/blob/a309f25fdbc143e49d5ca852171ce57970871c85/openim_common/lib/src/config.dart#L59)为您的服务器 ip 即可。 + + ```dart + static const _host = "your-server-ip or your-domain"; + ``` + +4. 通过终端执行 `flutter run` 或者IDE的启动菜单来运行iOS/Android应用程序。 + +5. 开始开发测试! 🎉 + +## 音视频通话 + +开源版支持一对一音视频通话,并且需要先部署并配置[服务端](https://github.com/openimsdk/chat/blob/main/HOW_TO_SETUP_LIVEKIT_SERVER.md)。多人音视频通话、视频会议请联系邮箱 [contact@openim.io](mailto:contact@openim.io) + +## 构建 🚀 + +> 该项目允许分别构建 iOS 应用程序和 Android 应用程序,但在构建过程中会有一些差异。 + + - iOS: + ```bash + flutter build ipa + ``` + - Android: + ```bash + flutter build apk + ``` + + 构建结果将位于 `build` 目录下。 + +## 功能列表 + +### 说明 + +| 功能模块 | 功能项 | 状态 | +| ------------------ | --------------------------------------------------------- | ---- | +| **账号功能** | 手机号注册\邮箱注册\验证码登录 | ✅ | +| | 个人信息查看\修改 | ✅ | +| | 多语言设置 | ✅ | +| | 修改密码\忘记密码 | ✅ | +| **好友功能** | 查找\申请\搜索\添加\删除好友 | ✅ | +| | 同意\拒绝好友申请 | ✅ | +| | 好友备注 | ✅ | +| | 是否允许添加好友 | ✅ | +| | 好友列表\好友资料实时同步 | ✅ | +| **黑名单功能** | 限制消息 | ✅ | +| | 黑名单列表实时同步 | ✅ | +| | 添加\移出黑名单 | ✅ | +| **群组功能** | 创建\解散群组 | ✅ | +| | 申请加群\邀请加群\退出群组\移除群成员 | ✅ | +| | 群名/群头像更改/群资料变更通知和实时同步 | ✅ | +| | 群成员邀请进群 | ✅ | +| | 群主转让 | ✅ | +| | 群主、管理员同意进群申请 | ✅ | +| | 搜索群成员 | ✅ | +| **消息功能** | 离线消息 | ✅ | +| | 漫游消息 | ✅ | +| | 多端消息 | ✅ | +| | 历史消息 | ✅ | +| | 消息删除 | ✅ | +| | 消息清空 | ✅ | +| | 消息复制 | ✅ | +| | 单聊正在输入 | ✅ | +| | 新消息勿扰 | ✅ | +| | 清空聊天记录 | ✅ | +| | 新成员查看群聊历史消息 | ✅ | +| | 新消息提示 | ✅ | +| | 文本消息 | ✅ | +| | 图片消息 | ✅ | +| | 视频消息 | ✅ | +| | 表情消息 | ✅ | +| | 文件消息 | ✅ | +| | 语音消息 | ✅ | +| | 名片消息 | ✅ | +| | 地理位置消息 | ✅ | +| | 自定义消息 | ✅ | +| **会话功能** | 置顶会话 | ✅ | +| | 会话已读 | ✅ | +| | 会话免打扰 | ✅ | +| **REST API** | 认证管理 | ✅ | +| | 用户管理 | ✅ | +| | 关系链管理 | ✅ | +| | 群组管理 | ✅ | +| | 会话管理 | ✅ | +| | 消息管理 | ✅ | +| **Webhook** | 群组回调 | ✅ | +| | 消息回调 | ✅ | +| | 推送回调 | ✅ | +| | 关系链回调 | ✅ | +| | 用户回调 | ✅ | +| **容量和性能** | 1 万好友 | ✅ | +| | 10 万人大群 | ✅ | +| | 秒级同步 | ✅ | +| | 集群部署 | ✅ | +| | 互踢策略 | | +| **在线状态** | 所有平台不互踢 | ✅ | +| | 每个平台各只能登录一个设备 | ✅ | +| | PC 端、移动端、Pad 端、Web 端、小程序端各只能登录一个设备 | ✅ | +| | PC 端不互踢,其他平台总计一个设备 | ✅ | +| **音视频通话** | 一对一音视频通话 | ✅ | +| **文件类对象存储** | 支持私有化部署 minio | ✅ | +| | 支持 COS、OSS、Kodo、S3 公有云 | ✅ | +| **推送** | 消息在线实时推送 | ✅ | +| | 消息离线推送,支持个推,Firebase | ✅ | + +更多高级功能、音视频通话、视频会议 请联系邮箱 [contact@openim.io](mailto:contact@openim.io) + +## 加入社区 :busts_in_silhouette: + +- 🚀 [加入我们的 Slack 社区](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +- :eyes: [加入我们的微信群](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) + +## 常见问题 + +##### 1. 是否支持多语言? +答:支持,默认跟随系统语言。 + +##### 2. 支持哪些平台? +答:目前 Demo 支持 Android 和 iOS。 + +##### 3. Android 安装包的 debug 版本可以运行,但 release 启动是白屏? +答:Flutter 的 release 包默认会进行混淆,可以使用以下命令: + +```bash + flutter build release --no-shrink +``` + +如果此命令无效,可以在 android/app/build.gradle 文件的 release 配置中添加以下代码: + +```bash + release { + minifyEnabled false + useProguard false + shrinkResources false + } +``` + +##### 4. 如果代码必须混淆该怎么办? +答:在混淆规则中添加以下配置: + +```bash + -keep class io.openim.**{*;} + -keep class open_im_sdk.**{*;} + -keep class open_im_sdk_callback.**{*;} +``` + +##### 5. Android 安装包无法安装在模拟器上? +答:由于 Demo 移除了一些 CPU 架构,如果需要在模拟器上运行,请在 android/build.gradle 配置中添加以下内容: + +```bash + ndk { + abiFilters "armeabi-v7a", "x86" + } +``` + +##### 6. iOS 运行/打包 release 包时报错? +答:请将 CPU 架构设置为 arm64,然后按以下步骤操作: + +```bash + 执行 flutter clean + 执行 flutter pub get + cd ios/ + rm -f Podfile.lock + rm -rf Pods + 执行 pod install + 连接真机后运行 Archive。 +``` +![ios cpu](https://user-images.githubusercontent.com/7018230/155913400-6231329a-aee9-4082-8d24-a25baad55261.png) + +##### 7. iOS 最低运行版本是多少? + +答:13.0 + +##### 8. 地图为什么不能使用? + +答: [文档](CONFIGKEY.zh-CN.md) + +##### 9. 离线推送为什么不能使用? + +答: [文档](CONFIGKEY.zh-CN.md) diff --git a/mobile-next/analysis_options.yaml b/mobile-next/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/mobile-next/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/mobile-next/android/.gitignore b/mobile-next/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/mobile-next/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/mobile-next/android/app/build.gradle b/mobile-next/android/app/build.gradle new file mode 100644 index 0000000..ac27bd4 --- /dev/null +++ b/mobile-next/android/app/build.gradle @@ -0,0 +1,188 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +subprojects { afterEvaluate { android { compileSdkVersion 34 } } } + +apply plugin: 'com.google.gms.google-services' +apply plugin: 'com.huawei.agconnect' +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 34 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.openim.flutter.demo" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 24 + targetSdkVersion 33 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + + multiDexEnabled true + + ndk { + abiFilters "arm64-v8a"//, "armeabi-v7a", "armeabi", "x86", "x86_64", "mips", "mips64" + } + + lintOptions { + disable 'InvalidPackage' + disable "Instantiatable" + checkReleaseBuilds false + abortOnError false + } + + manifestPlaceholders = [ + GETUI_APPID : "", + XIAOMI_APP_ID : "", + XIAOMI_APP_KEY : "", + MEIZU_APP_ID : "", + MEIZU_APP_KEY : "", + HUAWEI_APP_ID : "", + OPPO_APP_KEY : "", + OPPO_APP_SECRET: "", + VIVO_APP_ID : "", + VIVO_APP_KEY : "", + HONOR_APP_ID : "", + ] + } + + signingConfigs { + release { + keyAlias 'openim' + keyPassword 'openim' + storeFile file('./openim') + storePassword 'openim' + } + } + + buildTypes { + debug { + signingConfig signingConfigs.release + } + profile { + signingConfig signingConfigs.release + + } + release { +// minifyEnabled false +// useProguard false +// shrinkResources false + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.release + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + android.applicationVariants.all { + variant -> + variant.outputs.all { + outputFileName = "F-DEMO_${versionName}(${versionCode})_${buildType.name}_${buildTime()}.apk" + } + } + + packagingOptions { + // other options here + + /* Fix for ffmpeg-kit-react-native START */ + pickFirst 'lib/arm64-v8a/libgojni.so' + pickFirst 'lib/armeabi-v7a/libgojni.so' + pickFirst 'lib/x86/libgojni.so' + pickFirst 'lib/x86_64/libgojni.so' + pickFirst 'lib/armeabi-v7a/libc++_shared.so' + pickFirst 'lib/x86/libc++_shared.so' + pickFirst 'lib/x86_64/libc++_shared.so' + pickFirst 'lib/arm64-v8a/libc++_shared.so' + pickFirst 'lib/arm64-v8a/libavcodec.so' + pickFirst 'lib/arm64-v8a/libavformat.so' + pickFirst 'lib/arm64-v8a/libavutil.so' + pickFirst 'lib/arm64-v8a/libswscale.so' + pickFirst 'lib/arm64-v8a/libavdevice.so' + pickFirst 'lib/arm64-v8a/libavfilter.so' + pickFirst 'lib/arm64-v8a/libffmpegkit.so' + pickFirst 'lib/arm64-v8a/libffmpegkit_abidetect.so' + pickFirst 'lib/arm64-v8a/libswresample.so' + pickFirst 'lib/arm64-v8a/libswscale.so' + pickFirst 'lib/x86/libavcodec.so' + pickFirst 'lib/x86/libavformat.so' + pickFirst 'lib/x86/libavutil.so' + pickFirst 'lib/x86/libswscale.so' + pickFirst 'lib/x86/libavdevice.so' + pickFirst 'lib/x86/libavfilter.so' + pickFirst 'lib/x86/libffmpegkit.so' + pickFirst 'lib/x86/libffmpegkit_abidetect.so' + pickFirst 'lib/x86/libswresample.so' + pickFirst 'lib/x86/libswscale.so' + pickFirst 'lib/x86_64/libavcodec.so' + pickFirst 'lib/x86_64/libavformat.so' + pickFirst 'lib/x86_64/libavutil.so' + pickFirst 'lib/x86_64/libswscale.so' + pickFirst 'lib/x86_64/libavdevice.so' + pickFirst 'lib/x86_64/libavfilter.so' + pickFirst 'lib/x86_64/libffmpegkit.so' + pickFirst 'lib/x86_64/libffmpegkit_abidetect.so' + pickFirst 'lib/x86_64/libswresample.so' + pickFirst 'lib/x86_64/libswscale.so' + /* Fix for ffmpeg-kit-react-native END */ + } +} + +flutter { + source '../..' +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + implementation 'com.getui:gtsdk:3.3.9.0' + implementation 'com.getui:gtc:3.2.18.0' + implementation 'com.huawei.hms:push:6.12.0.300' + implementation 'com.huawei.agconnect:agconnect-core:1.5.2.300' + implementation 'com.getui.opt:hwp:3.1.0' + implementation 'com.getui.opt:xmp:3.2.0' + implementation('com.assist-v3:oppo:3.1.0') { + exclude group: 'commons-codec' + } + implementation 'com.assist-v3:vivo:3.1.0' + implementation 'com.getui.opt:mzp:3.2.0' + implementation 'com.getui.opt:ups:3.0.3' + implementation 'com.getui.opt:honor:3.2.0' + + implementation 'androidx.multidex:multidex:2.0.1' + + implementation platform("com.google.firebase:firebase-bom:33.7.0") + implementation "com.google.firebase:firebase-messaging" // FCM +} + +static def buildTime() { + return new Date().format("MMddHHmm", TimeZone.getTimeZone("GMT+08:00")) +} \ No newline at end of file diff --git a/mobile-next/android/app/google-services.json b/mobile-next/android/app/google-services.json new file mode 100644 index 0000000..b3f33ca --- /dev/null +++ b/mobile-next/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "299075855003", + "project_id": "im-fer-c3347", + "storage_bucket": "im-fer-c3347.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:299075855003:android:292449dc34ad498109dc3e", + "android_client_info": { + "package_name": "io.openim.flutter.demo" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCw-ohvxFisvm3Yrb4kCzPBuL0YfZkzI9Q" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/mobile-next/android/app/openim b/mobile-next/android/app/openim new file mode 100644 index 0000000..9aae8fa Binary files /dev/null and b/mobile-next/android/app/openim differ diff --git a/mobile-next/android/app/proguard-rules.pro b/mobile-next/android/app/proguard-rules.pro new file mode 100644 index 0000000..0aece21 --- /dev/null +++ b/mobile-next/android/app/proguard-rules.pro @@ -0,0 +1,206 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\COMPETITION\Android Studio\Android Studio SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +#指定代码的压缩级别 +-optimizationpasses 5 + +#包明不混合大小写 +-dontusemixedcaseclassnames + +#不去忽略非公共的库类 +-dontskipnonpubliclibraryclasses + + #优化 不优化输入的类文件 +-dontoptimize + + #预校验 +-dontpreverify + + #混淆时是否记录日志 +-verbose + + # 混淆时所采用的算法 +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +#保护注解 +-keepattributes *Annotation* + +# 保持哪些类不被混淆 +-keep public class * extends android.support.v4.app.Fragment +-keep public class * extends android.app.Fragment +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class com.android.vending.licensing.ILicensingService +-keep public class * extends android.support.multidex.MultiDexApplication +-keep public class * extends android.view.View +-keep class android.support.** {*;} + +#忽略警告 +-ignorewarnings + +##记录生成的日志数据,gradle build时在本项目根目录输出## +#apk 包内所有 class 的内部结构 +-dump proguard/class_files.txt +#未混淆的类和成员 +-printseeds proguard/seeds.txt +#列出从 apk 中删除的代码 +-printusage proguard/unused.txt +#混淆前后的映射 +-printmapping proguard/mapping.txt +########记录生成的日志数据,gradle build时 在本项目根目录输出-end###### + +#如果引用了v4或者v7包 +-dontwarn android.support.** + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +-keepclassmembers class fqcn.of.javascript.interface.for.webview { + public *; +} + +####混淆保护自己项目的部分代码以及引用的第三方jar包library-end#### + +#保持 native 方法不被混淆 +-keepclasseswithmembernames class * { + native ; +} + +#保持自定义控件类不被混淆 +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet); +} + +#保持自定义控件类不被混淆 +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +-keep public class * extends android.view.View { + public (android.content.Context); + public (android.content.Context, android.util.AttributeSet); + public (android.content.Context, android.util.AttributeSet, int); + public void set*(...); +} + +-keepclassmembers class * extends android.app.Activity { # 保持自定义控件类不被混淆 + public void *(android.view.View); +} + +#保持 Parcelable 不被混淆 +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +#保持 Serializable 不被混淆 +-keepnames class * implements java.io.Serializable + +#保持 Serializable 不被混淆并且enum 类也不被混淆 +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + !private ; + !private ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +#保持枚举 enum 类不被混淆 +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keepclassmembers class * { + public void *ButtonClicked(android.view.View); +} + +#不混淆资源类 +-keepclassmembers class **.R$* { + public static ; +} + +#泛型与反射 +-keepattributes Signature +-keepattributes EnclosingMethod +-keepattributes *Annotation* +-dontoptimize +-dontpreverify +############################################################################################# +######################## 以上通用 ################################## +############################################################################################# + +#------------------ 下方是共性的排除项目 ---------------- +# 方法名中含有“JNI”字符的,认定是Java Native Interface方法,自动排除 +# 方法名中含有“JRI”字符的,认定是Java Reflection Interface方法,自动排除 + +-keepclasseswithmembers class * { + ... *JNI*(...); +} + +-keepclasseswithmembernames class * { + ... *JRI*(...); +} + +-keep class **JNI* {*;} + +# Keep native methods +-keepclassmembers class * { + native ; +} + +# UCrop +-dontwarn com.yalantis.ucrop** +-keep class com.yalantis.ucrop** { *; } +-keep interface com.yalantis.ucrop** { *; } + +# zxing +-keep class com.google.zxing.** {*;} +-dontwarn com.google.zxing.** + + +-keep public class **.R$*{ + public static final int *; +} +# jpush +-dontoptimize + +-dontwarn cn.jpush.** + +-keep class cn.jpush.** { *; } + +-keep class * extends cn.jpush.android.service.JPushMessageReceiver { *; } + +-dontwarn cn.jiguang.** + +-keep class cn.jiguang.** { *; } + +#腾讯 Bugly +-dontwarn com.tencent.bugly.** + +-keep public class com.tencent.bugly.**{*;} + +# OpenIM +-keep class io.openim.**{*;} +-keep class open_im_sdk.**{*;} +-keep class open_im_sdk_callback.**{*;} + +#getui +-dontwarn com.igexin.** +-keep class com.igexin.** { *; } \ No newline at end of file diff --git a/mobile-next/android/app/src/debug/AndroidManifest.xml b/mobile-next/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..33619df --- /dev/null +++ b/mobile-next/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/mobile-next/android/app/src/main/AndroidManifest.xml b/mobile-next/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7420534 --- /dev/null +++ b/mobile-next/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/android/app/src/main/java/cn/rentsoft/flutter/openim/business/MainActivity.java b/mobile-next/android/app/src/main/java/cn/rentsoft/flutter/openim/business/MainActivity.java new file mode 100644 index 0000000..f297d25 --- /dev/null +++ b/mobile-next/android/app/src/main/java/cn/rentsoft/flutter/openim/business/MainActivity.java @@ -0,0 +1,6 @@ +package cn.rentsoft.flutter.openim.business; + +import io.flutter.embedding.android.FlutterFragmentActivity; + +public class MainActivity extends FlutterFragmentActivity { +} diff --git a/mobile-next/android/app/src/main/res/drawable-v21/launch_background.xml b/mobile-next/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/mobile-next/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mobile-next/android/app/src/main/res/drawable/launch_background.xml b/mobile-next/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/mobile-next/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mobile-next/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile-next/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..8e37e6e Binary files /dev/null and b/mobile-next/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile-next/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile-next/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..b229a57 Binary files /dev/null and b/mobile-next/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile-next/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile-next/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..bb17338 Binary files /dev/null and b/mobile-next/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile-next/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile-next/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..4e00ff5 Binary files /dev/null and b/mobile-next/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile-next/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile-next/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..7f219ac Binary files /dev/null and b/mobile-next/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile-next/android/app/src/main/res/values-night/styles.xml b/mobile-next/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/mobile-next/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mobile-next/android/app/src/main/res/values/styles.xml b/mobile-next/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/mobile-next/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mobile-next/android/app/src/main/res/xml/file_paths.xml b/mobile-next/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..dcdb0b7 --- /dev/null +++ b/mobile-next/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile-next/android/app/src/profile/AndroidManifest.xml b/mobile-next/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..33619df --- /dev/null +++ b/mobile-next/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/mobile-next/android/build.gradle b/mobile-next/android/build.gradle new file mode 100644 index 0000000..6cc8dcd --- /dev/null +++ b/mobile-next/android/build.gradle @@ -0,0 +1,64 @@ +buildscript { + ext.kotlin_version = '1.9.23' + repositories { + maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin' } + maven { url "https://mvn.gt.getui.com/nexus/content/repositories/releases/" } + maven { url 'https://developer.huawei.com/repo/' } // hms + maven { url "https://dl.google.com/dl/android/maven2/" } // fcm + maven { + url 'https://maven.columbus.heytapmobi.com/repository/releases/' + credentials { + username 'nexus' + password 'c0b08da17e3ec36c3870fed674a0bcb36abc2e23' + } + } + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools:r8:8.3.37") + classpath 'com.android.tools.build:gradle:7.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.google.gms:google-services:4.4.2' // fcm + classpath 'com.huawei.agconnect:agcp:1.6.0.300' // hms + } +} + +allprojects { + repositories { + maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin' } + maven { url "https://mvn.gt.getui.com/nexus/content/repositories/releases/" } + maven { url 'https://developer.huawei.com/repo/' } // hms + maven { url "https://dl.google.com/dl/android/maven2/" } // fcm + maven { + url 'https://maven.columbus.heytapmobi.com/repository/releases/' + credentials { + username 'nexus' + password 'c0b08da17e3ec36c3870fed674a0bcb36abc2e23' + } + } + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { afterEvaluate { android { compileSdkVersion 34 } } } + +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/mobile-next/android/gradle.properties b/mobile-next/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/mobile-next/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/mobile-next/android/gradle/wrapper/gradle-wrapper.properties b/mobile-next/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e1ca574 --- /dev/null +++ b/mobile-next/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/mobile-next/android/settings.gradle b/mobile-next/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/mobile-next/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/mobile-next/data.json b/mobile-next/data.json new file mode 100644 index 0000000..4959b7b --- /dev/null +++ b/mobile-next/data.json @@ -0,0 +1,52 @@ +{ + errCode: 0, + errMsg: "", + data: { + userFullInfoList: [ + { + userID: "2380124277", + password: "", + account: "11002", + phoneNumber: "", + areaCode: "", + nickname: "林玟书", + englishName: "", + faceURL: "", + gender: 1, + mobileAreaCode: "+86", + mobile: "", + telephone: "", + level: 1, + birth: 288316800000, + email: "", + order: 0, + status: 0, + allowAddFriend: 1, + allowBeep: 1, + allowVibration: 1, + forbidden: 0, + station: "", + departmentList: [ + { + departmentID: "936609157", + departmentFaceURL: "", + departmentName: "前端", + departmentParentID: "1852915335", + departmentOrder: 0, + departmentDepartmentType: 0, + departmentRelatedGroupID: "", + departmentCreateTime: 1678095053014, + memberOrder: 0, + memberPosition: "IOS", + memberLeader: 0, + memberStatus: 0, + memberEntryTime: 0, + memberTerminationTime: 0, + memberCreateTime: 1678095467228 + } + ] + } + ], + totalNumber: 1 + } +} diff --git a/mobile-next/devtools_options.yaml b/mobile-next/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/mobile-next/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/mobile-next/docs/images/logo.jpg b/mobile-next/docs/images/logo.jpg new file mode 100644 index 0000000..0849fde Binary files /dev/null and b/mobile-next/docs/images/logo.jpg differ diff --git a/mobile-next/docs/images/preveiw1.zh-CN.jpeg b/mobile-next/docs/images/preveiw1.zh-CN.jpeg new file mode 100644 index 0000000..66dda92 Binary files /dev/null and b/mobile-next/docs/images/preveiw1.zh-CN.jpeg differ diff --git a/mobile-next/docs/images/preveiw2.zh-CN.jpeg b/mobile-next/docs/images/preveiw2.zh-CN.jpeg new file mode 100644 index 0000000..27115bc Binary files /dev/null and b/mobile-next/docs/images/preveiw2.zh-CN.jpeg differ diff --git a/mobile-next/docs/images/preview1.jpeg b/mobile-next/docs/images/preview1.jpeg new file mode 100644 index 0000000..94ae099 Binary files /dev/null and b/mobile-next/docs/images/preview1.jpeg differ diff --git a/mobile-next/docs/images/preview2.jpeg b/mobile-next/docs/images/preview2.jpeg new file mode 100644 index 0000000..cad4c38 Binary files /dev/null and b/mobile-next/docs/images/preview2.jpeg differ diff --git a/mobile-next/firebase.json b/mobile-next/firebase.json new file mode 100644 index 0000000..8519ce8 --- /dev/null +++ b/mobile-next/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"im-fer-c3347","appId":"1:299075855003:android:292449dc34ad498109dc3e","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"im-fer-c3347","appId":"1:299075855003:ios:23b06300e33d984309dc3e","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"im-fer-c3347","configurations":{"android":"1:299075855003:android:292449dc34ad498109dc3e","ios":"1:299075855003:ios:23b06300e33d984309dc3e"}}}}}} \ No newline at end of file diff --git a/mobile-next/flutter_launcher_icons.yaml b/mobile-next/flutter_launcher_icons.yaml new file mode 100644 index 0000000..803a20d --- /dev/null +++ b/mobile-next/flutter_launcher_icons.yaml @@ -0,0 +1,13 @@ +flutter_icons: + android: true + ios: true + remove_alpha_ios: true + image_path: "launcher_icon/app-icon.png" + + + +# flutter pub get +# flutter pub run flutter_launcher_icons:main -f + +# flutter pub get +# flutter pub run flutter_launcher_icons:main \ No newline at end of file diff --git a/mobile-next/flutter_native_splash.yaml b/mobile-next/flutter_native_splash.yaml new file mode 100644 index 0000000..ef79721 --- /dev/null +++ b/mobile-next/flutter_native_splash.yaml @@ -0,0 +1,138 @@ +flutter_native_splash: + # This package generates native code to customize Flutter's default white native splash screen + # with background color and splash image. + # Customize the parameters below, and run the following command in the terminal: + # flutter pub run flutter_native_splash:create + # To restore Flutter's default white splash screen, run the following command in the terminal: + # flutter pub run flutter_native_splash:remove + + # color or background_image is the only required parameter. Use color to set the background + # of your splash screen to a solid color. Use background_image to set the background of your + # splash screen to a png image. This is useful for gradients. The image will be stretch to the + # size of the app. Only one parameter can be used, color and background_image cannot both be set. + color: "#42a5f5" + #background_image: "assets/background.png" + + # Optional parameters are listed below. To enable a parameter, uncomment the line by removing + # the leading # character. + + # The image parameter allows you to specify an image used in the splash screen. It must be a + # png file and should be sized for 4x pixel density. + #image: assets/splash.png + + # The branding property allows you to specify an image used as branding in the splash screen. + # It must be a png file. It is supported for Android, iOS and the Web. For Android 12, + # see the Android 12 section below. + #branding: assets/dart.png + + # To position the branding image at the bottom of the screen you can use bottom, bottomRight, + # and bottomLeft. The default values is bottom if not specified or specified something else. + #branding_mode: bottom + + # The color_dark, background_image_dark, image_dark, branding_dark are parameters that set the background + # and image when the device is in dark mode. If they are not specified, the app will use the + # parameters from above. If the image_dark parameter is specified, color_dark or + # background_image_dark must be specified. color_dark and background_image_dark cannot both be + # set. + #color_dark: "#042a49" + #background_image_dark: "assets/dark-background.png" + #image_dark: assets/splash-invert.png + #branding_dark: assets/dart_dark.png + + # Android 12 handles the splash screen differently than previous versions. Please visit + # https://developer.android.com/guide/topics/ui/splash-screen + # Following are Android 12 specific parameter. + android_12: + # The image parameter sets the splash screen icon image. If this parameter is not specified, + # the app's launcher icon will be used instead. + # Please note that the splash screen will be clipped to a circle on the center of the screen. + # App icon with an icon background: This should be 960×960 pixels, and fit within a circle + # 640 pixels in diameter. + # App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle + # 768 pixels in diameter. + #image: assets/android12splash.png + + # Splash screen background color. + #color: "#42a5f5" + + # App icon background color. + #icon_background_color: "#111111" + + # The branding property allows you to specify an image used as branding in the splash screen. + #branding: assets/dart.png + + # The image_dark, color_dark, icon_background_color_dark, and branding_dark set values that + # apply when the device is in dark mode. If they are not specified, the app will use the + # parameters from above. + #image_dark: assets/android12splash-invert.png + #color_dark: "#042a49" + #icon_background_color_dark: "#eeeeee" + + # The android, ios and web parameters can be used to disable generating a splash screen on a given + # platform. + #android: false + #ios: false + #web: false + + # Platform specific images can be specified with the following parameters, which will override + # the respective parameter. You may specify all, selected, or none of these parameters: + #color_android: "#42a5f5" + #color_dark_android: "#042a49" + #color_ios: "#42a5f5" + #color_dark_ios: "#042a49" + #color_web: "#42a5f5" + #color_dark_web: "#042a49" + #image_android: assets/splash-android.png + #image_dark_android: assets/splash-invert-android.png + #image_ios: assets/splash-ios.png + #image_dark_ios: assets/splash-invert-ios.png + #image_web: assets/splash-web.png + #image_dark_web: assets/splash-invert-web.png + #background_image_android: "assets/background-android.png" + #background_image_dark_android: "assets/dark-background-android.png" + #background_image_ios: "assets/background-ios.png" + #background_image_dark_ios: "assets/dark-background-ios.png" + #background_image_web: "assets/background-web.png" + #background_image_dark_web: "assets/dark-background-web.png" + #branding_android: assets/brand-android.png + #branding_dark_android: assets/dart_dark-android.png + #branding_ios: assets/brand-ios.png + #branding_dark_ios: assets/dart_dark-ios.png + + # The position of the splash image can be set with android_gravity, ios_content_mode, and + # web_image_mode parameters. All default to center. + # + # android_gravity can be one of the following Android Gravity (see + # https://developer.android.com/reference/android/view/Gravity): bottom, center, + # center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal, + # fill_vertical, left, right, start, or top. + #android_gravity: center + # + # ios_content_mode can be one of the following iOS UIView.ContentMode (see + # https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill, + # scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight, + # bottomLeft, or bottomRight. + #ios_content_mode: center + # + # web_image_mode can be one of the following modes: center, contain, stretch, and cover. + #web_image_mode: center + + # The screen orientation can be set in Android with the android_screen_orientation parameter. + # Valid parameters can be found here: + # https://developer.android.com/guide/topics/manifest/activity-element#screen + #android_screen_orientation: sensorLandscape + + # To hide the notification bar, use the fullscreen parameter. Has no effect in web since web + # has no notification bar. Defaults to false. + # NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads. + # To show the notification bar, add the following code to your Flutter app: + # WidgetsFlutterBinding.ensureInitialized(); + # SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]); + #fullscreen: true + + # If you have changed the name(s) of your info.plist file(s), you can specify the filename(s) + # with the info_plist_files parameter. Remove only the # characters in the three lines below, + # do not remove any spaces: + #info_plist_files: + # - 'ios/Runner/Info-Debug.plist' + # - 'ios/Runner/Info-Release.plist' \ No newline at end of file diff --git a/mobile-next/ios/.DS_Store b/mobile-next/ios/.DS_Store new file mode 100644 index 0000000..810144e Binary files /dev/null and b/mobile-next/ios/.DS_Store differ diff --git a/mobile-next/ios/.gitignore b/mobile-next/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/mobile-next/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/mobile-next/ios/Flutter/AppFrameworkInfo.plist b/mobile-next/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/mobile-next/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/mobile-next/ios/Flutter/Debug.xcconfig b/mobile-next/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/mobile-next/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/mobile-next/ios/Flutter/Release.xcconfig b/mobile-next/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/mobile-next/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/mobile-next/ios/NotificationService/Info.plist b/mobile-next/ios/NotificationService/Info.plist new file mode 100644 index 0000000..57421eb --- /dev/null +++ b/mobile-next/ios/NotificationService/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/mobile-next/ios/NotificationService/NotificationService.entitlements b/mobile-next/ios/NotificationService/NotificationService.entitlements new file mode 100644 index 0000000..4a2ab25 --- /dev/null +++ b/mobile-next/ios/NotificationService/NotificationService.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.cn.rentsoft.openim.flutter.rtc + + + diff --git a/mobile-next/ios/NotificationService/NotificationService.swift b/mobile-next/ios/NotificationService/NotificationService.swift new file mode 100644 index 0000000..861d48e --- /dev/null +++ b/mobile-next/ios/NotificationService/NotificationService.swift @@ -0,0 +1,33 @@ +import UserNotifications + +class NotificationService: UNNotificationServiceExtension { + var contentHandler: ((UNNotificationContent) -> Void)? + var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + + if let bestAttemptContent = bestAttemptContent { + // Modify the notification content here... + if let userInfo = request.content.userInfo as? [String: Any], + let clientID = userInfo["clientMsgID"] as? String, + !clientID.isEmpty == true + { + let key = "key" + UserDefaults.standard.set(request.identifier, forKey: key) + bestAttemptContent.title = "\(bestAttemptContent.title)" + bestAttemptContent.sound = UNNotificationSound(named: .init(rawValue: "call.caf")) + } + contentHandler(bestAttemptContent) + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } +} diff --git a/mobile-next/ios/Podfile b/mobile-next/ios/Podfile new file mode 100644 index 0000000..0b3f82c --- /dev/null +++ b/mobile-next/ios/Podfile @@ -0,0 +1,99 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + # Start of the permission_handler configuration + target.build_configurations.each do |config| + + # You can enable the permissions needed here. For example to enable camera + # permission, just remove the `#` character in front so it looks like this: + # + # ## dart: PermissionGroup.camera + # 'PERMISSION_CAMERA=1' + # + # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.calendar + # 'PERMISSION_EVENTS=1', + + ## dart: PermissionGroup.reminders + # 'PERMISSION_REMINDERS=1', + + ## dart: PermissionGroup.contacts + # 'PERMISSION_CONTACTS=1', + + ## dart: PermissionGroup.camera + 'PERMISSION_CAMERA=1', + + ## dart: PermissionGroup.microphone + 'PERMISSION_MICROPHONE=1', + + ## dart: PermissionGroup.speech + # 'PERMISSION_SPEECH_RECOGNIZER=1', + + ## dart: PermissionGroup.photos + 'PERMISSION_PHOTOS=1', + + ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] + 'PERMISSION_LOCATION=1', + + ## dart: PermissionGroup.notification + 'PERMISSION_NOTIFICATIONS=1', + + ## dart: PermissionGroup.mediaLibrary + # 'PERMISSION_MEDIA_LIBRARY=1', + + ## dart: PermissionGroup.sensors + # 'PERMISSION_SENSORS=1', + + ## dart: PermissionGroup.bluetooth + # 'PERMISSION_BLUETOOTH=1', + + ## dart: PermissionGroup.appTrackingTransparency + # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', + + ## dart: PermissionGroup.criticalAlerts + # 'PERMISSION_CRITICAL_ALERTS=1' + ] + + end + end +end diff --git a/mobile-next/ios/Podfile.lock b/mobile-next/ios/Podfile.lock new file mode 100644 index 0000000..b21d0be --- /dev/null +++ b/mobile-next/ios/Podfile.lock @@ -0,0 +1,505 @@ +PODS: + - audio_session (0.0.1): + - Flutter + - camera_avfoundation (0.0.1): + - Flutter + - connectivity_plus (0.0.1): + - Flutter + - FlutterMacOS + - device_info_plus (0.0.1): + - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - emoji_picker_flutter (0.0.1): + - Flutter + - ffmpeg-kit-ios-full-gpl (6.0) + - ffmpeg_kit_flutter_full_gpl (6.0.3): + - ffmpeg_kit_flutter_full_gpl/full-gpl (= 6.0.3) + - Flutter + - ffmpeg_kit_flutter_full_gpl/full-gpl (6.0.3): + - ffmpeg-kit-ios-full-gpl (= 6.0) + - Flutter + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Firebase/CoreOnly (11.6.0): + - FirebaseCore (~> 11.6.0) + - Firebase/Messaging (11.6.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 11.6.0) + - firebase_core (3.10.0): + - Firebase/CoreOnly (= 11.6.0) + - Flutter + - firebase_messaging (15.2.0): + - Firebase/Messaging (= 11.6.0) + - firebase_core + - Flutter + - FirebaseCore (11.6.0): + - FirebaseCoreInternal (~> 11.6.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreInternal (11.6.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.6.0): + - FirebaseCore (~> 11.6.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (11.6.0): + - FirebaseCore (~> 11.6.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Reachability (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) + - Flutter (1.0.0) + - flutter_image_compress_common (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder + - flutter_keyboard_visibility (0.0.1): + - Flutter + - flutter_local_notifications (0.0.1): + - Flutter + - flutter_native_splash (2.4.3): + - Flutter + - flutter_new_badger (0.0.1): + - Flutter + - flutter_openim_sdk (0.0.1): + - Flutter + - OpenIMSDKCore (= 3.8.3) + - flutter_webrtc (0.11.3): + - Flutter + - WebRTC-SDK (= 125.6422.04) + - geolocator_apple (1.2.0): + - Flutter + - getuiflut (0.0.1): + - Flutter + - GTSDK + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTCommonSDK (3.1.7.0): + - ZXSDK + - GTSDK (3.0.9.0): + - GTCommonSDK (> 3.0.9.0) + - image_cropper (0.0.4): + - Flutter + - TOCropViewController (~> 2.6.1) + - image_gallery_saver_plus (0.0.1): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - just_audio (0.0.1): + - Flutter + - libwebp (1.3.2): + - libwebp/demux (= 1.3.2) + - libwebp/mux (= 1.3.2) + - libwebp/sharpyuv (= 1.3.2) + - libwebp/webp (= 1.3.2) + - libwebp/demux (1.3.2): + - libwebp/webp + - libwebp/mux (1.3.2): + - libwebp/demux + - libwebp/sharpyuv (1.3.2) + - libwebp/webp (1.3.2): + - libwebp/sharpyuv + - livekit_client (2.2.5): + - Flutter + - WebRTC-SDK (= 125.6422.04) + - local_auth_darwin (0.0.1): + - Flutter + - FlutterMacOS + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) + - map_launcher (0.0.1): + - Flutter + - media_kit_libs_ios_video (1.0.4): + - Flutter + - media_kit_native_event_loop (1.0.0): + - Flutter + - media_kit_video (0.0.1): + - Flutter + - MTBBarcodeScanner (5.0.11) + - Mute (0.6.1) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - open_filex (0.0.2): + - Flutter + - OpenIMSDKCore (3.8.3) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter + - photo_manager (2.0.0): + - Flutter + - FlutterMacOS + - PromisesObjC (2.4.0) + - qr_code_scanner_plus (0.2.6): + - Flutter + - MTBBarcodeScanner + - record_darwin (1.0.0): + - Flutter + - scan (0.0.1): + - Flutter + - screen_brightness_ios (0.1.0): + - Flutter + - SDWebImage (5.20.0): + - SDWebImage/Core (= 5.20.0) + - SDWebImage/Core (5.20.0) + - SDWebImageWebPCoder (0.14.6): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) + - sensors_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sound_mode (0.0.1): + - Flutter + - Mute (~> 0.6.0) + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - SwiftyGif (5.4.5) + - TOCropViewController (2.6.1) + - url_launcher_ios (0.0.1): + - Flutter + - vibration (1.7.5): + - Flutter + - video_compress (0.3.0): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - volume_controller (0.0.1): + - Flutter + - wakelock_plus (0.0.1): + - Flutter + - WebRTC-SDK (125.6422.04) + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS + - ZXSDK (3.3.2) + +DEPENDENCIES: + - audio_session (from `.symlinks/plugins/audio_session/ios`) + - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) + - ffmpeg_kit_flutter_full_gpl (from `.symlinks/plugins/ffmpeg_kit_flutter_full_gpl/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) + - Flutter (from `Flutter`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - flutter_new_badger (from `.symlinks/plugins/flutter_new_badger/ios`) + - flutter_openim_sdk (from `.symlinks/plugins/flutter_openim_sdk/ios`) + - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) + - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) + - getuiflut (from `.symlinks/plugins/getuiflut/ios`) + - image_cropper (from `.symlinks/plugins/image_cropper/ios`) + - image_gallery_saver_plus (from `.symlinks/plugins/image_gallery_saver_plus/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - just_audio (from `.symlinks/plugins/just_audio/ios`) + - livekit_client (from `.symlinks/plugins/livekit_client/ios`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) + - map_launcher (from `.symlinks/plugins/map_launcher/ios`) + - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) + - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) + - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) + - open_filex (from `.symlinks/plugins/open_filex/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - photo_manager (from `.symlinks/plugins/photo_manager/ios`) + - qr_code_scanner_plus (from `.symlinks/plugins/qr_code_scanner_plus/ios`) + - record_darwin (from `.symlinks/plugins/record_darwin/ios`) + - scan (from `.symlinks/plugins/scan/ios`) + - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) + - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sound_mode (from `.symlinks/plugins/sound_mode/ios`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - vibration (from `.symlinks/plugins/vibration/ios`) + - video_compress (from `.symlinks/plugins/video_compress/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - volume_controller (from `.symlinks/plugins/volume_controller/ios`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - ffmpeg-kit-ios-full-gpl + - Firebase + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging + - GoogleDataTransport + - GoogleUtilities + - GTCommonSDK + - GTSDK + - libwebp + - Mantle + - MTBBarcodeScanner + - Mute + - nanopb + - OpenIMSDKCore + - PromisesObjC + - SDWebImage + - SDWebImageWebPCoder + - SwiftyGif + - TOCropViewController + - WebRTC-SDK + - ZXSDK + +EXTERNAL SOURCES: + audio_session: + :path: ".symlinks/plugins/audio_session/ios" + camera_avfoundation: + :path: ".symlinks/plugins/camera_avfoundation/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/darwin" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + emoji_picker_flutter: + :path: ".symlinks/plugins/emoji_picker_flutter/ios" + ffmpeg_kit_flutter_full_gpl: + :path: ".symlinks/plugins/ffmpeg_kit_flutter_full_gpl/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" + Flutter: + :path: Flutter + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_native_splash: + :path: ".symlinks/plugins/flutter_native_splash/ios" + flutter_new_badger: + :path: ".symlinks/plugins/flutter_new_badger/ios" + flutter_openim_sdk: + :path: ".symlinks/plugins/flutter_openim_sdk/ios" + flutter_webrtc: + :path: ".symlinks/plugins/flutter_webrtc/ios" + geolocator_apple: + :path: ".symlinks/plugins/geolocator_apple/ios" + getuiflut: + :path: ".symlinks/plugins/getuiflut/ios" + image_cropper: + :path: ".symlinks/plugins/image_cropper/ios" + image_gallery_saver_plus: + :path: ".symlinks/plugins/image_gallery_saver_plus/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + just_audio: + :path: ".symlinks/plugins/just_audio/ios" + livekit_client: + :path: ".symlinks/plugins/livekit_client/ios" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" + map_launcher: + :path: ".symlinks/plugins/map_launcher/ios" + media_kit_libs_ios_video: + :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" + media_kit_native_event_loop: + :path: ".symlinks/plugins/media_kit_native_event_loop/ios" + media_kit_video: + :path: ".symlinks/plugins/media_kit_video/ios" + open_filex: + :path: ".symlinks/plugins/open_filex/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + photo_manager: + :path: ".symlinks/plugins/photo_manager/ios" + qr_code_scanner_plus: + :path: ".symlinks/plugins/qr_code_scanner_plus/ios" + record_darwin: + :path: ".symlinks/plugins/record_darwin/ios" + scan: + :path: ".symlinks/plugins/scan/ios" + screen_brightness_ios: + :path: ".symlinks/plugins/screen_brightness_ios/ios" + sensors_plus: + :path: ".symlinks/plugins/sensors_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sound_mode: + :path: ".symlinks/plugins/sound_mode/ios" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + vibration: + :path: ".symlinks/plugins/vibration/ios" + video_compress: + :path: ".symlinks/plugins/video_compress/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + volume_controller: + :path: ".symlinks/plugins/volume_controller/ios" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" + +SPEC CHECKSUMS: + audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207 + camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 + connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + emoji_picker_flutter: fe2e6151c5b548e975d546e6eeb567daf0962a58 + ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b + ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + Firebase: 374a441a91ead896215703a674d58cdb3e9d772b + firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10 + firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c + FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa + FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 + FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c + FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9 + flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a + flutter_new_badger: e632df3f84b5bbd7f3b37b8122f105edf1b483b3 + flutter_openim_sdk: 170b54f6d5c7c287bd9079436d3280197ea27556 + flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f + geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1 + getuiflut: 8628ba2b64a738a847a07d2b67e0ac305778e2ee + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GTCommonSDK: d3299839818ffc2629bf587e096256a7cd0f0dcc + GTSDK: 4392fb9f02e370c8dd3e77fa51ffc511b5ac1ac1 + image_cropper: a3291c624a953049bc6a02e1f8c8ceb162a24b25 + image_gallery_saver_plus: 782ade975fe6a4600b53e7c1983e3a2979d1e9e5 + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa + libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 + livekit_client: 9c8080879256a0fb16da13c9be4845248209d896 + local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d + map_launcher: e325db1261d029ff33e08e03baccffe09593ffea + media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 + media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a + media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e + MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb + Mute: 20135a96076f140cc82bfc8b810e2d6150d8ec7e + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 + OpenIMSDKCore: 6fba8ae1b25e33e2d5cfb16f4eab934f3d7c5ee8 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + qr_code_scanner_plus: 3bfe4deb7f28996a63a2a580819d49dae80d5ed3 + record_darwin: 3b1a8e7d5c0cbf45ad6165b4d83a6ca643d929c3 + scan: aea35bb4aa59ccc8839c576a18cd57c7d492cc86 + screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 + SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 + SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 + sensors_plus: 7229095999f30740798f0eeef5cd120357a8f4f2 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sound_mode: ef0072f20e7306c1c371984a443156e39be092d5 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241 + video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 + volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 + wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 + WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117 + +PODFILE CHECKSUM: 3003a7d95e8a59d1b52038824bad0a4b90bc52ca + +COCOAPODS: 1.15.2 diff --git a/mobile-next/ios/Runner.xcodeproj/project.pbxproj b/mobile-next/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2425435 --- /dev/null +++ b/mobile-next/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,823 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 173BE1932A67E13300A4D9A9 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173BE1922A67E13300A4D9A9 /* NotificationService.swift */; }; + 173BE1972A67E13300A4D9A9 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 173BE1902A67E13200A4D9A9 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 55219944D9DA5CD2EDF4C8C3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E6DB8DF4A03CB207F9CF840 /* Pods_Runner.framework */; }; + 701BDEDF100D82D14BF9DC9C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B4A878B5BD9E53AD68260EDF /* GoogleService-Info.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 173BE1952A67E13300A4D9A9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 173BE18F2A67E13200A4D9A9; + remoteInfo = NotificationService; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 173BE19C2A67E13300A4D9A9 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 173BE1972A67E13300A4D9A9 /* NotificationService.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1706ED5D2AC2917E004FD73C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + 1706ED5E2AC2917E004FD73C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 173BE1902A67E13200A4D9A9 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 173BE1922A67E13300A4D9A9 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 173BE1942A67E13300A4D9A9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 173BE19D2A67E16300A4D9A9 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 173BE19E2A67E18F00A4D9A9 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; + 176C056E2AA9AAA20006F277 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; + 1E6DB8DF4A03CB207F9CF840 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5BB0FD4B4EF99F87CBCBE59A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 5FF4A91C6E553A5915EBB93E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 6BADC328EF88768955F7D03E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B4A878B5BD9E53AD68260EDF /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 173BE18D2A67E13200A4D9A9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 55219944D9DA5CD2EDF4C8C3 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 173BE1912A67E13300A4D9A9 /* NotificationService */ = { + isa = PBXGroup; + children = ( + 173BE19E2A67E18F00A4D9A9 /* NotificationService.entitlements */, + 173BE1922A67E13300A4D9A9 /* NotificationService.swift */, + 173BE1942A67E13300A4D9A9 /* Info.plist */, + ); + path = NotificationService; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 173BE1912A67E13300A4D9A9 /* NotificationService */, + 97C146EF1CF9000F007C117D /* Products */, + 9B5BFEE13D0DBBAB9D1487A5 /* Pods */, + C68B34A13F8B54B6D32ED277 /* Frameworks */, + B4A878B5BD9E53AD68260EDF /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 173BE1902A67E13200A4D9A9 /* NotificationService.appex */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 173BE19D2A67E16300A4D9A9 /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 9B5BFEE13D0DBBAB9D1487A5 /* Pods */ = { + isa = PBXGroup; + children = ( + 6BADC328EF88768955F7D03E /* Pods-Runner.debug.xcconfig */, + 5BB0FD4B4EF99F87CBCBE59A /* Pods-Runner.release.xcconfig */, + 5FF4A91C6E553A5915EBB93E /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + C68B34A13F8B54B6D32ED277 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 176C056E2AA9AAA20006F277 /* ReplayKit.framework */, + 1E6DB8DF4A03CB207F9CF840 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 173BE18F2A67E13200A4D9A9 /* NotificationService */ = { + isa = PBXNativeTarget; + buildConfigurationList = 173BE1982A67E13300A4D9A9 /* Build configuration list for PBXNativeTarget "NotificationService" */; + buildPhases = ( + 173BE18C2A67E13200A4D9A9 /* Sources */, + 173BE18D2A67E13200A4D9A9 /* Frameworks */, + 173BE18E2A67E13200A4D9A9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NotificationService; + productName = NotificationService; + productReference = 173BE1902A67E13200A4D9A9 /* NotificationService.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 6D26B612E7CE061D882E2C44 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 173BE19C2A67E13300A4D9A9 /* Embed Foundation Extensions */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + D93E9AE0652B0DF2EDE1169B /* [CP] Embed Pods Frameworks */, + 170C561DA4A2C0A19C3C3AB0 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 173BE1962A67E13300A4D9A9 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 173BE18F2A67E13200A4D9A9 = { + CreatedOnToolsVersion = 14.3; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + "zh-Hans", + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 173BE18F2A67E13200A4D9A9 /* NotificationService */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 173BE18E2A67E13200A4D9A9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 701BDEDF100D82D14BF9DC9C /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 170C561DA4A2C0A19C3C3AB0 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 6D26B612E7CE061D882E2C44 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + D93E9AE0652B0DF2EDE1169B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 173BE18C2A67E13200A4D9A9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 173BE1932A67E13300A4D9A9 /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 173BE1962A67E13300A4D9A9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 173BE18F2A67E13200A4D9A9 /* NotificationService */; + targetProxy = 173BE1952A67E13300A4D9A9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + 1706ED5D2AC2917E004FD73C /* zh-Hans */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + 1706ED5E2AC2917E004FD73C /* zh-Hans */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 173BE1992A67E13300A4D9A9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 17; + DEVELOPMENT_TEAM = V57RT7LMFH; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 3.3.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.openim.flutter.demoo.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 173BE19A2A67E13300A4D9A9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 17; + DEVELOPMENT_TEAM = V57RT7LMFH; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 3.3.1; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.openim.flutter.demoo.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 173BE19B2A67E13300A4D9A9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 17; + DEVELOPMENT_TEAM = V57RT7LMFH; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 3.3.1; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.openim.flutter.demoo.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Profile; + }; + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V57RT7LMFH; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 3.8.2; + PRODUCT_BUNDLE_IDENTIFIER = io.openim.flutter.demoo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V57RT7LMFH; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 3.8.2; + PRODUCT_BUNDLE_IDENTIFIER = io.openim.flutter.demoo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V57RT7LMFH; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 3.8.2; + PRODUCT_BUNDLE_IDENTIFIER = io.openim.flutter.demoo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 173BE1982A67E13300A4D9A9 /* Build configuration list for PBXNativeTarget "NotificationService" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 173BE1992A67E13300A4D9A9 /* Debug */, + 173BE19A2A67E13300A4D9A9 /* Release */, + 173BE19B2A67E13300A4D9A9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/mobile-next/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/mobile-next/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile-next/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..5e31d3d --- /dev/null +++ b/mobile-next/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/ios/Runner.xcworkspace/contents.xcworkspacedata b/mobile-next/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/mobile-next/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/mobile-next/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile-next/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/mobile-next/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile-next/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile-next/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/mobile-next/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/mobile-next/ios/Runner/AppDelegate.swift b/mobile-next/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..e218ec6 --- /dev/null +++ b/mobile-next/ios/Runner/AppDelegate.swift @@ -0,0 +1,74 @@ +import UIKit +import Flutter +import FirebaseCore + +@main +@objc class AppDelegate: FlutterAppDelegate { + + var replayKitChannel: FlutterMethodChannel! = nil + var observeTimer: Timer? + var hasEmittedFirstSample = false; + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + guard let controller = window?.rootViewController as? FlutterViewController else { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + FirebaseApp.configure() + + replayKitChannel = FlutterMethodChannel(name: "io.livekit.example.flutter/replaykit-channel",binaryMessenger: controller.binaryMessenger) + + replayKitChannel.setMethodCallHandler({ + (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + self.handleReplayKitFromFlutter(result: result, call:call) + }) + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func handleReplayKitFromFlutter(result:FlutterResult, call: FlutterMethodCall){ + switch (call.method) { + case "startReplayKit": + self.hasEmittedFirstSample = false + let group=UserDefaults(suiteName: "group.io.livekit.example.flutter") + group!.set(false, forKey: "closeReplayKitFromNative") + group!.set(false, forKey: "closeReplayKitFromFlutter") + self.observeReplayKitStateChanged() + break + case "closeReplayKit": + let group=UserDefaults(suiteName: "group.io.livekit.example.flutter") + group!.set(true,forKey: "closeReplayKitFromFlutter") + result(true) + break + default: + return result(FlutterMethodNotImplemented) + } + } + + + func observeReplayKitStateChanged(){ + if (self.observeTimer != nil) { + return + } + + let group=UserDefaults(suiteName: "group.io.livekit.example.flutter") + self.observeTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in + let closeReplayKitFromNative=group!.bool(forKey: "closeReplayKitFromNative") + let hasSampleBroadcast=group!.bool(forKey: "hasSampleBroadcast") + + if (closeReplayKitFromNative) { + self.hasEmittedFirstSample = false + self.replayKitChannel.invokeMethod("closeReplayKitFromNative", arguments: true) + } else if (hasSampleBroadcast) { + if (!self.hasEmittedFirstSample) { + self.hasEmittedFirstSample = true + self.replayKitChannel.invokeMethod("hasSampleBroadcast", arguments: true) + } + } + } + } +} \ No newline at end of file diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..06d650c Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..8e96a35 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..e884b01 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..9a66800 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4b3698d Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fee9dd3 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..6b76e29 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..e884b01 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..9b3ffb5 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..6f3887d Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..6f3887d Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..d7d4f0f Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..991ba13 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..9a1a344 Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..c2b20ff Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/mobile-next/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/mobile-next/ios/Runner/Base.lproj/LaunchScreen.storyboard b/mobile-next/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/mobile-next/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/ios/Runner/Base.lproj/Main.storyboard b/mobile-next/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/mobile-next/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/ios/Runner/GoogleService-Info.plist b/mobile-next/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..526a257 --- /dev/null +++ b/mobile-next/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyC0BhV-mo84LvOuqWf2bS966jlf0f8mh74 + GCM_SENDER_ID + 299075855003 + PLIST_VERSION + 1 + BUNDLE_ID + io.openim.flutter.base + PROJECT_ID + im-fer-c3347 + STORAGE_BUCKET + im-fer-c3347.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:299075855003:ios:23b06300e33d984309dc3e + + \ No newline at end of file diff --git a/mobile-next/ios/Runner/Info.plist b/mobile-next/ios/Runner/Info.plist new file mode 100644 index 0000000..87d0465 --- /dev/null +++ b/mobile-next/ios/Runner/Info.plist @@ -0,0 +1,99 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + + zh-CN + en-US + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + F-DEMO + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + comgooglemaps + baidumap + iosamap + waze + yandexmaps + yandexnavi + citymapper + mapswithme + osmandmaps + dgis + qqmap + here-location + + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSAppleMusicUsageDescription + 请点击“好”以允许访问。若不允许,你将无法给好友发送或者上传本地相册图片及视频内容。 + NSBluetoothAlwaysUsageDescription + App requires access to Bluetooth to connect to devices. + NSBluetoothPeripheralUsageDescription + App requires access to Bluetooth to communicate with peripheral devices. + NSCameraUsageDescription + 请点击“好”以允许访问。若不允许,你将无法使用拍照、录制视频、扫一扫等功能。 + NSFaceIDUsageDescription + 请点击“好”以允许访问。若不允许,你将无法使用Touch ID 或 Face ID解锁功能。 + NSLocationAlwaysAndWhenInUseUsageDescription + 请点击“好”以允许访问。若不允许,你将无法使用聊天时发送定位等功能。 + NSLocationAlwaysUsageDescription + 请点击“好”以允许访问。若不允许,你将无法使用聊天时发送定位等功能。 + NSLocationWhenInUseUsageDescription + 请点击“好”以允许访问。若不允许,你将无法使用聊天时发送定位等功能。 + NSMicrophoneUsageDescription + 请点击“好”以允许访问。若不允许,你将无法使用视频通话、发送语音消息或录制视频等功能。 + NSPhotoLibraryAddUsageDescription + 请点击“好”以允许访问。若不允许,你将无法给好友发送或者上传本地相册图片及视频内容。 + NSPhotoLibraryUsageDescription + 请点击“好”以允许访问。若不允许,你将无法给好友发送或者上传本地相册图片及视频内容。 + RTCAppGroupIdentifier + group.cn.rentsoft.openim.flutter.rtc + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiresFullScreen + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportsDocumentBrowser + + UIViewControllerBasedStatusBarAppearance + + io.flutter.embedded_views_preview + + + diff --git a/mobile-next/ios/Runner/Runner-Bridging-Header.h b/mobile-next/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/mobile-next/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/mobile-next/ios/Runner/Runner.entitlements b/mobile-next/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..2478cd6 --- /dev/null +++ b/mobile-next/ios/Runner/Runner.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.cn.rentsoft.openim.flutter.rtc + + + diff --git a/mobile-next/ios/Runner/zh-Hans.lproj/LaunchScreen.strings b/mobile-next/ios/Runner/zh-Hans.lproj/LaunchScreen.strings new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mobile-next/ios/Runner/zh-Hans.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/mobile-next/ios/Runner/zh-Hans.lproj/Main.strings b/mobile-next/ios/Runner/zh-Hans.lproj/Main.strings new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mobile-next/ios/Runner/zh-Hans.lproj/Main.strings @@ -0,0 +1 @@ + diff --git a/mobile-next/launcher_icon/app-icon.png b/mobile-next/launcher_icon/app-icon.png new file mode 100644 index 0000000..b2f50cc Binary files /dev/null and b/mobile-next/launcher_icon/app-icon.png differ diff --git a/mobile-next/lib/app.dart b/mobile-next/lib/app.dart new file mode 100644 index 0000000..80dbb92 --- /dev/null +++ b/mobile-next/lib/app.dart @@ -0,0 +1,113 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'core/controller/im_controller.dart'; +import 'routes/app_pages.dart'; +import 'widgets/app_view.dart'; + +class ChatApp extends StatelessWidget { + const ChatApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppView( + builder: (locale, builder) => GetMaterialApp( + debugShowCheckedModeBanner: false, + enableLog: true, + builder: builder, + logWriterCallback: Logger.print, + translations: TranslationService(), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + fallbackLocale: TranslationService.fallbackLocale, + locale: locale, + localeResolutionCallback: (locale, list) { + Get.locale ??= locale; + return locale; + }, + supportedLocales: const [Locale('zh', 'CN'), Locale('en', 'US')], + getPages: AppPages.routes, + initialBinding: InitBinding(), + initialRoute: AppRoutes.splash, + theme: _themeData, + ), + ); + } + + ThemeData get _themeData => ThemeData.light().copyWith( + scaffoldBackgroundColor: Colors.grey.shade50, + canvasColor: Colors.white, + appBarTheme: const AppBarTheme(color: Colors.white), + textSelectionTheme: const TextSelectionThemeData().copyWith(cursorColor: Colors.blue), + checkboxTheme: const CheckboxThemeData().copyWith( + checkColor: WidgetStateProperty.all(Colors.white), + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return Colors.grey; + } + if (states.contains(WidgetState.selected)) { + return Colors.blue; + } + return Colors.white; + }), + side: BorderSide(color: Colors.grey.shade500, width: 1), + ), + dialogTheme: const DialogTheme().copyWith( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.0), + ), + ), + textStyle: WidgetStatePropertyAll( + TextStyle( + fontSize: 16.sp, + color: Colors.black, + ), + ), + foregroundColor: const WidgetStatePropertyAll(Colors.black), + ), + ), + progressIndicatorTheme: const ProgressIndicatorThemeData() + .copyWith(color: Colors.white, linearTrackColor: Colors.grey[300], circularTrackColor: Colors.grey[300]), + cupertinoOverrideTheme: CupertinoThemeData( + brightness: Brightness.light, + primaryColor: CupertinoColors.systemBlue, + barBackgroundColor: Colors.white, + applyThemeToAll: true, + textTheme: const CupertinoTextThemeData().copyWith( + navActionTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), + actionTextStyle: TextStyle(color: CupertinoColors.systemBlue, fontSize: 17.sp), + textStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), + navLargeTitleTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 20.sp), + navTitleTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), + pickerTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), + tabLabelTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), + dateTimePickerTextStyle: TextStyle(color: CupertinoColors.label, fontSize: 17.sp), + ), + ), + ); +} + +class InitBinding extends Bindings { + @override + void dependencies() { + Get.put(IMController()); + Get.put(PushController()); + Get.put(CacheController()); + Get.put(DownloadController()); + } +} diff --git a/mobile-next/lib/core/controller/app_controller.dart b/mobile-next/lib/core/controller/app_controller.dart new file mode 100644 index 0000000..88f104b --- /dev/null +++ b/mobile-next/lib/core/controller/app_controller.dart @@ -0,0 +1,288 @@ +import 'dart:io'; + +import 'package:audio_session/audio_session.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_new_badger/flutter_new_badger.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart' as im; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:openim/core/im_callback.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sound_mode/sound_mode.dart'; +import 'package:sound_mode/utils/ringer_mode_statuses.dart'; +import 'package:vibration/vibration.dart'; + +import '../../utils/upgrade_manager.dart'; +import 'im_controller.dart'; + +class AppController extends GetxController with UpgradeManger { + var isRunningBackground = false; + + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + final initializationSettingsAndroid = const AndroidInitializationSettings('@mipmap/ic_launcher'); + + final DarwinInitializationSettings initializationSettingsDarwin = const DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + ); + + RTCBridge? get rtcBridge => PackageBridge.rtcBridge; + + bool get shouldMuted => + rtcBridge?.hasConnection == true || + Get.find().imSdkStatusSubject.values.last.status != IMSdkStatus.syncEnded; + + final _ring = 'assets/audio/message_ring.wav'; + final _audioPlayer = AudioPlayer(); + final configuration = const AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.ambient, + avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.mixWithOthers, + androidAudioFocusGainType: AndroidAudioFocusGainType.gainTransientMayDuck, + androidAudioAttributes: AndroidAudioAttributes( + contentType: AndroidAudioContentType.sonification, + usage: AndroidAudioUsage.notification, + ), + ); + late AudioSession session; + + late BaseDeviceInfo deviceInfo; + + final clientConfigMap = {}.obs; + + Future runningBackground(bool run) async { + Logger.print('-----App running background : $run-------------'); + + if (isRunningBackground && !run) {} + isRunningBackground = run; + if (!run) { + _cancelAllNotifications(); + } + } + + @override + void onInit() async { + _initPlayer(); + final initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + ); + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: (notificationResponse) {}, + ); + + autoCheckVersionUpgrade(); + super.onInit(); + } + + void _requestPermissions() { + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.requestNotificationsPermission(); + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + } + + Future showNotification(im.Message message, {bool showNotification = true}) async { + if (_isGlobalNotDisturb() || + message.attachedInfoElem?.notSenderNotificationPush == true || + message.contentType == im.MessageType.typing || + message.sendID == OpenIM.iMManager.userID || + (message.contentType! >= 1000 && message.contentType != 1400)) return; + + var sourceID = message.sessionType == ConversationType.single ? message.sendID : message.groupID; + if (sourceID != null && message.sessionType != null) { + var i = await OpenIM.iMManager.conversationManager.getOneConversation( + sourceID: sourceID, + sessionType: message.sessionType!, + ); + if (i.recvMsgOpt != 0) return; + } + + if (showNotification) { + promptSoundOrNotification(message.seq!); + } + } + + Future promptSoundOrNotification(int seq) async { + if (Get.find().imSdkStatusSubject.values.lastOrNull?.status != IMSdkStatus.syncEnded) { + return; + } + if (!isRunningBackground) { + _playMessageSound(); + } else { + if (Platform.isAndroid) { + final id = seq; + + const androidPlatformChannelSpecifics = AndroidNotificationDetails('chat', 'OpenIM聊天消息', + channelDescription: '来自OpenIM的信息', importance: Importance.max, priority: Priority.high, ticker: 'ticker'); + const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show(id, '您收到了一条新消息', '消息内容:.....', platformChannelSpecifics, + payload: ''); + } + } + } + + Future _cancelAllNotifications() async { + await flutterLocalNotificationsPlugin.cancelAll(); + } + + Future _startForegroundService() async { + await getAppInfo(); + const androidPlatformChannelSpecifics = AndroidNotificationDetails('pro', 'OpenIM后台进程', + channelDescription: '保证app能收到信息', importance: Importance.max, priority: Priority.high, ticker: 'ticker'); + + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.startForegroundService(1, packageInfo!.appName, '正在运行...', + notificationDetails: androidPlatformChannelSpecifics, payload: ''); + } + + Future _stopForegroundService() async { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.stopForegroundService(); + } + + void showBadge(count) { + OpenIM.iMManager.messageManager.setAppBadge(count); + + if (count == 0) { + removeBadge(); + PushController.resetBadge(); + } else { + FlutterNewBadger.setBadge(count); + PushController.setBadge(count); + } + } + + void removeBadge() { + FlutterNewBadger.removeBadge(); + } + + @override + void onClose() { + closeSubject(); + _audioPlayer.dispose(); + super.onClose(); + } + + Locale? getLocale() { + var local = Get.locale; + var index = DataSp.getLanguage() ?? 0; + switch (index) { + case 1: + local = const Locale('zh', 'CN'); + break; + case 2: + local = const Locale('en', 'US'); + break; + } + return local; + } + + @override + void onReady() { + queryClientConfig(); + _getDeviceInfo(); + _cancelAllNotifications(); + super.onReady(); + } + + bool _isGlobalNotDisturb() { + bool isRegistered = Get.isRegistered(); + if (isRegistered) { + var logic = Get.find(); + return logic.userInfo.value.globalRecvMsgOpt == 2; + } + return false; + } + + void _initPlayer() async { + session = await AudioSession.instance; + await session.configure(configuration); + + _audioPlayer.setAsset(_ring, package: 'openim_common'); + _audioPlayer.playerStateStream.listen((state) { + switch (state.processingState) { + case ProcessingState.idle: + case ProcessingState.loading: + case ProcessingState.buffering: + case ProcessingState.ready: + break; + case ProcessingState.completed: + _stopMessageSound(); + + break; + } + }); + } + + void _playMessageSound() async { + if (shouldMuted) { + return; + } + bool isRegistered = Get.isRegistered(); + bool isAllowVibration = true; + bool isAllowBeep = true; + if (isRegistered) { + var logic = Get.find(); + isAllowVibration = logic.userInfo.value.allowVibration == 1; + isAllowBeep = logic.userInfo.value.allowBeep == 1; + } + + RingerModeStatus ringerStatus = await SoundMode.ringerModeStatus; + + Logger.print('System ringer status: $ringerStatus, user is allow beep: $isAllowBeep', + fileName: 'app_controller.dart'); + + if (!_audioPlayer.playerState.playing && + isAllowBeep && + (ringerStatus == RingerModeStatus.normal || ringerStatus == RingerModeStatus.unknown)) { + await session.setActive(true); + _audioPlayer.setAsset(_ring, package: 'openim_common'); + _audioPlayer.setLoopMode(LoopMode.off); + _audioPlayer.setVolume(1.0); + _audioPlayer.play(); + } + + if (isAllowVibration && + (ringerStatus == RingerModeStatus.normal || + ringerStatus == RingerModeStatus.vibrate || + ringerStatus == RingerModeStatus.unknown)) { + if (await Vibration.hasVibrator() == true) { + Vibration.vibrate(); + } + } + } + + void _stopMessageSound() async { + if (_audioPlayer.playerState.playing) { + _audioPlayer.stop(); + } + await session.setActive(false); + } + + void _getDeviceInfo() async { + final deviceInfoPlugin = DeviceInfoPlugin(); + deviceInfo = await deviceInfoPlugin.deviceInfo; + } + + Future queryClientConfig() async { + final map = await Apis.getClientConfig(); + clientConfigMap.assignAll(map); + + return clientConfigMap; + } +} diff --git a/mobile-next/lib/core/controller/im_controller.dart b/mobile-next/lib/core/controller/im_controller.dart new file mode 100644 index 0000000..f2c5038 --- /dev/null +++ b/mobile-next/lib/core/controller/im_controller.dart @@ -0,0 +1,214 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:openim_live/openim_live.dart'; + +import '../im_callback.dart'; + +class IMController extends GetxController with IMCallback, OpenIMLive { + late Rx userInfo; + late String atAllTag; + + @override + void onClose() { + super.close(); + onCloseLive(); + super.onClose(); + } + + @override + void onInit() async { + super.onInit(); + onInitLive(); + WidgetsBinding.instance.addPostFrameCallback((_) => initOpenIM()); + } + + void initOpenIM() async { + final initialized = await OpenIM.iMManager.initSDK( + platformID: IMUtils.getPlatform(), + apiAddr: Config.imApiUrl, + wsAddr: Config.imWsUrl, + dataDir: Config.cachePath, + logLevel: Config.logLevel, + logFilePath: Config.cachePath, + listener: OnConnectListener( + onConnecting: () { + imSdkStatus(IMSdkStatus.connecting); + }, + onConnectFailed: (code, error) { + imSdkStatus(IMSdkStatus.connectionFailed); + }, + onConnectSuccess: () { + imSdkStatus(IMSdkStatus.connectionSucceeded); + }, + onKickedOffline: kickedOffline, + onUserTokenExpired: kickedOffline, + onUserTokenInvalid: userTokenInvalid, + ), + ); + + OpenIM.iMManager + ..setUploadLogsListener(OnUploadLogsListener(onUploadProgress: uploadLogsProgress)) + ..userManager.setUserListener(OnUserListener( + onSelfInfoUpdated: (u) { + selfInfoUpdated(u); + + userInfo.update((val) { + val?.nickname = u.nickname; + val?.faceURL = u.faceURL; + + val?.remark = u.remark; + val?.ex = u.ex; + val?.globalRecvMsgOpt = u.globalRecvMsgOpt; + }); + }, + onUserStatusChanged: userStausChanged)) + ..messageManager.setAdvancedMsgListener(OnAdvancedMsgListener( + onRecvC2CReadReceipt: recvC2CMessageReadReceipt, + onRecvNewMessage: recvNewMessage, + onNewRecvMessageRevoked: recvMessageRevoked, + onRecvOfflineNewMessage: recvOfflineMessage, + onRecvOnlineOnlyMessage: (msg) { + if (msg.isCustomType) { + final data = msg.customElem!.data; + final map = jsonDecode(data!); + final customType = map['customType']; + if (customType == CustomMessageType.callingInvite || + customType == CustomMessageType.callingAccept || + customType == CustomMessageType.callingReject || + customType == CustomMessageType.callingCancel || + customType == CustomMessageType.callingHungup) { + final signaling = SignalingInfo(invitation: InvitationInfo.fromJson(map['data'])); + signaling.userID = signaling.invitation?.inviterUserID; + + switch (customType) { + case CustomMessageType.callingInvite: + receiveNewInvitation(signaling); + break; + case CustomMessageType.callingAccept: + inviteeAccepted(signaling); + break; + case CustomMessageType.callingReject: + inviteeRejected(signaling); + break; + case CustomMessageType.callingCancel: + invitationCancelled(signaling); + break; + case CustomMessageType.callingHungup: + beHangup(signaling); + break; + } + } + } + }, + )) + ..messageManager.setMsgSendProgressListener(OnMsgSendProgressListener( + onProgress: progressCallback, + )) + ..messageManager.setCustomBusinessListener(OnCustomBusinessListener( + onRecvCustomBusinessMessage: recvCustomBusinessMessage, + )) + ..friendshipManager.setFriendshipListener(OnFriendshipListener( + onBlackAdded: blacklistAdded, + onBlackDeleted: blacklistDeleted, + onFriendApplicationAccepted: friendApplicationAccepted, + onFriendApplicationAdded: friendApplicationAdded, + onFriendApplicationDeleted: friendApplicationDeleted, + onFriendApplicationRejected: friendApplicationRejected, + onFriendInfoChanged: friendInfoChanged, + onFriendAdded: friendAdded, + onFriendDeleted: friendDeleted, + )) + ..conversationManager.setConversationListener(OnConversationListener( + onConversationChanged: conversationChanged, + onNewConversation: newConversation, + onTotalUnreadMessageCountChanged: totalUnreadMsgCountChanged, + onInputStatusChanged: inputStateChanged, + onSyncServerFailed: (reInstall) { + imSdkStatus(IMSdkStatus.syncFailed, reInstall: reInstall ?? false); + }, + onSyncServerFinish: (reInstall) { + imSdkStatus(IMSdkStatus.syncEnded, reInstall: reInstall ?? false); + if (Platform.isAndroid) { + Permissions.request([Permission.systemAlertWindow]); + } + }, + onSyncServerStart: (reInstall) { + imSdkStatus(IMSdkStatus.syncStart, reInstall: reInstall ?? false); + }, + onSyncServerProgress: (progress) { + imSdkStatus(IMSdkStatus.syncProgress, progress: progress); + })) + ..groupManager.setGroupListener(OnGroupListener( + onGroupApplicationAccepted: groupApplicationAccepted, + onGroupApplicationAdded: groupApplicationAdded, + onGroupApplicationDeleted: groupApplicationDeleted, + onGroupApplicationRejected: groupApplicationRejected, + onGroupInfoChanged: groupInfoChanged, + onGroupMemberAdded: groupMemberAdded, + onGroupMemberDeleted: groupMemberDeleted, + onGroupMemberInfoChanged: groupMemberInfoChanged, + onJoinedGroupAdded: joinedGroupAdded, + onJoinedGroupDeleted: joinedGroupDeleted, + )); + + initializedSubject.sink.add(initialized); + } + + Future login(String userID, String token) async { + try { + var user = await OpenIM.iMManager.login( + userID: userID, + token: token, + defaultValue: () async => UserInfo(userID: userID), + ); + ApiService().setToken(token); + userInfo = UserFullInfo.fromJson(user.toJson()).obs; + _queryMyFullInfo(); + _queryAtAllTag(); + } catch (e, s) { + Logger.print('e: $e s:$s'); + await _handleLoginRepeatError(e); + + return Future.error(e, s); + } + } + + Future logout() { + return OpenIM.iMManager.logout(); + } + + void _queryAtAllTag() async { + atAllTag = OpenIM.iMManager.conversationManager.atAllTag; + } + + void _queryMyFullInfo() async { + final data = await Apis.queryMyFullInfo(); + if (data is UserFullInfo) { + userInfo.update((val) { + val?.allowAddFriend = data.allowAddFriend; + val?.allowBeep = data.allowBeep; + val?.allowVibration = data.allowVibration; + val?.nickname = data.nickname; + val?.faceURL = data.faceURL; + val?.phoneNumber = data.phoneNumber; + val?.email = data.email; + val?.birth = data.birth; + val?.gender = data.gender; + }); + } + } + + _handleLoginRepeatError(e) async { + if (e is PlatformException && (e.code == "13002" || e.code == '1507')) { + await logout(); + await DataSp.removeLoginCertificate(); + } + } +} diff --git a/mobile-next/lib/core/im_callback.dart b/mobile-next/lib/core/im_callback.dart new file mode 100644 index 0000000..3e93473 --- /dev/null +++ b/mobile-next/lib/core/im_callback.dart @@ -0,0 +1,250 @@ +import 'dart:convert'; + +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'controller/app_controller.dart'; + +enum IMSdkStatus { + connectionFailed, + connecting, + connectionSucceeded, + syncStart, + synchronizing, + syncEnded, + syncFailed, + syncProgress, +} + +enum KickoffType { + kickedOffline, + userTokenInvalid, + userTokenExpired, +} + +mixin IMCallback { + final initLogic = Get.find(); + + Function(RevokedInfo info)? onRecvMessageRevoked; + + Function(List list)? onRecvC2CReadReceipt; + + Function(Message msg)? onRecvNewMessage; + + Function(Message msg)? onRecvOfflineMessage; + + Function(String msgId, int progress)? onMsgSendProgress; + + Function(BlacklistInfo u)? onBlacklistAdd; + + Function(BlacklistInfo u)? onBlacklistDeleted; + + Function(int current, int size)? onUploadProgress; + + final conversationAddedSubject = BehaviorSubject>(); + + final conversationChangedSubject = BehaviorSubject>(); + + final unreadMsgCountEventSubject = PublishSubject(); + + final friendApplicationChangedSubject = BehaviorSubject(); + + final friendAddSubject = BehaviorSubject(); + + final friendDelSubject = BehaviorSubject(); + + final friendInfoChangedSubject = PublishSubject(); + + final selfInfoUpdatedSubject = BehaviorSubject(); + + final userStatusChangedSubject = BehaviorSubject(); + + final groupInfoUpdatedSubject = BehaviorSubject(); + + final groupApplicationChangedSubject = BehaviorSubject(); + + final initializedSubject = PublishSubject(); + + final memberAddedSubject = BehaviorSubject(); + + final memberDeletedSubject = BehaviorSubject(); + + final memberInfoChangedSubject = PublishSubject(); + + final joinedGroupDeletedSubject = BehaviorSubject(); + + final joinedGroupAddedSubject = BehaviorSubject(); + + final onKickedOfflineSubject = PublishSubject(); + + final imSdkStatusSubject = ReplaySubject<({IMSdkStatus status, bool reInstall, int? progress})>(); + + final imSdkStatusPublishSubject = PublishSubject<({IMSdkStatus status, bool reInstall, int? progress})>(); + + final inputStateChangedSubject = PublishSubject(); + + void imSdkStatus(IMSdkStatus status, {bool reInstall = false, int? progress}) { + imSdkStatusSubject.add((status: status, reInstall: reInstall, progress: progress)); + imSdkStatusPublishSubject.add((status: status, reInstall: reInstall, progress: progress)); + } + + void kickedOffline() { + onKickedOfflineSubject.add(KickoffType.kickedOffline); + } + + void userTokenInvalid() { + onKickedOfflineSubject.add(KickoffType.userTokenInvalid); + } + + void selfInfoUpdated(UserInfo u) { + selfInfoUpdatedSubject.addSafely(u); + } + + void userStausChanged(UserStatusInfo u) { + userStatusChangedSubject.addSafely(u); + } + + void uploadLogsProgress(int current, int size) { + onUploadProgress?.call(current, size); + } + + void recvMessageRevoked(RevokedInfo info) { + onRecvMessageRevoked?.call(info); + } + + void recvC2CMessageReadReceipt(List list) { + onRecvC2CReadReceipt?.call(list); + } + + void recvNewMessage(Message msg) { + initLogic.showNotification(msg); + onRecvNewMessage?.call(msg); + } + + void recvOfflineMessage(Message msg) { + initLogic.showNotification(msg); + onRecvOfflineMessage?.call(msg); + } + + void recvCustomBusinessMessage(String s) {} + + void progressCallback(String msgId, int progress) { + onMsgSendProgress?.call(msgId, progress); + } + + void blacklistAdded(BlacklistInfo u) { + onBlacklistAdd?.call(u); + } + + void blacklistDeleted(BlacklistInfo u) { + onBlacklistDeleted?.call(u); + } + + void friendApplicationAccepted(FriendApplicationInfo u) { + friendApplicationChangedSubject.addSafely(u); + } + + void friendApplicationAdded(FriendApplicationInfo u) { + friendApplicationChangedSubject.addSafely(u); + } + + void friendApplicationDeleted(FriendApplicationInfo u) { + friendApplicationChangedSubject.addSafely(u); + } + + void friendApplicationRejected(FriendApplicationInfo u) { + friendApplicationChangedSubject.addSafely(u); + } + + void friendInfoChanged(FriendInfo u) { + friendInfoChangedSubject.addSafely(u); + } + + void friendAdded(FriendInfo u) { + friendAddSubject.addSafely(u); + } + + void friendDeleted(FriendInfo u) { + friendDelSubject.addSafely(u); + } + + void conversationChanged(List list) { + conversationChangedSubject.addSafely(list); + } + + void newConversation(List list) { + conversationAddedSubject.addSafely(list); + } + + void groupApplicationAccepted(GroupApplicationInfo info) { + groupApplicationChangedSubject.add(info); + } + + void groupApplicationAdded(GroupApplicationInfo info) { + groupApplicationChangedSubject.add(info); + } + + void groupApplicationDeleted(GroupApplicationInfo info) { + groupApplicationChangedSubject.add(info); + } + + void groupApplicationRejected(GroupApplicationInfo info) { + groupApplicationChangedSubject.add(info); + } + + void groupInfoChanged(GroupInfo info) { + groupInfoUpdatedSubject.addSafely(info); + } + + void groupMemberAdded(GroupMembersInfo info) { + memberAddedSubject.add(info); + } + + void groupMemberDeleted(GroupMembersInfo info) { + memberDeletedSubject.add(info); + } + + void groupMemberInfoChanged(GroupMembersInfo info) { + memberInfoChangedSubject.add(info); + } + + void joinedGroupAdded(GroupInfo info) { + joinedGroupAddedSubject.add(info); + } + + void joinedGroupDeleted(GroupInfo info) { + joinedGroupDeletedSubject.add(info); + } + + void totalUnreadMsgCountChanged(int count) { + initLogic.showBadge(count); + unreadMsgCountEventSubject.addSafely(count); + } + + void inputStateChanged(InputStatusChangedData status) { + inputStateChangedSubject.addSafely(status); + } + + void close() { + initializedSubject.close(); + friendApplicationChangedSubject.close(); + friendAddSubject.close(); + friendDelSubject.close(); + friendInfoChangedSubject.close(); + selfInfoUpdatedSubject.close(); + groupInfoUpdatedSubject.close(); + conversationAddedSubject.close(); + conversationChangedSubject.close(); + memberAddedSubject.close(); + memberDeletedSubject.close(); + memberInfoChangedSubject.close(); + onKickedOfflineSubject.close(); + groupApplicationChangedSubject.close(); + imSdkStatusSubject.close(); + imSdkStatusPublishSubject.close(); + joinedGroupDeletedSubject.close(); + joinedGroupAddedSubject.close(); + } +} diff --git a/mobile-next/lib/main.dart b/mobile-next/lib/main.dart new file mode 100644 index 0000000..a4a423b --- /dev/null +++ b/mobile-next/lib/main.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'app.dart'; + +void main() { + runZonedGuarded(() { + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + Logger.print('FlutterError: ${details.exception.toString()}, ${details.stack.toString()}'); + }; + + Config.init(() => runApp(const ChatApp())); + }, (error, stackTrace) { + Logger.print('FlutterError: ${error.toString()}, ${stackTrace.toString()}'); + }); +} diff --git a/mobile-next/lib/pages/chat/chat_binding.dart b/mobile-next/lib/pages/chat/chat_binding.dart new file mode 100644 index 0000000..4c05c5f --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_binding.dart @@ -0,0 +1,13 @@ +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'chat_logic.dart'; +import 'group_setup/group_setup_logic.dart'; + +class ChatBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ChatLogic(), tag: GetTags.chat); + Get.lazyPut(() => GroupSetupLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/chat_logic.dart b/mobile-next/lib/pages/chat/chat_logic.dart new file mode 100644 index 0000000..725d27f --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_logic.dart @@ -0,0 +1,1833 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:common_utils/common_utils.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:mime/mime.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:sprintf/sprintf.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:video_compress/video_compress.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; +import 'package:openim_live/openim_live.dart'; + +import '../../core/controller/app_controller.dart'; +import '../../core/controller/im_controller.dart'; +import '../../core/im_callback.dart'; +import '../../routes/app_navigator.dart'; +import '../contacts/select_contacts/select_contacts_logic.dart'; +import '../conversation/conversation_logic.dart'; +import 'group_setup/group_member_list/group_member_list_logic.dart'; + +class ChatLogic extends SuperController { + final imLogic = Get.find(); + final appLogic = Get.find(); + final conversationLogic = Get.find(); + final cacheLogic = Get.find(); + final downloadLogic = Get.find(); + + final inputCtrl = TextEditingController(); + final focusNode = FocusNode(); + final scrollController = ScrollController(); + final refreshController = RefreshController(); + bool playOnce = false; // 点击的当前视频只能播放一次 + + final forceCloseToolbox = PublishSubject(); + final forceCloseMenuSub = PublishSubject(); + final sendStatusSub = PublishSubject>(); + + late ConversationInfo conversationInfo; + Message? searchMessage; + final nickname = ''.obs; + final faceUrl = ''.obs; + Timer? typingTimer; + final typing = false.obs; + Timer? _debounce; + Message? quoteMsg; + final messageList = [].obs; + final tempMessages = []; // 临时存放消息体,例如图片消息 + var _lastCursorIndex = -1; + final onlineStatus = false.obs; + final onlineStatusDesc = ''.obs; + Timer? onlineStatusTimer; + final favoriteList = [].obs; + final scaleFactor = Config.textScaleFactor.obs; + final background = "".obs; + final memberUpdateInfoMap = {}; + final groupMessageReadMembers = >{}; + final groupMutedStatus = 0.obs; + final groupMemberRoleLevel = 1.obs; + final muteEndTime = 0.obs; + GroupInfo? groupInfo; + GroupMembersInfo? groupMembersInfo; + List ownerAndAdmin = []; + + final isInGroup = true.obs; + final memberCount = 0.obs; + final privateMessageList = []; + final isInBlacklist = false.obs; + final _audioPlayer = AudioPlayer(); + final _currentPlayClientMsgID = "".obs; + final isShowPopMenu = false.obs; + + final scrollingCacheMessageList = []; + final announcement = ''.obs; + late StreamSubscription conversationSub; + late StreamSubscription memberAddSub; + late StreamSubscription memberDelSub; + late StreamSubscription joinedGroupAddedSub; + late StreamSubscription joinedGroupDeletedSub; + late StreamSubscription memberInfoChangedSub; + late StreamSubscription groupInfoUpdatedSub; + late StreamSubscription friendInfoChangedSub; + StreamSubscription? userStatusChangedSub; + StreamSubscription? selfInfoUpdatedSub; + + late StreamSubscription connectionSub; + final syncStatus = IMSdkStatus.syncEnded.obs; + int? lastMinSeq; + + final showCallingMember = false.obs; + + bool _isReceivedMessageWhenSyncing = false; + bool _isStartSyncing = false; + bool _isFirstLoad = true; + + final copyTextMap = {}; + final revokedTextMessage = {}; + + String? groupOwnerID; + + final _pageSize = 40; + + RTCBridge? get rtcBridge => PackageBridge.rtcBridge; + + bool get rtcIsBusy => rtcBridge?.hasConnection == true; + + String? get userID => conversationInfo.userID; + + String? get groupID => conversationInfo.groupID; + + bool get isSingleChat => null != userID && userID!.trim().isNotEmpty; + + bool get isGroupChat => null != groupID && groupID!.trim().isNotEmpty; + + String get memberStr => isSingleChat ? "" : "($memberCount)"; + + String? get senderName => isSingleChat ? OpenIM.iMManager.userInfo.nickname : groupMembersInfo?.nickname; + + bool get isAdminOrOwner => + groupMemberRoleLevel.value == GroupRoleLevel.admin || groupMemberRoleLevel.value == GroupRoleLevel.owner; + + final directionalUsers = [].obs; + + bool isCurrentChat(Message message) { + var senderId = message.sendID; + var receiverId = message.recvID; + var groupId = message.groupID; + + var isCurSingleChat = message.isSingleChat && + isSingleChat && + (senderId == userID || senderId == OpenIM.iMManager.userID && receiverId == userID); + var isCurGroupChat = message.isGroupChat && isGroupChat && groupID == groupId; + return isCurSingleChat || isCurGroupChat; + } + + void scrollBottom() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + scrollController.jumpTo(0); + }); + } + + Future> searchMediaMessage() async { + final messageList = await OpenIM.iMManager.messageManager.searchLocalMessages( + conversationID: conversationInfo.conversationID, + messageTypeList: [MessageType.picture, MessageType.video], + count: 500); + return messageList.searchResultItems?.first.messageList?.reversed.toList() ?? []; + } + + @override + void onReady() { + _readDraftText(); + _queryUserOnlineStatus(); + _resetGroupAtType(); + _getInputState(); + _clearUnreadCount(); + + scrollController.addListener(() { + focusNode.unfocus(); + }); + super.onReady(); + } + + @override + void onInit() { + var arguments = Get.arguments; + conversationInfo = arguments['conversationInfo']; + searchMessage = arguments['searchMessage']; + nickname.value = conversationInfo.showName ?? ''; + faceUrl.value = conversationInfo.faceURL ?? ''; + _initChatConfig(); + _initPlayListener(); + _setSdkSyncDataListener(); + + conversationSub = imLogic.conversationChangedSubject.listen((value) { + final obj = value.firstWhereOrNull((e) => e.conversationID == conversationInfo.conversationID); + + if (obj != null) { + conversationInfo = obj; + } + }); + + imLogic.onRecvNewMessage = (Message message) async { + if (isCurrentChat(message)) { + if (message.contentType == MessageType.typing) { + } else { + if (!messageList.contains(message) && !scrollingCacheMessageList.contains(message)) { + _isReceivedMessageWhenSyncing = true; + if (isShowPopMenu.value || scrollController.offset != 0) { + scrollingCacheMessageList.add(message); + } else { + messageList.add(message); + scrollBottom(); + } + } + } + } + }; + + imLogic.onRecvMessageRevoked = (RevokedInfo info) { + var message = messageList.firstWhereOrNull((e) => e.clientMsgID == info.clientMsgID); + message?.notificationElem = NotificationElem(detail: jsonEncode(info)); + message?.contentType = MessageType.revokeMessageNotification; + + if (null != message) { + messageList.refresh(); + } + }; + + imLogic.onRecvC2CReadReceipt = (List list) { + try { + for (var readInfo in list) { + if (readInfo.userID == userID) { + for (var e in messageList) { + if (readInfo.msgIDList?.contains(e.clientMsgID) == true) { + e.isRead = true; + e.hasReadTime = _timestamp; + } + } + } + } + messageList.refresh(); + } catch (e) {} + }; + + joinedGroupAddedSub = imLogic.joinedGroupAddedSubject.listen((event) { + if (event.groupID == groupID) { + isInGroup.value = true; + _queryGroupInfo(); + } + }); + + joinedGroupDeletedSub = imLogic.joinedGroupDeletedSubject.listen((event) { + if (event.groupID == groupID) { + isInGroup.value = false; + inputCtrl.clear(); + } + }); + + memberAddSub = imLogic.memberAddedSubject.listen((info) { + var groupId = info.groupID; + if (groupId == groupID) { + _putMemberInfo([info]); + } + }); + + memberDelSub = imLogic.memberDeletedSubject.listen((info) { + if (info.groupID == groupID && info.userID == OpenIM.iMManager.userID) { + isInGroup.value = false; + inputCtrl.clear(); + } + }); + + memberInfoChangedSub = imLogic.memberInfoChangedSubject.listen((info) { + if (info.groupID == groupID) { + if (info.userID == OpenIM.iMManager.userID) { + muteEndTime.value = info.muteEndTime ?? 0; + groupMemberRoleLevel.value = info.roleLevel ?? GroupRoleLevel.member; + groupMembersInfo = info; + (); + } + _putMemberInfo([info]); + + final index = ownerAndAdmin.indexWhere((element) => element.userID == info.userID); + if (info.roleLevel == GroupRoleLevel.member) { + if (index > -1) { + ownerAndAdmin.removeAt(index); + } + } else if (info.roleLevel == GroupRoleLevel.admin || info.roleLevel == GroupRoleLevel.owner) { + if (index == -1) { + ownerAndAdmin.add(info); + } else { + ownerAndAdmin[index] = info; + } + } + + for (var msg in messageList) { + if (msg.sendID == info.userID) { + if (msg.isNotificationType) { + final map = json.decode(msg.notificationElem!.detail!); + final ntf = GroupNotification.fromJson(map); + ntf.opUser?.nickname = info.nickname; + ntf.opUser?.faceURL = info.faceURL; + msg.notificationElem?.detail = jsonEncode(ntf); + } else { + msg.senderFaceUrl = info.faceURL; + msg.senderNickname = info.nickname; + } + } + } + + messageList.refresh(); + } + }); + + groupInfoUpdatedSub = imLogic.groupInfoUpdatedSubject.listen((value) { + if (groupID == value.groupID) { + groupInfo = value; + nickname.value = value.groupName ?? ''; + faceUrl.value = value.faceURL ?? ''; + groupMutedStatus.value = value.status ?? 0; + memberCount.value = value.memberCount ?? 0; + } + }); + + friendInfoChangedSub = imLogic.friendInfoChangedSubject.listen((value) { + if (userID == value.userID) { + nickname.value = value.getShowName(); + faceUrl.value = value.faceURL ?? ''; + + for (var msg in messageList) { + if (msg.sendID == value.userID) { + msg.senderFaceUrl = value.faceURL; + msg.senderNickname = value.nickname; + } + } + + messageList.refresh(); + } + }); + + selfInfoUpdatedSub = imLogic.selfInfoUpdatedSubject.listen((value) { + for (var msg in messageList) { + if (msg.sendID == value.userID) { + msg.senderFaceUrl = value.faceURL; + msg.senderNickname = value.nickname; + } + } + + messageList.refresh(); + }); + + inputCtrl.addListener(() { + sendTypingMsg(focus: true); + if (_debounce?.isActive ?? false) _debounce?.cancel(); + + _debounce = Timer(1.seconds, () { + sendTypingMsg(focus: false); + }); + }); + + focusNode.addListener(() { + _lastCursorIndex = inputCtrl.selection.start; + focusNodeChanged(focusNode.hasFocus); + }); + + imLogic.onSignalingMessage = (value) { + if (value.userID == userID) { + messageList.add(value.message); + scrollBottom(); + } + }; + + imLogic.inputStateChangedSubject.listen((value) { + if (value.conversationID == conversationInfo.conversationID && value.userID == userID) { + typing.value = value.platformIDs?.isNotEmpty == true; + } + }); + super.onInit(); + } + + Future chatSetup() => isSingleChat + ? AppNavigator.startChatSetup(conversationInfo: conversationInfo) + : AppNavigator.startGroupChatSetup(conversationInfo: conversationInfo); + + void _putMemberInfo(List? list) { + list?.forEach((member) { + memberUpdateInfoMap[member.userID!] = member; + }); + + messageList.refresh(); + } + + void sendTextMsg() async { + var content = IMUtils.safeTrim(inputCtrl.text); + if (content.isEmpty) return; + Message message = await OpenIM.iMManager.messageManager.createTextMessage( + text: content, + ); + + _sendMessage(message); + } + + Future sendPicture({required String path, bool sendNow = true}) async { + final file = await IMUtils.compressImageAndGetFile(File(path)); + + var message = await OpenIM.iMManager.messageManager.createImageMessageFromFullPath( + imagePath: file!.path, + ); + + if (sendNow) { + return _sendMessage(message); + } else { + messageList.add(message); + tempMessages.add(message); + } + } + + void sendVoice(int duration, String path) async { + var message = await OpenIM.iMManager.messageManager.createSoundMessageFromFullPath( + soundPath: path, + duration: duration, + ); + _sendMessage(message); + } + + Future sendVideo( + {required String videoPath, + required String mimeType, + required int duration, + required String thumbnailPath, + bool sendNow = true}) async { + var d = duration > 1000.0 ? duration / 1000.0 : duration; + var message = await OpenIM.iMManager.messageManager.createVideoMessageFromFullPath( + videoPath: videoPath, + videoType: mimeType, + duration: d.toInt(), + snapshotPath: thumbnailPath, + ); + + if (sendNow) { + return _sendMessage(message); + } else { + messageList.add(message); + tempMessages.add(message); + } + } + + void sendFile({required String filePath, required String fileName}) async { + var message = await OpenIM.iMManager.messageManager.createFileMessageFromFullPath( + filePath: filePath, + fileName: fileName, + ); + _sendMessage(message); + } + + void sendLocation({ + required dynamic location, + }) async { + var message = await OpenIM.iMManager.messageManager.createLocationMessage( + latitude: location['latitude'], + longitude: location['longitude'], + description: location['description'], + ); + _sendMessage(message); + } + + sendForwardRemarkMsg( + String content, { + String? userId, + String? groupId, + }) async { + final message = await OpenIM.iMManager.messageManager.createTextMessage( + text: content, + ); + _sendMessage(message, userId: userId, groupId: groupId); + } + + sendForwardMsg( + Message originalMessage, { + String? userId, + String? groupId, + }) async { + var message = await OpenIM.iMManager.messageManager.createForwardMessage( + message: originalMessage, + ); + _sendMessage(message, userId: userId, groupId: groupId); + } + + void sendTypingMsg({bool focus = false}) async { + if (isSingleChat) { + OpenIM.iMManager.conversationManager + .changeInputStates(conversationID: conversationInfo.conversationID, focus: focus); + } + } + + void sendCarte({ + required String userID, + String? nickname, + String? faceURL, + }) async { + var message = await OpenIM.iMManager.messageManager.createCardMessage( + userID: userID, + nickname: nickname!, + faceURL: faceURL, + ); + _sendMessage(message); + } + + void sendCustomMsg({ + required String data, + required String extension, + required String description, + }) async { + var message = await OpenIM.iMManager.messageManager.createCustomMessage( + data: data, + extension: extension, + description: description, + ); + _sendMessage(message); + } + + Future _sendMessage( + Message message, { + String? userId, + String? groupId, + bool addToUI = true, + }) { + log('send : ${json.encode(message)}'); + userId = IMUtils.emptyStrToNull(userId); + groupId = IMUtils.emptyStrToNull(groupId); + if (null == userId && null == groupId || + userId == userID && userId != null || + groupId == groupID && groupId != null) { + if (addToUI) { + messageList.add(message); + scrollBottom(); + } + } + Logger.print('uid:$userID userId:$userId gid:$groupID groupId:$groupId'); + _reset(message); + bool useOuterValue = null != userId || null != groupId; + + final recvUserID = useOuterValue ? userId : userID; + message.recvID = recvUserID; + + return OpenIM.iMManager.messageManager + .sendMessage( + message: message, + userID: recvUserID, + groupID: useOuterValue ? groupId : groupID, + offlinePushInfo: Config.offlinePushInfo, + ) + .then((value) => _sendSucceeded(message, value)) + .catchError((error, _) => _senFailed(message, groupId, userId, error, _)) + .whenComplete(() => _completed()); + } + + void _sendSucceeded(Message oldMsg, Message newMsg) { + Logger.print('message send success----'); + oldMsg.update(newMsg); + sendStatusSub.addSafely(MsgStreamEv( + id: oldMsg.clientMsgID!, + value: true, + )); + } + + void _senFailed(Message message, String? groupId, String? userId, error, stack) async { + Logger.print('message send failed userID: $userId groupId:$groupId, catch error :$error $stack'); + message.status = MessageStatus.failed; + sendStatusSub.addSafely(MsgStreamEv( + id: message.clientMsgID!, + value: false, + )); + if (error is PlatformException) { + int code = int.tryParse(error.code) ?? 0; + if (isSingleChat) { + int? customType; + if (code == SDKErrorCode.hasBeenBlocked) { + customType = CustomMessageType.blockedByFriend; + } else if (code == SDKErrorCode.notFriend) { + customType = CustomMessageType.deletedByFriend; + } + if (null != customType) { + final hintMessage = (await OpenIM.iMManager.messageManager.createFailedHintMessage(type: customType)) + ..status = 2 + ..isRead = true; + if (userId != null) { + if (userId == userID) { + messageList.add(hintMessage); + } + } else { + messageList.add(hintMessage); + } + OpenIM.iMManager.messageManager.insertSingleMessageToLocalStorage( + message: hintMessage, + receiverID: userId ?? userID, + senderID: OpenIM.iMManager.userID, + ); + } + } else { + if ((code == SDKErrorCode.userIsNotInGroup || code == SDKErrorCode.groupDisbanded) && null == groupId) { + final status = groupInfo?.status; + final hintMessage = (await OpenIM.iMManager.messageManager.createFailedHintMessage( + type: status == 2 ? CustomMessageType.groupDisbanded : CustomMessageType.removedFromGroup)) + ..status = 2 + ..isRead = true; + messageList.add(hintMessage); + OpenIM.iMManager.messageManager.insertGroupMessageToLocalStorage( + message: hintMessage, + groupID: groupID, + senderID: OpenIM.iMManager.userID, + ); + } + } + } + } + + void _reset(Message message) { + if (message.contentType == MessageType.text) { + inputCtrl.clear(); + } + } + + void _completed() { + messageList.refresh(); + } + + void deleteMsg(Message message) async { + LoadingView.singleton.wrap(asyncFunction: () => _deleteMessage(message)); + } + + _deleteMessage(Message message) async { + try { + await OpenIM.iMManager.messageManager + .deleteMessageFromLocalAndSvr( + conversationID: conversationInfo.conversationID, + clientMsgID: message.clientMsgID!, + ) + .then((value) => privateMessageList.remove(message)) + .then((value) => messageList.remove(message)); + } catch (e) { + await OpenIM.iMManager.messageManager + .deleteMessageFromLocalStorage( + conversationID: conversationInfo.conversationID, + clientMsgID: message.clientMsgID!, + ) + .then((value) => privateMessageList.remove(message)) + .then((value) => messageList.remove(message)); + } + } + + void forward(Message? message) async { + final result = await AppNavigator.startSelectContacts( + action: SelAction.forward, + ex: null != message ? IMUtils.parseMsg(message) : null, + ); + if (null != result) { + final checkedList = result['checkedList']; + for (var info in checkedList) { + final userID = IMUtils.convertCheckedToUserID(info); + final groupID = IMUtils.convertCheckedToGroupID(info); + + if (null != message) { + sendForwardMsg(message, userId: userID, groupId: groupID); + } + } + } + } + + void markMessageAsRead(Message message, bool visible) async { + Logger.print('markMessageAsRead: ${message.textElem?.content}, $visible'); + if (visible && message.contentType! < 1000 && message.contentType! != MessageType.voice) { + var data = IMUtils.parseCustomMessage(message); + if (null != data && data['viewType'] == CustomMessageType.call) { + Logger.print('markMessageAsRead: call message $data'); + return; + } + _markMessageAsRead(message); + } + } + + _markMessageAsRead(Message message) async { + if (!message.isRead! && message.sendID != OpenIM.iMManager.userID) { + try { + Logger.print('mark conversation message as read:${message.clientMsgID!} ${message.isRead}'); + await OpenIM.iMManager.conversationManager + .markConversationMessageAsRead(conversationID: conversationInfo.conversationID); + } catch (e) { + Logger.print('failed to send group message read receipt: ${message.clientMsgID} ${message.isRead}'); + } finally { + message.isRead = true; + message.hasReadTime = _timestamp; + messageList.refresh(); + } + } + } + + _clearUnreadCount() { + if (conversationInfo.unreadCount > 0) { + OpenIM.iMManager.conversationManager + .markConversationMessageAsRead(conversationID: conversationInfo.conversationID); + } + } + + void _getInputState() async { + if (conversationInfo.isSingleChat) { + final result = + await OpenIM.iMManager.conversationManager.getInputStates(conversationInfo.conversationID, userID!); + typing.value = result?.isNotEmpty == true; + } + } + + void _changeInputStatus(bool focus) async { + if (conversationInfo.isSingleChat) { + await OpenIM.iMManager.conversationManager + .changeInputStates(conversationID: conversationInfo.conversationID, focus: focus); + } + } + + void closeToolbox() { + forceCloseToolbox.addSafely(true); + } + + void onTapLocation() async { + var location = await Get.to( + const ChatWebViewMap(host: Config.locationHost, webKey: Config.webKey, webServerKey: Config.webServerKey), + transition: Transition.cupertino, + popGesture: true, + ); + if (null != location) { + Logger.print(location); + sendLocation(location: location); + } + } + + void onTapAlbum() async { + final List? assets = await AssetPicker.pickAssets(Get.context!, + pickerConfig: AssetPickerConfig( + sortPathsByModifiedDate: true, + filterOptions: PMFilter.defaultValue(containsPathModified: true), + selectPredicate: (_, entity, isSelected) async { + if (entity.type == AssetType.image) { + if (await allowSendImageType(entity)) { + return true; + } + + IMViews.showToast(StrRes.supportsTypeHint); + + return false; + } + + if (entity.videoDuration > const Duration(seconds: 5 * 60)) { + IMViews.showToast(sprintf(StrRes.selectVideoLimit, [5]) + StrRes.minute); + return false; + } + return true; + })); + if (null != assets) { + for (var asset in assets) { + await _handleAssets(asset, sendNow: false); + } + + for (var msg in tempMessages) { + await _sendMessage(msg, addToUI: false); + } + + tempMessages.clear(); + } + } + + void onTapCamera() async { + final AssetEntity? entity = await CameraPicker.pickFromCamera( + Get.context!, + locale: Get.locale, + pickerConfig: CameraPickerConfig( + enableAudio: true, + enableRecording: true, + enableScaledPreview: false, + maximumRecordingDuration: 60.seconds, + onMinimumRecordDurationNotMet: () { + IMViews.showToast(StrRes.tapTooShort); + }, + ), + ); + _handleAssets(entity); + } + + void onTapFile() async { + await FilePicker.platform.clearTemporaryFiles(); + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: true, + ); + + if (result != null) { + for (var file in result.files) { + String? mimeType = lookupMimeType(file.name); + if (mimeType != null) { + if (IMUtils.allowImageType(mimeType)) { + sendPicture(path: file.path!); + continue; + } else if (mimeType.contains('video/')) { + try { + final videoPath = file.path!; + final mediaInfo = await VideoCompress.getMediaInfo(videoPath); + var thumbnailFile = await VideoCompress.getFileThumbnail( + videoPath, + quality: 60, + ); + sendVideo( + videoPath: videoPath, + mimeType: mimeType, + duration: mediaInfo.duration!.toInt(), + thumbnailPath: thumbnailFile.path, + ); + continue; + } catch (e, s) { + Logger.print('e :$e s:$s'); + } + } + } + sendFile(filePath: file.path!, fileName: file.name); + } + } else {} + } + + Future allowSendImageType(AssetEntity entity) async { + final mimeType = await entity.mimeTypeAsync; + + return IMUtils.allowImageType(mimeType); + } + + void onTapCarte() async { + var result = await AppNavigator.startSelectContacts( + action: SelAction.carte, + ); + if (result is UserInfo || result is FriendInfo) { + sendCarte( + userID: result.userID!, + nickname: result.nickname, + faceURL: result.faceURL, + ); + } + } + + Future _handleAssets(AssetEntity? asset, {bool sendNow = true}) async { + if (null != asset) { + Logger.print('--------assets type-----${asset.type} create time: ${asset.createDateTime}'); + final originalFile = await asset.file; + final originalPath = originalFile!.path; + var path = originalPath.toLowerCase().endsWith('.gif') ? originalPath : originalFile.path; + Logger.print('--------assets path-----$path'); + switch (asset.type) { + case AssetType.image: + await sendPicture(path: path, sendNow: sendNow); + break; + case AssetType.video: + var thumbnailFile = await IMUtils.getVideoThumbnail(File(path)); + LoadingView.singleton.show(); + final file = await IMUtils.compressVideoAndGetFile(File(path)); + LoadingView.singleton.dismiss(); + + await sendVideo( + videoPath: file!.path, + mimeType: asset.mimeType ?? IMUtils.getMediaType(path) ?? '', + duration: asset.duration, + thumbnailPath: thumbnailFile.path, + sendNow: sendNow, + ); + break; + default: + break; + } + if (Platform.isIOS) { + originalFile.deleteSync(); + } + } + } + + void onTapDirectionalMessage() async { + if (null != groupInfo) { + final list = await AppNavigator.startGroupMemberList( + groupInfo: groupInfo!, + opType: GroupMemberOpType.call, + ); + if (list is List) { + directionalUsers.assignAll(list); + } + } + } + + TextSpan? directionalText() { + if (directionalUsers.isNotEmpty) { + final temp = []; + + for (var e in directionalUsers) { + final r = TextSpan( + text: '${e.nickname ?? ''} ${directionalUsers.last == e ? '' : ','} ', + style: Styles.ts_0089FF_14sp, + ); + + temp.add(r); + } + + return TextSpan( + text: '${StrRes.directedTo}:', + style: Styles.ts_8E9AB0_14sp, + children: temp, + ); + } + + return null; + } + + void onClearDirectional() { + directionalUsers.clear(); + } + + void parseClickEvent(Message msg) async { + log('parseClickEvent:${jsonEncode(msg)}'); + if (msg.contentType == MessageType.custom) { + var data = msg.customElem!.data; + var map = json.decode(data!); + var customType = map['customType']; + if (CustomMessageType.call == customType && !isInBlacklist.value) { + } else if (CustomMessageType.meeting == customType) { + joinMeeting(msg); + } else if (CustomMessageType.tag == customType) { + final data = map['data']; + if (null != data['soundElem']) { + final soundElem = SoundElem.fromJson(data['soundElem']); + msg.soundElem = soundElem; + _playVoiceMessage(msg); + } + } + return; + } + if (msg.contentType == MessageType.voice) { + _playVoiceMessage(msg); + _markMessageAsRead(msg); + return; + } + + IMUtils.parseClickEvent( + msg, + onViewUserInfo: (userInfo) { + viewUserInfo(userInfo, isCard: msg.isCardType); + }, + meetingItemClick: joinMeeting, + onForward: () => forward(msg), + ); + } + + void onLongPressLeftAvatar(Message message) { + if (isInvalidGroup) return; + if (isGroupChat) { + var uid = message.sendID!; + + var cursor = inputCtrl.selection.base.offset; + if (!focusNode.hasFocus) { + focusNode.requestFocus(); + cursor = _lastCursorIndex; + } + if (cursor < 0) cursor = 0; + var start = inputCtrl.text.substring(0, cursor); + var end = inputCtrl.text.substring(cursor); + var at = '@$uid '; + inputCtrl.text = '$start$at$end'; + Logger.print('start:$start end:$end at:$at content:${inputCtrl.text}'); + inputCtrl.selection = TextSelection.collapsed(offset: '$start$at'.length); + _lastCursorIndex = inputCtrl.selection.start; + } + } + + void onTapLeftAvatar(Message message) { + viewUserInfo(UserInfo() + ..userID = message.sendID + ..nickname = message.senderNickname + ..faceURL = message.senderFaceUrl); + } + + void onTapRightAvatar() { + viewUserInfo(OpenIM.iMManager.userInfo); + } + + void viewUserInfo(UserInfo userInfo, {bool isCard = false}) { + if (isGroupChat && !isAdminOrOwner && !isCard) { + if (groupInfo!.lookMemberInfo != 1) { + AppNavigator.startUserProfilePane( + userID: userInfo.userID!, + nickname: userInfo.nickname, + faceURL: userInfo.faceURL, + groupID: groupID, + offAllWhenDelFriend: isSingleChat, + ); + } + } else { + AppNavigator.startUserProfilePane( + userID: userInfo.userID!, + nickname: userInfo.nickname, + faceURL: userInfo.faceURL, + groupID: groupID, + offAllWhenDelFriend: isSingleChat, + forceCanAdd: isCard, + ); + } + } + + void clickLinkText(url, type) async { + if (await canLaunch(url)) { + await launch(url); + } + } + + void _readDraftText() { + var draftText = Get.arguments['draftText']; + Logger.print('readDraftText:$draftText'); + if (null != draftText && "" != draftText) { + var map = json.decode(draftText!); + String text = map['text']; + Map atMap = map['at']; + Logger.print('text:$text atMap:$atMap'); + inputCtrl.text = text; + inputCtrl.selection = TextSelection.fromPosition(TextPosition( + offset: text.length, + )); + if (text.isNotEmpty) { + focusNode.requestFocus(); + } + } + } + + exit() async { + if (isShowPopMenu.value) { + forceCloseMenuSub.add(true); + return false; + } + Get.back(); + + return true; + } + + void focusNodeChanged(bool hasFocus) { + _changeInputStatus(hasFocus); + if (hasFocus) { + Logger.print('focus:$hasFocus'); + scrollBottom(); + } + } + + void copy(Message message) { + final content = copyTextMap[message.clientMsgID] ?? message.textElem?.content; + + if (null != content) { + IMUtils.copy(text: content.replaceAll('\u200B', '')); + } + } + + Message indexOfMessage(int index, {bool calculate = true}) => IMUtils.calChatTimeInterval( + messageList, + calculate: calculate, + ).reversed.elementAt(index); + + ValueKey itemKey(Message message) => ValueKey(message.clientMsgID!); + + @override + void onClose() { + sendTypingMsg(); + _clearUnreadCount(); + _unSubscribeUserOnlineStatus(); + inputCtrl.dispose(); + focusNode.dispose(); + _audioPlayer.dispose(); + forceCloseToolbox.close(); + conversationSub.cancel(); + sendStatusSub.close(); + memberAddSub.cancel(); + memberDelSub.cancel(); + memberInfoChangedSub.cancel(); + groupInfoUpdatedSub.cancel(); + friendInfoChangedSub.cancel(); + userStatusChangedSub?.cancel(); + selfInfoUpdatedSub?.cancel(); + forceCloseMenuSub.close(); + joinedGroupAddedSub.cancel(); + joinedGroupDeletedSub.cancel(); + connectionSub.cancel(); + + _debounce?.cancel(); + super.onClose(); + } + + String? getShowTime(Message message) { + if (message.exMap['showTime'] == true) { + return IMUtils.getChatTimeline(message.sendTime!); + } + return null; + } + + void clearAllMessage() { + messageList.clear(); + } + + void onAddEmoji(String emoji) { + var input = inputCtrl.text; + if (_lastCursorIndex != -1 && input.isNotEmpty) { + var part1 = input.substring(0, _lastCursorIndex); + var part2 = input.substring(_lastCursorIndex); + inputCtrl.text = '$part1$emoji$part2'; + _lastCursorIndex = _lastCursorIndex + emoji.length; + } else { + inputCtrl.text = '$input$emoji'; + _lastCursorIndex = emoji.length; + } + inputCtrl.selection = TextSelection.fromPosition(TextPosition( + offset: _lastCursorIndex, + )); + } + + void onDeleteEmoji() { + final input = inputCtrl.text; + final regexEmoji = emojiFaces.keys.toList().join('|').replaceAll('[', '\\[').replaceAll(']', '\\]'); + final list = [regexEmoji]; + final pattern = '(${list.toList().join('|')})'; + final emojiReg = RegExp(regexEmoji); + var reg = RegExp(pattern); + var cursor = _lastCursorIndex; + if (cursor == 0) return; + Match? match; + if (reg.hasMatch(input)) { + for (var m in reg.allMatches(input)) { + var matchText = m.group(0)!; + var start = m.start; + var end = start + matchText.length; + if (end == cursor) { + match = m; + break; + } + } + } + var matchText = match?.group(0); + if (matchText != null) { + var start = match!.start; + var end = start + matchText.length; + if (emojiReg.hasMatch(matchText)) { + inputCtrl.text = input.replaceRange(start, end, ""); + cursor = start; + } else { + inputCtrl.text = input.replaceRange(cursor - 1, cursor, ''); + --cursor; + } + } else { + inputCtrl.text = input.replaceRange(cursor - 1, cursor, ''); + --cursor; + } + _lastCursorIndex = cursor; + } + + String? get subTile => typing.value ? StrRes.typing : onlineStatusDesc.value; + + bool showOnlineStatus() => !typing.value && onlineStatusDesc.isNotEmpty; + + bool enabledReadStatus(Message message) { + if (message.isNotificationType) { + return false; + } + return true; + } + + void favoriteManage() => AppNavigator.startFavoriteMange(); + + void addEmoji(Message message) { + if (message.contentType == MessageType.picture) { + var url = message.pictureElem?.sourcePicture?.url; + var width = message.pictureElem?.sourcePicture?.width; + var height = message.pictureElem?.sourcePicture?.height; + cacheLogic.addFavoriteFromUrl(url, width, height); + IMViews.showToast(StrRes.addSuccessfully); + } else if (message.contentType == MessageType.customFace) { + var index = message.faceElem?.index; + var data = message.faceElem?.data; + if (-1 != index) { + } else if (null != data) { + var map = json.decode(data); + var url = map['url']; + var width = map['width']; + var height = map['height']; + cacheLogic.addFavoriteFromUrl(url, width, height); + IMViews.showToast(StrRes.addSuccessfully); + } + } + } + + void sendFavoritePic(int index, String url) async { + var emoji = cacheLogic.favoriteList.elementAt(index); + var message = await OpenIM.iMManager.messageManager.createFaceMessage( + data: json.encode({'url': emoji.url, 'width': emoji.width, 'height': emoji.height}), + ); + _sendMessage(message); + } + + void _initChatConfig() async { + scaleFactor.value = DataSp.getChatFontSizeFactor(); + var path = DataSp.getChatBackground(otherId) ?? ''; + if (path.isNotEmpty && (await File(path).exists())) { + background.value = path; + } + } + + String get otherId => isSingleChat ? userID! : groupID!; + + void failedResend(Message message) { + Logger.print('failedResend: ${message.clientMsgID}'); + if (message.status == MessageStatus.sending) { + return; + } + sendStatusSub.addSafely(MsgStreamEv( + id: message.clientMsgID!, + value: true, + )); + + Logger.print('failedResending: ${message.clientMsgID}'); + _sendMessage(message..status = MessageStatus.sending, addToUI: false); + } + + int readTime(Message message) { + var isPrivate = message.attachedInfoElem?.isPrivateChat ?? false; + var burnDuration = message.attachedInfoElem?.burnDuration ?? 30; + burnDuration = burnDuration > 0 ? burnDuration : 30; + if (isPrivate) { + var hasReadTime = message.hasReadTime ?? 0; + if (hasReadTime > 0) { + var end = hasReadTime + (burnDuration * 1000); + + var diff = (end - _timestamp) ~/ 1000; + + if (diff > 0) { + privateMessageList.addIf(() => !privateMessageList.contains(message), message); + } + return diff < 0 ? 0 : diff; + } + } + return 0; + } + + static int get _timestamp => DateTime.now().millisecondsSinceEpoch; + + void destroyMsg() { + for (var message in privateMessageList) { + OpenIM.iMManager.messageManager.deleteMessageFromLocalAndSvr( + conversationID: conversationInfo.conversationID, + clientMsgID: message.clientMsgID!, + ); + } + } + + Future _queryMyGroupMemberInfo() async { + if (!isGroupChat) { + return; + } + var list = await OpenIM.iMManager.groupManager.getGroupMembersInfo( + groupID: groupID!, + userIDList: [OpenIM.iMManager.userID], + ); + groupMembersInfo = list.firstOrNull; + groupMemberRoleLevel.value = groupMembersInfo?.roleLevel ?? GroupRoleLevel.member; + muteEndTime.value = groupMembersInfo?.muteEndTime ?? 0; + if (null != groupMembersInfo) { + memberUpdateInfoMap[OpenIM.iMManager.userID] = groupMembersInfo!; + } + + return; + } + + Future _queryOwnerAndAdmin() async { + if (isGroupChat) { + ownerAndAdmin = await OpenIM.iMManager.groupManager.getGroupMemberList(groupID: groupID!, filter: 5, count: 20); + } + return; + } + + void _isJoinedGroup() async { + if (!isGroupChat) { + return; + } + isInGroup.value = await OpenIM.iMManager.groupManager.isJoinedGroup( + groupID: groupID!, + ); + if (!isInGroup.value) { + return; + } + _queryGroupInfo(); + _queryOwnerAndAdmin(); + } + + void _queryGroupInfo() async { + if (!isGroupChat) { + return; + } + var list = await OpenIM.iMManager.groupManager.getGroupsInfo( + groupIDList: [groupID!], + ); + groupInfo = list.firstOrNull; + groupOwnerID = groupInfo?.ownerUserID; + groupMutedStatus.value = groupInfo?.status ?? 0; + if (null != groupInfo?.memberCount) { + memberCount.value = groupInfo!.memberCount!; + } + _queryMyGroupMemberInfo(); + } + + bool get havePermissionMute => + isGroupChat && + (groupInfo?.ownerUserID == OpenIM.iMManager.userID /*|| + groupMembersInfo?.roleLevel == 2*/ + ); + + bool isNotificationType(Message message) => message.contentType! >= 1000; + + Map getAtMapping(Message message) { + return {}; + } + + void _queryUserOnlineStatus() { + if (isSingleChat) { + OpenIM.iMManager.userManager.subscribeUsersStatus([userID!]).then((value) { + final status = value.firstWhereOrNull((element) => element.userID == userID); + _configUserStatusChanged(status); + }); + userStatusChangedSub = imLogic.userStatusChangedSubject.listen((value) { + if (value.userID == userID) { + _configUserStatusChanged(value); + } + }); + } + } + + void _unSubscribeUserOnlineStatus() { + if (isSingleChat) { + OpenIM.iMManager.userManager.unsubscribeUsersStatus([userID!]); + } + } + + void _configUserStatusChanged(UserStatusInfo? status) { + if (status != null) { + onlineStatus.value = status.status == 1; + onlineStatusDesc.value = + status.status == 0 ? StrRes.offline : _onlineStatusDes(status.platformIDs!) + StrRes.online; + } + } + + String _onlineStatusDes(List plamtforms) { + var des = []; + for (final platform in plamtforms) { + switch (platform) { + case 1: + des.add('iOS'); + break; + case 2: + des.add('Android'); + break; + case 3: + des.add('Windows'); + break; + case 4: + des.add('Mac'); + break; + case 5: + des.add('Web'); + break; + case 6: + des.add('mini_web'); + break; + case 7: + des.add('Linux'); + break; + case 8: + des.add('Android_pad'); + break; + case 9: + des.add('iPad'); + break; + default: + } + } + + return des.join('/'); + } + + void _checkInBlacklist() async { + if (userID != null) { + var list = await OpenIM.iMManager.friendshipManager.getBlacklist(); + var user = list.firstWhereOrNull((e) => e.userID == userID); + isInBlacklist.value = user != null; + } + } + + bool isExceed24H(Message message) { + int milliseconds = message.sendTime!; + return !DateUtil.isToday(milliseconds); + } + + bool isPlaySound(Message message) { + return _currentPlayClientMsgID.value == message.clientMsgID!; + } + + void _initPlayListener() { + _audioPlayer.playerStateStream.listen((state) { + switch (state.processingState) { + case ProcessingState.idle: + case ProcessingState.loading: + case ProcessingState.buffering: + case ProcessingState.ready: + break; + case ProcessingState.completed: + _currentPlayClientMsgID.value = ""; + break; + } + }); + } + + void _playVoiceMessage(Message message) async { + var isClickSame = _currentPlayClientMsgID.value == message.clientMsgID; + if (_audioPlayer.playerState.playing) { + _currentPlayClientMsgID.value = ""; + _audioPlayer.stop(); + } + if (!isClickSame) { + bool isValid = await _initVoiceSource(message); + if (isValid) { + _audioPlayer.setVolume(rtcIsBusy ? 0 : 1.0); + _audioPlayer.seek(Duration.zero); + _audioPlayer.play(); + _currentPlayClientMsgID.value = message.clientMsgID!; + } + } + } + + void stopVoice() { + if (_audioPlayer.playerState.playing) { + _currentPlayClientMsgID.value = ''; + _audioPlayer.stop(); + } + } + + Future _initVoiceSource(Message message) async { + bool isReceived = message.sendID != OpenIM.iMManager.userID; + String? path = message.soundElem?.soundPath; + String? url = message.soundElem?.sourceUrl; + bool isExistSource = false; + if (isReceived) { + if (null != url && url.trim().isNotEmpty) { + isExistSource = true; + _audioPlayer.setUrl(url); + } + } else { + bool existFile = false; + if (path != null && path.trim().isNotEmpty) { + var file = File(path); + existFile = await file.exists(); + } + if (existFile) { + isExistSource = true; + _audioPlayer.setFilePath(path!); + } else if (null != url && url.trim().isNotEmpty) { + isExistSource = true; + _audioPlayer.setUrl(url); + } + } + return isExistSource; + } + + void onPopMenuShowChanged(show) { + isShowPopMenu.value = show; + if (!show && scrollingCacheMessageList.isNotEmpty) { + messageList.addAll(scrollingCacheMessageList); + scrollingCacheMessageList.clear(); + } + } + + String? getNewestNickname(Message message) { + if (isSingleChat) null; + + return message.senderNickname; + } + + String? getNewestFaceURL(Message message) { + return message.senderFaceUrl; + } + + bool get isInvalidGroup => !isInGroup.value && isGroupChat; + + void _resetGroupAtType() { + if (conversationInfo.groupAtType != GroupAtType.atNormal) { + OpenIM.iMManager.conversationManager.resetConversationGroupAtType( + conversationID: conversationInfo.conversationID, + ); + } + } + + void revokeMsgV2(Message message) async { + late bool canRevoke; + if (isGroupChat) { + if (message.sendID == OpenIM.iMManager.userID) { + canRevoke = true; + } else { + var list = await LoadingView.singleton + .wrap(asyncFunction: () => OpenIM.iMManager.groupManager.getGroupOwnerAndAdmin(groupID: groupID!)); + var sender = list.firstWhereOrNull((e) => e.userID == message.sendID); + var revoker = list.firstWhereOrNull((e) => e.userID == OpenIM.iMManager.userID); + + if (revoker != null && sender == null) { + canRevoke = true; + } else if (revoker == null && sender != null) { + canRevoke = false; + } else if (revoker != null && sender != null) { + if (revoker.roleLevel == sender.roleLevel) { + canRevoke = false; + } else if (revoker.roleLevel == GroupRoleLevel.owner) { + canRevoke = true; + } else { + canRevoke = false; + } + } else { + canRevoke = false; + } + } + } else { + if (message.sendID == OpenIM.iMManager.userID) { + canRevoke = true; + } + } + if (canRevoke) { + try { + await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.messageManager.revokeMessage( + conversationID: conversationInfo.conversationID, + clientMsgID: message.clientMsgID!, + ), + ); + message.contentType = MessageType.revokeMessageNotification; + message.notificationElem = NotificationElem(detail: jsonEncode(_buildRevokeInfo(message))); + messageList.refresh(); + } catch (e) { + IMViews.showToast(e.toString()); + } + } else { + IMViews.showToast('no permission'); + } + } + + RevokedInfo _buildRevokeInfo(Message message) { + return RevokedInfo.fromJson({ + 'revokerID': OpenIM.iMManager.userInfo.userID, + 'revokerRole': 0, + 'revokerNickname': OpenIM.iMManager.userInfo.nickname, + 'clientMsgID': message.clientMsgID, + 'revokeTime': 0, + 'sourceMessageSendTime': 0, + 'sourceMessageSendID': message.sendID, + 'sourceMessageSenderNickname': message.senderNickname, + 'sessionType': message.sessionType, + }); + } + + bool showCopyMenu(Message message) { + return message.isTextType; + } + + bool showDelMenu(Message message) { + return true; + } + + bool showForwardMenu(Message message) { + if (message.status != MessageStatus.succeeded) { + return false; + } + if (message.isNotificationType) { + return false; + } + return true; + } + + bool showReplyMenu(Message message) { + if (message.status != MessageStatus.succeeded) { + return false; + } + return message.isTextType || + message.isVideoType || + message.isPictureType || + message.isLocationType || + message.isFileType || + message.isCardType || + message.isCustomFaceType; + } + + bool showRevokeMenu(Message message) { + if (message.status != MessageStatus.succeeded || + message.isNotificationType || + isExceed24H(message) && isSingleChat) { + return false; + } + if (isGroupChat) { + if (groupMemberRoleLevel.value == GroupRoleLevel.owner || + (groupMemberRoleLevel.value == GroupRoleLevel.admin && + ownerAndAdmin.firstWhereOrNull((element) => element.userID == message.sendID) == null)) { + return true; + } + } + if (message.sendID == OpenIM.iMManager.userID) { + if (DateTime.now().millisecondsSinceEpoch - (message.sendTime ??= 0) < (1000 * 60 * 5)) { + return true; + } + } + return false; + } + + bool showAddEmojiMenu(Message message) { + if (message.status != MessageStatus.succeeded) { + return false; + } + return message.contentType == MessageType.picture || message.contentType == MessageType.customFace; + } + + WillPopCallback? willPop() { + return isShowPopMenu.value ? () async => exit() : null; + } + + void expandCallingMemberPanel() { + showCallingMember.value = !showCallingMember.value; + } + + void call() { + if (rtcIsBusy) { + IMViews.showToast(StrRes.callingBusy); + return; + } + + IMViews.openIMCallSheet(nickname.value, (index) { + imLogic.call( + callObj: CallObj.single, + callType: index == 0 ? CallType.audio : CallType.video, + inviteeUserIDList: [if (isSingleChat) userID!], + ); + }); + } + + void onScrollToTop() { + if (scrollingCacheMessageList.isNotEmpty) { + messageList.addAll(scrollingCacheMessageList); + scrollingCacheMessageList.clear(); + } + } + + String get markText { + String? phoneNumber = imLogic.userInfo.value.phoneNumber; + if (phoneNumber != null) { + int start = phoneNumber.length > 4 ? phoneNumber.length - 4 : 0; + final sub = phoneNumber.substring(start); + return "${OpenIM.iMManager.userInfo.nickname!}$sub"; + } + return OpenIM.iMManager.userInfo.nickname ?? ''; + } + + bool isFailedHintMessage(Message message) { + if (message.contentType == MessageType.custom) { + var data = message.customElem!.data; + var map = json.decode(data!); + var customType = map['customType']; + return customType == CustomMessageType.deletedByFriend || customType == CustomMessageType.blockedByFriend; + } + return false; + } + + void sendFriendVerification() => AppNavigator.startSendVerificationApplication(userID: userID); + + void _setSdkSyncDataListener() { + connectionSub = imLogic.imSdkStatusPublishSubject.listen((value) { + syncStatus.value = value.status; + if (value.status == IMSdkStatus.syncStart) { + _isStartSyncing = true; + } else if (value.status == IMSdkStatus.syncEnded) { + if (/*_isReceivedMessageWhenSyncing &&*/ _isStartSyncing) { + _isReceivedMessageWhenSyncing = false; + _isStartSyncing = false; + _isFirstLoad = true; + _loadHistoryForSyncEnd(); + } + } else if (value.status == IMSdkStatus.syncFailed) { + _isReceivedMessageWhenSyncing = false; + _isStartSyncing = false; + } + }); + } + + bool get isSyncFailed => syncStatus.value == IMSdkStatus.syncFailed; + + String? get syncStatusStr { + switch (syncStatus.value) { + case IMSdkStatus.syncStart: + case IMSdkStatus.synchronizing: + return StrRes.synchronizing; + case IMSdkStatus.syncFailed: + return StrRes.syncFailed; + default: + return null; + } + } + + bool showBubbleBg(Message message) { + return !isNotificationType(message) && !isFailedHintMessage(message) && !isRevokeMessage(message); + } + + bool isRevokeMessage(Message message) { + return message.contentType == MessageType.revokeMessageNotification; + } + + void markRevokedMessage(Message message) { + if (message.contentType == MessageType.text) { + revokedTextMessage[message.clientMsgID!] = jsonEncode(message); + } + } + + Future _fetchHistoryMessages() { + Logger.print( + '_fetchHistoryMessages: is first load: $_isFirstLoad, last client id: ${_isFirstLoad ? null : messageList.firstOrNull?.clientMsgID}'); + return OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList( + conversationID: conversationInfo.conversationID, + count: _pageSize, + startMsg: _isFirstLoad ? null : messageList.firstOrNull, + ); + } + + Future onScrollToBottomLoad() async { + late List list; + final result = await _fetchHistoryMessages(); + if (result.messageList == null || result.messageList!.isEmpty) { + _getGroupInfoAfterLoadMessage(); + + return false; + } + list = result.messageList!; + if (_isFirstLoad) { + _isFirstLoad = false; + // remove the message that has been timed down + list.removeWhere((msg) => _isBeDeleteMessage(msg)); + messageList.assignAll(list); + scrollBottom(); + + _getGroupInfoAfterLoadMessage(); + } else { + list.removeWhere((msg) => _isBeDeleteMessage(msg)); + messageList.insertAll(0, list); + } + + return result.isEnd != true; + } + + Future _loadHistoryForSyncEnd() async { + final result = await OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList( + conversationID: conversationInfo.conversationID, + count: messageList.length < _pageSize ? _pageSize : messageList.length, + startMsg: null, + ); + if (result.messageList == null || result.messageList!.isEmpty) return; + final list = result.messageList!; + list.removeWhere((msg) => _isBeDeleteMessage(msg)); + + final offset = scrollController.offset; + messageList.assignAll(list); + scrollController.jumpTo(offset); + } + + bool _isBeDeleteMessage(Message message) { + final isPrivate = message.attachedInfoElem?.isPrivateChat ?? false; + final hasReadTime = message.hasReadTime ?? 0; + if (isPrivate && hasReadTime > 0) { + return readTime(message) <= 0; + } + return false; + } + + void _getGroupInfoAfterLoadMessage() { + if (isGroupChat && ownerAndAdmin.isEmpty) { + _isJoinedGroup(); + } else { + _checkInBlacklist(); + } + } + + recommendFriendCarte(UserInfo userInfo) async { + final result = await AppNavigator.startSelectContacts( + action: SelAction.recommend, + ex: '[${StrRes.carte}]${userInfo.nickname}', + ); + if (null != result) { + final customEx = result['customEx']; + final checkedList = result['checkedList']; + for (var info in checkedList) { + final userID = IMUtils.convertCheckedToUserID(info); + final groupID = IMUtils.convertCheckedToGroupID(info); + if (customEx is String && customEx.isNotEmpty) { + _sendMessage( + await OpenIM.iMManager.messageManager.createTextMessage( + text: customEx, + ), + userId: userID, + groupId: groupID, + ); + } + _sendMessage( + await OpenIM.iMManager.messageManager.createCardMessage( + userID: userInfo.userID!, + nickname: userInfo.nickname!, + faceURL: userInfo.faceURL, + ), + userId: userID, + groupId: groupID, + ); + } + } + } + + void joinMeeting(Message msg) {} + + @override + void onDetached() {} + + @override + void onHidden() {} + + @override + void onInactive() {} + + @override + void onPaused() {} + + @override + void onResumed() { + _loadHistoryForSyncEnd(); + } +} diff --git a/mobile-next/lib/pages/chat/chat_setup/chat_setup_binding.dart b/mobile-next/lib/pages/chat/chat_setup/chat_setup_binding.dart new file mode 100644 index 0000000..50153c8 --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_setup/chat_setup_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'chat_setup_logic.dart'; + +class ChatSetupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ChatSetupLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/chat_setup/chat_setup_logic.dart b/mobile-next/lib/pages/chat/chat_setup/chat_setup_logic.dart new file mode 100644 index 0000000..23678b3 --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_setup/chat_setup_logic.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/app_controller.dart'; +import '../../../core/controller/im_controller.dart'; +import '../../../routes/app_navigator.dart'; +import '../chat_logic.dart'; + +class ChatSetupLogic extends GetxController { + final chatLogic = Get.find(tag: GetTags.chat); + final appLogic = Get.find(); + final imLogic = Get.find(); + late Rx conversationInfo; + late StreamSubscription ccSub; + late StreamSubscription fcSub; + + String get conversationID => conversationInfo.value.conversationID; + + bool get isPinned => conversationInfo.value.isPinned == true; + + @override + void onClose() { + ccSub.cancel(); + fcSub.cancel(); + super.onClose(); + } + + @override + void onInit() { + conversationInfo = Rx(Get.arguments['conversationInfo']); + final sourceID = conversationInfo.value.conversationType == ConversationType.single + ? conversationInfo.value.userID + : conversationInfo.value.groupID; + OpenIM.iMManager.conversationManager + .getOneConversation(sourceID: sourceID!, sessionType: conversationInfo.value.conversationType!) + .then((value) { + conversationInfo.value = value; + }); + + ccSub = imLogic.conversationChangedSubject.listen((newList) { + for (var newValue in newList) { + if (newValue.conversationID == conversationID) { + conversationInfo.update((val) { + val?.burnDuration = newValue.burnDuration ?? 30; + val?.isPrivateChat = newValue.isPrivateChat; + val?.isPinned = newValue.isPinned; + + val?.recvMsgOpt = newValue.recvMsgOpt; + val?.isMsgDestruct = newValue.isMsgDestruct; + val?.msgDestructTime = newValue.msgDestructTime; + val?.showName = newValue.showName; + }); + break; + } + } + }); + + fcSub = imLogic.friendInfoChangedSubject.listen((value) { + if (conversationInfo.value.userID == value.userID) { + conversationInfo.update((val) { + val?.showName = value.getShowName(); + val?.faceURL = value.faceURL; + }); + } + }); + super.onInit(); + } + + void toggleTopContacts() async { + await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.conversationManager.pinConversation( + conversationID: conversationID, + isPinned: !isPinned, + ), + ); + } + + void clearChatHistory() async { + var confirm = await Get.dialog(CustomDialog( + title: StrRes.confirmClearChatHistory, + rightText: StrRes.clearAll, + )); + if (confirm == true) { + await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.conversationManager.clearConversationAndDeleteAllMsg( + conversationID: conversationID, + ), + ); + chatLogic.clearAllMessage(); + IMViews.showToast(StrRes.clearSuccessfully); + } + } + + void createGroup() => AppNavigator.startCreateGroup(defaultCheckedList: [ + UserInfo( + userID: conversationInfo.value.userID, + faceURL: conversationInfo.value.faceURL, + nickname: conversationInfo.value.showName, + ), + OpenIM.iMManager.userInfo, + ]); + + void viewUserInfo() => AppNavigator.startUserProfilePane( + userID: conversationInfo.value.userID!, + nickname: conversationInfo.value.showName, + faceURL: conversationInfo.value.faceURL, + ); +} diff --git a/mobile-next/lib/pages/chat/chat_setup/chat_setup_view.dart b/mobile-next/lib/pages/chat/chat_setup/chat_setup_view.dart new file mode 100644 index 0000000..27b450e --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_setup/chat_setup_view.dart @@ -0,0 +1,154 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'chat_setup_logic.dart'; + +class ChatSetupPage extends StatelessWidget { + final logic = Get.find(); + + ChatSetupPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(), + backgroundColor: Styles.c_F8F9FA, + body: SingleChildScrollView( + child: Obx(() => Column( + children: [ + _buildBaseInfoView(), + 17.verticalSpace, + _buildItemView( + text: StrRes.topContacts, + switchOn: logic.isPinned, + onChanged: (_) => logic.toggleTopContacts(), + showSwitchButton: true, + isTopRadius: true, + ), + 10.verticalSpace, + _buildItemView( + text: StrRes.clearChatHistory, + textStyle: Styles.ts_FF381F_17sp, + onTap: logic.clearChatHistory, + showRightArrow: true, + isTopRadius: true, + isBottomRadius: true, + ), + ], + )), + ), + ); + } + + Widget _buildBaseInfoView() => Container( + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.h), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 10.h), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(6.r), + ), + child: Row( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.viewUserInfo, + child: SizedBox( + width: 60.w, + child: Column( + children: [ + AvatarView( + width: 44.w, + height: 44.h, + text: logic.conversationInfo.value.showName, + url: logic.conversationInfo.value.faceURL, + ), + 8.verticalSpace, + (logic.conversationInfo.value.showName ?? '').toText + ..style = Styles.ts_8E9AB0_14sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ], + ), + ), + ), + SizedBox( + width: 60.w, + child: Column( + children: [ + ImageRes.addFriendTobeGroup.toImage + ..width = 44.w + ..height = 44.h + ..onTap = logic.createGroup, + 8.verticalSpace, + ''.toText + ..style = Styles.ts_8E9AB0_14sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ], + ), + ), + ], + ), + ); + + Widget _buildItemView({ + required String text, + String? hintText, + TextStyle? textStyle, + String? value, + bool switchOn = false, + bool isTopRadius = false, + bool isBottomRadius = false, + bool showRightArrow = false, + bool showSwitchButton = false, + ValueChanged? onChanged, + Function()? onTap, + }) => + GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.translucent, + child: Container( + height: hintText == null ? 46.h : 68.h, + margin: EdgeInsets.symmetric(horizontal: 10.w), + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topRight: Radius.circular(isTopRadius ? 6.r : 0), + topLeft: Radius.circular(isTopRadius ? 6.r : 0), + bottomLeft: Radius.circular(isBottomRadius ? 6.r : 0), + bottomRight: Radius.circular(isBottomRadius ? 6.r : 0), + ), + ), + child: Row( + children: [ + null != hintText + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text.toText..style = textStyle ?? Styles.ts_0C1C33_17sp, + hintText.toText..style = Styles.ts_8E9AB0_14sp, + ], + ) + : (text.toText..style = textStyle ?? Styles.ts_0C1C33_17sp), + const Spacer(), + if (null != value) value.toText..style = Styles.ts_8E9AB0_14sp, + if (showSwitchButton) + CupertinoSwitch( + value: switchOn, + activeColor: Styles.c_0089FF, + onChanged: onChanged, + ), + if (showRightArrow) + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ); +} diff --git a/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_binding.dart b/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_binding.dart new file mode 100644 index 0000000..2d3b23c --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'favorite_manage_logic.dart'; + +class FavoriteManageBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => FavoriteManageLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_logic.dart b/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_logic.dart new file mode 100644 index 0000000..106e9a2 --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_logic.dart @@ -0,0 +1,53 @@ +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; + +class FavoriteManageLogic extends GetxController { + var cacheLogic = Get.find(); + var isMultiModel = false.obs; + var selectedList = [].obs; + + void addFavorite() async { + final List? assets = await AssetPicker.pickAssets( + Get.context!, + pickerConfig: const AssetPickerConfig(requestType: RequestType.image), + ); + if (null != assets) { + for (var asset in assets) { + var path = (await asset.file)!.path; + var width = asset.width; + var height = asset.height; + switch (asset.type) { + case AssetType.image: + cacheLogic.addFavoriteFromPath(path, width, height); + IMViews.showToast(StrRes.addSuccessfully); + break; + default: + break; + } + } + } + } + + void updateSelectedStatus(String url) { + if (selectedList.contains(url)) { + selectedList.remove(url); + } else { + selectedList.add(url); + } + } + + void manage() { + isMultiModel.value = !isMultiModel.value; + selectedList.clear(); + } + + void delete() { + if (selectedList.isNotEmpty) { + cacheLogic.delFavoriteList(selectedList); + selectedList.clear(); + } + } + + bool isChecked(String url) => selectedList.contains(url); +} diff --git a/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_view.dart b/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_view.dart new file mode 100644 index 0000000..587313e --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_setup/favorite_manage/favorite_manage_view.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'favorite_manage_logic.dart'; + +class FavoriteManagePage extends StatelessWidget { + final logic = Get.find(); + + FavoriteManagePage({super.key}); + + @override + Widget build(BuildContext context) { + return Obx(() => Scaffold( + appBar: TitleBar.back( + title: StrRes.favoriteFace, + right: StrRes.favoriteManage.toText + ..onTap = logic.manage + ..style = Styles.ts_0C1C33_17sp, + ), + body: Column( + children: [ + Expanded( + child: GridView.builder( + padding: EdgeInsets.symmetric( + horizontal: 22.w, + vertical: 10.h, + ), + itemCount: logic.cacheLogic.urlList.length + 1, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 1, + mainAxisSpacing: 22.h, + crossAxisSpacing: 22.w, + ), + itemBuilder: (BuildContext context, int index) { + if (index == 0) { + return GestureDetector( + onTap: logic.addFavorite, + child: ImageRes.addFavorite.toImage + ..width = 66.w + ..height = 66.h, + ); + } + var url = logic.cacheLogic.urlList.elementAt(index - 1); + return GestureDetector( + onTap: logic.isMultiModel.value ? () => logic.updateSelectedStatus(url) : null, + child: Stack( + children: [ + ImageUtil.networkImage( + url: url, + width: 66.w, + height: 66.h, + cacheWidth: 66.w.toInt(), + fit: BoxFit.cover, + ), + if (logic.isMultiModel.value) + Positioned( + right: 4.w, + bottom: 4.h, + child: ChatRadio(checked: logic.isChecked(url)), + ), + ], + ), + ); + }, + ), + ), + _buildBottomBar(), + ], + ), + )); + } + + Widget _buildBottomBar() => Container( + height: MediaQuery.of(Get.context!).viewPadding.bottom + 48.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + border: BorderDirectional( + top: BorderSide(color: Styles.c_E8EAEF, width: 1), + ), + ), + child: Row( + children: [ + sprintf(StrRes.favoriteCount, [logic.cacheLogic.favoriteList.length]).toText..style = Styles.ts_8E9AB0_16sp, + const Spacer(), + if (logic.isMultiModel.value) + GestureDetector( + onTap: logic.delete, + behavior: HitTestBehavior.translucent, + child: sprintf(StrRes.favoriteDel, [logic.selectedList.length]).toText..style = Styles.ts_0089FF_16sp, + ), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/chat/chat_view.dart b/mobile-next/lib/pages/chat/chat_view.dart new file mode 100644 index 0000000..48fa8f1 --- /dev/null +++ b/mobile-next/lib/pages/chat/chat_view.dart @@ -0,0 +1,273 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../widgets/file_download_progress.dart'; +import 'chat_logic.dart'; + +class ChatPage extends StatelessWidget { + final logic = Get.find(tag: GetTags.chat); + + ChatPage({super.key}); + + Widget _buildItemView(Message message) => ChatItemView( + key: logic.itemKey(message), + message: message, + textScaleFactor: logic.scaleFactor.value, + allAtMap: logic.getAtMapping(message), + timelineStr: logic.getShowTime(message), + sendStatusSubject: logic.sendStatusSub, + closePopMenuSubject: logic.forceCloseMenuSub, + enabledReadStatus: logic.enabledReadStatus(message), + readingDuration: logic.readTime(message), + isPlayingSound: logic.isPlaySound(message), + showLongPressMenu: !logic.isInvalidGroup, + leftNickname: logic.getNewestNickname(message), + leftFaceUrl: logic.getNewestFaceURL(message), + rightNickname: logic.senderName, + rightFaceUrl: OpenIM.iMManager.userInfo.faceURL, + showLeftNickname: !logic.isSingleChat, + showRightNickname: !logic.isSingleChat, + enabledCopyMenu: logic.showCopyMenu(message), + enabledRevokeMenu: logic.showRevokeMenu(message), + enabledReplyMenu: logic.showReplyMenu(message), + enabledForwardMenu: logic.showForwardMenu(message), + enabledDelMenu: logic.showDelMenu(message), + enabledAddEmojiMenu: logic.showAddEmojiMenu(message), + onFailedToResend: () => logic.failedResend(message), + onPopMenuShowChanged: logic.onPopMenuShowChanged, + onClickItemView: () => logic.parseClickEvent(message), + onTapCopyMenu: () => logic.copy(message), + onTapDelMenu: () => logic.deleteMsg(message), + onTapForwardMenu: () => logic.forward(message), + onTapRevokeMenu: () { + logic.markRevokedMessage(message); + logic.revokeMsgV2(message); + }, + onTapAddEmojiMenu: () => logic.addEmoji(message), + visibilityChange: (msg, visible) { + logic.markMessageAsRead(message, visible); + }, + onLongPressLeftAvatar: () { + logic.onLongPressLeftAvatar(message); + }, + onLongPressRightAvatar: () {}, + onTapLeftAvatar: () { + logic.onTapLeftAvatar(message); + }, + onVisibleTrulyText: (text) { + logic.copyTextMap[message.clientMsgID] = text; + }, + customTypeBuilder: _buildCustomTypeItemView, + fileDownloadProgressView: FileDownloadProgressView(message), + patterns: [ + MatchPattern( + type: PatternType.email, + onTap: logic.clickLinkText, + ), + MatchPattern( + type: PatternType.url, + onTap: logic.clickLinkText, + ), + MatchPattern( + type: PatternType.mobile, + onTap: logic.clickLinkText, + ), + MatchPattern( + type: PatternType.tel, + onTap: logic.clickLinkText, + ), + ], + mediaItemBuilder: (context, message) { + return _buildMediaItem(context, message); + }, + onTapUserProfile: handleUserProfileTap, + ); + + void handleUserProfileTap(({String userID, String name, String? faceURL, String? groupID}) userProfile) { + final userInfo = UserInfo(userID: userProfile.userID, nickname: userProfile.name, faceURL: userProfile.faceURL); + logic.viewUserInfo(userInfo); + } + + Widget? _buildMediaItem(BuildContext context, Message message) { + if (message.contentType != MessageType.picture && message.contentType != MessageType.video) { + return null; + } + + return GestureDetector( + onTap: () async { + try { + logic.stopVoice(); + + IMUtils.previewMediaFile( + context: context, + message: message, + onAutoPlay: (index) { + return !logic.playOnce; + }, + muted: logic.rtcIsBusy, + onPageChanged: (index) { + logic.playOnce = true; + }, + onOperate: (type) { + if (type == OperateType.forward) { + logic.forward(message); + } + }).then((value) { + logic.playOnce = false; + }); + } catch (e) { + IMViews.showToast(e.toString()); + } + }, + child: Hero( + tag: message.clientMsgID!, + child: _buildMediaContent(message), + placeholderBuilder: (BuildContext context, Size heroSize, Widget child) => child, + ), + ); + } + + Widget _buildMediaContent(Message message) { + final isOutgoing = message.sendID == OpenIM.iMManager.userID; + + if (message.isVideoType) { + return ChatVideoView( + isISend: isOutgoing, + message: message, + ); + } else { + return ChatPictureView( + isISend: isOutgoing, + message: message, + ); + } + } + + CustomTypeInfo? _buildCustomTypeItemView(_, Message message) { + final data = IMUtils.parseCustomMessage(message); + if (null != data) { + final viewType = data['viewType']; + if (viewType == CustomMessageType.call) { + final type = data['type']; + final content = data['content']; + final view = ChatCallItemView(type: type, content: content); + return CustomTypeInfo(view); + } else if (viewType == CustomMessageType.deletedByFriend || viewType == CustomMessageType.blockedByFriend) { + final view = ChatFriendRelationshipAbnormalHintView( + name: logic.nickname.value, + onTap: logic.sendFriendVerification, + blockedByFriend: viewType == CustomMessageType.blockedByFriend, + deletedByFriend: viewType == CustomMessageType.deletedByFriend, + ); + return CustomTypeInfo(view, false, false); + } else if (viewType == CustomMessageType.removedFromGroup) { + return CustomTypeInfo( + StrRes.removedFromGroupHint.toText..style = Styles.ts_8E9AB0_12sp, + false, + false, + ); + } else if (viewType == CustomMessageType.groupDisbanded) { + return CustomTypeInfo( + StrRes.groupDisbanded.toText..style = Styles.ts_8E9AB0_12sp, + false, + false, + ); + } else if (viewType == CustomMessageType.tag) { + final isISend = message.sendID == OpenIM.iMManager.userID; + if (null != data['textElem']) { + final textElem = TextElem.fromJson(data['textElem']); + return CustomTypeInfo( + ChatText( + text: textElem.content ?? '', + textScaleFactor: logic.scaleFactor.value, + model: TextModel.normal, + ), + ); + } else if (null != data['soundElem']) { + final soundElem = SoundElem.fromJson(data['soundElem']); + return CustomTypeInfo( + ChatVoiceView( + isISend: isISend, + soundPath: soundElem.soundPath, + soundUrl: soundElem.sourceUrl, + duration: soundElem.duration, + isPlaying: logic.isPlaySound(message), + ), + ); + } + } + } + return null; + } + + Widget? get _groupCallHintView => null; + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: logic.willPop(), + child: ChatVoiceRecordLayout( + onCompleted: logic.sendVoice, + builder: (bar) => Obx(() { + return Scaffold( + backgroundColor: Styles.c_F0F2F6, + appBar: TitleBar.chat( + title: logic.nickname.value, + member: logic.memberStr, + subTitle: logic.subTile, + showOnlineStatus: logic.showOnlineStatus(), + isOnline: logic.onlineStatus.value, + onCloseMultiModel: logic.exit, + onClickMoreBtn: logic.chatSetup, + onClickCallBtn: logic.call, + ), + body: SafeArea( + child: WaterMarkBgView( + text: '', + path: logic.background.value, + backgroundColor: Styles.c_FFFFFF, + floatView: _groupCallHintView, + bottomView: ChatInputBox( + forceCloseToolboxSub: logic.forceCloseToolbox, + controller: logic.inputCtrl, + focusNode: logic.focusNode, + isNotInGroup: logic.isInvalidGroup, + directionalText: logic.directionalText(), + onCloseDirectional: logic.onClearDirectional, + onSend: (v) => logic.sendTextMsg(), + toolbox: ChatToolBox( + onTapAlbum: logic.onTapAlbum, + onTapCamera: logic.onTapCamera, + onTapCall: logic.call, + onTapCard: logic.onTapCarte, + onTapFile: logic.onTapFile, + onTapLocation: logic.onTapLocation, + ), + voiceRecordBar: bar, + emojiView: ChatEmojiView( + textEditingController: logic.inputCtrl, + favoriteList: logic.cacheLogic.urlList, + onAddFavorite: logic.favoriteManage, + onSelectedFavorite: logic.sendFavoritePic, + ), + ), + child: ChatListView( + onTouch: () => logic.closeToolbox(), + itemCount: logic.messageList.length, + controller: logic.scrollController, + onScrollToBottomLoad: logic.onScrollToBottomLoad, + onScrollToTop: logic.onScrollToTop, + itemBuilder: (_, index) { + final message = logic.indexOfMessage(index); + return Obx(() => _buildItemView(message)); + }, + ), + ), + )); + }), + ), + ); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_binding.dart b/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_binding.dart new file mode 100644 index 0000000..b722553 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'edit_name_logic.dart'; + +class EditGroupNameBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => EditGroupNameLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_logic.dart b/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_logic.dart new file mode 100644 index 0000000..032659c --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_logic.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/chat/group_setup/group_setup_logic.dart'; +import 'package:openim_common/openim_common.dart'; + +enum EditNameType { + myGroupMemberNickname, + groupNickname, +} + +class EditGroupNameLogic extends GetxController { + final groupSetupLogic = Get.find(); + late TextEditingController inputCtrl; + late EditNameType type; + String? faceUrl; + + @override + void onInit() { + type = Get.arguments['type']; + faceUrl = Get.arguments['faceUrl']; + inputCtrl = TextEditingController( + text: type == EditNameType.groupNickname ? groupSetupLogic.groupInfo.value.groupName : groupSetupLogic.myGroupMembersInfo.value.nickname, + ); + super.onInit(); + } + + @override + void onClose() { + inputCtrl.dispose(); + super.onClose(); + } + + String? get title => type == EditNameType.myGroupMemberNickname ? StrRes.myGroupMemberNickname : StrRes.groupName; + + void save() async { + if (inputCtrl.text.trim().length > 16) { + return IMViews.showToast(StrRes.createGroupTips); + } + await LoadingView.singleton.wrap(asyncFunction: () async { + if (type == EditNameType.groupNickname) { + await OpenIM.iMManager.groupManager + .setGroupInfo(GroupInfo(groupID: groupSetupLogic.groupInfo.value.groupID, groupName: inputCtrl.text.trim())); + } else if (type == EditNameType.myGroupMemberNickname) { + await OpenIM.iMManager.groupManager.setGroupMemberNickname( + groupID: groupSetupLogic.groupInfo.value.groupID, + userID: OpenIM.iMManager.userID, + groupNickname: inputCtrl.text.trim(), + ); + } + }); + IMViews.showToast(StrRes.setSuccessfully); + Get.back(); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_view.dart b/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_view.dart new file mode 100644 index 0000000..ca68f40 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/edit_name/edit_name_view.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'edit_name_logic.dart'; + +class EditGroupNamePage extends StatelessWidget { + final logic = Get.find(); + + EditGroupNamePage({super.key}); + + @override + Widget build(BuildContext context) { + if (logic.type == EditNameType.groupNickname) { + return Scaffold( + backgroundColor: Colors.white, + appBar: TitleBar.back(), + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 67.verticalSpace, + StrRes.editGroupName.toText..style = Styles.ts_0C1C33_20sp, + 10.verticalSpace, + StrRes.editGroupTips.toText..style = Styles.ts_8E9AB0_15sp, + 37.verticalSpace, + Row( + children: [ + 50.horizontalSpace, + AvatarView( + url: logic.faceUrl, + isGroup: true, + ), + Expanded( + child: TextField( + controller: logic.inputCtrl, + style: Styles.ts_0C1C33_17sp, + autofocus: true, + inputFormatters: [LengthLimitingTextInputFormatter(16)], + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric( + vertical: 10.h, + horizontal: 12.w, + ), + ), + ), + ), + 40.horizontalSpace, + ], + ), + const Expanded(child: SizedBox.shrink()), + Button( + margin: EdgeInsets.symmetric(horizontal: 100.w), + text: StrRes.save, + onTap: logic.save, + ), + const Expanded(child: SizedBox.shrink()), + ], + )); + } + return Scaffold( + appBar: TitleBar.back( + title: logic.title, + right: StrRes.save.toText + ..style = Styles.ts_0C1C33_17sp + ..onTap = logic.save, + ), + backgroundColor: Styles.c_FFFFFF, + body: Column( + children: [ + 22.verticalSpace, + Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Styles.c_E8EAEF, + borderRadius: BorderRadius.circular(4.r), + ), + child: TextField( + controller: logic.inputCtrl, + style: Styles.ts_0C1C33_17sp, + autofocus: true, + inputFormatters: [LengthLimitingTextInputFormatter(16)], + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric( + vertical: 10.h, + horizontal: 12.w, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_binding.dart b/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_binding.dart new file mode 100644 index 0000000..e6d771c --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'group_manage_logic.dart'; + +class GroupManageBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GroupManageLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_logic.dart b/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_logic.dart new file mode 100644 index 0000000..71af0db --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_logic.dart @@ -0,0 +1,32 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/chat/group_setup/group_setup_logic.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../../routes/app_navigator.dart'; +import '../group_member_list/group_member_list_logic.dart'; + +class GroupManageLogic extends GetxController { + final groupSetupLogic = Get.find(); + + Rx get groupInfo => groupSetupLogic.groupInfo; + + void transferGroupOwnerRight() async { + var result = await AppNavigator.startGroupMemberList( + groupInfo: groupInfo.value, + opType: GroupMemberOpType.transferRight, + ); + if (result is GroupMembersInfo) { + await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.groupManager.transferGroupOwner( + groupID: groupInfo.value.groupID, + userID: result.userID!, + ), + ); + groupInfo.update((val) { + val?.ownerUserID = result.userID; + }); + Get.back(); + } + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_view.dart b/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_view.dart new file mode 100644 index 0000000..e258a75 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_manage/group_manage_view.dart @@ -0,0 +1,90 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'group_manage_logic.dart'; + +class GroupManagePage extends StatelessWidget { + final logic = Get.find(); + + GroupManagePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: StrRes.groupManage, + ), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + _buildItemView( + text: StrRes.transferGroupOwnerRight, + onTap: logic.transferGroupOwnerRight, + showRightArrow: true, + isTopRadius: true, + isBottomRadius: true, + ), + ], + ), + ); + } + + Widget _buildItemView({ + required String text, + TextStyle? textStyle, + String? value, + bool switchOn = false, + bool isTopRadius = false, + bool isBottomRadius = false, + bool showRightArrow = false, + bool showSwitchButton = false, + ValueChanged? onChanged, + Function()? onTap, + }) => + GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.translucent, + child: Container( + height: 46.h, + margin: EdgeInsets.symmetric(horizontal: 10.w), + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topRight: Radius.circular(isTopRadius ? 6.r : 0), + topLeft: Radius.circular(isTopRadius ? 6.r : 0), + bottomLeft: Radius.circular(isBottomRadius ? 6.r : 0), + bottomRight: Radius.circular(isBottomRadius ? 6.r : 0), + ), + ), + child: Row( + children: [ + Expanded( + child: text.toText..style = textStyle ?? Styles.ts_0C1C33_17sp, + ), + if (null != value) + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 150.w), + child: value.toText + ..style = Styles.ts_8E9AB0_14sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + if (showSwitchButton) + CupertinoSwitch( + value: switchOn, + activeColor: Styles.c_0089FF, + onChanged: onChanged, + ), + if (showRightArrow) + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ); +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_binding.dart b/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_binding.dart new file mode 100644 index 0000000..cd7a56f --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_binding.dart @@ -0,0 +1,13 @@ +import 'package:get/get.dart'; + +import '../group_setup_logic.dart'; +import 'group_member_list_logic.dart'; + +class GroupMemberListBinding extends Bindings { + @override + void dependencies() { + GroupMemberOpType opType = Get.arguments['opType']; + Get.lazyPut(() => GroupMemberListLogic(), tag: opType.name); + Get.lazyPut(() => GroupSetupLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_logic.dart b/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_logic.dart new file mode 100644 index 0000000..63d51ee --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_logic.dart @@ -0,0 +1,212 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:sprintf/sprintf.dart'; + +import '../../../../core/controller/im_controller.dart'; +import '../group_setup_logic.dart'; + +enum GroupMemberOpType { + view, + transferRight, + call, + at, + del, +} + +class GroupMemberListLogic extends GetxController { + final imLogic = Get.find(); + final groupSetupLogic = Get.find(); + final controller = RefreshController(); + final memberList = [].obs; + final checkedList = [].obs; + final poController = CustomPopupMenuController(); + int count = 500; + final myGroupMemberLevel = 1.obs; + late GroupInfo groupInfo; + late GroupMemberOpType opType; + late StreamSubscription mISub; + + bool get isMultiSelMode => + opType == GroupMemberOpType.call || opType == GroupMemberOpType.at || opType == GroupMemberOpType.del; + + bool get excludeSelfFromList => + opType == GroupMemberOpType.call || opType == GroupMemberOpType.at || opType == GroupMemberOpType.transferRight; + + bool get isDelMember => opType == GroupMemberOpType.del; + + bool get isAdmin => myGroupMemberLevel.value == GroupRoleLevel.admin; + + bool get isOwner => myGroupMemberLevel.value == GroupRoleLevel.owner; + + bool get isOwnerOrAdmin => isAdmin || isOwner; + + int get maxLength => min(groupInfo.memberCount!, 10); + + @override + void onClose() { + mISub.cancel(); + super.onClose(); + } + + @override + void onInit() { + groupInfo = Get.arguments['groupInfo']; + opType = Get.arguments['opType']; + mISub = imLogic.memberInfoChangedSubject.listen(_updateMemberLevel); + super.onInit(); + } + + @override + void onReady() { + _queryMyGroupMemberLevel(); + super.onReady(); + } + + void _updateMemberLevel(GroupMembersInfo e) { + if (e.groupID == groupInfo.groupID) { + equal(GroupMembersInfo el) => el.userID == e.userID; + final member = memberList.firstWhereOrNull(equal); + if (null != member && e.roleLevel != member.roleLevel) { + member.roleLevel = e.roleLevel; + } + memberList.sort((a, b) { + if (b.roleLevel != a.roleLevel) { + return b.roleLevel!.compareTo(a.roleLevel!); + } else { + return b.joinTime!.compareTo(a.joinTime!); + } + }); + } + } + + void _queryMyGroupMemberLevel() async { + LoadingView.singleton.wrap(asyncFunction: () async { + final list = await OpenIM.iMManager.groupManager.getGroupMembersInfo( + groupID: groupInfo.groupID, + userIDList: [OpenIM.iMManager.userID], + ); + final myInfo = list.firstOrNull; + if (null != myInfo) { + myGroupMemberLevel.value = myInfo.roleLevel ?? 1; + } + await onLoad(); + }); + } + + Future> _getGroupMembers() { + final result = OpenIM.iMManager.groupManager.getGroupMemberList( + groupID: groupInfo.groupID, + count: count, + offset: memberList.length, + filter: isDelMember ? (isOwner ? 4 : (isAdmin ? 3 : 0)) : 0, + ); + + count = 100; + + return result; + } + + onLoad() async { + final list = await _getGroupMembers(); + memberList.addAll(list); + + if (list.length < count) { + controller.loadNoData(); + } else { + controller.loadComplete(); + } + } + + bool isChecked(GroupMembersInfo membersInfo) => checkedList.contains(membersInfo); + + clickMember(GroupMembersInfo membersInfo) async { + if (opType == GroupMemberOpType.transferRight) { + _transferGroupRight(membersInfo); + return; + } + if (isMultiSelMode) { + if (isChecked(membersInfo)) { + checkedList.remove(membersInfo); + } else if (checkedList.length < maxLength) { + checkedList.add(membersInfo); + } + } else { + viewMemberInfo(membersInfo); + } + } + + static _transferGroupRight(GroupMembersInfo membersInfo) async { + var confirm = await Get.dialog(CustomDialog( + title: sprintf(StrRes.confirmTransferGroupToUser, [membersInfo.nickname]), + )); + if (confirm == true) { + Get.back(result: membersInfo); + } + } + + void removeSelectedMember(GroupMembersInfo membersInfo) { + checkedList.remove(membersInfo); + } + + viewMemberInfo(GroupMembersInfo membersInfo) => AppNavigator.startUserProfilePane( + userID: membersInfo.userID!, + groupID: membersInfo.groupID, + nickname: membersInfo.nickname, + faceURL: membersInfo.faceURL, + ); + + void addMember() async { + poController.hideMenu(); + await groupSetupLogic.addMember(); + refreshData(); + } + + void refreshData() { + LoadingView.singleton.wrap(asyncFunction: () async { + memberList.clear(); + await onLoad(); + }); + } + + void delMember() async { + poController.hideMenu(); + await groupSetupLogic.removeMember(); + refreshData(); + } + + void search() async { + final memberInfo = await AppNavigator.startSearchGroupMember( + groupInfo: groupInfo, + opType: opType, + ); + if (opType == GroupMemberOpType.transferRight) { + Get.back(result: memberInfo); + } else if (isMultiSelMode) { + clickMember(memberInfo); + } + } + + static _buildEveryoneMemberInfo() => GroupMembersInfo( + userID: OpenIM.iMManager.conversationManager.atAllTag, + nickname: StrRes.everyone, + ); + + void selectEveryone() { + Get.back(result: [_buildEveryoneMemberInfo()]); + } + + void confirmSelectedMember() { + Get.back(result: checkedList.value); + } + + bool hiddenMember(GroupMembersInfo membersInfo) => + excludeSelfFromList && membersInfo.userID == OpenIM.iMManager.userID; +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_view.dart b/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_view.dart new file mode 100644 index 0000000..f89f310 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_member_list/group_member_list_view.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'group_member_list_logic.dart'; + +class GroupMemberListPage extends StatelessWidget { + final logic = Get.find(tag: (Get.arguments['opType'] as GroupMemberOpType).name); + + GroupMemberListPage({super.key}); + + @override + Widget build(BuildContext context) { + return Obx(() => Scaffold( + appBar: TitleBar.back( + title: logic.opType == GroupMemberOpType.del ? StrRes.removeGroupMember : StrRes.groupMember, + right: logic.opType == GroupMemberOpType.view + ? PopButton( + popCtrl: logic.poController, + horizontalMargin: 1.w, + menus: [ + PopMenuInfo(text: StrRes.addMember, onTap: logic.addMember), + if (logic.isOwnerOrAdmin) PopMenuInfo(text: StrRes.delMember, onTap: logic.delMember), + ], + child: ImageRes.moreBlack.toImage + ..width = 28.w + ..height = 28.h) + : null, + ), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.search, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + color: Styles.c_FFFFFF, + child: const SearchBox(), + ), + ), + if (logic.opType == GroupMemberOpType.at && logic.isOwnerOrAdmin) + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.selectEveryone, + child: Container( + height: 64.h, + color: Styles.c_FFFFFF, + margin: EdgeInsets.symmetric(vertical: 10.h), + padding: EdgeInsets.symmetric(horizontal: 16.w), + alignment: Alignment.centerLeft, + child: Row( + children: [ + AvatarView( + width: 44.w, + height: 44.h, + text: '@', + textStyle: Styles.ts_FFFFFF_21sp, + ), + 10.horizontalSpace, + StrRes.everyone.toText..style = Styles.ts_0C1C33_17sp, + ], + ), + ), + ), + Flexible( + child: SmartRefresher( + controller: logic.controller, + onLoading: logic.onLoad, + enablePullDown: false, + enablePullUp: true, + header: IMViews.buildHeader(), + footer: IMViews.buildFooter(), + child: ListView.builder( + itemCount: logic.memberList.length, + itemBuilder: (_, index) => Obx(() => _buildItemView(logic.memberList[index])), + ), + ), + ), + if (logic.isMultiSelMode) _buildCheckedConfirmView(), + ], + ), + )); + } + + Widget _buildItemView(GroupMembersInfo membersInfo) => logic.hiddenMember(membersInfo) + ? const SizedBox() + : GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => logic.clickMember(membersInfo), + child: Container( + height: 64.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + color: Styles.c_FFFFFF, + child: Row( + children: [ + if (logic.isMultiSelMode) + Padding( + padding: EdgeInsets.only(right: 15.w), + child: ChatRadio(checked: logic.isChecked(membersInfo)), + ), + AvatarView( + url: membersInfo.faceURL, + text: membersInfo.nickname, + ), + 10.horizontalSpace, + Expanded( + child: (membersInfo.nickname ?? '').toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + if (membersInfo.roleLevel == GroupRoleLevel.owner) + StrRes.groupOwner.toText..style = Styles.ts_8E9AB0_17sp, + if (membersInfo.roleLevel == GroupRoleLevel.admin) + StrRes.groupAdmin.toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ), + ); + + Widget _buildCheckedConfirmView() => Container( + height: 66.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + boxShadow: [ + BoxShadow( + offset: Offset(0, -1.h), + blurRadius: 4.r, + spreadRadius: 1.r, + color: Styles.c_000000_opacity4, + ), + ], + ), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Get.bottomSheet( + SelectedMemberListView(), + isScrollControlled: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + sprintf(StrRes.selectedPeopleCount, [logic.checkedList.length]).toText + ..style = Styles.ts_0089FF_14sp, + ImageRes.expandUpArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + if (logic.checkedList.isNotEmpty) 4.verticalSpace, + logic.checkedList.map((e) => e.nickname ?? '').join('、').toText + ..style = Styles.ts_8E9AB0_14sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ], + ), + ), + ), + Button( + height: 40.h, + padding: EdgeInsets.symmetric(horizontal: 14.w), + text: sprintf(StrRes.confirmSelectedPeople, [ + logic.checkedList.length, + logic.maxLength, + ]), + textStyle: Styles.ts_FFFFFF_14sp, + onTap: logic.confirmSelectedMember, + ), + ], + ), + ); +} + +class SelectedMemberListView extends StatelessWidget { + SelectedMemberListView({Key? key}) : super(key: key); + final logic = Get.find(tag: (Get.arguments['opType'] as GroupMemberOpType).name); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints(maxHeight: 548.h), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(6.r), + topRight: Radius.circular(6.r), + ), + ), + child: Obx(() => Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + border: BorderDirectional( + bottom: BorderSide(color: Styles.c_E8EAEF, width: 1), + ), + ), + child: Row( + children: [ + sprintf(StrRes.selectedPeopleCount, [logic.checkedList.length]).toText + ..style = Styles.ts_0C1C33_17sp_medium, + const Spacer(), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Get.back(), + child: Container( + height: 52.h, + alignment: Alignment.center, + child: StrRes.confirm.toText..style = Styles.ts_0089FF_17sp, + ), + ), + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: logic.checkedList.length, + shrinkWrap: true, + itemBuilder: (_, index) => _buildItemView(logic.checkedList[index]), + ), + ), + ], + )), + ); + } + + Widget _buildItemView(GroupMembersInfo membersInfo) => Container( + height: 64.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + color: Styles.c_FFFFFF, + child: Row( + children: [ + AvatarView( + url: membersInfo.faceURL, + text: membersInfo.nickname, + ), + 10.horizontalSpace, + Expanded( + child: (membersInfo.nickname ?? '').toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => logic.removeSelectedMember(membersInfo), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 13.w, vertical: 4.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + border: Border.all( + color: Styles.c_E8EAEF, + width: 1, + ), + ), + child: StrRes.remove.toText..style = Styles.ts_0089FF_17sp, + ), + ), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_binding.dart b/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_binding.dart new file mode 100644 index 0000000..c1153a7 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'search_group_member_logic.dart'; + +class SearchGroupMemberBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SearchGroupMemberLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_logic.dart b/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_logic.dart new file mode 100644 index 0000000..a92d2e5 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_logic.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/chat/group_setup/group_setup_logic.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:sprintf/sprintf.dart'; + +import '../../../../../core/controller/im_controller.dart'; +import '../../../../../routes/app_navigator.dart'; +import '../group_member_list_logic.dart'; + +class SearchGroupMemberLogic extends GetxController { + final imLogic = Get.find(); + final controller = RefreshController(); + final focusNode = FocusNode(); + final searchCtrl = TextEditingController(); + final memberList = [].obs; + final count = 100; + late GroupInfo groupInfo; + late GroupMemberOpType opType; + late StreamSubscription mISub; + + @override + void onInit() { + groupInfo = Get.arguments['groupInfo']; + opType = Get.arguments['opType']; + searchCtrl.addListener(_clearInput); + mISub = imLogic.memberInfoChangedSubject.listen((e) { + if (e.groupID == groupInfo.groupID) { + final member = memberList.firstWhereOrNull((el) => el.userID == e.userID); + if (null != member && e.roleLevel != member.roleLevel) { + member.roleLevel = e.roleLevel; + memberList.refresh(); + } + } + }); + super.onInit(); + } + + @override + void onClose() { + focusNode.dispose(); + searchCtrl.dispose(); + mISub.cancel(); + super.onClose(); + } + + bool get isSearchNotResult => searchCtrl.text.trim().isNotEmpty && memberList.isEmpty; + + _clearInput() { + final key = searchCtrl.text.trim(); + if (key.isEmpty) { + memberList.clear(); + } + } + + Future> _request(int offset) => LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.groupManager.searchGroupMembers( + groupID: groupInfo.groupID, + isSearchMemberNickname: true, + isSearchUserID: true, + keywordList: [searchCtrl.text.trim()], + offset: offset, + count: count, + ), + ); + + search() async { + final key = searchCtrl.text.trim(); + if (key.isNotEmpty) { + final list = await _request(0); + memberList.assignAll(list); + if (list.length < count) { + controller.loadNoData(); + } else { + controller.loadComplete(); + } + } + } + + load() async { + final key = searchCtrl.text.trim(); + if (key.isNotEmpty) { + final list = await _request(memberList.length); + memberList.addAll(list); + if (list.length < count) { + controller.loadNoData(); + } else { + controller.loadComplete(); + } + } + } + + bool hiddenMembers(GroupMembersInfo info) { + if (opType == GroupMemberOpType.transferRight || opType == GroupMemberOpType.at || opType == GroupMemberOpType.call) { + return info.userID == OpenIM.iMManager.userID; + } else if (opType == GroupMemberOpType.del) { + final logic = Get.find(); + return logic.isAdmin && info.roleLevel != GroupRoleLevel.member || logic.isOwner && info.roleLevel == GroupRoleLevel.owner; + } + return false; + } + + clickMember(GroupMembersInfo membersInfo) { + if (opType == GroupMemberOpType.transferRight) { + _transferGroupRight(membersInfo); + } else if (opType == GroupMemberOpType.at || opType == GroupMemberOpType.call || opType == GroupMemberOpType.del) { + Get.back(result: membersInfo); + } else { + viewMemberInfo(membersInfo); + } + } + + static _transferGroupRight(GroupMembersInfo membersInfo) async { + var confirm = await Get.dialog(CustomDialog( + title: sprintf(StrRes.confirmTransferGroupToUser, [membersInfo.nickname]), + )); + if (confirm == true) { + Get.back(result: membersInfo); + } + } + + viewMemberInfo(GroupMembersInfo membersInfo) => AppNavigator.startUserProfilePane( + userID: membersInfo.userID!, + groupID: membersInfo.groupID, + nickname: membersInfo.nickname, + faceURL: membersInfo.faceURL, + ); +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_view.dart b/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_view.dart new file mode 100644 index 0000000..ed55c9e --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_member_list/search_group_member/search_group_member_view.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:search_keyword_text/search_keyword_text.dart'; + +import 'search_group_member_logic.dart'; + +class SearchGroupMemberPage extends StatelessWidget { + final logic = Get.find(); + + SearchGroupMemberPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.search( + focusNode: logic.focusNode, + controller: logic.searchCtrl, + onSubmitted: (_) => logic.search(), + onCleared: () => logic.focusNode.requestFocus(), + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => logic.isSearchNotResult + ? _emptyListView + : SmartRefresher( + controller: logic.controller, + enablePullUp: true, + enablePullDown: false, + footer: IMViews.buildFooter(), + onLoading: logic.load, + child: ListView.builder( + itemCount: logic.memberList.length, + itemBuilder: (_, index) { + final info = logic.memberList.elementAt(index); + if (logic.hiddenMembers(info)) { + return const SizedBox(); + } else { + return _buildItemView(info); + } + }, + ), + )), + ), + ); + } + + Widget _buildItemView(GroupMembersInfo membersInfo) => GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => logic.clickMember(membersInfo), + child: Container( + height: 64.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + color: Styles.c_FFFFFF, + child: Row( + children: [ + AvatarView( + url: membersInfo.faceURL, + text: membersInfo.nickname, + ), + 10.horizontalSpace, + Expanded( + child: SearchKeywordText( + text: membersInfo.nickname ?? '', + keyText: logic.searchCtrl.text.trim(), + style: Styles.ts_0C1C33_17sp, + keyStyle: Styles.ts_0089FF_17sp, + ), + ), + if (membersInfo.roleLevel == GroupRoleLevel.owner) + StrRes.groupOwner.toText..style = Styles.ts_8E9AB0_17sp, + if (membersInfo.roleLevel == GroupRoleLevel.admin) + StrRes.groupAdmin.toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ), + ); + + Widget get _emptyListView => SizedBox( + width: 1.sw, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 44.verticalSpace, + StrRes.searchNotFound.toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ); +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_binding.dart b/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_binding.dart new file mode 100644 index 0000000..4672816 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'group_qrcode_logic.dart'; + +class GroupQrcodeBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GroupQrcodeLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_logic.dart b/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_logic.dart new file mode 100644 index 0000000..dbac28e --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_logic.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:openim/pages/chat/group_setup/group_setup_logic.dart'; +import 'package:openim_common/openim_common.dart'; + +class GroupQrcodeLogic extends GetxController { + final groupSetupLogic = Get.find(); + + String buildQRContent() { + return '${Config.groupScheme}${groupSetupLogic.groupInfo.value.groupID}'; + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_view.dart b/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_view.dart new file mode 100644 index 0000000..b8c3510 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_qrcode/group_qrcode_view.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +import 'group_qrcode_logic.dart'; + +class GroupQrcodePage extends StatelessWidget { + final logic = Get.find(); + + GroupQrcodePage({super.key}); + + @override + Widget build(BuildContext context) { + return Obx( + () => Scaffold( + appBar: TitleBar.back(title: StrRes.groupQrcode), + backgroundColor: Styles.c_F8F9FA, + body: Container( + alignment: Alignment.topCenter, + child: Container( + margin: EdgeInsets.only(top: 22.h), + padding: EdgeInsets.symmetric(horizontal: 30.w), + width: 331.w, + height: 460.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(10.r), + boxShadow: [ + BoxShadow( + blurRadius: 7.r, + spreadRadius: 1.r, + color: Styles.c_000000.withOpacity(.08), + ), + ], + ), + child: Stack( + children: [ + Positioned( + top: 30.h, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AvatarView( + width: 48.w, + height: 48.h, + url: logic.groupSetupLogic.groupInfo.value.faceURL, + text: logic.groupSetupLogic.groupInfo.value.groupName, + textStyle: Styles.ts_FFFFFF_14sp, + ), + 12.horizontalSpace, + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 180.w), + child: (logic.groupSetupLogic.groupInfo.value.groupName ?? '').toText + ..style = Styles.ts_0C1C33_20sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + ], + ), + ), + Positioned( + top: 140.h, + width: 272.w, + child: StrRes.groupQrcodeHint.toText + ..style = Styles.ts_8E9AB0_15sp + ..textAlign = TextAlign.center, + ), + Positioned( + top: 183.h, + width: 272.w, + child: Container( + alignment: Alignment.center, + child: Container( + width: 180.w, + height: 180.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + border: Border.all(color: Styles.c_E8EAEF, width: 4.w), + ), + child: QrImageView( + data: logic.buildQRContent(), + size: 140.w, + backgroundColor: Styles.c_FFFFFF, + ), + ), + ), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_setup_binding.dart b/mobile-next/lib/pages/chat/group_setup/group_setup_binding.dart new file mode 100644 index 0000000..13d9d10 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_setup_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'group_setup_logic.dart'; + +class GroupSetupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GroupSetupLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_setup_logic.dart b/mobile-next/lib/pages/chat/group_setup/group_setup_logic.dart new file mode 100644 index 0000000..e7c6651 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_setup_logic.dart @@ -0,0 +1,438 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:synchronized/synchronized.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; + +import '../../../core/controller/app_controller.dart'; +import '../../../core/controller/im_controller.dart'; +import '../../../routes/app_navigator.dart'; +import '../../contacts/select_contacts/select_contacts_logic.dart'; +import '../../conversation/conversation_logic.dart'; +import '../chat_logic.dart'; +import 'edit_name/edit_name_logic.dart'; +import 'group_member_list/group_member_list_logic.dart'; + +class GroupSetupLogic extends GetxController { + final imLogic = Get.find(); + + final chatLogic = Get.find(tag: GetTags.chat); + final appLogic = Get.find(); + final conversationLogic = Get.find(); + final memberList = [].obs; + late Rx conversationInfo; + late Rx groupInfo; + late Rx myGroupMembersInfo; + late StreamSubscription _guSub; + late StreamSubscription _mASub; + late StreamSubscription _mISub; + late StreamSubscription _mDSub; + late StreamSubscription _ccSub; + late StreamSubscription _jasSub; + late StreamSubscription _jdsSub; + final lock = Lock(); + final isJoinedGroup = false.obs; + final avatar = Rx(null); + + @override + void onInit() async { + if (Get.arguments['conversationInfo'] != null) { + conversationInfo = Rx(Get.arguments['conversationInfo']); + } else { + final temp = await OpenIM.iMManager.conversationManager.getOneConversation( + sourceID: chatLogic.conversationInfo.isGroupChat + ? chatLogic.conversationInfo.groupID! + : chatLogic.conversationInfo.userID!, + sessionType: chatLogic.conversationInfo.conversationType!); + conversationInfo = Rx(temp); + } + groupInfo = Rx(_defaultGroupInfo); + myGroupMembersInfo = Rx(_defaultMemberInfo); + + _ccSub = imLogic.conversationChangedSubject.listen((newList) { + final newValue = + newList.firstWhereOrNull((element) => element.conversationID == conversationInfo.value.conversationID); + if (newValue != null) { + conversationInfo.update((val) { + val?.isPinned = newValue.isPinned; + + val?.recvMsgOpt = newValue.recvMsgOpt; + val?.isMsgDestruct = newValue.isMsgDestruct; + val?.msgDestructTime = newValue.msgDestructTime; + }); + } + }); + + _guSub = imLogic.groupInfoUpdatedSubject.listen((value) { + if (value.groupID == groupInfo.value.groupID) { + _updateGroupInfo(value); + } + }); + + _jasSub = imLogic.joinedGroupAddedSubject.listen((value) { + if (value.groupID == groupInfo.value.groupID) { + isJoinedGroup.value = true; + _queryAllInfo(); + } + }); + + _jdsSub = imLogic.joinedGroupDeletedSubject.listen((value) { + if (value.groupID == groupInfo.value.groupID) { + isJoinedGroup.value = false; + } + }); + + _mISub = imLogic.memberInfoChangedSubject.listen((e) { + if (e.groupID == groupInfo.value.groupID && e.userID == myGroupMembersInfo.value.userID) { + myGroupMembersInfo.update((val) { + val?.nickname = e.nickname; + val?.roleLevel = e.roleLevel; + }); + } + if (e.groupID == groupInfo.value.groupID && e.userID == groupInfo.value.ownerUserID) { + var index = memberList.indexWhere((element) => element.userID == groupInfo.value.ownerUserID); + if (index == -1) { + memberList.insert(0, e); + } else if (index != 0) { + memberList.insert(0, memberList.removeAt(index)); + } + } + memberList.sort((a, b) { + if (b.roleLevel != a.roleLevel) { + return b.roleLevel!.compareTo(a.roleLevel!); + } else { + return b.joinTime!.compareTo(a.joinTime!); + } + }); + }); + _mASub = imLogic.memberAddedSubject.listen((e) async { + if (e.groupID == groupInfo.value.groupID) { + if (e.userID == OpenIM.iMManager.userID) { + isJoinedGroup.value = true; + _queryAllInfo(); + } else { + memberList.add(e); + } + } + }); + _mDSub = imLogic.memberDeletedSubject.listen((e) { + if (e.groupID == groupInfo.value.groupID) { + if (e.userID == OpenIM.iMManager.userID) { + isJoinedGroup.value = false; + } else { + memberList.removeWhere((element) => element.userID == e.userID); + } + } + }); + super.onInit(); + } + + @override + void onReady() { + _checkIsJoinedGroup(); + super.onReady(); + } + + @override + void onClose() { + _guSub.cancel(); + _mASub.cancel(); + _mDSub.cancel(); + _ccSub.cancel(); + _mISub.cancel(); + _jdsSub.cancel(); + _jasSub.cancel(); + super.onClose(); + } + + get _defaultGroupInfo => GroupInfo( + groupID: conversationInfo.value.groupID!, + groupName: conversationInfo.value.showName, + faceURL: conversationInfo.value.faceURL, + memberCount: 0, + ); + + get _defaultMemberInfo => GroupMembersInfo( + userID: OpenIM.iMManager.userID, + nickname: OpenIM.iMManager.userInfo.nickname, + ); + + bool get isOwnerOrAdmin => isOwner || isAdmin; + + bool get isAdmin => myGroupMembersInfo.value.roleLevel == GroupRoleLevel.admin; + + bool get isNotDisturb => conversationInfo.value.recvMsgOpt != 0; + + bool get isOwner => groupInfo.value.ownerUserID == OpenIM.iMManager.userID; + + String get conversationID => conversationInfo.value.conversationID; + + void _checkIsJoinedGroup() async { + isJoinedGroup.value = await OpenIM.iMManager.groupManager.isJoinedGroup( + groupID: groupInfo.value.groupID, + ); + _queryAllInfo(); + } + + void _queryAllInfo() { + if (isJoinedGroup.value) { + getGroupInfo(); + getGroupMembers(); + getMyGroupMemberInfo(); + } + } + + getGroupMembers() async { + var list = await OpenIM.iMManager.groupManager.getGroupMemberList( + groupID: groupInfo.value.groupID, + count: 10, + ); + memberList.assignAll(list); + } + + getGroupInfo() async { + var list = await OpenIM.iMManager.groupManager.getGroupsInfo( + groupIDList: [groupInfo.value.groupID], + ); + var value = list.firstOrNull; + if (null != value) { + _updateGroupInfo(value); + } + } + + getMyGroupMemberInfo() async { + final list = await OpenIM.iMManager.groupManager.getGroupMembersInfo( + groupID: groupInfo.value.groupID, + userIDList: [OpenIM.iMManager.userID], + ); + final info = list.firstOrNull; + if (null != info) { + myGroupMembersInfo.update((val) { + val?.nickname = info.nickname; + val?.roleLevel = info.roleLevel; + }); + } + } + + void _updateGroupInfo(GroupInfo value) { + groupInfo.update((val) { + val?.groupName = value.groupName; + val?.faceURL = value.faceURL; + val?.notification = value.notification; + val?.introduction = value.introduction; + val?.memberCount = value.memberCount; + val?.ownerUserID = value.ownerUserID; + val?.status = value.status; + val?.needVerification = value.needVerification; + val?.groupType = value.groupType; + val?.lookMemberInfo = value.lookMemberInfo; + val?.applyMemberFriend = value.applyMemberFriend; + val?.notificationUserID = value.notificationUserID; + val?.notificationUpdateTime = value.notificationUpdateTime; + val?.ex = value.ex; + }); + } + + void modifyGroupAvatar() async { + final List? assets = await AssetPicker.pickAssets( + Get.context!, + pickerConfig: const AssetPickerConfig(maxAssets: 1, requestType: RequestType.image), + ); + if (assets != null) { + final file = await assets.first.file; + final result = await IMViews.uCropPic(file!.path); + + final path = result['path']; + final url = result['url']; + + if (url != null) { + avatar.value = File(path); + await _modifyGroupInfo(faceUrl: url); + groupInfo.update((val) { + val?.faceURL = url; + }); + } + } + } + + void modifyGroupName(String? faceUrl) => AppNavigator.startEditGroupName( + type: EditNameType.groupNickname, + faceUrl: faceUrl, + ); + + _modifyGroupInfo({ + String? groupName, + String? notification, + String? introduction, + String? faceUrl, + }) => + OpenIM.iMManager.groupManager.setGroupInfo(GroupInfo( + groupID: groupInfo.value.groupID, + groupName: groupName, + notification: notification, + introduction: introduction, + faceURL: faceUrl, + )); + + void viewGroupQrcode() => AppNavigator.startGroupQrcode(); + + void viewGroupMembers() => AppNavigator.startGroupMemberList( + groupInfo: groupInfo.value, + ); + + void groupManage() => AppNavigator.startGroupManage( + groupInfo: groupInfo.value, + ); + + void _removeConversation() async { + await OpenIM.iMManager.conversationManager.deleteConversationAndDeleteAllMsg( + conversationID: conversationInfo.value.conversationID, + ); + + conversationLogic.removeConversation(conversationInfo.value.conversationID); + } + + void quitGroup() async { + if (isJoinedGroup.value) { + if (isOwner) { + var confirm = await Get.dialog(CustomDialog( + title: StrRes.dismissGroupHint, + )); + if (confirm == true) { + await OpenIM.iMManager.groupManager.dismissGroup( + groupID: groupInfo.value.groupID, + ); + } else { + return; + } + } else { + var confirm = await Get.dialog(CustomDialog( + title: StrRes.quitGroupHint, + )); + if (confirm == true) { + await OpenIM.iMManager.groupManager.quitGroup( + groupID: groupInfo.value.groupID, + ); + } else { + return; + } + } + } else { + _removeConversation(); + } + + AppNavigator.startBackMain(); + } + + void copyGroupID() { + IMUtils.copy(text: groupInfo.value.groupID); + } + + int length() { + int buttons = isOwnerOrAdmin ? 2 : 1; + return (memberList.length + buttons) > 10 ? 10 : (memberList.length + buttons); + } + + Widget itemBuilder({ + required int index, + required Widget Function(GroupMembersInfo info) builder, + required Widget Function() addButton, + required Widget Function() delButton, + }) { + var length = isOwnerOrAdmin ? 8 : 9; + if (memberList.length > length) { + if (index < length) { + var info = memberList.elementAt(index); + return builder(info); + } else if (index == length) { + return addButton(); + } else { + return delButton(); + } + } else { + if (index < memberList.length) { + var info = memberList.elementAt(index); + return builder(info); + } else if (index == memberList.length) { + return addButton(); + } else { + return delButton(); + } + } + } + + void toggleNotDisturb() { + LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.conversationManager.setConversationRecvMessageOpt( + conversationID: conversationID, + status: !isNotDisturb ? 2 : 0, + )); + } + + void clearChatHistory() async { + var confirm = await Get.dialog(CustomDialog( + title: StrRes.confirmClearChatHistory, + rightText: StrRes.clearAll, + )); + if (confirm == true) { + await OpenIM.iMManager.conversationManager.clearConversationAndDeleteAllMsg( + conversationID: conversationID, + ); + chatLogic.clearAllMessage(); + IMViews.showToast(StrRes.clearSuccessfully); + } + } + + addMember() async { + final result = await AppNavigator.startSelectContacts( + action: SelAction.addMember, + groupID: groupInfo.value.groupID, + ); + + final list = IMUtils.convertSelectContactsResultToUserID(result); + if (list is List) { + try { + await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.groupManager.inviteUserToGroup( + groupID: groupInfo.value.groupID, + userIDList: list, + reason: 'Come on baby', + ), + ); + } catch (_) {} + getGroupMembers(); + } + } + + removeMember() async { + final list = await AppNavigator.startGroupMemberList( + groupInfo: groupInfo.value, + opType: GroupMemberOpType.del, + ); + if (list is List) { + var removeUidList = list.map((e) => e.userID!).toList(); + try { + await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.groupManager.kickGroupMember( + groupID: groupInfo.value.groupID, + userIDList: removeUidList, + reason: 'Get out baby', + ), + ); + } catch (_) {} + getGroupMembers(); + } + } + + void viewMemberInfo(GroupMembersInfo membersInfo) => AppNavigator.startUserProfilePane( + userID: membersInfo.userID!, + nickname: membersInfo.nickname, + faceURL: membersInfo.faceURL, + groupID: membersInfo.groupID, + ); +} diff --git a/mobile-next/lib/pages/chat/group_setup/group_setup_view.dart b/mobile-next/lib/pages/chat/group_setup/group_setup_view.dart new file mode 100644 index 0000000..afacf14 --- /dev/null +++ b/mobile-next/lib/pages/chat/group_setup/group_setup_view.dart @@ -0,0 +1,311 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'group_setup_logic.dart'; + +class GroupSetupPage extends StatelessWidget { + final logic = Get.find(); + + GroupSetupPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.groupChatSetup), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => SingleChildScrollView( + child: Column( + children: [ + if (logic.isJoinedGroup.value) _buildBaseInfoView(), + if (logic.isJoinedGroup.value) _buildMemberView(), + if (logic.isOwner) + _buildItemView( + text: StrRes.groupManage, + showRightArrow: true, + isBottomRadius: true, + onTap: logic.groupManage, + ), + 10.verticalSpace, + + _buildItemView( + text: StrRes.messageNotDisturb, + switchOn: logic.isNotDisturb, + showSwitchButton: true, + isBottomRadius: true, + onChanged: (_) => logic.toggleNotDisturb(), + ), + 10.verticalSpace, + _buildItemView( + text: StrRes.clearChatHistory, + textStyle: Styles.ts_FF381F_17sp, + isTopRadius: true, + showRightArrow: true, + onTap: logic.clearChatHistory, + ), + if (!logic.isOwner) + _buildItemView( + text: logic.isJoinedGroup.value ? StrRes.exitGroup : StrRes.delete, + textStyle: Styles.ts_FF381F_17sp, + showRightArrow: true, + onTap: logic.quitGroup, + ), + if (logic.isOwner) + _buildItemView( + text: StrRes.dismissGroup, + textStyle: Styles.ts_FF381F_17sp, + isBottomRadius: true, + showRightArrow: true, + onTap: logic.quitGroup, + ), + 40.verticalSpace, + ], + ), + )), + ); + } + + Widget _buildBaseInfoView() => Container( + height: 80.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.h), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(6.r), + ), + child: Row( + children: [ + SizedBox( + width: 50.h, + height: 50.h, + child: Stack( + children: [ + AvatarView( + width: 48.w, + height: 48.h, + url: logic.groupInfo.value.faceURL, + file: logic.avatar.value, + text: logic.groupInfo.value.groupName, + textStyle: Styles.ts_FFFFFF_14sp, + isGroup: true, + onTap: logic.isOwnerOrAdmin ? logic.modifyGroupAvatar : null, + ), + if (logic.isOwnerOrAdmin) + Align( + alignment: Alignment.bottomRight, + child: ImageRes.editAvatar.toImage + ..width = 14.w + ..height = 14.h) + ], + ), + ), + 10.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.isOwnerOrAdmin ? () => logic.modifyGroupName(logic.conversationInfo.value.faceURL) : null, + child: Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 200.w), + child: (logic.groupInfo.value.groupName ?? '').toText..style = Styles.ts_0C1C33_17sp), + '(${logic.groupInfo.value.memberCount ?? 0})'.toText..style = Styles.ts_0C1C33_17sp, + 6.horizontalSpace, + if (logic.isOwnerOrAdmin) + ImageRes.editName.toImage + ..width = 12.w + ..height = 12.h, + ], + ), + ), + 4.verticalSpace, + logic.groupInfo.value.groupID.toText + ..style = Styles.ts_8E9AB0_14sp + ..onTap = logic.copyGroupID, + ], + ), + ), + ImageRes.mineQr.toImage + ..width = 18.w + ..height = 18.h + ..onTap = logic.viewGroupQrcode, + ], + ), + ); + + Widget _buildMemberView() => Container( + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(6.r), + ), + margin: EdgeInsets.symmetric(horizontal: 10.w), + child: Column( + children: [ + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: logic.length(), + shrinkWrap: true, + padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 8.h), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + crossAxisSpacing: 3.w, + mainAxisSpacing: 2.h, + childAspectRatio: 68.w / 78.h, + ), + itemBuilder: (BuildContext context, int index) { + return logic.itemBuilder( + index: index, + builder: (info) => Column( + children: [ + SizedBox( + width: 58.w, + child: Stack( + alignment: Alignment.center, + children: [ + AvatarView( + width: 48.w, + height: 48.h, + url: info.faceURL, + text: info.nickname, + textStyle: Styles.ts_FFFFFF_14sp, + onTap: () => logic.viewMemberInfo(info), + ), + if (logic.groupInfo.value.ownerUserID == info.userID) + Positioned( + bottom: 0.h, + child: Container( + width: 52.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Styles.c_E8EAEF, + borderRadius: BorderRadius.circular(6.r), + ), + child: StrRes.groupOwner.toText + ..style = Styles.ts_8E9AB0_10sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + ) + ], + ), + ), + 2.verticalSpace, + (info.nickname ?? '').toText + ..style = Styles.ts_8E9AB0_10sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ], + ), + addButton: () => GestureDetector( + onTap: logic.addMember, + child: Column( + children: [ + ImageRes.addMember.toImage + ..width = 48.w + ..height = 48.h, + StrRes.addMember.toText..style = Styles.ts_8E9AB0_10sp, + ], + ), + ), + delButton: () => GestureDetector( + onTap: logic.removeMember, + child: Column( + children: [ + ImageRes.delMember.toImage + ..width = 48.w + ..height = 48.h, + StrRes.delMember.toText..style = Styles.ts_8E9AB0_10sp, + ], + ), + ), + ); + }, + ), + Container( + color: Styles.c_E8EAEF, + height: 1, + margin: EdgeInsets.symmetric(horizontal: 10.w), + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.viewGroupMembers, + child: Container( + padding: EdgeInsets.only(left: 12.w, right: 16.w), + height: 46.h, + child: Row( + children: [ + sprintf(StrRes.viewAllGroupMembers, [logic.groupInfo.value.memberCount]).toText + ..style = Styles.ts_0C1C33_17sp, + const Spacer(), + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + ], + ), + ); + + Widget _buildItemView({ + required String text, + TextStyle? textStyle, + String? value, + bool switchOn = false, + bool isTopRadius = false, + bool isBottomRadius = false, + bool showRightArrow = false, + bool showSwitchButton = false, + ValueChanged? onChanged, + Function()? onTap, + }) => + GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.translucent, + child: Container( + height: 46.h, + margin: EdgeInsets.symmetric(horizontal: 10.w), + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topRight: Radius.circular(isTopRadius ? 6.r : 0), + topLeft: Radius.circular(isTopRadius ? 6.r : 0), + bottomLeft: Radius.circular(isBottomRadius ? 6.r : 0), + bottomRight: Radius.circular(isBottomRadius ? 6.r : 0), + ), + ), + child: Row( + children: [ + Expanded( + child: text.toText + ..style = textStyle ?? Styles.ts_0C1C33_17sp + ..maxLines = 1), + if (null != value) + value.toText + ..style = Styles.ts_8E9AB0_14sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + if (showSwitchButton) + CupertinoSwitch( + value: switchOn, + activeColor: Styles.c_0089FF, + onChanged: onChanged, + ), + if (showRightArrow) + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ); +} diff --git a/mobile-next/lib/pages/chat/oa_notification/oa_notification_binding.dart b/mobile-next/lib/pages/chat/oa_notification/oa_notification_binding.dart new file mode 100644 index 0000000..c39a9b4 --- /dev/null +++ b/mobile-next/lib/pages/chat/oa_notification/oa_notification_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'oa_notification_logic.dart'; + +class OANotificationBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => OANotificationLogic()); + } +} diff --git a/mobile-next/lib/pages/chat/oa_notification/oa_notification_logic.dart b/mobile-next/lib/pages/chat/oa_notification/oa_notification_logic.dart new file mode 100644 index 0000000..0f138e2 --- /dev/null +++ b/mobile-next/lib/pages/chat/oa_notification/oa_notification_logic.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; + +import '../../../core/controller/im_controller.dart'; + +class OANotificationLogic extends GetxController { + late ConversationInfo info; + var messageList = [].obs; + final pageSize = 40; + final refreshController = RefreshController(initialRefresh: false); + final imLogic = Get.find(); + int? lastMinSeq; + bool _isFirstLoad = false; + + @override + void onInit() { + info = Get.arguments; + + imLogic.onRecvNewMessage = (Message message) { + if (message.contentType == MessageType.oaNotification) { + if (!messageList.contains(message)) messageList.add(message); + } + }; + super.onInit(); + } + + @override + void onReady() { + loadNotification(); + super.onReady(); + } + + void loadNotification() async { + final result = await OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList( + conversationID: info.conversationID, + count: 200, + startMsg: _isFirstLoad ? null : messageList.firstOrNull, + ); + if (result.messageList == null || result.messageList!.isEmpty) { + return refreshController.loadNoData(); + } + final list = result.messageList!; + lastMinSeq = result.lastMinSeq; + + if (_isFirstLoad) { + _isFirstLoad = false; + messageList.assignAll(list); + } else { + messageList.insertAll(0, list); + } + if (result.isEnd == true) { + refreshController.loadNoData(); + } else { + refreshController.loadComplete(); + } + } + + OANotification parse(Message message) => OANotification.fromJson(json.decode(message.notificationElem!.detail!)); + + Size calSize(OANotification oa, double w, double h) { + final width = 50.w; + + final height = width * h / w; + print('----${oa.videoElem?.snapshotWidth}---width:$width'); + print('----${oa.videoElem?.snapshotHeight}---height:$height'); + return Size(width, height); + } +} diff --git a/mobile-next/lib/pages/chat/oa_notification/oa_notification_view.dart b/mobile-next/lib/pages/chat/oa_notification/oa_notification_view.dart new file mode 100644 index 0000000..75553f3 --- /dev/null +++ b/mobile-next/lib/pages/chat/oa_notification/oa_notification_view.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import 'oa_notification_logic.dart'; + +class OANotificationPage extends StatelessWidget { + final logic = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: logic.info.showName, + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => SmartRefresher( + controller: logic.refreshController, + header: IMViews.buildHeader(), + footer: IMViews.buildFooter(), + enablePullDown: false, + enablePullUp: true, + onLoading: () => logic.loadNotification(), + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 22.w), + itemCount: logic.messageList.length, + shrinkWrap: true, + itemBuilder: (_, index) { + final message = logic.messageList.reversed.elementAt(index); + return _buildItemView(index, message, logic.parse(message)); + }, + ), + )), + ); + } + + Widget _buildItemView(int index, Message message, OANotification oa) => Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 15.h, + ), + Text( + IMUtils.getChatTimeline(message.sendTime!), + style: Styles.ts_8E9AB0_10sp, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AvatarView( + url: oa.notificationFaceURL, + builder: oa.notificationFaceURL == null ? () => _buildCustomAvatar() : null, + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(oa.notificationName!, style: Styles.ts_0C1C33_14sp), + GestureDetector( + onTap: () {}, + behavior: HitTestBehavior.translucent, + child: Container( + margin: EdgeInsets.only(top: 8.h), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(2), + ), + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 8.h, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + oa.notificationName!, + style: Styles.ts_8E9AB0_14sp, + ), + Text( + oa.text!, + style: Styles.ts_8E9AB0_12sp, + ), + if (oa.mixType == 1 || oa.mixType == 2 || oa.mixType == 3) + Container( + margin: EdgeInsets.only(top: 12.h), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (oa.mixType == 1) _buildPictureView(message, oa, index), + if (oa.mixType == 2) _buildVideoView(message, oa, index), + if (oa.mixType == 3) _buildFileView(message, oa, index), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ) + ], + ), + ], + ); + + Widget _buildPictureView(Message message, OANotification oa, int index) => GestureDetector( + onTap: () async { + final url = oa.externalUrl; + final canLunch = await canLaunchUrlString(url!); + + if (url.isNotEmpty == true && canLunch) { + launchUrlString(url); + } + }, + child: ChatPictureView( + message: message..pictureElem = oa.pictureElem, + isISend: false, + ), + ); + + Widget _buildVideoView(Message message, OANotification oa, int index) => GestureDetector( + onTap: () { + IMUtils.previewMediaFile( + context: Get.context!, + message: message, + onAutoPlay: (p0) => true, + onlySave: true, + ); + }, + child: ChatVideoView( + message: message..videoElem = oa.videoElem, + isISend: false, + ), + ); + + Widget _buildFileView(Message message, OANotification oa, int index) => GestureDetector( + onTap: () { + IMUtils.previewFile(message); + }, + child: ChatFileView( + message: message..fileElem = oa.fileElem, + isISend: false, + ), + ); + + Widget? _buildCustomAvatar() => Container( + color: Styles.c_0089FF, + height: 48.h, + width: 48.h, + alignment: Alignment.center, + child: FaIcon( + FontAwesomeIcons.solidBell, + color: Styles.c_FFFFFF, + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/add_by_search/add_by_search_binding.dart b/mobile-next/lib/pages/contacts/add_by_search/add_by_search_binding.dart new file mode 100644 index 0000000..47b79d5 --- /dev/null +++ b/mobile-next/lib/pages/contacts/add_by_search/add_by_search_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'add_by_search_logic.dart'; + +class AddContactsBySearchBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AddContactsBySearchLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/add_by_search/add_by_search_logic.dart b/mobile-next/lib/pages/contacts/add_by_search/add_by_search_logic.dart new file mode 100644 index 0000000..0531f6e --- /dev/null +++ b/mobile-next/lib/pages/contacts/add_by_search/add_by_search_logic.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/contacts/group_profile_panel/group_profile_panel_logic.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:sprintf/sprintf.dart'; + +enum SearchType { + user, + group, +} + +class AddContactsBySearchLogic extends GetxController { + final refreshCtrl = RefreshController(); + final searchCtrl = TextEditingController(); + final focusNode = FocusNode(); + final userInfoList = [].obs; + final groupInfoList = [].obs; + late SearchType searchType; + int pageNo = 0; + + @override + void onClose() { + searchCtrl.dispose(); + focusNode.dispose(); + super.onClose(); + } + + @override + void onInit() { + searchType = Get.arguments['searchType'] ?? SearchType.user; + searchCtrl.addListener(() { + if (searchKey.isEmpty) { + focusNode.requestFocus(); + userInfoList.clear(); + groupInfoList.clear(); + } + }); + super.onInit(); + } + + bool get isSearchUser => searchType == SearchType.user; + + String get searchKey => searchCtrl.text.trim(); + + bool get isNotFoundUser => userInfoList.isEmpty && searchKey.isNotEmpty; + + bool get isNotFoundGroup => groupInfoList.isEmpty && searchKey.isNotEmpty; + + void search() { + if (searchKey.isEmpty) return; + if (isSearchUser) { + searchUser(); + } else { + searchGroup(); + } + } + + void searchUser() async { + var list = await LoadingView.singleton.wrap( + asyncFunction: () => Apis.searchUserFullInfo( + content: searchKey, + pageNumber: pageNo = 1, + showNumber: 20, + ), + ); + userInfoList.assignAll(list ?? []); + refreshCtrl.refreshCompleted(); + if (null == list || list.isEmpty || list.length < 20) { + refreshCtrl.loadNoData(); + } else { + refreshCtrl.loadComplete(); + } + } + + void loadMoreUser() async { + var list = await LoadingView.singleton.wrap( + asyncFunction: () => Apis.searchUserFullInfo( + content: searchKey, + pageNumber: ++pageNo, + showNumber: 20, + ), + ); + userInfoList.addAll(list ?? []); + refreshCtrl.refreshCompleted(); + if (null == list || list.isEmpty || list.length < 20) { + refreshCtrl.loadNoData(); + } else { + refreshCtrl.loadComplete(); + } + } + + void searchGroup() async { + var list = await OpenIM.iMManager.groupManager.getGroupsInfo( + groupIDList: [searchKey], + ); + groupInfoList.assignAll(list); + } + + String getMatchContent(UserFullInfo userInfo) { + final keyword = searchCtrl.text; + + return sprintf(StrRes.searchNicknameIs, [userInfo.nickname]); + } + + String? getShowName(dynamic info) { + if (info is UserFullInfo) { + return info.nickname; + } else if (info is GroupInfo) { + return info.groupName; + } + return null; + } + + void viewInfo(dynamic info) { + if (info is UserFullInfo) { + AppNavigator.startUserProfilePane( + userID: info.userID!, + nickname: info.nickname, + faceURL: info.faceURL, + ); + } else if (info is GroupInfo) { + AppNavigator.startGroupProfilePanel( + groupID: info.groupID, + joinGroupMethod: JoinGroupMethod.search, + ); + } + } + + String getShowTitle(info) { + if (!isSearchUser) { + return sprintf(StrRes.searchGroupNicknameIs, [getShowName(info)]); + } + + UserFullInfo userFullInfo = info; + String? tips, content; + if (int.tryParse(searchKey) != null) { + if (searchKey.length == 11) { + tips = StrRes.phoneNumber; + content = userFullInfo.phoneNumber ?? searchKey; + } else { + tips = StrRes.userID; + content = userFullInfo.userID; + } + } else { + tips = StrRes.searchNicknameIs; + content = getShowName(info); + } + return "$tips:$content"; + } +} diff --git a/mobile-next/lib/pages/contacts/add_by_search/add_by_search_view.dart b/mobile-next/lib/pages/contacts/add_by_search/add_by_search_view.dart new file mode 100644 index 0000000..90c29af --- /dev/null +++ b/mobile-next/lib/pages/contacts/add_by_search/add_by_search_view.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'add_by_search_logic.dart'; + +class AddContactsBySearchPage extends StatelessWidget { + final logic = Get.find(); + + AddContactsBySearchPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: logic.isSearchUser ? StrRes.addFriend : StrRes.addGroup, + ), + backgroundColor: Styles.c_FFFFFF, + body: Column( + children: [ + SearchBox( + focusNode: logic.focusNode, + controller: logic.searchCtrl, + hintText: logic.isSearchUser ? StrRes.searchByPhoneAndUid : StrRes.searchIDAddGroup, + enabled: true, + autofocus: true, + margin: EdgeInsets.symmetric(horizontal: 17.w, vertical: 10.h), + onSubmitted: (_) => logic.search(), + ), + Divider(color: Styles.c_E8EAEF, height: 1.h), + if (logic.isSearchUser) + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: AppNavigator.startScan, + child: Container( + margin: EdgeInsets.only(top: 22.h), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + ImageRes.scanBlue.toImage + ..width = 40.w + ..height = 40.h, + 8.horizontalSpace, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + StrRes.scan.toText..style = Styles.ts_0C1C33_17sp_medium, + 8.verticalSpace, + StrRes.scanHint.toText..style = Styles.ts_8E9AB0_12sp, + ], + ), + ], + ), + ), + ), + Obx(() => Expanded( + child: logic.isSearchUser + ? (logic.isNotFoundUser ? _buildNotFoundView() : _buildUserListView()) + : (logic.isNotFoundGroup + ? _buildNotFoundView() + : (Column( + children: logic.groupInfoList.map((e) => _buildItemView(e)).toList(), + ))), + )) + ], + ), + ); + } + + Widget _buildUserListView() => SmartRefresher( + controller: logic.refreshCtrl, + enablePullDown: false, + enablePullUp: true, + footer: IMViews.buildFooter(), + onLoading: logic.loadMoreUser, + child: ListView.builder( + itemCount: logic.userInfoList.length, + itemBuilder: (_, index) { + final userInfo = logic.userInfoList.elementAt(index); + return _buildItemView(userInfo); + }, + ), + ); + + Widget _buildItemView(dynamic info) => InkWell( + onTap: () => logic.viewInfo(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + border: BorderDirectional( + bottom: BorderSide(color: Styles.c_E8EAEF, width: 1.h), + ), + ), + height: 49.h, + child: Row( + children: [ + (logic.isSearchUser ? ImageRes.searchPersonIcon : ImageRes.searchGroupIcon).toImage + ..width = 24.w + ..height = 24.h, + 12.horizontalSpace, + logic.getShowTitle(info).toText + ..style = Styles.ts_0089FF_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ], + ), + ), + ); + + Widget _buildNotFoundView() => Container( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: (logic.isSearchUser ? StrRes.noFoundUser : StrRes.noFoundGroup).toText..style = Styles.ts_8E9AB0_17sp, + ); +} diff --git a/mobile-next/lib/pages/contacts/add_method/add_method_binding.dart b/mobile-next/lib/pages/contacts/add_method/add_method_binding.dart new file mode 100644 index 0000000..f5661c3 --- /dev/null +++ b/mobile-next/lib/pages/contacts/add_method/add_method_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'add_method_logic.dart'; + +class AddContactsMethodBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AddContactsMethodLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/add_method/add_method_logic.dart b/mobile-next/lib/pages/contacts/add_method/add_method_logic.dart new file mode 100644 index 0000000..662b026 --- /dev/null +++ b/mobile-next/lib/pages/contacts/add_method/add_method_logic.dart @@ -0,0 +1,17 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/contacts/add_by_search/add_by_search_logic.dart'; +import 'package:openim/routes/app_navigator.dart'; + +class AddContactsMethodLogic extends GetxController { + scan() => AppNavigator.startScan(); + + addFriend() => + AppNavigator.startAddContactsBySearch(searchType: SearchType.user); + + createGroup() => AppNavigator.startCreateGroup( + defaultCheckedList: [OpenIM.iMManager.userInfo]); + + addGroup() => + AppNavigator.startAddContactsBySearch(searchType: SearchType.group); +} diff --git a/mobile-next/lib/pages/contacts/add_method/add_method_view.dart b/mobile-next/lib/pages/contacts/add_method/add_method_view.dart new file mode 100644 index 0000000..e3965b0 --- /dev/null +++ b/mobile-next/lib/pages/contacts/add_method/add_method_view.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'add_method_logic.dart'; + +class AddContactsMethodPage extends StatelessWidget { + final logic = Get.find(); + + AddContactsMethodPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.add), + body: Column( + children: [ + 10.verticalSpace, + _buildItemView( + icon: ImageRes.scanBlue, + text: StrRes.scan, + hintText: StrRes.scanHint, + onTap: logic.scan, + ), + _buildItemView( + icon: ImageRes.addFriendBlue, + text: StrRes.addFriend, + hintText: StrRes.addFriendHint, + onTap: logic.addFriend, + ), + _buildItemView( + icon: ImageRes.createGroupBlue, + text: StrRes.createGroup, + hintText: StrRes.createGroupHint, + onTap: logic.createGroup, + ), + _buildItemView( + icon: ImageRes.addGroupBLue, + text: StrRes.addGroup, + hintText: StrRes.addGroupHint, + onTap: logic.addGroup, + underline: false, + ), + ], + ), + ); + } + + Widget _buildItemView({ + required String icon, + required String text, + required String hintText, + bool underline = true, + Function()? onTap, + }) => + Ink( + color: Styles.c_FFFFFF, + child: InkWell( + onTap: onTap, + child: SizedBox( + height: 74.h, + child: Row( + children: [ + 22.horizontalSpace, + icon.toImage + ..width = 28.w + ..height = 28.h, + 16.horizontalSpace, + Expanded( + child: Container( + decoration: underline + ? BoxDecoration( + border: BorderDirectional( + bottom: BorderSide( + color: Styles.c_E8EAEF, + width: .5, + ), + ), + ) + : null, + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text.toText..style = Styles.ts_0C1C33_17sp, + 4.verticalSpace, + hintText.toText..style = Styles.ts_8E9AB0_12sp, + ], + ), + ), + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + 16.horizontalSpace, + ], + ), + ), + ) + ], + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/contacts_binding.dart b/mobile-next/lib/pages/contacts/contacts_binding.dart new file mode 100644 index 0000000..a633478 --- /dev/null +++ b/mobile-next/lib/pages/contacts/contacts_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'contacts_logic.dart'; + +class ContactsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ContactsLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/contacts_logic.dart b/mobile-next/lib/pages/contacts/contacts_logic.dart new file mode 100644 index 0000000..77369a5 --- /dev/null +++ b/mobile-next/lib/pages/contacts/contacts_logic.dart @@ -0,0 +1,89 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/contacts/group_profile_panel/group_profile_panel_logic.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../core/controller/im_controller.dart'; +import '../home/home_logic.dart'; +import 'select_contacts/select_contacts_logic.dart'; + +class ContactsLogic extends GetxController + implements ViewUserProfileBridge, SelectContactsBridge, ScanBridge { + final imLogic = Get.find(); + final homeLogic = Get.find(); + + final friendApplicationList = []; + + int get friendApplicationCount => homeLogic.unhandledFriendApplicationCount.value; + + int get groupApplicationCount => homeLogic.unhandledGroupApplicationCount.value; + + @override + void onInit() { + PackageBridge.selectContactsBridge = this; + PackageBridge.viewUserProfileBridge = this; + PackageBridge.scanBridge = this; + + super.onInit(); + } + + @override + void onClose() { + PackageBridge.selectContactsBridge = null; + PackageBridge.viewUserProfileBridge = null; + PackageBridge.scanBridge = null; + super.onClose(); + } + + void newFriend() => AppNavigator.startFriendRequests(); + + void newGroup() => AppNavigator.startGroupRequests(); + + void myFriend() => AppNavigator.startFriendList(); + + void myGroup() => AppNavigator.startGroupList(); + + void searchContacts() => AppNavigator.startGlobalSearch(); + + void addContacts() => AppNavigator.startAddContactsMethod(); + + @override + Future? selectContacts( + int type, { + List? defaultCheckedIDList, + List? checkedList, + List? excludeIDList, + bool openSelectedSheet = false, + String? groupID, + String? ex, + }) => + AppNavigator.startSelectContacts( + action: SelAction.values[type], + defaultCheckedIDList: defaultCheckedIDList, + checkedList: checkedList, + excludeIDList: excludeIDList, + openSelectedSheet: openSelectedSheet, + groupID: groupID, + ex: ex, + ); + + @override + viewUserProfile(String userID, String? nickname, String? faceURL, [String? groupID]) => + AppNavigator.startUserProfilePane( + userID: userID, + nickname: nickname, + faceURL: faceURL, + groupID: groupID, + ); + + @override + scanOutGroupID(String groupID) => AppNavigator.startGroupProfilePanel( + groupID: groupID, + joinGroupMethod: JoinGroupMethod.qrcode, + offAndToNamed: true, + ); + + @override + scanOutUserID(String userID) => AppNavigator.startUserProfilePane(userID: userID, offAndToNamed: true); +} diff --git a/mobile-next/lib/pages/contacts/contacts_view.dart b/mobile-next/lib/pages/contacts/contacts_view.dart new file mode 100644 index 0000000..7013533 --- /dev/null +++ b/mobile-next/lib/pages/contacts/contacts_view.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'contacts_logic.dart'; + +class ContactsPage extends StatelessWidget { + final logic = Get.find(); + + ContactsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.contacts( + onClickAddContacts: logic.addContacts, + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx( + () => SingleChildScrollView( + child: Column( + children: [ + _buildItemView( + assetsName: ImageRes.newFriend, + label: StrRes.newFriend, + count: logic.friendApplicationCount, + onTap: logic.newFriend, + ), + _buildItemView( + assetsName: ImageRes.newGroup, + label: StrRes.newGroupRequest, + count: logic.groupApplicationCount, + onTap: logic.newGroup, + ), + 10.verticalSpace, + _buildItemView( + assetsName: ImageRes.myFriend, + label: StrRes.myFriend, + onTap: logic.myFriend, + ), + _buildItemView( + assetsName: ImageRes.myGroup, + label: StrRes.myGroup, + onTap: logic.myGroup, + ), + ], + ), + ), + ), + ); + } + + Widget _buildItemView({ + String? assetsName, + required String label, + Widget? icon, + int count = 0, + bool showRightArrow = true, + double? height, + Function()? onTap, + }) => + Ink( + color: Styles.c_FFFFFF, + child: InkWell( + onTap: onTap, + child: Container( + height: height ?? 60.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + if (null != assetsName) + assetsName.toImage + ..width = 42.w + ..height = 42.h, + if (null != icon) icon, + 12.horizontalSpace, + label.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + if (count > 0) UnreadCountView(count: count), + 4.horizontalSpace, + if (showRightArrow) + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/create_group/create_group_binding.dart b/mobile-next/lib/pages/contacts/create_group/create_group_binding.dart new file mode 100644 index 0000000..20c0d8c --- /dev/null +++ b/mobile-next/lib/pages/contacts/create_group/create_group_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'create_group_logic.dart'; + +class CreateGroupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => CreateGroupLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/create_group/create_group_logic.dart b/mobile-next/lib/pages/contacts/create_group/create_group_logic.dart new file mode 100644 index 0000000..d6cc8a4 --- /dev/null +++ b/mobile-next/lib/pages/contacts/create_group/create_group_logic.dart @@ -0,0 +1,130 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../routes/app_navigator.dart'; +import '../../conversation/conversation_logic.dart'; +import '../select_contacts/select_contacts_logic.dart'; + +class CreateGroupLogic extends GetxController { + final conversationLogic = Get.find(); + final nameCtrl = TextEditingController(); + final checkedList = []; + final defaultCheckedList = []; + final allList = [].obs; + final faceURL = ''.obs; + + @override + void onInit() { + defaultCheckedList.addAll(Get.arguments['defaultCheckedList']); + checkedList.addAll(Get.arguments['checkedList']); + allList.addAll(defaultCheckedList); + allList.addAll(checkedList); + super.onInit(); + } + + String get groupName { + String name = nameCtrl.text.trim(); + if (name.isEmpty) { + int limit = min(allList.length, 3); + name = allList.sublist(0, limit).map((e) => e.nickname).join('、'); + } + return name; + } + + completeCreation() async { + if (allList.length > 1) { + var info = await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.groupManager.createGroup( + groupInfo: GroupInfo( + groupID: '', + groupName: groupName, + faceURL: faceURL.value, + groupType: GroupType.work, + ), + memberUserIDs: allList.where((e) => e.userID != OpenIM.iMManager.userID).map((e) => e.userID!).toList(), + ), + ); + conversationLogic.toChat( + offUntilHome: true, + groupID: info.groupID, + nickname: groupName, + faceURL: faceURL.value, + sessionType: info.sessionType, + ); + } else { + conversationLogic.toChat( + offUntilHome: true, + userID: checkedList.firstOrNull?.userID, + nickname: checkedList.firstOrNull?.nickname, + faceURL: checkedList.firstOrNull?.faceURL, + sessionType: ConversationType.single, + ); + } + } + + void selectAvatar() { + IMViews.openPhotoSheet(onData: (path, url) { + if (url != null) faceURL.value = url; + }); + } + + int length() { + return (allList.length + 2) > 10 ? 10 : (allList.length + 2); + } + + Widget itemBuilder({ + required int index, + required Widget Function(UserInfo info) builder, + required Widget Function() addButton, + required Widget Function() delButton, + }) { + if (allList.length > 8) { + if (index < 8) { + var info = allList.elementAt(index); + return builder(info); + } else if (index == 8) { + return addButton(); + } else { + return delButton(); + } + } else { + if (index < allList.length) { + var info = allList.elementAt(index); + return builder(info); + } else if (index == allList.length) { + return addButton(); + } else { + return delButton(); + } + } + } + + @override + void onClose() { + nameCtrl.dispose(); + super.onClose(); + } + + void opMember({bool isDel = false}) async { + final result = await AppNavigator.startSelectContacts( + action: SelAction.addMember, + checkedList: checkedList, + defaultCheckedIDList: defaultCheckedList.map((e) => e.userID!).toList(), + openSelectedSheet: isDel, + ); + final list = IMUtils.convertSelectContactsResultToUserInfo(result); + if (list is List) { + checkedList + ..clear() + ..addAll(list); + allList + ..assignAll(defaultCheckedList) + ..addAll(list); + } + } +} diff --git a/mobile-next/lib/pages/contacts/create_group/create_group_view.dart b/mobile-next/lib/pages/contacts/create_group/create_group_view.dart new file mode 100644 index 0000000..5829326 --- /dev/null +++ b/mobile-next/lib/pages/contacts/create_group/create_group_view.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'create_group_logic.dart'; + +class CreateGroupPage extends StatelessWidget { + final logic = Get.find(); + + CreateGroupPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.back(title: StrRes.createGroup), + backgroundColor: Styles.c_F8F9FA, + body: SingleChildScrollView( + child: SizedBox( + height: 1.sh - 44.h - 10.h - 34.h, + child: Column( + children: [ + _buildGroupBaseInfoView(), + _buildGroupMemberView(), + const Spacer(), + Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 12.h, + ), + child: Button( + text: StrRes.completeCreation, + onTap: logic.completeCreation, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildGroupBaseInfoView() => Container( + margin: EdgeInsets.fromLTRB(12.w, 8.h, 12.w, 12.h), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(6.r), + ), + child: Obx(() => Row( + children: [ + if (logic.faceURL.isNotEmpty) + AvatarView( + width: 48.w, + height: 48.h, + url: logic.faceURL.value, + onTap: logic.selectAvatar, + ) + else + ImageRes.cameraGray.toImage + ..width = 48.w + ..height = 48.h + ..onTap = logic.selectAvatar, + 12.horizontalSpace, + Flexible( + child: TextField( + style: Styles.ts_0C1C33_17sp, + autofocus: true, + controller: logic.nameCtrl, + inputFormatters: [LengthLimitingTextInputFormatter(16)], + decoration: InputDecoration( + hintStyle: Styles.ts_8E9AB0_17sp, + hintText: StrRes.plsEnterGroupNameHint, + border: InputBorder.none, + ), + ), + ), + ], + )), + ); + + Widget _buildGroupMemberView() => Obx(() => Container( + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(6.r), + ), + margin: EdgeInsets.symmetric(horizontal: 10.w), + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + child: Row( + children: [ + StrRes.groupMember.toText..style = Styles.ts_8E9AB0_17sp, + const Spacer(), + sprintf(StrRes.nPerson, [logic.allList.length]).toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ), + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: logic.length(), + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + crossAxisSpacing: 3.w, + mainAxisSpacing: 2.h, + childAspectRatio: 68.w / 78.h, + ), + itemBuilder: (BuildContext context, int index) { + return logic.itemBuilder( + index: index, + builder: (info) => Column( + children: [ + AvatarView( + width: 48.w, + height: 48.h, + url: info.faceURL, + text: info.nickname, + textStyle: Styles.ts_FFFFFF_14sp, + ), + 2.verticalSpace, + (info.nickname ?? '').toText + ..style = Styles.ts_8E9AB0_10sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ], + ), + addButton: () => const SizedBox.shrink(), + delButton: () => const SizedBox.shrink(), + ); + }, + ), + ], + ), + )); +} diff --git a/mobile-next/lib/pages/contacts/friend_list/friend_list_binding.dart b/mobile-next/lib/pages/contacts/friend_list/friend_list_binding.dart new file mode 100644 index 0000000..283e9de --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_list/friend_list_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'friend_list_logic.dart'; + +class FriendListBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => FriendListLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/friend_list/friend_list_logic.dart b/mobile-next/lib/pages/contacts/friend_list/friend_list_logic.dart new file mode 100644 index 0000000..186d942 --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_list/friend_list_logic.dart @@ -0,0 +1,109 @@ +import 'dart:async'; + +import 'package:azlistview/azlistview.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; + +class FriendListLogic extends GetxController { + final imLoic = Get.find(); + final friendList = [].obs; + final userIDList = []; + late StreamSubscription delSub; + late StreamSubscription addSub; + late StreamSubscription infoChangedSub; + + int _count = 10000; + + @override + void onInit() { + delSub = imLoic.friendDelSubject.listen(_delFriend); + addSub = imLoic.friendAddSubject.listen(_addFriend); + infoChangedSub = imLoic.friendInfoChangedSubject.listen(_friendInfoChanged); + imLoic.onBlacklistAdd = _delFriend; + imLoic.onBlacklistDeleted = _addFriend; + super.onInit(); + } + + @override + void onReady() { + _getFriendList(); + super.onReady(); + } + + @override + void onClose() { + delSub.cancel(); + addSub.cancel(); + infoChangedSub.cancel(); + super.onClose(); + } + + _getFriendList() async { + List list = []; + for (int i = 0;; i++) { + final temp = await OpenIM.iMManager.friendshipManager.getFriendListPage( + offset: list.length, + count: _count, + filterBlack: true, + ); + list.addAll(temp); + + if (temp.length < _count) { + break; + } + + _count = 1000; + } + + final result = list.map((e) { + userIDList.add(e.userID!); + + return ISUserInfo.fromJson(e.toJson()); + }).toList(); + + final convertResult = IMUtils.convertToAZList(result); + + onUserIDList(userIDList); + friendList.assignAll(convertResult.cast()); + } + + void onUserIDList(List userIDList) {} + + _addFriend(dynamic user) { + if (user is FriendInfo || user is BlacklistInfo) { + _addUser(user.toJson()); + } + } + + _delFriend(dynamic user) { + if (user is FriendInfo || user is BlacklistInfo) { + friendList.removeWhere((e) => e.userID == user.userID); + } + } + + _friendInfoChanged(FriendInfo user) { + friendList.removeWhere((e) => e.userID == user.userID); + _addUser(user.toJson()); + } + + void _addUser(Map json) { + final info = ISUserInfo.fromJson(json); + friendList.add(IMUtils.setAzPinyinAndTag(info) as ISUserInfo); + + SuspensionUtil.sortListBySuspensionTag(friendList); + + SuspensionUtil.setShowSuspensionStatus(friendList); + } + + void viewFriendInfo(ISUserInfo info) => AppNavigator.startUserProfilePane( + userID: info.userID!, + nickname: info.nickname, + faceURL: info.faceURL, + ); + + void searchFriend() => AppNavigator.startSearchFriend(); +} diff --git a/mobile-next/lib/pages/contacts/friend_list/friend_list_view.dart b/mobile-next/lib/pages/contacts/friend_list/friend_list_view.dart new file mode 100644 index 0000000..011c2a8 --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_list/friend_list_view.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'friend_list_logic.dart'; + +class FriendListPage extends StatelessWidget { + final logic = Get.find(); + + FriendListPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.myFriend), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.searchFriend, + child: Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + child: const SearchBox(), + ), + ), + Flexible( + child: Obx( + () => WrapAzListView( + data: logic.friendList, + itemCount: logic.friendList.length, + itemBuilder: (_, data, index) => _buildItemView(data), + ), + ), + ), + ], + ), + ); + } + + Widget _buildItemView(ISUserInfo info) => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: () => logic.viewFriendInfo(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + AvatarView( + url: info.faceURL, + text: info.showName, + ), + 10.horizontalSpace, + info.showName.toText..style = Styles.ts_0C1C33_17sp, + ], + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_binding.dart b/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_binding.dart new file mode 100644 index 0000000..f16185a --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'search_friend_logic.dart'; + +class SearchFriendBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SearchFriendLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_logic.dart b/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_logic.dart new file mode 100644 index 0000000..145e166 --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_logic.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../../routes/app_navigator.dart'; +import '../friend_list_logic.dart'; + +class SearchFriendLogic extends GetxController { + final logic = Get.find(); + final focusNode = FocusNode(); + final searchCtrl = TextEditingController(); + final resultList = [].obs; + late bool isMultiModel; + + @override + void onInit() { + searchCtrl.addListener(_clearInput); + super.onInit(); + } + + @override + void onClose() { + focusNode.dispose(); + searchCtrl.dispose(); + super.onClose(); + } + + bool get isSearchNotResult => searchCtrl.text.trim().isNotEmpty && resultList.isEmpty; + + _clearInput() { + final key = searchCtrl.text.trim(); + if (key.isEmpty) { + resultList.clear(); + } + } + + search() async { + var key = searchCtrl.text.trim(); + resultList.clear(); + if (key.isNotEmpty) { + final result = await OpenIM.iMManager.friendshipManager + .searchFriends(keywordList: [key], isSearchNickname: true, isSearchRemark: true, isSearchUserID: true); + final users = result.map((e) => ISUserInfo.fromJson(e.toJson())).toList(); + resultList.addAll(users); + } + } + + viewFriendInfo(ISUserInfo info) => AppNavigator.startUserProfilePane( + userID: info.userID!, + offAndToNamed: true, + ); +} diff --git a/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_view.dart b/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_view.dart new file mode 100644 index 0000000..86fb442 --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_list/search_friend/search_friend_view.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:search_keyword_text/search_keyword_text.dart'; + +import 'search_friend_logic.dart'; + +class SearchFriendPage extends StatelessWidget { + final logic = Get.find(); + + SearchFriendPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.search( + focusNode: logic.focusNode, + controller: logic.searchCtrl, + onSubmitted: (_) => logic.search(), + onCleared: () => logic.focusNode.requestFocus(), + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => logic.isSearchNotResult + ? _emptyListView + : ListView.builder( + itemCount: logic.resultList.length, + itemBuilder: (_, index) => _buildItemView(logic.resultList[index]), + )), + ), + ); + } + + Widget _buildItemView(ISUserInfo info) => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: () => logic.viewFriendInfo(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + AvatarView( + url: info.faceURL, + text: info.showName, + ), + 10.horizontalSpace, + SearchKeywordText( + text: info.showName, + keyText: logic.searchCtrl.text.trim(), + style: Styles.ts_0C1C33_17sp, + keyStyle: Styles.ts_0089FF_17sp, + ), + ], + ), + ), + ), + ); + + Widget get _emptyListView => SizedBox( + width: 1.sw, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 44.verticalSpace, + StrRes.searchNotFound.toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/friend_requests/friend_requests_binding.dart b/mobile-next/lib/pages/contacts/friend_requests/friend_requests_binding.dart new file mode 100644 index 0000000..f6c004a --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_requests/friend_requests_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'friend_requests_logic.dart'; + +class FriendRequestsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => FriendRequestsLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/friend_requests/friend_requests_logic.dart b/mobile-next/lib/pages/contacts/friend_requests/friend_requests_logic.dart new file mode 100644 index 0000000..d95c906 --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_requests/friend_requests_logic.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; +import '../../home/home_logic.dart'; + +class FriendRequestsLogic extends GetxController { + final imLogic = Get.find(); + final homeLogic = Get.find(); + final applicationList = [].obs; + late StreamSubscription faSub; + + @override + void onInit() { + faSub = imLogic.friendApplicationChangedSubject.listen((value) { + _getFriendRequestsList(); + }); + super.onInit(); + } + + @override + void onReady() { + _getFriendRequestsList(); + super.onReady(); + } + + @override + void onClose() { + faSub.cancel(); + homeLogic.getUnhandledFriendApplicationCount(); + super.onClose(); + } + + void _getFriendRequestsList() async { + final list = await Future.wait([ + OpenIM.iMManager.friendshipManager.getFriendApplicationListAsRecipient(), + OpenIM.iMManager.friendshipManager.getFriendApplicationListAsApplicant(), + ]); + + final allList = []; + allList + ..addAll(list[0]) + ..addAll(list[1]); + + allList.sort((a, b) { + if (a.createTime! > b.createTime!) { + return -1; + } else if (a.createTime! < b.createTime!) { + return 1; + } + return 0; + }); + + var haveReadList = DataSp.getHaveReadUnHandleFriendApplication(); + haveReadList ??= []; + for (var e in list[0]) { + var id = IMUtils.buildFriendApplicationID(e); + if (!haveReadList.contains(id)) { + haveReadList.add(id); + } + } + DataSp.putHaveReadUnHandleFriendApplication(haveReadList); + applicationList.assignAll(allList); + } + + bool isISendRequest(FriendApplicationInfo info) => info.fromUserID == OpenIM.iMManager.userID; + + void acceptFriendApplication(FriendApplicationInfo info) => AppNavigator.startProcessFriendRequests( + applicationInfo: info, + ); + + void refuseFriendApplication(FriendApplicationInfo info) async {} +} diff --git a/mobile-next/lib/pages/contacts/friend_requests/friend_requests_view.dart b/mobile-next/lib/pages/contacts/friend_requests/friend_requests_view.dart new file mode 100644 index 0000000..9e60ab8 --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_requests/friend_requests_view.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'friend_requests_logic.dart'; + +class FriendRequestsPage extends StatelessWidget { + final logic = Get.find(); + + FriendRequestsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.newFriend), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => ListView.builder( + padding: EdgeInsets.only(top: 10.h), + itemCount: logic.applicationList.length, + itemBuilder: (_, index) => + _buildItemView(logic.applicationList[index]), + )), + ); + } + + Widget _buildItemView(FriendApplicationInfo info) { + final isISendRequest = info.fromUserID == OpenIM.iMManager.userID; + String? name = isISendRequest ? info.toNickname : info.fromNickname; + String? faceURL = isISendRequest ? info.toFaceURL : info.fromFaceURL; + String? reason = info.reqMsg; + + return Container( + height: 68.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + border: BorderDirectional( + bottom: BorderSide( + color: Styles.c_F8F9FA, + width: 1, + ), + ), + ), + child: Row( + children: [ + AvatarView(url: faceURL, text: name), + 10.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + (name ?? '').toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + 4.verticalSpace, + if (IMUtils.isNotNullEmptyStr(reason)) + (reason ?? '').toText + ..style = Styles.ts_8E9AB0_14sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ], + ), + ), + if (/*info.isWaitingHandle && */ isISendRequest) + ImageRes.sendRequests.toImage + ..width = 20.w + ..height = 20.h, + if (info.isWaitingHandle && !isISendRequest) + Button( + text: StrRes.lookOver, + textStyle: Styles.ts_FFFFFF_14sp, + onTap: () => logic.acceptFriendApplication(info), + height: 28.h, + padding: EdgeInsets.symmetric(horizontal: 13.w), + ), + if (info.isWaitingHandle && isISendRequest) + StrRes.waitingForVerification.toText..style = Styles.ts_8E9AB0_14sp, + if (info.isRejected) + StrRes.rejected.toText..style = Styles.ts_8E9AB0_14sp, + if (info.isAgreed) + StrRes.approved.toText..style = Styles.ts_8E9AB0_14sp, + ], + ), + ); + } +} diff --git a/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_binding.dart b/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_binding.dart new file mode 100644 index 0000000..dc3b3d6 --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'process_friend_requests_logic.dart'; + +class ProcessFriendRequestsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ProcessFriendRequestsLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_logic.dart b/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_logic.dart new file mode 100644 index 0000000..7bd473a --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_logic.dart @@ -0,0 +1,43 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +class ProcessFriendRequestsLogic extends GetxController { + late FriendApplicationInfo applicationInfo; + + @override + void onInit() { + applicationInfo = Get.arguments['applicationInfo']; + super.onInit(); + } + + void acceptFriendApplication() async { + LoadingView.singleton + .wrap( + asyncFunction: () => + OpenIM.iMManager.friendshipManager.acceptFriendApplication(userID: applicationInfo.fromUserID!)) + .then(_addSuccessfully) + .catchError((_) => IMViews.showToast(StrRes.addFailed)); + } + + void refuseFriendApplication() async { + LoadingView.singleton + .wrap( + asyncFunction: () => + OpenIM.iMManager.friendshipManager.refuseFriendApplication(userID: applicationInfo.fromUserID!)) + .then(_rejectSuccessfully) + .catchError((_) => IMViews.showToast(StrRes.rejectFailed)); + } + + _addSuccessfully(_) { + IMViews.showToast(StrRes.addSuccessfully); + Get.back(result: 1); + return _; + } + + _rejectSuccessfully(_) { + IMViews.showToast(StrRes.rejectSuccessfully); + Get.back(result: -1); + return _; + } +} diff --git a/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_view.dart b/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_view.dart new file mode 100644 index 0000000..183f595 --- /dev/null +++ b/mobile-next/lib/pages/contacts/friend_requests/process_friend_requests/process_friend_requests_view.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'process_friend_requests_logic.dart'; + +class ProcessFriendRequestsPage extends StatelessWidget { + final logic = Get.find(); + + ProcessFriendRequestsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.newFriend), + backgroundColor: Styles.c_F8F9FA, + body: Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + AvatarView( + width: 48.w, + height: 48.h, + url: logic.applicationInfo.fromFaceURL, + text: logic.applicationInfo.fromNickname, + ), + 10.horizontalSpace, + (logic.applicationInfo.fromNickname ?? '').toText + ..style = Styles.ts_0C1C33_17sp, + ], + ), + 12.verticalSpace, + if (IMUtils.isNotNullEmptyStr(logic.applicationInfo.reqMsg)) + Container( + height: 80.h, + width: 343.w, + margin: EdgeInsets.only(bottom: 12.h), + decoration: BoxDecoration( + color: Styles.c_E8EAEF_opacity50, + borderRadius: BorderRadius.circular(6.r), + ), + child: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 16.h, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (logic.applicationInfo.reqMsg ?? '').toText + ..style = Styles.ts_0C1C33_17sp, + ], + ), + ), + ), + Row( + children: [ + Flexible(child: _buildRejectButton()), + 12.horizontalSpace, + Flexible( + child: Button( + text: StrRes.accept, + textStyle: Styles.ts_FFFFFF_17sp, + onTap: logic.acceptFriendApplication, + ), + ), + ], + ) + ], + ), + ), + ); + } + + Widget _buildRejectButton() => Material( + child: Ink( + height: 44.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + border: Border.all( + color: Styles.c_E8EAEF, + width: 1, + ), + borderRadius: BorderRadius.circular(6.r), + ), + child: InkWell( + onTap: logic.refuseFriendApplication, + child: Container( + alignment: Alignment.center, + child: StrRes.reject.toText..style = Styles.ts_0C1C33_17sp, + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/group_list/group_list_binding.dart b/mobile-next/lib/pages/contacts/group_list/group_list_binding.dart new file mode 100644 index 0000000..3e5dab9 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_list/group_list_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'group_list_logic.dart'; + +class GroupListBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GroupListLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/group_list/group_list_logic.dart b/mobile-next/lib/pages/contacts/group_list/group_list_logic.dart new file mode 100644 index 0000000..a63c497 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_list/group_list_logic.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; + +import '../../../core/controller/im_controller.dart'; +import '../../../core/im_callback.dart'; +import '../../conversation/conversation_logic.dart'; + +class GroupListLogic extends GetxController { + final imLoic = Get.find(); + final iCreateRefreshController = RefreshController(initialRefresh: true); + final iJoinRefreshController = RefreshController(initialRefresh: true); + + final iCreateGlobalKey = GlobalKey(); + final iJoinGlobalKey = GlobalKey(); + + final conversationLogic = Get.find(); + final index = 0.obs; + final iCreatedList = [].obs; + final iJoinedList = [].obs; + + int iCreatedOffset = 0; + int iJoinedOffset = 0; + + int count = 1000; + + @override + void onInit() { + imLoic.imSdkStatusPublishSubject.last.then((con) { + if (con.status == IMSdkStatus.syncEnded) { + iCreatedInitial(); + iJoinedInitial(); + } + }); + + iCreatedInitial(); + iJoinedInitial(); + + super.onInit(); + } + + void switchTab(i) { + index.value = i; + } + + void iCreatedInitial() async { + iCreatedOffset = 0; + iCreatedList.clear(); + final length = await initial(iCreate: true, offset: iCreatedOffset); + + iCreateRefreshController.refreshCompleted(); + + if (length >= count) { + iCreatedOffset += length; + } else { + iCreateRefreshController.loadNoData(); + } + } + + void iCreatedLoadMore() async { + final length = await loadMore(iCreate: true, offset: iCreatedOffset); + + if (length < count) { + iCreateRefreshController.loadNoData(); + } else { + iCreateRefreshController.loadComplete(); + iCreatedOffset += length; + } + } + + void iJoinedInitial() async { + iJoinedOffset = 0; + iJoinedList.clear(); + final length = await initial(iCreate: false, offset: iJoinedOffset); + + iJoinRefreshController.refreshCompleted(); + + if (length >= count) { + iJoinedOffset += length; + } else { + iJoinRefreshController.loadNoData(); + } + } + + void iJoinedLoadMore() async { + final length = await loadMore(iCreate: false, offset: iJoinedOffset); + + if (length < count) { + iJoinRefreshController.loadNoData(); + } else { + iJoinRefreshController.loadComplete(); + iJoinedOffset += length; + } + } + + Future initial({bool iCreate = true, int offset = 0}) async { + final list = await OpenIM.iMManager.groupManager.getJoinedGroupListPage(offset: offset, count: count); + + int iCreatedCount = 0; + int iJoinedCount = 0; + + if (iCreate) { + final result = list.where((e) => e.ownerUserID == OpenIM.iMManager.userID); + iCreatedList.addAll(result); + iCreatedCount = result.length; + } else { + final result = list.where((e) => e.ownerUserID != OpenIM.iMManager.userID); + iJoinedList.addAll(result); + iJoinedCount = result.length; + } + + return iCreate ? iCreatedCount : iJoinedCount; + } + + Future loadMore({bool iCreate = true, int offset = 0}) async { + final list = await OpenIM.iMManager.groupManager.getJoinedGroupListPage(offset: offset, count: count); + + int iCreatedCount = 0; + int iJoinedCount = 0; + + if (iCreate) { + final result = list.where((e) => e.ownerUserID == OpenIM.iMManager.userID); + iCreatedList.addAll(result); + iCreatedCount = result.length; + } else { + final result = list.where((e) => e.ownerUserID != OpenIM.iMManager.userID); + iJoinedList.addAll(result); + iJoinedCount = result.length; + } + + return iCreate ? iCreatedCount : iJoinedCount; + } + + void toGroupChat(GroupInfo info) { + conversationLogic.toChat( + offUntilHome: false, + groupID: info.groupID, + nickname: info.groupName, + faceURL: info.faceURL, + sessionType: info.sessionType, + ); + } + + void searchGroup() => AppNavigator.startSearchGroup(); +} diff --git a/mobile-next/lib/pages/contacts/group_list/group_list_view.dart b/mobile-next/lib/pages/contacts/group_list/group_list_view.dart new file mode 100644 index 0000000..1028c5f --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_list/group_list_view.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'group_list_logic.dart'; + +class GroupListPage extends StatelessWidget { + final logic = Get.find(); + + GroupListPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.myGroup), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.searchGroup, + child: Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + child: const SearchBox(), + ), + ), + Obx( + () => CustomTabBar( + labels: [StrRes.iCreatedGroup, StrRes.iJoinedGroup], + index: logic.index.value, + onTabChanged: (i) => logic.switchTab(i), + showUnderline: true, + ), + ), + Expanded( + child: Obx( + () => logic.index.value == 0 ? _buildICreatedListView() : _buildIJoinedListView(), + ), + ), + ], + ), + ); + } + + Widget _buildICreatedListView() => SmartRefresher( + key: logic.iCreateGlobalKey, + controller: logic.iCreateRefreshController, + header: IMViews.buildHeader(30), + footer: IMViews.buildFooter(), + enablePullUp: true, + enablePullDown: true, + onRefresh: logic.iCreatedInitial, + onLoading: logic.iCreatedLoadMore, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: logic.iCreatedList.length, + itemBuilder: (_, index) => _buildItemView(logic.iCreatedList[index]), + ), + ); + + Widget _buildIJoinedListView() => SmartRefresher( + key: logic.iJoinGlobalKey, + controller: logic.iJoinRefreshController, + header: IMViews.buildHeader(30), + footer: IMViews.buildFooter(), + enablePullUp: true, + enablePullDown: true, + onRefresh: logic.iJoinedInitial, + onLoading: logic.iJoinedLoadMore, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: logic.iJoinedList.length, + itemBuilder: (_, index) => _buildItemView(logic.iJoinedList[index]), + ), + ); + + Widget _buildItemView(GroupInfo info) => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: () => logic.toGroupChat(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + AvatarView( + url: info.faceURL, + text: info.groupName, + isGroup: true, + ), + 10.horizontalSpace, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + (info.groupName ?? '').toText..style = Styles.ts_0C1C33_17sp, + sprintf(StrRes.nPerson, [info.memberCount]).toText..style = Styles.ts_8E9AB0_14sp, + ], + ), + ], + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/group_list/search_group/search_group_binding.dart b/mobile-next/lib/pages/contacts/group_list/search_group/search_group_binding.dart new file mode 100644 index 0000000..9eaf760 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_list/search_group/search_group_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'search_group_logic.dart'; + +class SearchGroupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SearchGroupLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/group_list/search_group/search_group_logic.dart b/mobile-next/lib/pages/contacts/group_list/search_group/search_group_logic.dart new file mode 100644 index 0000000..4c91b0b --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_list/search_group/search_group_logic.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; + +import '../../../conversation/conversation_logic.dart'; +import '../group_list_logic.dart'; + +class SearchGroupLogic extends GetxController { + final conversationLogic = Get.find(); + final logic = Get.find(); + final focusNode = FocusNode(); + final searchCtrl = TextEditingController(); + final resultList = [].obs; + + @override + void onInit() { + searchCtrl.addListener(_clearInput); + super.onInit(); + } + + @override + void onClose() { + focusNode.dispose(); + searchCtrl.dispose(); + super.onClose(); + } + + bool get isSearchNotResult => searchCtrl.text.trim().isNotEmpty && resultList.isEmpty; + + _clearInput() { + final key = searchCtrl.text.trim(); + if (key.isEmpty) { + resultList.clear(); + } + } + + search() async { + var key = searchCtrl.text.trim(); + + final result = await OpenIM.iMManager.groupManager.searchGroups(keywordList: [key], isSearchGroupName: true); + resultList.clear(); + resultList.addAll(result); + } + + void toGroupChat(GroupInfo info) { + conversationLogic.toChat( + offUntilHome: false, + groupID: info.groupID, + nickname: info.groupName, + faceURL: info.faceURL, + sessionType: info.sessionType, + ); + } +} diff --git a/mobile-next/lib/pages/contacts/group_list/search_group/search_group_view.dart b/mobile-next/lib/pages/contacts/group_list/search_group/search_group_view.dart new file mode 100644 index 0000000..a1b4053 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_list/search_group/search_group_view.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:search_keyword_text/search_keyword_text.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'search_group_logic.dart'; + +class SearchGroupPage extends StatelessWidget { + final logic = Get.find(); + + SearchGroupPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.search( + focusNode: logic.focusNode, + controller: logic.searchCtrl, + onSubmitted: (_) => logic.search(), + onCleared: () => logic.focusNode.requestFocus(), + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => logic.isSearchNotResult + ? _emptyListView + : ListView.builder( + itemCount: logic.resultList.length, + itemBuilder: (_, index) => _buildItemView(logic.resultList[index]), + )), + ), + ); + } + + Widget _buildItemView(GroupInfo info) => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: () => logic.toGroupChat(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + AvatarView( + url: info.faceURL, + text: info.groupName, + isGroup: true, + ), + 10.horizontalSpace, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SearchKeywordText( + text: info.groupName ?? '', + keyText: logic.searchCtrl.text.trim(), + style: Styles.ts_0C1C33_17sp, + keyStyle: Styles.ts_0089FF_17sp, + ), + sprintf(StrRes.nPerson, [info.memberCount]).toText..style = Styles.ts_8E9AB0_14sp, + ], + ), + ], + ), + ), + ), + ); + + Widget get _emptyListView => SizedBox( + width: 1.sw, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 44.verticalSpace, + StrRes.searchNotFound.toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_binding.dart b/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_binding.dart new file mode 100644 index 0000000..cacb93b --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'group_profile_panel_logic.dart'; + +class GroupProfilePanelBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GroupProfilePanelLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_logic.dart b/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_logic.dart new file mode 100644 index 0000000..59adfe6 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_logic.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; + +import '../../../core/controller/im_controller.dart'; +import '../../../routes/app_navigator.dart'; +import '../../conversation/conversation_logic.dart'; + +enum JoinGroupMethod { search, qrcode, invite } + +class GroupProfilePanelLogic extends GetxController { + final conversationLogic = Get.find(); + final imLogic = Get.find(); + final isJoined = false.obs; + final members = [].obs; + late Rx groupInfo; + late JoinGroupMethod joinGroupMethod; + + late StreamSubscription sub; + late StreamSubscription joinedGroupAddedSub; + late StreamSubscription memberAddedSub; + + @override + void onInit() { + groupInfo = Rx(GroupInfo(groupID: Get.arguments['groupID'])); + joinGroupMethod = Get.arguments['joinGroupMethod']; + sub = imLogic.groupApplicationChangedSubject.listen(_onChanged); + joinedGroupAddedSub = imLogic.joinedGroupAddedSubject.listen(_onChanged); + memberAddedSub = imLogic.memberAddedSubject.listen(_onChanged); + _checkGroup(); + _getGroupInfo(); + _getMembers(); + super.onInit(); + } + + _onChanged(dynamic value) { + if (value is GroupApplicationInfo) { + if (value.groupID == groupInfo.value.groupID && value.handleResult == 1) { + if (!isJoined.value) { + isJoined.value = true; + _getGroupInfo(); + _getMembers(); + } + } + } else if (value is GroupInfo) { + if (value.groupID == groupInfo.value.groupID) { + if (!isJoined.value) { + isJoined.value = true; + _getGroupInfo(); + _getMembers(); + } + } + } else if (value is GroupMembersInfo) { + if (value.groupID == groupInfo.value.groupID && value.userID == OpenIM.iMManager.userID) { + if (!isJoined.value) { + isJoined.value = true; + _getGroupInfo(); + _getMembers(); + } + } + } + } + + _getGroupInfo() async { + var list = await OpenIM.iMManager.groupManager.getGroupsInfo( + groupIDList: [groupInfo.value.groupID], + ); + var info = list.firstOrNull; + if (null != info) { + groupInfo.update((val) { + val?.groupName = info.groupName; + val?.faceURL = info.faceURL; + val?.memberCount = info.memberCount; + val?.groupType = info.groupType; + val?.createTime = info.createTime; + }); + } + } + + _checkGroup() async { + isJoined.value = await OpenIM.iMManager.groupManager.isJoinedGroup( + groupID: groupInfo.value.groupID, + ); + } + + _getMembers() async { + var list = await OpenIM.iMManager.groupManager.getGroupMemberList( + groupID: groupInfo.value.groupID, + count: 10, + ); + members.assignAll(list); + } + + enterGroup() async { + if (isJoined.value) { + conversationLogic.toChat( + groupID: groupInfo.value.groupID, + nickname: groupInfo.value.groupName, + faceURL: groupInfo.value.faceURL, + sessionType: groupInfo.value.sessionType, + ); + } else { + AppNavigator.startSendVerificationApplication( + groupID: groupInfo.value.groupID, + joinGroupMethod: joinGroupMethod, + ); + } + } + + @override + void onClose() { + sub.cancel(); + joinedGroupAddedSub.cancel(); + memberAddedSub.cancel(); + super.onClose(); + } +} diff --git a/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_view.dart b/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_view.dart new file mode 100644 index 0000000..1b33b62 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_profile_panel/group_profile_panel_view.dart @@ -0,0 +1,147 @@ +import 'dart:math'; + +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'group_profile_panel_logic.dart'; + +class GroupProfilePanelPage extends StatelessWidget { + final logic = Get.find(); + + GroupProfilePanelPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => Column( + children: [ + _buildBaseInfo(), + if (logic.members.isNotEmpty) _buildGroupMemberList(), + Container( + height: 56.h, + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + StrRes.groupID.toText..style = Styles.ts_0C1C33_17sp, + 12.horizontalSpace, + logic.groupInfo.value.groupID.toText + ..style = Styles.ts_8E9AB0_17sp, + ], + ), + ), + const Spacer(), + Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + color: Styles.c_FFFFFF, + child: Button( + text: logic.isJoined.value + ? StrRes.enterGroup + : StrRes.applyJoin, + onTap: logic.enterGroup, + ), + ), + ], + )), + ); + } + + Widget _buildBaseInfo() => Container( + height: 80.h, + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w), + margin: EdgeInsets.only(bottom: 10.h), + child: Row( + children: [ + AvatarView( + width: 48.w, + height: 48.h, + url: logic.groupInfo.value.faceURL, + text: logic.groupInfo.value.groupName, + isGroup: true, + ), + 12.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + (logic.groupInfo.value.groupName ?? '').toText + ..style = Styles.ts_0C1C33_17sp_medium, + 4.verticalSpace, + Row( + children: [ + ImageRes.createGroupTime.toImage + ..width = 12.w + ..height = 12.h, + 6.horizontalSpace, + DateUtil.formatDateMs( + (logic.groupInfo.value.createTime ?? 0), + format: IMUtils.getTimeFormat1(), + ).toText + ..style = Styles.ts_8E9AB0_14sp, + ], + ), + ], + ), + ), + ], + ), + ); + + Widget _buildGroupMemberList() => Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + margin: EdgeInsets.only(bottom: 10.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: StrRes.groupMember, + style: Styles.ts_0C1C33_17sp, + children: [ + WidgetSpan(child: 12.horizontalSpace), + TextSpan( + text: sprintf( + StrRes.nPerson, [logic.groupInfo.value.memberCount]), + style: Styles.ts_8E9AB0_17sp, + ), + ], + ), + ), + 10.verticalSpace, + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + crossAxisSpacing: 6.w, + childAspectRatio: 1, + ), + itemCount: min(logic.members.length, 7), + shrinkWrap: true, + itemBuilder: (_, index) { + final member = logic.members.elementAt(index); + if (index == 6 && logic.members.length != 7) { + return ImageRes.moreMembers.toImage + ..width = 44.w + ..height = 44.h; + } + return AvatarView( + width: 44.w, + height: 44.h, + text: member.nickname, + url: member.faceURL, + ); + }, + ), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/group_requests/group_requests_binding.dart b/mobile-next/lib/pages/contacts/group_requests/group_requests_binding.dart new file mode 100644 index 0000000..4586fe6 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_requests/group_requests_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'group_requests_logic.dart'; + +class GroupRequestsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GroupRequestsLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/group_requests/group_requests_logic.dart b/mobile-next/lib/pages/contacts/group_requests/group_requests_logic.dart new file mode 100644 index 0000000..455ed9a --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_requests/group_requests_logic.dart @@ -0,0 +1,140 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; +import '../../home/home_logic.dart'; + +class GroupRequestsLogic extends GetxController { + final imLogic = Get.find(); + final homeLogic = Get.find(); + final list = [].obs; + final groupList = {}.obs; + final memberList = [].obs; + final userInfoList = [].obs; + + @override + void onReady() { + getApplicationList(); + getJoinedGroup(); + super.onReady(); + } + + @override + void onInit() { + imLogic.groupApplicationChangedSubject.listen((info) { + getApplicationList(); + }); + super.onInit(); + } + + @override + void onClose() { + homeLogic.getUnhandledGroupApplicationCount(); + super.onClose(); + } + + bool isInvite(GroupApplicationInfo info) { + if (info.joinSource == 2) { + return info.inviterUserID != null && info.inviterUserID!.isNotEmpty; + } + return false; + } + + getApplicationList() async { + final list = await LoadingView.singleton.wrap(asyncFunction: () async { + final list = await Future.wait([ + OpenIM.iMManager.groupManager.getGroupApplicationListAsRecipient(), + OpenIM.iMManager.groupManager.getGroupApplicationListAsApplicant(), + ]); + + final allList = []; + allList + ..addAll(list[0]) + ..addAll(list[1]); + + allList.sort((a, b) { + if (a.reqTime! > b.reqTime!) { + return -1; + } else if (a.reqTime! < b.reqTime!) { + return 1; + } + return 0; + }); + + var map = >{}; + var inviterList = []; + + var haveReadList = DataSp.getHaveReadUnHandleGroupApplication(); + haveReadList ??= []; + for (var a in list[0]) { + var id = IMUtils.buildGroupApplicationID(a); + if (!haveReadList.contains(id)) { + haveReadList.add(id); + } + } + DataSp.putHaveReadUnHandleGroupApplication(haveReadList); + + var groupIDList = []; + + for (var a in allList) { + if (isInvite(a)) { + if (!map.containsKey(a.groupID)) { + map[a.groupID!] = [a.inviterUserID!]; + } else { + if (!map[a.groupID!]!.contains(a.inviterUserID!)) { + map[a.groupID!]!.add(a.inviterUserID!); + } + } + if (!inviterList.contains(a.inviterUserID!)) { + inviterList.add(a.inviterUserID!); + } + } + } + + if (map.isNotEmpty) { + await Future.wait(map.entries.map((e) => OpenIM.iMManager.groupManager + .getGroupMembersInfo(groupID: e.key, userIDList: e.value) + .then((list) => memberList.assignAll(list)))); + } + + if (inviterList.isNotEmpty) { + await OpenIM.iMManager.userManager + .getUsersInfo(userIDList: inviterList) + .then((list) => userInfoList.assignAll(list.map((e) => e.simpleUserInfo).toList())); + } + + return allList; + }); + + this.list.assignAll(list); + } + + void getJoinedGroup() { + OpenIM.iMManager.groupManager.getJoinedGroupList().then((list) { + var map = {}; + for (var e in list) { + map[e.groupID] = e; + } + groupList.addAll(map); + }); + } + + String getGroupName(GroupApplicationInfo info) => info.groupName ?? groupList[info.groupID]?.groupName ?? ''; + + String getInviterNickname(GroupApplicationInfo info) => + (getMemberInfo(info.inviterUserID!)?.nickname) ?? (getUserInfo(info.inviterUserID!)?.nickname) ?? '-'; + + GroupMembersInfo? getMemberInfo(inviterUserID) => memberList.firstWhereOrNull((e) => e.userID == inviterUserID); + + UserInfo? getUserInfo(inviterUserID) => userInfoList.firstWhereOrNull((e) => e.userID == inviterUserID); + + void handle(GroupApplicationInfo info) async { + var result = await AppNavigator.startProcessGroupRequests(applicationInfo: info); + if (result is int) { + info.handleResult = result; + list.refresh(); + } + } +} diff --git a/mobile-next/lib/pages/contacts/group_requests/group_requests_view.dart b/mobile-next/lib/pages/contacts/group_requests/group_requests_view.dart new file mode 100644 index 0000000..cc37efd --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_requests/group_requests_view.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'group_requests_logic.dart'; + +class GroupRequestsPage extends StatelessWidget { + final logic = Get.find(); + + GroupRequestsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.newGroupRequest), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => ListView.builder( + padding: EdgeInsets.only(top: 10.h), + itemCount: logic.list.length, + itemBuilder: (_, index) => _buildItemView(logic.list[index]), + )), + ); + } + + Widget _buildItemView(GroupApplicationInfo info) { + final isISendRequest = info.userID == OpenIM.iMManager.userID; + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + border: BorderDirectional( + bottom: BorderSide(color: Styles.c_F8F9FA, width: 1.h), + ), + ), + child: Row( + children: [ + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AvatarView( + url: info.userFaceURL, + text: info.nickname, + ), + 10.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + (info.nickname ?? '').toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + 4.verticalSpace, + if (!logic.isInvite(info)) + RichText( + text: TextSpan( + text: StrRes.applyJoin, + style: Styles.ts_8E9AB0_14sp, + children: [ + WidgetSpan(child: 2.horizontalSpace), + TextSpan( + text: logic.getGroupName(info), + style: Styles.ts_0089FF_14sp, + ), + ], + ), + ) + else + RichText( + text: TextSpan( + text: logic.getInviterNickname(info), + style: Styles.ts_0089FF_14sp, + children: [ + WidgetSpan(child: 2.horizontalSpace), + TextSpan( + text: StrRes.invite, + style: Styles.ts_8E9AB0_14sp, + ), + WidgetSpan(child: 2.horizontalSpace), + TextSpan( + text: info.nickname, + style: Styles.ts_0089FF_14sp, + ), + WidgetSpan(child: 2.horizontalSpace), + TextSpan( + text: StrRes.joinIn, + style: Styles.ts_8E9AB0_14sp, + ), + WidgetSpan(child: 2.horizontalSpace), + TextSpan( + text: logic.getGroupName(info), + style: Styles.ts_0089FF_14sp, + ), + ], + ), + ), + if (null != IMUtils.emptyStrToNull(info.reqMsg)) + Padding( + padding: EdgeInsets.only(top: 4.h), + child: sprintf(StrRes.applyReason, [info.reqMsg!]).toText + ..style = Styles.ts_8E9AB0_14sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + ], + ), + ) + ], + ), + ), + if (/*info.handleResult == 0 && */ isISendRequest) + ImageRes.sendRequests.toImage + ..width = 20.w + ..height = 20.h, + if (info.handleResult == 0 && !isISendRequest) + Button( + text: StrRes.lookOver, + textStyle: Styles.ts_FFFFFF_14sp, + height: 28.h, + padding: EdgeInsets.symmetric(horizontal: 13.w), + onTap: () => logic.handle(info), + ), + if (info.handleResult == 0 && isISendRequest) + StrRes.waitingForVerification.toText..style = Styles.ts_8E9AB0_14sp, + if (info.handleResult == -1) StrRes.rejected.toText..style = Styles.ts_8E9AB0_14sp, + if (info.handleResult == 1) StrRes.approved.toText..style = Styles.ts_8E9AB0_14sp, + ], + ), + ); + } +} diff --git a/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_binding.dart b/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_binding.dart new file mode 100644 index 0000000..0bd4380 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'process_group_requests_logic.dart'; + +class ProcessGroupRequestsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ProcessGroupRequestsLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_logic.dart b/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_logic.dart new file mode 100644 index 0000000..93eb0d3 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_logic.dart @@ -0,0 +1,71 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../group_requests_logic.dart'; + +class ProcessGroupRequestsLogic extends GetxController { + final groupRequestsLogic = Get.find(); + late GroupApplicationInfo applicationInfo; + + @override + void onInit() { + applicationInfo = Get.arguments['applicationInfo']; + super.onInit(); + } + + bool get isInvite => groupRequestsLogic.isInvite(applicationInfo); + + String get groupName => groupRequestsLogic.getGroupName(applicationInfo); + + String get inviterNickname => groupRequestsLogic.getInviterNickname(applicationInfo); + + GroupMembersInfo? getMemberInfo(inviterUserID) => groupRequestsLogic.getMemberInfo(inviterUserID); + + UserInfo? getUserInfo(inviterUserID) => groupRequestsLogic.getUserInfo(inviterUserID); + + String get sourceFrom { + if (applicationInfo.joinSource == 2) { + return '$inviterNickname${StrRes.byMemberInvite}'; + } else if (applicationInfo.joinSource == 4) { + return StrRes.byScanQrcode; + } + return StrRes.bySearch; + } + + void approve() { + LoadingView.singleton + .wrap( + asyncFunction: () => OpenIM.iMManager.groupManager.acceptGroupApplication( + groupID: applicationInfo.groupID!, + userID: applicationInfo.userID!, + handleMsg: "reason", + )) + .then((value) => Get.back(result: 1)) + .catchError(_parse); + } + + void reject() { + LoadingView.singleton + .wrap( + asyncFunction: () => OpenIM.iMManager.groupManager.refuseGroupApplication( + groupID: applicationInfo.groupID!, + userID: applicationInfo.userID!, + handleMsg: "reason", + )) + .then((value) => Get.back(result: -1)) + .catchError(_parse) + .catchError((_) => IMViews.showToast(StrRes.rejectFailed)); + } + + _parse(e) { + if (e is PlatformException) { + if (e.code == '${SDKErrorCode.groupApplicationHasBeenProcessed}') { + IMViews.showToast(StrRes.groupRequestHandled); + return; + } + } + throw e; + } +} diff --git a/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_view.dart b/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_view.dart new file mode 100644 index 0000000..3e40147 --- /dev/null +++ b/mobile-next/lib/pages/contacts/group_requests/process_group_requests/process_group_requests_view.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'process_group_requests_logic.dart'; + +class ProcessGroupRequestsPage extends StatelessWidget { + final logic = Get.find(); + + ProcessGroupRequestsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.newGroup), + backgroundColor: Styles.c_F8F9FA, + body: Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 16.h, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + AvatarView( + width: 48.w, + height: 48.h, + url: logic.applicationInfo.userFaceURL, + text: logic.applicationInfo.nickname, + ), + 10.horizontalSpace, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (logic.applicationInfo.nickname ?? '').toText..style = Styles.ts_0C1C33_17sp, + RichText( + text: TextSpan( + text: StrRes.applyJoin, + style: Styles.ts_8E9AB0_14sp, + children: [ + WidgetSpan(child: 2.horizontalSpace), + TextSpan( + text: logic.groupName, + style: Styles.ts_0089FF_14sp, + ), + ], + ), + ) + ], + ), + ], + ), + 12.verticalSpace, + if (IMUtils.isNotNullEmptyStr(logic.applicationInfo.reqMsg)) + Container( + height: 80.h, + width: 343.w, + margin: EdgeInsets.only(bottom: 12.h), + decoration: BoxDecoration( + color: Styles.c_E8EAEF_opacity50, + borderRadius: BorderRadius.circular(6.r), + ), + child: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 8.h, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (logic.applicationInfo.reqMsg ?? '').toText..style = Styles.ts_0C1C33_17sp, + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + sprintf(StrRes.sourceFrom, [logic.sourceFrom]).toText..style = Styles.ts_8E9AB0_14sp + ], + ), + 12.verticalSpace, + Row( + children: [ + Flexible(child: _buildRejectButton()), + 12.horizontalSpace, + Flexible( + child: Button( + onTap: logic.approve, + text: StrRes.accept, + textStyle: Styles.ts_FFFFFF_17sp, + ), + ), + ], + ) + ], + ), + ), + ); + } + + Widget _buildRejectButton() => Material( + child: Ink( + height: 44.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + border: Border.all( + color: Styles.c_E8EAEF, + width: 1, + ), + borderRadius: BorderRadius.circular(6.r), + ), + child: InkWell( + onTap: logic.reject, + child: Container( + alignment: Alignment.center, + child: StrRes.reject.toText..style = Styles.ts_0C1C33_17sp, + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_binding.dart b/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_binding.dart new file mode 100644 index 0000000..14c96fc --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'friend_list_logic.dart'; + +class SelectContactsFromFriendsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SelectContactsFromFriendsLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_logic.dart b/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_logic.dart new file mode 100644 index 0000000..11e6dc0 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_logic.dart @@ -0,0 +1,54 @@ +import 'package:get/get.dart'; +import 'package:openim/pages/contacts/friend_list/friend_list_logic.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../select_contacts_logic.dart'; + +class SelectContactsFromFriendsLogic extends FriendListLogic { + final selectContactsLogic = Get.find(); + + @override + searchFriend() async { + final result = await AppNavigator.startSelectContactsFromSearchFriends(); + if (null != result) { + Get.back(result: result); + } + } + + @override + void onUserIDList(List userIDList) { + selectContactsLogic.updateDefaultCheckedList(userIDList); + super.onUserIDList(userIDList); + } + + bool get isSelectAll { + if (selectContactsLogic.checkedList.isEmpty) { + return false; + } else if (operableList + .every((item) => selectContactsLogic.isChecked(item))) { + return true; + } else { + return false; + } + } + + Iterable get operableList => friendList.where(_remove); + + bool _remove(ISUserInfo info) => !selectContactsLogic.isDefaultChecked(info); + + selectAll() { + if (isSelectAll) { + for (var info in operableList) { + selectContactsLogic.removeItem(info); + } + } else { + for (var info in operableList) { + final isChecked = selectContactsLogic.isChecked(info); + if (!isChecked) { + selectContactsLogic.toggleChecked(info); + } + } + } + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_view.dart b/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_view.dart new file mode 100644 index 0000000..4444144 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/friend_list/friend_list_view.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../select_contacts_logic.dart'; +import 'friend_list_logic.dart'; + +class SelectContactsFromFriendsPage extends StatelessWidget { + final logic = Get.find(); + final selectContactsLogic = Get.find(); + + SelectContactsFromFriendsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.myFriend), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.searchFriend, + child: Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + child: const SearchBox(), + ), + ), + if (selectContactsLogic.isMultiModel) + Padding( + padding: EdgeInsets.symmetric(vertical: 10.h), + child: Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: logic.selectAll, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + Obx(() => Padding( + padding: EdgeInsets.only(right: 10.w), + child: ChatRadio(checked: logic.isSelectAll), + )), + 10.horizontalSpace, + StrRes.selectAll.toText..style = Styles.ts_0C1C33_17sp, + ], + ), + ), + ), + ), + ), + Flexible( + child: Obx( + () => WrapAzListView( + data: logic.friendList, + itemCount: logic.friendList.length, + itemBuilder: (_, data, index) => _buildItemView(data), + ), + ), + ), + selectContactsLogic.checkedConfirmView, + ], + ), + ); + } + + Widget _buildItemView(ISUserInfo info) { + Widget buildChild() => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: selectContactsLogic.onTap(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + if (selectContactsLogic.isMultiModel) + Padding( + padding: EdgeInsets.only(right: 10.w), + child: ChatRadio( + checked: selectContactsLogic.isChecked(info), + enabled: !selectContactsLogic.isDefaultChecked(info), + ), + ), + AvatarView( + url: info.faceURL, + text: info.showName, + ), + 10.horizontalSpace, + info.showName.toText..style = Styles.ts_0C1C33_17sp, + ], + ), + ), + ), + ); + return selectContactsLogic.isMultiModel ? Obx(buildChild) : buildChild(); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_binding.dart b/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_binding.dart new file mode 100644 index 0000000..52f21ea --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'search_friend_logic.dart'; + +class SelectContactsFromSearchFriendsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SelectContactsFromSearchFriendsLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_logic.dart b/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_logic.dart new file mode 100644 index 0000000..d6dfb57 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_logic.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../friend_list_logic.dart'; + +class SelectContactsFromSearchFriendsLogic extends GetxController { + final logic = Get.find(); + final focusNode = FocusNode(); + final searchCtrl = TextEditingController(); + final resultList = [].obs; + + @override + void onInit() { + searchCtrl.addListener(_clearInput); + super.onInit(); + } + + @override + void onClose() { + focusNode.dispose(); + searchCtrl.dispose(); + super.onClose(); + } + + bool get isSearchNotResult => searchCtrl.text.trim().isNotEmpty && resultList.isEmpty; + + _clearInput() { + final key = searchCtrl.text.trim(); + if (key.isEmpty) { + resultList.clear(); + } + } + + search() { + var key = searchCtrl.text.trim(); + resultList.clear(); + if (key.isNotEmpty) { + for (var element in logic.friendList) { + if (element.showName.toUpperCase().contains(key.toUpperCase())) { + resultList.add(element); + } + } + } + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_view.dart b/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_view.dart new file mode 100644 index 0000000..94ecfbb --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/friend_list/search_friend/search_friend_view.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:search_keyword_text/search_keyword_text.dart'; + +import '../../select_contacts_logic.dart'; +import 'search_friend_logic.dart'; + +class SelectContactsFromSearchFriendsPage extends StatelessWidget { + final logic = Get.find(); + final selectContactsLogic = Get.find(); + + SelectContactsFromSearchFriendsPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.search( + focusNode: logic.focusNode, + controller: logic.searchCtrl, + onSubmitted: (_) => logic.search(), + onCleared: () => logic.focusNode.requestFocus(), + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => logic.isSearchNotResult + ? _emptyListView + : ListView.builder( + itemCount: logic.resultList.length, + itemBuilder: (_, index) => _buildItemView(logic.resultList[index]), + )), + ), + ); + } + + Widget _buildItemView(ISUserInfo info) { + Widget buildChild() => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: selectContactsLogic.onTap(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + if (selectContactsLogic.isMultiModel) + Padding( + padding: EdgeInsets.only(right: 10.w), + child: ChatRadio( + checked: selectContactsLogic.isChecked(info), + enabled: !selectContactsLogic.isDefaultChecked(info), + ), + ), + AvatarView( + url: info.faceURL, + text: info.showName, + ), + 10.horizontalSpace, + SearchKeywordText( + text: info.showName, + keyText: logic.searchCtrl.text.trim(), + style: Styles.ts_0C1C33_17sp, + keyStyle: Styles.ts_0089FF_17sp, + ), + ], + ), + ), + ), + ); + return selectContactsLogic.isMultiModel ? Obx(buildChild) : buildChild(); + } + + Widget get _emptyListView => SizedBox( + width: 1.sw, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 44.verticalSpace, + StrRes.searchNotFound.toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_binding.dart b/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_binding.dart new file mode 100644 index 0000000..11ec20e --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'group_list_logic.dart'; + +class SelectContactsFromGroupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SelectContactsFromGroupLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_logic.dart b/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_logic.dart new file mode 100644 index 0000000..0e12eb4 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_logic.dart @@ -0,0 +1,58 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; + +import '../../../../routes/app_navigator.dart'; +import '../select_contacts_logic.dart'; + +class SelectContactsFromGroupLogic extends GetxController { + final selectContactsLogic = Get.find(); + final allList = [].obs; + + @override + void onReady() { + _getGroupRelatedToMe(); + super.onReady(); + } + + void _getGroupRelatedToMe() async { + final list = await OpenIM.iMManager.groupManager.getJoinedGroupList(); + allList.addAll(list); + } + + Iterable get operableList => allList.where(_remove); + + bool _remove(GroupInfo info) => !selectContactsLogic.isDefaultChecked(info); + + bool get isSelectAll { + if (selectContactsLogic.checkedList.isEmpty) { + return false; + } else if (operableList + .every((item) => selectContactsLogic.isChecked(item))) { + return true; + } else { + return false; + } + } + + selectAll() { + if (isSelectAll) { + for (var info in operableList) { + selectContactsLogic.removeItem(info); + } + } else { + for (var info in operableList) { + final isChecked = selectContactsLogic.isChecked(info); + if (!isChecked) { + selectContactsLogic.toggleChecked(info); + } + } + } + } + + void searchGroup() async { + final result = await AppNavigator.startSelectContactsFromSearchGroup(); + if (null != result) { + Get.back(result: result); + } + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_view.dart b/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_view.dart new file mode 100644 index 0000000..7011aa0 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/group_list/group_list_view.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import '../select_contacts_logic.dart'; +import 'group_list_logic.dart'; + +class SelectContactsFromGroupPage extends StatelessWidget { + final logic = Get.find(); + final selectContactsLogic = Get.find(); + + SelectContactsFromGroupPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.myGroup), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.searchGroup, + child: Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + child: const SearchBox(), + ), + ), + if (selectContactsLogic.isMultiModel) + Padding( + padding: EdgeInsets.symmetric(vertical: 10.h), + child: Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: logic.selectAll, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + Obx(() => Padding( + padding: EdgeInsets.only(right: 10.w), + child: ChatRadio(checked: logic.isSelectAll), + )), + 10.horizontalSpace, + StrRes.selectAll.toText..style = Styles.ts_0C1C33_17sp, + ], + ), + ), + ), + ), + ), + Expanded( + child: Obx(() => ListView.builder( + itemCount: logic.allList.length, + itemBuilder: (_, index) => + _buildItemView(logic.allList[index]), + ))), + selectContactsLogic.checkedConfirmView, + ], + ), + ); + } + + Widget _buildItemView(GroupInfo info) { + Widget buildChild() => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: selectContactsLogic.onTap(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + if (selectContactsLogic.isMultiModel) + Padding( + padding: EdgeInsets.only(right: 10.w), + child: ChatRadio( + checked: selectContactsLogic.isChecked(info), + enabled: !selectContactsLogic.isDefaultChecked(info), + ), + ), + AvatarView( + url: info.faceURL, + text: info.groupName, + isGroup: true, + ), + 10.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + (info.groupName ?? '').toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + sprintf(StrRes.nPerson, [info.memberCount]).toText + ..style = Styles.ts_8E9AB0_14sp, + ], + ), + ), + ], + ), + ), + ), + ); + return selectContactsLogic.isMultiModel ? Obx(buildChild) : buildChild(); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_binding.dart b/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_binding.dart new file mode 100644 index 0000000..9a06f8f --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'search_group_logic.dart'; + +class SelectContactsFromSearchGroupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SelectContactsFromSearchGroupLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_logic.dart b/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_logic.dart new file mode 100644 index 0000000..1c7a115 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_logic.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; + +import '../group_list_logic.dart'; + +class SelectContactsFromSearchGroupLogic extends GetxController { + final logic = Get.find(); + final focusNode = FocusNode(); + final searchCtrl = TextEditingController(); + final resultList = [].obs; + + @override + void onInit() { + searchCtrl.addListener(_clearInput); + super.onInit(); + } + + @override + void onClose() { + focusNode.dispose(); + searchCtrl.dispose(); + super.onClose(); + } + + bool get isSearchNotResult => + searchCtrl.text.trim().isNotEmpty && resultList.isEmpty; + + _clearInput() { + final key = searchCtrl.text.trim(); + if (key.isEmpty) { + resultList.clear(); + } + } + + search() { + var key = searchCtrl.text.trim(); + resultList.clear(); + if (key.isNotEmpty) { + for (var element in logic.allList) { + if ((element.groupName ?? '') + .toUpperCase() + .contains(key.toUpperCase())) { + resultList.add(element); + } + } + } + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_view.dart b/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_view.dart new file mode 100644 index 0000000..30dcded --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/group_list/search_group/search_group_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:search_keyword_text/search_keyword_text.dart'; +import 'package:sprintf/sprintf.dart'; + +import '../../select_contacts_logic.dart'; +import 'search_group_logic.dart'; + +class SelectContactsFromSearchGroupPage extends StatelessWidget { + final logic = Get.find(); + final selectContactsLogic = Get.find(); + + SelectContactsFromSearchGroupPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.search( + focusNode: logic.focusNode, + controller: logic.searchCtrl, + onSubmitted: (_) => logic.search(), + onCleared: () => logic.focusNode.requestFocus(), + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => logic.isSearchNotResult + ? _emptyListView + : ListView.builder( + itemCount: logic.resultList.length, + itemBuilder: (_, index) => _buildItemView(logic.resultList[index]), + )), + ), + ); + } + + Widget _buildItemView(GroupInfo info) { + Widget buildChild() => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: () { + selectContactsLogic.toggleChecked(info); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + if (selectContactsLogic.isMultiModel) + Padding( + padding: EdgeInsets.only(right: 10.w), + child: ChatRadio( + checked: selectContactsLogic.isChecked(info), + ), + ), + AvatarView( + url: info.faceURL, + text: info.groupName, + isGroup: true, + ), + 10.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SearchKeywordText( + text: info.groupName ?? '', + keyText: logic.searchCtrl.text.trim(), + style: Styles.ts_0C1C33_17sp, + keyStyle: Styles.ts_0089FF_17sp, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + sprintf(StrRes.nPerson, [info.memberCount]).toText..style = Styles.ts_8E9AB0_14sp, + ], + ), + ), + ], + ), + ), + ), + ); + return selectContactsLogic.isMultiModel ? Obx(buildChild) : buildChild(); + } + + Widget get _emptyListView => SizedBox( + width: 1.sw, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 44.verticalSpace, + StrRes.searchNotFound.toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_binding.dart b/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_binding.dart new file mode 100644 index 0000000..8ca62f7 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'search_contacts_logic.dart'; + +class SelectContactsFromSearchBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SelectContactsFromSearchLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_logic.dart b/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_logic.dart new file mode 100644 index 0000000..c939b0e --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_logic.dart @@ -0,0 +1,45 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../global_search/global_search_logic.dart'; +import '../select_contacts_logic.dart'; + +class SelectContactsFromSearchLogic extends CommonSearchLogic { + final selectContactsLogic = Get.find(); + final resultList = {}.obs; + + bool get isSearchNotResult => searchCtrl.text.trim().isNotEmpty && resultList.isEmpty; + + @override + void clearList() { + resultList.clear(); + } + + void search() async { + final result = await LoadingView.singleton.wrap( + asyncFunction: () => Future.wait([ + searchFriend(), + if (!selectContactsLogic.hiddenGroup) searchGroup(), + ])); + final friendList = result[0] as List; + clearList(); + resultList + ..addAll(friendList); + if (selectContactsLogic.action == SelAction.addMember) { + var memberInfoList = await getMemberInfo(friendList.map((e) => e.userID!).toList()); + for (var element in memberInfoList) { + selectContactsLogic.defaultCheckedIDList.add(element.userID!); + } + } + if (result.length == 3) { + final groupList = result[2] as List; + resultList.addAll(groupList); + } + } + + Future> getMemberInfo(List uidList) async { + return await OpenIM.iMManager.groupManager + .getGroupMembersInfo(groupID: selectContactsLogic.groupID!, userIDList: uidList); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_view.dart b/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_view.dart new file mode 100644 index 0000000..c1eae35 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/search_contacts/search_contacts_view.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:search_keyword_text/search_keyword_text.dart'; + +import '../select_contacts_logic.dart'; +import 'search_contacts_logic.dart'; + +class SelectContactsFromSearchPage extends StatelessWidget { + final logic = Get.find(); + final selectContactsLogic = Get.find(); + + SelectContactsFromSearchPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.search( + focusNode: logic.focusNode, + controller: logic.searchCtrl, + onSubmitted: (_) => logic.search(), + onCleared: () => logic.focusNode.requestFocus(), + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => logic.isSearchNotResult + ? _emptyListView + : ListView.builder( + itemCount: logic.resultList.length, + itemBuilder: (_, index) => _buildItemView(logic.resultList.elementAt(index)), + )), + ), + ); + } + + Widget _buildItemView(dynamic info) { + Widget buildChild() => Ink( + height: 64.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: selectContactsLogic.onTap(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + if (selectContactsLogic.isMultiModel) + Padding( + padding: EdgeInsets.only(right: 10.w), + child: ChatRadio( + checked: selectContactsLogic.isChecked(info), + enabled: !selectContactsLogic.isDefaultChecked(info), + ), + ), + AvatarView( + url: logic.parseFaceURL(info), + text: logic.parseNickname(info), + isGroup: info is GroupInfo, + ), + 10.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SearchKeywordText( + text: logic.parseNickname(info) ?? '', + keyText: logic.searchCtrl.text.trim(), + style: Styles.ts_0C1C33_17sp, + keyStyle: Styles.ts_0089FF_17sp, + ), + ], + ), + ), + ], + ), + ), + ), + ); + return selectContactsLogic.isMultiModel ? Obx(buildChild) : buildChild(); + } + + Widget get _emptyListView => SizedBox( + width: 1.sw, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 44.verticalSpace, + StrRes.searchNotFound.toText..style = Styles.ts_8E9AB0_17sp, + ], + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/select_contacts_binding.dart b/mobile-next/lib/pages/contacts/select_contacts/select_contacts_binding.dart new file mode 100644 index 0000000..9e08213 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/select_contacts_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'select_contacts_logic.dart'; + +class SelectContactsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SelectContactsLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/select_contacts_logic.dart b/mobile-next/lib/pages/contacts/select_contacts/select_contacts_logic.dart new file mode 100644 index 0000000..cd32cf4 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/select_contacts_logic.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/conversation/conversation_logic.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'select_contacts_view.dart'; + +enum SelAction { + forward, + + carte, + + crateGroup, + + addMember, + + recommend, +} + +class SelectContactsLogic extends GetxController implements OrganizationMultiSelBridge { + final checkedList = {}.obs; // 已经选中的 + final defaultCheckedIDList = {}.obs; // 默认选中,且不能修改 + List? excludeIDList; // 剔除某些数据 + late SelAction action; + late bool openSelectedSheet; + String? groupID; + final conversationList = [].obs; + String? ex; + final inputCtrl = TextEditingController(); + + @override + void onInit() { + action = Get.arguments['action']; + groupID = Get.arguments['groupID']; + excludeIDList = Get.arguments['excludeIDList']; + defaultCheckedIDList.addAll(Get.arguments['defaultCheckedIDList'] ?? []); + checkedList.addAll(Get.arguments['checkedList'] ?? {}); + openSelectedSheet = Get.arguments['openSelectedSheet']; + ex = Get.arguments['ex']; + super.onInit(); + } + + @override + void onClose() { + inputCtrl.dispose(); + super.onClose(); + } + + @override + void onReady() { + _queryConversationList(); + if (openSelectedSheet) viewSelectedContactsList(); + super.onReady(); + } + + @override + bool get isMultiModel => action != SelAction.carte; + + bool get hiddenGroup => action == SelAction.carte || action == SelAction.crateGroup || action == SelAction.addMember; + + bool get hiddenConversations => + action == SelAction.carte || action == SelAction.crateGroup || action == SelAction.addMember; + + _queryConversationList() async { + if (!hiddenConversations) { + final cons = Get.find().list; + + final futures = cons.map((con) async { + if (con.isGroupChat) { + final result = await OpenIM.iMManager.groupManager.isJoinedGroup(groupID: con.groupID!); + return result ? con : null; + } + return con.conversationType == ConversationType.notification ? null : con; + }).toList(); + + final results = await Future.wait(futures); + final filteredCons = results.where((con) => con != null).cast().toList(); + + conversationList.addAll(filteredCons); + } + } + + static String? parseID(e) { + if (e is ConversationInfo) { + return e.isSingleChat ? e.userID : e.groupID; + } else if (e is GroupInfo) { + return e.groupID; + } else if (e is UserInfo || e is FriendInfo || e is UserFullInfo) { + return e.userID; + } else { + return null; + } + } + + static String? parseName(e) { + if (e is ConversationInfo) { + return e.showName; + } else if (e is GroupInfo) { + return e.groupName; + } else if (e is UserInfo || e is FriendInfo || e is UserFullInfo) { + return e.nickname; + } else { + return null; + } + } + + static String? parseFaceURL(e) { + if (e is ConversationInfo) { + return e.faceURL; + } else if (e is GroupInfo) { + return e.faceURL; + } else if (e is UserInfo || e is FriendInfo || e is UserFullInfo) { + return e.faceURL; + } else { + return null; + } + } + + @override + bool isChecked(info) => checkedList.containsKey(parseID(info)); + + @override + bool isDefaultChecked(info) => defaultCheckedIDList.contains(parseID(info)); + + @override + Function()? onTap(dynamic info) => isDefaultChecked(info) ? null : () => toggleChecked(info); + + @override + removeItem(dynamic info) { + checkedList.remove(parseID(info)); + } + + @override + toggleChecked(dynamic info) { + if (isMultiModel) { + final key = parseID(info); + if (checkedList.containsKey(key)) { + checkedList.remove(key); + } else { + checkedList.putIfAbsent(key ?? '', () => info); + } + } else { + confirmSelectedItem(info); + } + } + + @override + updateDefaultCheckedList(List userIDList) async { + if (groupID != null) { + var list = await OpenIM.iMManager.groupManager.getGroupMembersInfo( + groupID: groupID!, + userIDList: userIDList, + ); + defaultCheckedIDList.addAll(list.map((e) => e.userID!)); + } + } + + String get checkedStrTips => checkedList.values.map(parseName).join('、'); + + viewSelectedContactsList() => Get.bottomSheet( + SelectedContactsListView(), + isScrollControlled: true, + ); + + selectFromMyFriend() async { + final result = await AppNavigator.startSelectContactsFromFriends(); + if (null != result) { + Get.back(result: result); + } + } + + selectFromMyGroup() async { + final result = await AppNavigator.startSelectContactsFromGroup(); + if (null != result) { + Get.back(result: result); + } + } + + void selectFromSearch() async { + final result = await AppNavigator.startSelectContactsFromSearch(); + if (null != result) { + Get.back(result: result); + } + } + + selectTagGroup() async { + final result = await await AppNavigator.startSelectContactsFromTag(); + if (null != result) { + Get.back(result: result); + } + } + + confirmSelectedList() async { + if (action == SelAction.forward || action == SelAction.recommend) { + final sure = await Get.dialog(ForwardHintDialog( + title: ex ?? '', + checkedList: checkedList.values.toList(), + )); + if (sure == true) { + Get.back(result: { + "checkedList": checkedList.values, + }); + } + } else { + Get.back(result: checkedList.value); + } + } + + confirmSelectedItem(dynamic info) async { + if (action == SelAction.carte) { + final sure = await Get.dialog(CustomDialog( + title: StrRes.sendCarteConfirmHint, + )); + if (sure == true) { + Get.back(result: UserInfo.fromJson(info.toJson())); + } + } + } + + bool get enabledConfirmButton => checkedList.isNotEmpty; + + @override + Widget get checkedConfirmView => isMultiModel ? CheckedConfirmView() : const SizedBox(); +} diff --git a/mobile-next/lib/pages/contacts/select_contacts/select_contacts_view.dart b/mobile-next/lib/pages/contacts/select_contacts/select_contacts_view.dart new file mode 100644 index 0000000..9b5faa7 --- /dev/null +++ b/mobile-next/lib/pages/contacts/select_contacts/select_contacts_view.dart @@ -0,0 +1,309 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'select_contacts_logic.dart'; + +class SelectContactsPage extends StatelessWidget { + final logic = Get.find(); + + SelectContactsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.selectFromSearch, + child: Container( + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + child: const SearchBox(), + ), + ), + 10.verticalSpace, + Flexible( + child: Obx(() => CustomScrollView( + slivers: [ + SliverFixedExtentList( + delegate: SliverChildListDelegate( + [ + _buildCategoryItemView( + label: StrRes.myFriend, + onTap: logic.selectFromMyFriend, + ), + if (!logic.hiddenGroup) + _buildCategoryItemView( + label: StrRes.myGroup, + onTap: logic.selectFromMyGroup, + ), + ], + ), + itemExtent: 56.h, + ), + if (logic.conversationList.isNotEmpty) + SliverToBoxAdapter( + child: Container( + height: 29.h, + alignment: Alignment.centerLeft, + margin: EdgeInsets.symmetric(horizontal: 16.w), + child: StrRes.recentConversations.toText..style = Styles.ts_8E9AB0_12sp, + ), + ), + SliverFixedExtentList( + delegate: SliverChildBuilderDelegate( + childCount: logic.conversationList.length, + (_, index) => _buildRecentConversationsItemView( + logic.conversationList.elementAt(index), + ), + ), + itemExtent: 64.h, + ), + ], + )), + ), + logic.checkedConfirmView, + ], + ), + ); + } + + Widget _buildCategoryItemView({ + required String label, + Function()? onTap, + }) => + Ink( + height: 56.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + label.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + ); + + Widget _buildRecentConversationsItemView(ConversationInfo info) { + Widget buildChild() => Ink( + height: 56.h, + color: Styles.c_FFFFFF, + child: InkWell( + onTap: logic.onTap(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + if (logic.isMultiModel) + Padding( + padding: EdgeInsets.only(right: 10.w), + child: ChatRadio( + checked: logic.isChecked(info), + ), + ), + AvatarView( + url: info.faceURL, + text: info.showName, + isGroup: !info.isSingleChat, + ), + 10.horizontalSpace, + Flexible( + child: (info.showName ?? '').toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + return logic.isMultiModel ? Obx(buildChild) : buildChild(); + } +} + +class CheckedConfirmView extends StatelessWidget { + CheckedConfirmView({Key? key}) : super(key: key); + final logic = Get.find(); + + @override + Widget build(BuildContext context) { + return Container( + height: 66.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + boxShadow: [ + BoxShadow( + offset: Offset(0, -1.h), + blurRadius: 4.r, + spreadRadius: 1.r, + color: Styles.c_000000_opacity4, + ), + ], + ), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Obx(() => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.viewSelectedContactsList, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + sprintf(StrRes.selectedPeopleCount, [logic.checkedList.length]).toText + ..style = Styles.ts_0089FF_14sp, + ImageRes.expandUpArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + if (logic.checkedList.isNotEmpty) 4.verticalSpace, + logic.checkedStrTips.toText + ..style = Styles.ts_8E9AB0_14sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ], + ), + ), + ), + Button( + height: 40.h, + enabled: logic.enabledConfirmButton, + padding: EdgeInsets.symmetric(horizontal: 14.w), + text: sprintf(StrRes.confirmSelectedPeople, [ + logic.checkedList.length, + '999', + ]), + textStyle: Styles.ts_FFFFFF_14sp, + onTap: logic.confirmSelectedList, + ), + ], + )), + ); + } +} + +class SelectedContactsListView extends StatelessWidget { + SelectedContactsListView({Key? key}) : super(key: key); + final logic = Get.find(); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints(maxHeight: 548.h), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(6.r), + topRight: Radius.circular(6.r), + ), + ), + child: Obx(() => Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + border: BorderDirectional( + bottom: BorderSide(color: Styles.c_E8EAEF, width: 1), + ), + ), + child: Row( + children: [ + sprintf(StrRes.selectedPeopleCount, [logic.checkedList.length]).toText + ..style = Styles.ts_0C1C33_17sp_medium, + const Spacer(), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Get.back(), + child: Container( + height: 52.h, + alignment: Alignment.center, + child: StrRes.confirm.toText..style = Styles.ts_0089FF_17sp, + ), + ), + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: logic.checkedList.length, + shrinkWrap: true, + itemBuilder: (_, index) => _buildItemView(index), + ), + ), + ], + )), + ); + } + + Widget _buildItemView(int index) { + final info = logic.checkedList.values.elementAt(index); + String? name; + String? faceURL; + bool isGroup = false; + name = SelectContactsLogic.parseName(info); + faceURL = SelectContactsLogic.parseFaceURL(info); + if (info is ConversationInfo) { + isGroup = !info.isSingleChat; + } else if (info is GroupInfo) { + isGroup = true; + name = info.groupName; + faceURL = info.faceURL; + } else if (info is UserInfo) { + name = info.nickname; + faceURL = info.faceURL; + } + return Container( + height: 64.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + color: Styles.c_FFFFFF, + child: Row( + children: [ + AvatarView(url: faceURL, text: name, isGroup: isGroup), + 10.horizontalSpace, + Expanded( + child: (name ?? '').toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => logic.removeItem(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 13.w, vertical: 4.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + border: Border.all( + color: Styles.c_E8EAEF, + width: 1, + ), + ), + child: StrRes.remove.toText..style = Styles.ts_0089FF_17sp, + ), + ), + ], + ), + ); + } +} diff --git a/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_binding.dart b/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_binding.dart new file mode 100644 index 0000000..499d0cc --- /dev/null +++ b/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'send_verification_application_logic.dart'; + +class SendVerificationApplicationBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SendVerificationApplicationLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_logic.dart b/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_logic.dart new file mode 100644 index 0000000..1c93a9f --- /dev/null +++ b/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_logic.dart @@ -0,0 +1,69 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../group_profile_panel/group_profile_panel_logic.dart'; + +class SendVerificationApplicationLogic extends GetxController { + final inputCtrl = TextEditingController(); + String? userID; + String? groupID; + JoinGroupMethod? joinGroupMethod; + + bool get isEnterGroup => groupID != null; + + bool get isAddFriend => userID != null; + + @override + void onInit() { + userID = Get.arguments['userID']; + groupID = Get.arguments['groupID']; + joinGroupMethod = Get.arguments['joinGroupMethod']; + super.onInit(); + } + + void send() async { + if (isAddFriend) { + _applyAddFriend(); + } else if (isEnterGroup) { + _applyEnterGroup(); + } + } + + _applyAddFriend() async { + try { + await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.friendshipManager.addFriend( + userID: userID!, + reason: inputCtrl.text.trim(), + ), + ); + Get.back(); + IMViews.showToast(StrRes.sendSuccessfully); + } catch (_) { + if (_ is PlatformException) { + if (_.code == '${SDKErrorCode.refuseToAddFriends}') { + IMViews.showToast(StrRes.canNotAddFriends); + return; + } + } + IMViews.showToast(StrRes.sendFailed); + } + } + + _applyEnterGroup() { + LoadingView.singleton + .wrap( + asyncFunction: () => OpenIM.iMManager.groupManager.joinGroup( + groupID: groupID!, + reason: inputCtrl.text.trim(), + joinSource: joinGroupMethod == JoinGroupMethod.qrcode ? 4 : 3, + ), + ) + .then((value) => IMViews.showToast(StrRes.sendSuccessfully)) + .then((value) => Get.back()) + .catchError((e) => IMViews.showToast(StrRes.sendFailed)); + } +} diff --git a/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_view.dart b/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_view.dart new file mode 100644 index 0000000..06a96bd --- /dev/null +++ b/mobile-next/lib/pages/contacts/send_verification_application/send_verification_application_view.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'send_verification_application_logic.dart'; + +class SendVerificationApplicationPage extends StatelessWidget { + final logic = Get.find(); + + SendVerificationApplicationPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.back( + title: logic.isEnterGroup ? StrRes.groupVerification : StrRes.friendVerification, + right: StrRes.send.toText + ..style = Styles.ts_0C1C33_17sp + ..onTap = logic.send, + ), + backgroundColor: Styles.c_F8F9FA, + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 6.h, horizontal: 16.w), + child: (logic.isEnterGroup ? StrRes.sendEnterGroupApplication : StrRes.sendToBeFriendApplication).toText + ..style = Styles.ts_8E9AB0_14sp, + ), + Container( + height: 122.h, + color: Styles.c_FFFFFF, + padding: EdgeInsets.symmetric(vertical: 12.h), + child: TextField( + controller: logic.inputCtrl, + autofocus: true, + maxLines: 10, + maxLength: 20, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 16.w, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_binding.dart b/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_binding.dart new file mode 100644 index 0000000..c4ed87e --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'friend_setup_logic.dart'; + +class FriendSetupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => FriendSetupLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_logic.dart b/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_logic.dart new file mode 100644 index 0000000..cb39edf --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_logic.dart @@ -0,0 +1,128 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../../routes/app_navigator.dart'; +import '../../../chat/chat_logic.dart'; +import '../../../conversation/conversation_logic.dart'; +import '../../select_contacts/select_contacts_logic.dart'; +import '../user_profile _panel_logic.dart'; + +class FriendSetupLogic extends GetxController { + final conversationLogic = Get.find(); + final userProfilesLogic = Get.find(tag: GetTags.userProfile); + late String userID; + + @override + void onInit() { + userID = Get.arguments['userID']; + super.onInit(); + } + + void toggleBlacklist() { + if (userProfilesLogic.userInfo.value.isBlacklist == true) { + removeBlacklist(); + } else { + addBlacklist(); + } + } + + void addBlacklist() async { + var confirm = await Get.dialog(CustomDialog(title: StrRes.areYouSureAddBlacklist)); + if (confirm == true) { + await OpenIM.iMManager.friendshipManager.addBlacklist( + userID: userProfilesLogic.userInfo.value.userID!, + ); + userProfilesLogic.userInfo.update((val) { + val?.isBlacklist = true; + }); + } + } + + void removeBlacklist() async { + await OpenIM.iMManager.friendshipManager.removeBlacklist( + userID: userProfilesLogic.userInfo.value.userID!, + ); + userProfilesLogic.userInfo.update((val) { + val?.isBlacklist = false; + }); + } + + void deleteFromFriendList() async { + var confirm = await Get.dialog(CustomDialog( + title: StrRes.areYouSureDelFriend, + rightText: StrRes.delete, + )); + if (confirm) { + await LoadingView.singleton.wrap(asyncFunction: () async { + await OpenIM.iMManager.friendshipManager.deleteFriend( + userID: userProfilesLogic.userInfo.value.userID!, + ); + userProfilesLogic.userInfo.update((val) { + val?.isFriendship = false; + }); + + final userIDList = [ + userProfilesLogic.userInfo.value.userID, + OpenIM.iMManager.userID, + ]; + userIDList.sort(); + final conversationID = 'si_${userIDList.join('_')}'; + + await OpenIM.iMManager.conversationManager.deleteConversationAndDeleteAllMsg(conversationID: conversationID); + + conversationLogic.list.removeWhere((e) => e.conversationID == conversationID); + }); + + if (userProfilesLogic.offAllWhenDelFriend == true) { + AppNavigator.startBackMain(); + } else { + Get.back(); + } + } + } + + recommendToFriend() async { + final isRegistered = Get.isRegistered(tag: GetTags.chat); + if (isRegistered) { + final logic = Get.find(tag: GetTags.chat); + logic.recommendFriendCarte(UserInfo.fromJson(userProfilesLogic.userInfo.value.toJson())); + return; + } + final result = await AppNavigator.startSelectContacts( + action: SelAction.recommend, + ex: '[${StrRes.carte}]${userProfilesLogic.userInfo.value.nickname}', + ); + if (null != result) { + final customEx = result['customEx']; + final checkedList = result['checkedList']; + for (var info in checkedList) { + final userID = IMUtils.convertCheckedToUserID(info); + final groupID = IMUtils.convertCheckedToGroupID(info); + if (customEx is String && customEx.isNotEmpty) { + OpenIM.iMManager.messageManager.sendMessage( + message: await OpenIM.iMManager.messageManager.createTextMessage( + text: customEx, + ), + userID: userID, + groupID: groupID, + offlinePushInfo: Config.offlinePushInfo, + ); + } + + OpenIM.iMManager.messageManager.sendMessage( + message: await OpenIM.iMManager.messageManager.createCardMessage( + userID: userProfilesLogic.userInfo.value.userID!, + nickname: userProfilesLogic.userInfo.value.showName, + faceURL: userProfilesLogic.userInfo.value.faceURL, + ), + userID: userID, + groupID: groupID, + offlinePushInfo: Config.offlinePushInfo, + ); + } + } + } + + void setFriendRemark() => AppNavigator.startSetFriendRemark(); +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_view.dart b/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_view.dart new file mode 100644 index 0000000..01bab93 --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/friend_setup/friend_setup_view.dart @@ -0,0 +1,105 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'friend_setup_logic.dart'; + +class FriendSetupPage extends StatelessWidget { + final logic = Get.find(); + + FriendSetupPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: StrRes.friendSetup, + ), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + 10.verticalSpace, + _buildItemView( + label: StrRes.setupRemark, + borderRadius: BorderRadius.only( + topRight: Radius.circular(6.r), + topLeft: Radius.circular(6.r), + ), + showRightArrow: true, + onTap: logic.setFriendRemark, + ), + _buildItemView( + label: StrRes.recommendToFriend, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(6.r), + bottomRight: Radius.circular(6.r), + ), + showRightArrow: true, + onTap: logic.recommendToFriend, + ), + 10.verticalSpace, + Obx(() => _buildItemView( + label: StrRes.addToBlacklist, + showSwitchButton: true, + switchOn: + logic.userProfilesLogic.userInfo.value.isBlacklist == true, + onChanged: (_) => logic.toggleBlacklist(), + )), + 10.verticalSpace, + _buildItemView( + isDelFriendButton: true, + onTap: logic.deleteFromFriendList, + ) + ], + ), + ); + } + + Widget _buildItemView({ + String? label, + bool showRightArrow = false, + bool showSwitchButton = false, + BorderRadius? borderRadius, + bool switchOn = false, + bool isDelFriendButton = false, + ValueChanged? onChanged, + Function()? onTap, + }) => + Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + child: Ink( + height: 46.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: borderRadius ?? BorderRadius.circular(6.r), + ), + child: InkWell( + onTap: onTap, + child: Container( + alignment: isDelFriendButton ? Alignment.center : null, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: isDelFriendButton + ? (StrRes.unfriend.toText..style = Styles.ts_FF381F_17sp) + : Row( + children: [ + (label ?? '').toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + if (showRightArrow) + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + if (showSwitchButton) + CupertinoSwitch( + value: switchOn, + onChanged: onChanged, + activeColor: Styles.c_0089FF, + ), + ], + ), + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_binding.dart b/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_binding.dart new file mode 100644 index 0000000..6d4dce5 --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'personal_info_logic.dart'; + +class PersonalInfoBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => PersonalInfoLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_logic.dart b/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_logic.dart new file mode 100644 index 0000000..237fbd6 --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_logic.dart @@ -0,0 +1,106 @@ +import 'package:collection/collection.dart'; +import 'package:common_utils/common_utils.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../user_profile _panel_logic.dart'; + +class PersonalInfoLogic extends GetxController { + final userProfilesLogic = Get.find(tag: GetTags.userProfile); + late String userID; + final userFullInfo = UserFullInfo().obs; + + @override + void onInit() { + userID = Get.arguments['userID']; + super.onInit(); + } + + @override + void onReady() { + _queryUserFullInfo(); + super.onReady(); + } + + void _queryUserFullInfo() async { + final existUser = UserCacheManager().getUserInfo(userID); + if (existUser != null) { + userFullInfo.update((val) { + val?.nickname = existUser.nickname; + val?.faceURL = existUser.faceURL; + val?.status = existUser.status; + val?.level = existUser.level; + val?.phoneNumber = existUser.phoneNumber; + val?.areaCode = existUser.areaCode; + val?.birth = existUser.birth; + val?.email = existUser.email; + val?.gender = existUser.gender; + val?.mobile = existUser.mobile; + }); + } + + final list = await LoadingView.singleton.wrap( + asyncFunction: () => Apis.getUserFullInfo(userIDList: [userID]), + ); + final info = list?.firstOrNull; + if (null != info) { + UserCacheManager().addOrUpdateUserInfo(userID, info); + + userFullInfo.update((val) { + val?.nickname = info.nickname; + val?.faceURL = info.faceURL; + val?.gender = info.gender; + val?.englishName = info.englishName; + val?.birth = info.birth; + val?.telephone = info.telephone; + val?.phoneNumber = info.phoneNumber; + val?.email = info.email; + }); + } + } + + String? get nickname => IMUtils.emptyStrToNull(userProfilesLogic.userInfo.value.nickname) ?? IMUtils.emptyStrToNull(userFullInfo.value.nickname); + + String? get faceURL => IMUtils.emptyStrToNull(userProfilesLogic.userInfo.value.faceURL) ?? IMUtils.emptyStrToNull(userFullInfo.value.faceURL); + + bool get isMale => (userProfilesLogic.userInfo.value.gender ?? userFullInfo.value.gender) == 1; + + String? get englishName => IMUtils.emptyStrToNull(userFullInfo.value.englishName) ?? '-'; + + int? get _birth => userProfilesLogic.userInfo.value.birth ?? userFullInfo.value.birth; + + String? get birth => _birth == null ? '-' : DateUtil.formatDateMs(_birth!, format: IMUtils.getTimeFormat1()); + + String? get telephone => IMUtils.emptyStrToNull(userFullInfo.value.telephone) ?? '-'; + + String? get phoneNumber => IMUtils.emptyStrToNull(userFullInfo.value.phoneNumber) ?? '-'; + + String? get email => IMUtils.emptyStrToNull(userFullInfo.value.email) ?? '-'; + + clickPhoneNumber() => _callSystemPhone(userFullInfo.value.phoneNumber); + + clickTel() => _callSystemPhone(userFullInfo.value.telephone); + + clickEmail() => _callSystemEmail(userFullInfo.value.email); + + _callSystemPhone(String? phone) async { + final value = IMUtils.emptyStrToNull(phone); + if (null != value) { + final uri = Uri.parse('tel:$value'); + try { + launchUrl(uri); + } catch (_) {} + } + } + + _callSystemEmail(String? email) { + final value = IMUtils.emptyStrToNull(email); + if (null != value) { + final uri = Uri.parse('mailto:$value'); + try { + launchUrl(uri); + } catch (_) {} + } + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_view.dart b/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_view.dart new file mode 100644 index 0000000..f663bf4 --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/personal_info/personal_info_view.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'personal_info_logic.dart'; + +class PersonalInfoPage extends StatelessWidget { + final logic = Get.find(); + + PersonalInfoPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: StrRes.personalInfo, + ), + backgroundColor: Styles.c_F8F9FA, + body: SingleChildScrollView( + child: Obx( + () => Column( + children: [ + 10.verticalSpace, + _buildCornerBgView( + children: [ + _buildItemView( + label: StrRes.avatar, + isAvatar: true, + value: logic.nickname, + url: logic.faceURL, + ), + _buildItemView( + label: StrRes.name, + value: logic.nickname, + ), + _buildItemView( + label: StrRes.gender, + value: logic.isMale ? StrRes.man : StrRes.woman, + ), + _buildItemView( + label: StrRes.englishName, + value: logic.englishName, + ), + _buildItemView( + label: StrRes.birthDay, + value: logic.birth, + ), + ], + ), + 10.verticalSpace, + _buildCornerBgView( + children: [ + _buildItemView( + label: StrRes.mobile, + value: logic.phoneNumber, + onTap: logic.clickPhoneNumber, + ), + _buildItemView( + label: StrRes.email, + value: logic.email, + onTap: logic.clickEmail, + ), + ], + ), + ], + ), + )), + ); + } + + Widget _buildCornerBgView({required List children}) => Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + margin: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(6.r), + topRight: Radius.circular(6.r), + bottomLeft: Radius.circular(6.r), + bottomRight: Radius.circular(6.r), + ), + ), + child: Column(children: children), + ); + + Widget _buildItemView({ + required String label, + String? value, + String? url, + bool isAvatar = false, + Function()? onTap, + }) => + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: onTap, + child: SizedBox( + height: 46.h, + child: Row( + children: [ + label.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + if (null != value && !isAvatar) value.toText..style = Styles.ts_0C1C33_17sp, + if (isAvatar) + AvatarView( + width: 32.w, + height: 32.h, + url: url, + text: value, + textStyle: Styles.ts_FFFFFF_10sp, + ), + ], + ), + ), + ); +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_binding.dart b/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_binding.dart new file mode 100644 index 0000000..71983d9 --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'set_remark_logic.dart'; + +class SetFriendRemarkBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SetFriendRemarkLogic()); + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_logic.dart b/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_logic.dart new file mode 100644 index 0000000..bee4df0 --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_logic.dart @@ -0,0 +1,40 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../user_profile _panel_logic.dart'; + +class SetFriendRemarkLogic extends GetxController { + final userProfilesLogic = + Get.find(tag: GetTags.userProfile); + late TextEditingController inputCtrl; + + void save() async { + try { + await LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.friendshipManager.setFriendRemark( + userID: userProfilesLogic.userInfo.value.userID!, + remark: inputCtrl.text.trim(), + ), + ); + IMViews.showToast(StrRes.saveSuccessfully); + Get.back(result: inputCtrl.text.trim()); + } catch (_) { + IMViews.showToast(StrRes.saveFailed); + } + } + + @override + void onInit() { + inputCtrl = + TextEditingController(text: userProfilesLogic.userInfo.value.remark); + super.onInit(); + } + + @override + void onClose() { + inputCtrl.dispose(); + super.onClose(); + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_view.dart b/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_view.dart new file mode 100644 index 0000000..4ac80ee --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/set_remark/set_remark_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'set_remark_logic.dart'; + +class SetFriendRemarkPage extends StatelessWidget { + final logic = Get.find(); + + SetFriendRemarkPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: StrRes.remark, + right: StrRes.save.toText + ..style = Styles.ts_0C1C33_17sp + ..onTap = logic.save, + ), + backgroundColor: Styles.c_FFFFFF, + body: Column( + children: [ + 22.verticalSpace, + Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Styles.c_E8EAEF, + borderRadius: BorderRadius.circular(4.r), + ), + child: TextField( + controller: logic.inputCtrl, + style: Styles.ts_0C1C33_17sp, + autofocus: true, + inputFormatters: [LengthLimitingTextInputFormatter(16)], + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric( + vertical: 10.h, + horizontal: 12.w, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_binding.dart b/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_binding.dart new file mode 100644 index 0000000..63e2f3b --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'user_profile _panel_logic.dart'; + +class UserProfilePanelBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => UserProfilePanelLogic(), tag: GetTags.userProfile); + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_logic.dart b/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_logic.dart new file mode 100644 index 0000000..4fc164e --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_logic.dart @@ -0,0 +1,373 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:common_utils/common_utils.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; +import 'package:openim_live/openim_live.dart'; + +import '../../../core/controller/app_controller.dart'; +import '../../../core/controller/im_controller.dart'; +import '../../conversation/conversation_logic.dart'; + +class UserProfilePanelLogic extends GetxController { + final appLogic = Get.find(); + final imLogic = Get.find(); + final conversationLogic = Get.find(); + late Rx userInfo; + GroupMembersInfo? groupMembersInfo; + GroupInfo? groupInfo; + String? groupID; + bool? offAllWhenDelFriend = false; + bool? forceCanAdd = false; + final iHasMutePermissions = false.obs; + final iAmOwner = false.obs; + final mutedTime = "".obs; + final onlineStatus = false.obs; + final onlineStatusDesc = ''.obs; + final groupUserNickname = "".obs; + final joinGroupTime = 0.obs; + final joinGroupMethod = ''.obs; + final hasAdminPermission = false.obs; + final notAllowLookGroupMemberProfiles = true.obs; + final notAllowAddGroupMemberFriend = false.obs; + final iHaveAdminOrOwnerPermission = false.obs; + late StreamSubscription _friendAddedSub; + late StreamSubscription _friendDeletedSub; + late StreamSubscription _friendInfoChangedSub; + late StreamSubscription _memberInfoChangedSub; + + @override + void onClose() { + _friendAddedSub.cancel(); + _friendInfoChangedSub.cancel(); + _memberInfoChangedSub.cancel(); + _friendDeletedSub.cancel(); + super.onClose(); + } + + @override + void onInit() { + userInfo = (UserFullInfo() + ..userID = Get.arguments['userID'] + ..nickname = Get.arguments['nickname'] + ..faceURL = Get.arguments['faceURL']) + .obs; + groupID = Get.arguments['groupID']; + offAllWhenDelFriend = Get.arguments['offAllWhenDelFriend']; + forceCanAdd = Get.arguments['forceCanAdd']; + + _friendAddedSub = imLogic.friendAddSubject.listen((user) { + if (user.userID == userInfo.value.userID) { + userInfo.update((val) { + val?.isFriendship = true; + }); + } + }); + + _friendDeletedSub = imLogic.friendDelSubject.listen((user) { + if (user.userID == userInfo.value.userID) { + UserCacheManager().removeUserInfo(user.userID!); + _getUsersInfo(); + } + }); + + _friendInfoChangedSub = imLogic.friendInfoChangedSubject.listen((user) { + if (user.userID == userInfo.value.userID) { + userInfo.update((val) { + val?.nickname = user.nickname; + val?.remark = user.remark; + }); + + UserCacheManager().addOrUpdateUserInfo(user.userID!, userInfo.value); + } + }); + + _memberInfoChangedSub = imLogic.memberInfoChangedSubject.listen((value) { + if (value.userID == userInfo.value.userID) { + if (null != value.muteEndTime) { + _calMuteTime(value.muteEndTime!); + } + groupUserNickname.value = value.nickname ?? ''; + } + }); + super.onInit(); + } + + @override + void onReady() { + _getUsersInfo(); + _queryGroupInfo(); + _queryGroupMemberInfo(); + + super.onReady(); + } + + bool get isMyself => userInfo.value.userID == OpenIM.iMManager.userID; + + bool get isGroupMemberPage => null != groupID && groupID!.isNotEmpty; + + bool get isFriendship => userInfo.value.isFriendship == true; + + bool get isAllowAddFriend => userInfo.value.allowAddFriend == 1; + + bool get allowSendMsgNotFriend { + final r = null == appLogic.clientConfigMap['allowSendMsgNotFriend'] || + appLogic.clientConfigMap['allowSendMsgNotFriend'] == '1'; + return r; + } + + void _getUsersInfo() async { + final userID = userInfo.value.userID!; + final existUser = UserCacheManager().getUserInfo(userID); + if (existUser != null) { + userInfo.update((val) { + val?.nickname = existUser.nickname; + val?.faceURL = existUser.faceURL; + val?.status = existUser.status; + val?.level = existUser.level; + val?.phoneNumber = existUser.phoneNumber; + val?.areaCode = existUser.areaCode; + val?.birth = existUser.birth; + val?.email = existUser.email; + val?.gender = existUser.gender; + val?.mobile = existUser.mobile; + }); + } + + if (userID == OpenIM.iMManager.userID) { + final user = await OpenIM.iMManager.userManager.getSelfUserInfo(); + + userInfo.update((val) { + val?.nickname = user.nickname; + val?.faceURL = user.faceURL; + }); + + UserCacheManager().addOrUpdateUserInfo(userID, userInfo.value); + + return; + } + + final friendInfo = (await OpenIM.iMManager.friendshipManager.getFriendsInfo( + userIDList: [userID], + )) + .firstOrNull; + + final blackList = await OpenIM.iMManager.friendshipManager.getBlacklist(); + + final isFriendship = friendInfo != null; + final isBlack = blackList.firstWhereOrNull((e) => e.userID == friendInfo?.userID) != null; + + if (friendInfo == null) { + final user = (await OpenIM.iMManager.userManager.getUsersInfoWithCache( + [userID], + )) + .firstOrNull; + if (user != null) { + userInfo.update((val) { + val?.nickname = user.nickname; + val?.faceURL = user.faceURL; + val?.remark = friendInfo?.remark; + val?.isBlacklist = isBlack; + val?.isFriendship = isFriendship; + }); + } + } else { + userInfo.update((val) { + val?.nickname = friendInfo.nickname; + val?.faceURL = friendInfo.faceURL; + val?.remark = friendInfo.remark; + val?.isBlacklist = isBlack; + val?.isFriendship = isFriendship; + }); + } + UserCacheManager().addOrUpdateUserInfo(userID, userInfo.value); + + final list2 = await Apis.getUserFullInfo(userIDList: [userID]); + final fullInfo = list2?.firstOrNull; + + if (null != fullInfo) { + userInfo.update((val) { + val?.allowAddFriend = fullInfo.allowAddFriend; + val?.status = fullInfo.status; + val?.level = fullInfo.level; + val?.phoneNumber = fullInfo.phoneNumber; + val?.areaCode = fullInfo.areaCode; + val?.birth = fullInfo.birth; + val?.email = fullInfo.email; + val?.gender = fullInfo.gender; + val?.mobile = fullInfo.mobile; + val?.nickname = fullInfo.nickname; + val?.faceURL = fullInfo.faceURL; + val?.remark = friendInfo?.remark; + val?.isBlacklist = isBlack; + val?.isFriendship = isFriendship; + }); + + UserCacheManager().addOrUpdateUserInfo(userID, userInfo.value); + } + } + + void _resetAvatar(String url) async { + clearMemoryImageCache(keyToMd5(url)); + await clearDiskCachedImage(url); + PaintingBinding.instance.imageCache.evict(keyToMd5(url)); + userInfo.refresh(); + } + + _queryGroupInfo() async { + if (isGroupMemberPage) { + var list = await OpenIM.iMManager.groupManager.getGroupsInfo( + groupIDList: [groupID!], + ); + groupInfo = list.firstOrNull; + + notAllowLookGroupMemberProfiles.value = groupInfo?.lookMemberInfo == 1; + + notAllowAddGroupMemberFriend.value = groupInfo?.applyMemberFriend == 1; + } + } + + _queryGroupMemberInfo() async { + if (isGroupMemberPage) { + final list = await OpenIM.iMManager.groupManager.getGroupMembersInfo( + groupID: groupID!, + userIDList: [userInfo.value.userID!, if (!isMyself) OpenIM.iMManager.userID], + ); + final other = list.firstWhereOrNull((e) => e.userID == userInfo.value.userID); + groupMembersInfo = other; + groupUserNickname.value = other?.nickname ?? ''; + joinGroupTime.value = other?.joinTime ?? 0; + + _getJoinGroupMethod(other); + + hasAdminPermission.value = other?.roleLevel == GroupRoleLevel.admin; + + if (!isMyself) { + var me = list.firstWhereOrNull((e) => e.userID == OpenIM.iMManager.userID); + + iAmOwner.value = me?.roleLevel == GroupRoleLevel.owner; + + iHasMutePermissions.value = me?.roleLevel == GroupRoleLevel.owner || + (me?.roleLevel == GroupRoleLevel.admin && other?.roleLevel == GroupRoleLevel.member); + + iHaveAdminOrOwnerPermission.value = + me?.roleLevel == GroupRoleLevel.owner || me?.roleLevel == GroupRoleLevel.admin; + } + + if (null != other && null != other.muteEndTime && other.muteEndTime! > 0) { + _calMuteTime(other.muteEndTime!); + } + } + } + + _getJoinGroupMethod(GroupMembersInfo? other) async { + if (other?.joinSource == 2) { + if (other!.inviterUserID != null && other.inviterUserID != other.userID) { + final list = await OpenIM.iMManager.groupManager.getGroupMembersInfo( + groupID: groupID!, + userIDList: [other.inviterUserID!], + ); + var inviterUserInfo = list.firstOrNull; + joinGroupMethod.value = sprintf( + StrRes.byInviteJoinGroup, + [inviterUserInfo?.nickname ?? ''], + ); + } + } else if (other?.joinSource == 3) { + joinGroupMethod.value = StrRes.byIDJoinGroup; + } else if (other?.joinSource == 4) { + joinGroupMethod.value = StrRes.byQrcodeJoinGroup; + } + } + + _calMuteTime(int time) { + var date = DateUtil.formatDateMs(time, format: IMUtils.getTimeFormat2()); + var now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + var diff = time - now; + if (diff > 0) { + mutedTime.value = date; + } else { + mutedTime.value = ""; + } + } + + String getShowName() { + if (isGroupMemberPage) { + if (isFriendship) { + if (null != IMUtils.emptyStrToNull(userInfo.value.remark)) { + return '${groupUserNickname.value}(${IMUtils.emptyStrToNull(userInfo.value.remark)})'; + } + } + if (groupUserNickname.value.isEmpty) { + return userInfo.value.nickname ??= ""; + } + return groupUserNickname.value; + } + if (userInfo.value.remark != null && userInfo.value.remark!.isNotEmpty) { + return '${userInfo.value.nickname}(${userInfo.value.remark})'; + } + return userInfo.value.nickname ?? ''; + } + + void toChat() { + conversationLogic.toChat( + userID: userInfo.value.userID, + nickname: userInfo.value.showName, + faceURL: userInfo.value.faceURL, + ); + } + + void toCall() { + IMViews.openIMCallSheet(userInfo.value.showName, (index) { + imLogic.call( + callObj: CallObj.single, + callType: index == 0 ? CallType.audio : CallType.video, + inviteeUserIDList: [userInfo.value.userID!], + ); + }); + } + + void copyID() { + IMUtils.copy(text: userInfo.value.userID!); + } + + void addFriend() => AppNavigator.startSendVerificationApplication( + userID: userInfo.value.userID!, + ); + + void viewPersonalInfo() => AppNavigator.startPersonalInfo( + userID: userInfo.value.userID!, + ); + + void friendSetup() => AppNavigator.startFriendSetup( + userID: userInfo.value.userID!, + ); +} + +class UserCacheManager { + static final UserCacheManager _instance = UserCacheManager._(); + UserCacheManager._(); + final Map _userInfoMap = {}; + + void addOrUpdateUserInfo(String userID, UserFullInfo userInfo) { + _userInfoMap[userID] = userInfo; + } + + UserFullInfo? getUserInfo(String userID) { + return _userInfoMap[userID]; + } + + void removeUserInfo(String userID) { + _userInfoMap.remove(userID); + } + + factory UserCacheManager() { + return _instance; + } +} diff --git a/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_view.dart b/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_view.dart new file mode 100644 index 0000000..48b8c2a --- /dev/null +++ b/mobile-next/lib/pages/contacts/user_profile_panel/user_profile _panel_view.dart @@ -0,0 +1,254 @@ +import 'dart:ui'; + +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'user_profile _panel_logic.dart'; + +class UserProfilePanelPage extends StatelessWidget { + final logic = Get.find(tag: GetTags.userProfile); + + UserProfilePanelPage({super.key}); + + @override + Widget build(BuildContext context) { + return Obx( + () => Scaffold( + appBar: TitleBar.back( + right: logic.isFriendship + ? (ImageRes.moreBlack.toImage + ..width = 24.w + ..height = 24.h + ..onTap = logic.friendSetup) + : null, + ), + backgroundColor: Styles.c_F8F9FA, + body: SizedBox( + height: 1.sh, + child: Stack( + children: [ + SingleChildScrollView( + child: Column( + children: [ + _buildBaseInfoView(), + if (logic.isGroupMemberPage) _buildEnterGroupMethodView(), + if (logic.isFriendship || + logic.isMyself || + logic.isGroupMemberPage && !logic.notAllowLookGroupMemberProfiles.value) + _buildItemView( + label: StrRes.personalInfo, + showRightArrow: true, + onTap: logic.viewPersonalInfo, + ), + SizedBox(height: 108.h), + ], + ), + ), + if ((logic.isFriendship || logic.allowSendMsgNotFriend) && !logic.isMyself) _buildButtonGroup(), + ], + ), + ), + ), + ); + } + + Widget _buildBaseInfoView() => Container( + color: Styles.c_FFFFFF, + height: 80.h, + margin: EdgeInsets.only(bottom: 10.h), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + AvatarView( + url: logic.userInfo.value.faceURL, + text: logic.userInfo.value.nickname, + width: 48.w, + height: 48.h, + textStyle: Styles.ts_FFFFFF_14sp, + enabledPreview: true, + ), + 12.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + logic.getShowName().toText + ..style = Styles.ts_0C1C33_17sp_medium + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + if (!logic.isGroupMemberPage || logic.isGroupMemberPage && !logic.notAllowAddGroupMemberFriend.value) + Padding( + padding: EdgeInsets.only(top: 4.h), + child: (logic.userInfo.value.userID ?? '').toText + ..style = Styles.ts_8E9AB0_14sp + ..onTap = logic.copyID, + ), + ], + ), + ), + if (!logic.isMyself && + logic.isAllowAddFriend && + !logic.isFriendship && + (!logic.isGroupMemberPage || + logic.forceCanAdd == true || + logic.isGroupMemberPage && !logic.notAllowAddGroupMemberFriend.value)) + Material( + child: Ink( + decoration: BoxDecoration( + color: Styles.c_0089FF, + borderRadius: BorderRadius.circular(6.r), + ), + child: InkWell( + onTap: logic.addFriend, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 9.w, + vertical: 4.h, + ), + child: Row( + children: [ + ImageRes.addContacts.toImage + ..width = 21.w + ..height = 21.h + ..color = Styles.c_FFFFFF, + 2.horizontalSpace, + StrRes.add.toText..style = Styles.ts_FFFFFF_14sp, + ], + ), + ), + ), + ), + ), + ], + ), + ); + + Widget _buildEnterGroupMethodView() { + if (logic.joinGroupTime.value == 0 && logic.joinGroupMethod.value.isEmpty) { + return Container(); + } + return Container( + color: Styles.c_FFFFFF, + margin: EdgeInsets.only(bottom: 10.h), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.top, + columnWidths: {0: FixedColumnWidth(100.w)}, + children: [ + if (logic.joinGroupTime.value > 0) + _buildTabRowView( + label: StrRes.joinGroupDate, + value: DateUtil.formatDateMs( + logic.joinGroupTime.value, + format: DateFormats.zh_y_mo_d, + ), + ), + if (logic.joinGroupMethod.value.isNotEmpty) + _buildTabRowView( + label: StrRes.joinGroupMethod, + value: logic.joinGroupMethod.value, + ), + ], + ), + ); + } + + TableRow _buildTabRowView({ + required String label, + String? value, + }) => + TableRow( + children: [ + TableCell( + child: Container( + constraints: BoxConstraints(minHeight: 40.h), + alignment: Alignment.centerLeft, + child: label.toText..style = Styles.ts_8E9AB0_17sp, + ), + ), + TableCell( + child: Container( + constraints: BoxConstraints(minHeight: 40.h), + alignment: Alignment.centerLeft, + child: (value ?? '').toText..style = Styles.ts_0C1C33_17sp, + ), + ), + ], + ); + + Widget _buildItemView({ + required String label, + String? value, + bool addMargin = false, + bool showSwitchButton = false, + bool showRightArrow = false, + bool switchOn = false, + ValueChanged? onChanged, + Function()? onTap, + }) => + Container( + margin: EdgeInsets.only(bottom: addMargin ? 10.h : 0), + child: Ink( + color: Styles.c_FFFFFF, + height: 56.h, + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + label.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + if (showSwitchButton) + CupertinoSwitch( + value: switchOn, + activeColor: Styles.c_0089FF, + onChanged: onChanged, + ), + if (null != value) value.toText..style = Styles.ts_0C1C33_17sp, + if (showRightArrow) + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + ), + ); + + Widget _buildButtonGroup() => Positioned( + bottom: 0.h, + width: 1.sw, + child: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), + child: Container( + color: Styles.c_F8F9FA.withOpacity(.3), + padding: EdgeInsets.symmetric(horizontal: 9.w), + height: 108.h, + child: Row( + children: [ + Expanded( + child: ImageTextButton.call( + onTap: logic.toCall, + ), + ), + 11.horizontalSpace, + Expanded( + child: ImageTextButton.message( + onTap: logic.toChat, + ), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/conversation/conversation_binding.dart b/mobile-next/lib/pages/conversation/conversation_binding.dart new file mode 100644 index 0000000..e08fa46 --- /dev/null +++ b/mobile-next/lib/pages/conversation/conversation_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'conversation_logic.dart'; + +class ConversationBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ConversationLogic()); + } +} diff --git a/mobile-next/lib/pages/conversation/conversation_logic.dart b/mobile-next/lib/pages/conversation/conversation_logic.dart new file mode 100644 index 0000000..9d737f7 --- /dev/null +++ b/mobile-next/lib/pages/conversation/conversation_logic.dart @@ -0,0 +1,446 @@ +import 'dart:convert'; + +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +import '../../core/controller/app_controller.dart'; +import '../../core/controller/im_controller.dart'; +import '../../core/im_callback.dart'; +import '../../routes/app_navigator.dart'; +import '../contacts/add_by_search/add_by_search_logic.dart'; +import '../home/home_logic.dart'; + +class ConversationLogic extends GetxController { + final popCtrl = CustomPopupMenuController(); + final list = [].obs; + final imLogic = Get.find(); + final homeLogic = Get.find(); + final appLogic = Get.find(); + final refreshController = RefreshController(); + final tempDraftText = {}; + final pageSize = 400; + + final imStatus = IMSdkStatus.connectionSucceeded.obs; + bool reInstall = false; + + final ItemScrollController itemScrollController = ItemScrollController(); + final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); + + int scrollIndex = -1; + final onChangeConversations = []; + + @override + void onInit() { + getFirstPage(); + imLogic.conversationAddedSubject.listen(onChanged); + imLogic.conversationChangedSubject.listen(onChanged); + homeLogic.onScrollToUnreadMessage = scrollTo; + imLogic.imSdkStatusSubject.listen((value) async { + final status = value.status; + final appReInstall = value.reInstall; + final progress = value.progress; + imStatus.value = status; + + if (status == IMSdkStatus.syncStart) { + reInstall = appReInstall; + if (reInstall) { + EasyLoading.showProgress(0, status: StrRes.synchronizing); + } + } + + Logger.print('IM SDK Status: $status, reinstall: $reInstall, progress: $progress'); + + if (status == IMSdkStatus.syncProgress && reInstall) { + final p = (progress!).toDouble() / 100.0; + + EasyLoading.showProgress(p, status: '${StrRes.synchronizing}(${(p * 100.0).truncate()}%)'); + } else if (status == IMSdkStatus.syncEnded || status == IMSdkStatus.syncFailed) { + EasyLoading.dismiss(); + if (reInstall) { + onRefresh(); + reInstall = false; + } + } + }); + super.onInit(); + } + + @override + void onClose() { + list.clear(); + reInstall = false; + super.onClose(); + } + + void onChanged(List newList) { + if (reInstall) { + onChangeConversations.addAll(newList); + } + for (var newValue in newList) { + Logger.print('======== conversation changed: ${newValue.toJson()} ========'); + list.removeWhere((e) => e.conversationID == newValue.conversationID); + } + + if (newList.length > pageSize) { + final tempList = newList; + + while (true) { + final temp = tempList.sublist(0, pageSize); + list.insertAll(0, temp); + _sortConversationList(); + + if (tempList.length <= pageSize) { + break; + } + + tempList.removeRange(0, pageSize); + } + } else { + list.insertAll(0, newList); + _sortConversationList(); + Logger.print( + '======== conversation sort result: ${list.where((e) => e.unreadCount > 0).toList().map((e) => '${e.showName} [${e.conversationID}]: ${e.unreadCount}')} ========'); + } + } + + void promptSoundOrNotification(ConversationInfo info) { + if (imLogic.userInfo.value.globalRecvMsgOpt == 0 && + info.recvMsgOpt == 0 && + info.unreadCount > 0 && + info.latestMsg?.sendID != OpenIM.iMManager.userID) { + appLogic.promptSoundOrNotification(info.latestMsg!.seq!); + } + } + + String getConversationID(ConversationInfo info) { + return info.conversationID; + } + + void markMessageHasRead(ConversationInfo info) { + _markMessageHasRead(info); + } + + void pinConversation(ConversationInfo info) async { + OpenIM.iMManager.conversationManager.pinConversation( + conversationID: info.conversationID, + isPinned: !info.isPinned!, + ); + } + + void deleteConversation(ConversationInfo info) async { + await OpenIM.iMManager.conversationManager.deleteConversationAndDeleteAllMsg( + conversationID: info.conversationID, + ); + list.remove(info); + } + + void removeConversation(String id) { + list.removeWhere((e) => e.conversationID == id); + } + + void setConversationDraft({required String cid, required String draftText}) { + OpenIM.iMManager.conversationManager.setConversationDraft( + conversationID: cid, + draftText: draftText, + ); + } + + String? getPrefixTag(ConversationInfo info) { + if (info.groupAtType == GroupAtType.groupNotification) { + return '[${StrRes.groupAc}]'; + } + + return null; + } + + String getContent(ConversationInfo info) { + try { + if (null != info.draftText && '' != info.draftText) { + var map = json.decode(info.draftText!); + String text = map['text']; + if (text.isNotEmpty) { + return text; + } + } + + if (null == info.latestMsg) return ""; + + final text = IMUtils.parseNtf(info.latestMsg!, isConversation: true); + if (text != null) return text; + if (info.isSingleChat || info.latestMsg!.sendID == OpenIM.iMManager.userID) + return IMUtils.parseMsg(info.latestMsg!, isConversation: true); + + return "${info.latestMsg!.senderNickname}: ${IMUtils.parseMsg(info.latestMsg!, isConversation: true)} "; + } catch (e, s) { + Logger.print('------e:$e s:$s'); + } + return '[${StrRes.unsupportedMessage}]'; + } + + String? getAvatar(ConversationInfo info) { + return info.faceURL; + } + + bool isGroupChat(ConversationInfo info) { + return info.isGroupChat; + } + + String getShowName(ConversationInfo info) { + if (info.showName == null || info.showName.isBlank!) { + return info.userID!; + } + return info.showName!; + } + + String getTime(ConversationInfo info) { + return IMUtils.getChatTimeline(info.latestMsgSendTime!); + } + + int getUnreadCount(ConversationInfo info) { + return info.unreadCount; + } + + bool existUnreadMsg(ConversationInfo info) { + return getUnreadCount(info) > 0; + } + + bool isPinned(ConversationInfo info) { + return info.isPinned!; + } + + bool isNotDisturb(ConversationInfo info) { + return info.recvMsgOpt != 0; + } + + bool isUserGroup(int index) => list.elementAt(index).isGroupChat; + + void _markMessageHasRead( + ConversationInfo conversation, + ) { + if (conversation.unreadCount == 0) { + return; + } + OpenIM.iMManager.conversationManager.markConversationMessageAsRead( + conversationID: conversation.conversationID, + ); + } + + void _setupDraftText({ + required String conversationID, + required String oldDraftText, + required String newDraftText, + }) { + if (oldDraftText.isEmpty && newDraftText.isEmpty) { + return; + } + + Logger.print('draftText:$newDraftText'); + OpenIM.iMManager.conversationManager.setConversationDraft( + conversationID: conversationID, + draftText: newDraftText, + ); + } + + String? get imSdkStatus { + switch (imStatus.value) { + case IMSdkStatus.syncStart: + case IMSdkStatus.synchronizing: + case IMSdkStatus.syncProgress: + return StrRes.synchronizing; + case IMSdkStatus.syncFailed: + return StrRes.syncFailed; + case IMSdkStatus.connecting: + return StrRes.connecting; + case IMSdkStatus.connectionFailed: + return StrRes.connectionFailed; + case IMSdkStatus.connectionSucceeded: + case IMSdkStatus.syncEnded: + return null; + } + } + + bool get isFailedSdkStatus => + imStatus.value == IMSdkStatus.connectionFailed || imStatus.value == IMSdkStatus.syncFailed; + + void _sortConversationList() => OpenIM.iMManager.conversationManager.simpleSort(list); + + void onRefresh() async { + late List list; + try { + list = await _request(); + this.list.assignAll(list); + + if (list.isEmpty || list.length < pageSize) { + refreshController.loadNoData(); + } else { + refreshController.loadComplete(); + } + } finally { + refreshController.refreshCompleted(); + } + } + + static Future> getConversationFirstPage() async { + final result = await OpenIM.iMManager.conversationManager.getConversationListSplit(offset: 0, count: 400); + + return result; + } + + void getFirstPage() async { + final result = homeLogic.conversationsAtFirstPage; + + list.assignAll(result); + _sortConversationList(); + } + + void clearConversations() { + list.clear(); + } + + _request() async { + final temp = []; + + while (true) { + var result = await OpenIM.iMManager.conversationManager.getConversationListSplit( + offset: temp.length, + count: pageSize, + ); + if (onChangeConversations.isNotEmpty) { + final bSet = Set.from(onChangeConversations); + + Logger.print('replace conversation: [${onChangeConversations.length}], $bSet'); + + for (int i = 0; i < result.length; i++) { + final info = result[i]; + + if (bSet.contains(info)) { + result[i] = onChangeConversations[onChangeConversations.indexOf(info)]; + } + } + } + temp.addAll(result); + + if (result.length < pageSize) { + break; + } + } + onChangeConversations.clear(); + + return temp; + } + + bool isValidConversation(ConversationInfo info) { + return info.isValid; + } + + void scrollTo() { + if (list.isEmpty) return; + + final positions = itemPositionsListener.itemPositions.value; + + int min = 0; + if (positions.isNotEmpty) { + min = (positions + .where((ItemPosition position) => position.itemTrailingEdge > 0) + .reduce((ItemPosition min, ItemPosition position) => + position.itemTrailingEdge < min.itemTrailingEdge ? position : min) + .index); + } + + var currentIndex = 0; + + for (var i = min + 1; i < list.length; i++) { + final item = list[i]; + + if (item.unreadCount > 0) { + currentIndex = i; + break; + } + } + + scrollIndex = currentIndex; + + if (scrollIndex == 0) { + itemScrollController.jumpTo(index: 0); + } else { + itemScrollController.scrollTo(index: scrollIndex, duration: const Duration(milliseconds: 300)); + } + } + + static Future _createConversation({ + required String sourceID, + required int sessionType, + }) => + LoadingView.singleton.wrap( + asyncFunction: () => OpenIM.iMManager.conversationManager.getOneConversation( + sourceID: sourceID, + sessionType: sessionType, + )); + + Future _jumpOANtf(ConversationInfo info) async { + if (info.conversationType == ConversationType.notification) { + await AppNavigator.startOANtfList(info: info); + _markMessageHasRead(info); + return true; + } + return false; + } + + void toChat({ + bool offUntilHome = true, + String? userID, + String? groupID, + String? nickname, + String? faceURL, + int? sessionType, + ConversationInfo? conversationInfo, + Message? searchMessage, + }) async { + conversationInfo ??= await _createConversation( + sourceID: userID ?? groupID!, + sessionType: userID == null ? sessionType! : ConversationType.single, + ); + + if (await _jumpOANtf(conversationInfo)) return; + + await AppNavigator.startChat( + offUntilHome: offUntilHome, + draftText: conversationInfo.draftText, + conversationInfo: conversationInfo, + searchMessage: searchMessage, + ); + + var newDraftText = tempDraftText[conversationInfo.conversationID]; + + _markMessageHasRead(conversationInfo); + + _setupDraftText( + conversationID: conversationInfo.conversationID, + oldDraftText: conversationInfo.draftText ?? '', + newDraftText: newDraftText!, + ); + + bool equal(e) => e.conversationID == conversationInfo?.conversationID; + + var groupAtType = list.firstWhereOrNull(equal)?.groupAtType; + if (groupAtType != GroupAtType.atNormal) { + OpenIM.iMManager.conversationManager.resetConversationGroupAtType( + conversationID: conversationInfo.conversationID, + ); + } + } + + scan() => AppNavigator.startScan(); + + addFriend() => AppNavigator.startAddContactsBySearch(searchType: SearchType.user); + + createGroup() => AppNavigator.startCreateGroup(defaultCheckedList: [OpenIM.iMManager.userInfo]); + + addGroup() => AppNavigator.startAddContactsBySearch(searchType: SearchType.group); + + void globalSearch() => AppNavigator.startGlobalSearch(); +} diff --git a/mobile-next/lib/pages/conversation/conversation_view.dart b/mobile-next/lib/pages/conversation/conversation_view.dart new file mode 100644 index 0000000..5e5a900 --- /dev/null +++ b/mobile-next/lib/pages/conversation/conversation_view.dart @@ -0,0 +1,205 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:get/get.dart'; +import 'package:openim/core/controller/im_controller.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:rotated_corner_decoration/rotated_corner_decoration.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'conversation_logic.dart'; + +class ConversationPage extends StatelessWidget { + final logic = Get.find(); + final im = Get.find(); + + ConversationPage({super.key}); + + @override + Widget build(BuildContext context) { + return Obx(() => Scaffold( + backgroundColor: Styles.c_F8F9FA, + appBar: TitleBar.conversation( + statusStr: logic.imSdkStatus, + isFailed: logic.isFailedSdkStatus, + popCtrl: logic.popCtrl, + onScan: logic.scan, + onAddFriend: logic.addFriend, + onAddGroup: logic.addGroup, + onCreateGroup: logic.createGroup, + left: Expanded( + flex: 2, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + AvatarView( + width: 42.w, + height: 42.h, + text: im.userInfo.value.nickname, + url: im.userInfo.value.faceURL, + ), + 10.horizontalSpace, + if (null != im.userInfo.value.nickname) + Flexible( + child: im.userInfo.value.nickname!.toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + 10.horizontalSpace, + if (null != logic.imSdkStatus && (!logic.reInstall || logic.isFailedSdkStatus)) + Flexible( + child: SyncStatusView( + isFailed: logic.isFailedSdkStatus, + statusStr: logic.imSdkStatus!, + )), + ], + ), + )), + body: Column( + children: [ + Expanded( + child: SlidableAutoCloseBehavior( + child: ScrollablePositionedList.builder( + itemScrollController: logic.itemScrollController, + itemBuilder: (_, index) => _buildConversationItemView( + logic.list.elementAt(index), + ), + itemCount: logic.list.length, + itemPositionsListener: logic.itemPositionsListener, + ), + ), + ), + ], + ), + )); + } + + Widget _buildConversationItemView(ConversationInfo info) => Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + extentRatio: logic.existUnreadMsg(info) ? 0.7 : (logic.isPinned(info) ? 0.5 : 0.4), + children: [ + CustomSlidableAction( + onPressed: (_) => logic.pinConversation(info), + flex: logic.isPinned(info) ? 3 : 2, + backgroundColor: Styles.c_0089FF, + padding: const EdgeInsets.all(1), + child: (logic.isPinned(info) ? StrRes.cancelTop : StrRes.top).toText..style = Styles.ts_FFFFFF_14sp, + ), + if (logic.existUnreadMsg(info)) + CustomSlidableAction( + onPressed: (_) => logic.markMessageHasRead(info), + flex: 3, + backgroundColor: Styles.c_8E9AB0, + padding: const EdgeInsets.all(1), + child: StrRes.markHasRead.toText + ..style = Styles.ts_FFFFFF_14sp + ..maxLines = 1, + ), + CustomSlidableAction( + onPressed: (_) => logic.deleteConversation(info), + flex: 2, + backgroundColor: Styles.c_FF381F, + padding: const EdgeInsets.all(1), + child: StrRes.delete.toText..style = Styles.ts_FFFFFF_14sp, + ), + ], + ), + child: _buildItemView(info), + ); + + Widget _buildItemView(ConversationInfo info) => Ink( + child: InkWell( + onTap: () => logic.toChat(conversationInfo: info), + child: Stack( + children: [ + Container( + height: 68, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + Stack( + children: [ + AvatarView( + width: 48.w, + height: 48.h, + text: logic.getShowName(info), + url: info.faceURL, + isGroup: logic.isGroupChat(info), + textStyle: Styles.ts_FFFFFF_14sp_medium, + ), + ], + ), + 12.horizontalSpace, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 180.w), + child: logic.getShowName(info).toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis, + ), + const Spacer(), + logic.getTime(info).toText..style = Styles.ts_8E9AB0_12sp, + ], + ), + 3.verticalSpace, + Row( + children: [ + MatchTextView( + text: logic.getContent(info), + textStyle: Styles.ts_8E9AB0_14sp, + prefixSpan: TextSpan( + text: '', + children: [ + if (logic.isNotDisturb(info) && logic.getUnreadCount(info) > 0) + TextSpan( + text: '[${sprintf(StrRes.nPieces, [logic.getUnreadCount(info)])}] ', + style: Styles.ts_8E9AB0_14sp, + ), + TextSpan( + text: logic.getPrefixTag(info), + style: Styles.ts_0089FF_14sp, + ), + ], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + if (logic.isNotDisturb(info)) + ImageRes.notDisturb.toImage + ..width = 13.63.w + ..height = 14.07.h + else + UnreadCountView(count: logic.getUnreadCount(info)), + ], + ), + ], + ), + ), + ], + ), + ), + if (logic.isPinned(info)) + Container( + height: 68.h, + margin: EdgeInsets.only(right: 6.w), + foregroundDecoration: RotatedCornerDecoration.withColor( + color: Styles.c_0089FF, + badgeSize: Size(8.29.w, 8.29.h), + ), + ) + ], + ), + ), + ); +} diff --git a/mobile-next/lib/pages/discover/discover_binding.dart b/mobile-next/lib/pages/discover/discover_binding.dart new file mode 100644 index 0000000..a5c5f5d --- /dev/null +++ b/mobile-next/lib/pages/discover/discover_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'discover_logic.dart'; + +class DiscoverBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => DiscoverLogic()); + } +} diff --git a/mobile-next/lib/pages/discover/discover_logic.dart b/mobile-next/lib/pages/discover/discover_logic.dart new file mode 100644 index 0000000..882ddc7 --- /dev/null +++ b/mobile-next/lib/pages/discover/discover_logic.dart @@ -0,0 +1,30 @@ +import 'package:get/get.dart'; +import 'package:openim/core/controller/app_controller.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; + +class DiscoverLogic extends GetxController { + final refreshCtrl = RefreshController(); + final appLogic = Get.find(); + final list = >[].obs; + final url = ''.obs; + + @override + void onReady() { + super.onReady(); + + final temp = appLogic.clientConfigMap['discoverPageURL']; + + if (temp == null) { + appLogic.queryClientConfig().then((value) { + if (value['discoverPageURL'] == null) { + url.value = 'https://www.openim.io'; + } else { + url.value = value['discoverPageURL']; + } + }); + } else { + url.value = temp; + } + } +} diff --git a/mobile-next/lib/pages/discover/discover_view.dart b/mobile-next/lib/pages/discover/discover_view.dart new file mode 100644 index 0000000..16f04f2 --- /dev/null +++ b/mobile-next/lib/pages/discover/discover_view.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'discover_logic.dart'; + +class DiscoverPage extends StatelessWidget { + final logic = Get.find(); + + DiscoverPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.workbench(), + backgroundColor: Styles.c_F8F9FA, + body: _buildBody(), + ); + } + + Widget _buildBody() { + return Obx(() => H5Container(url: logic.url.value)); + } +} diff --git a/mobile-next/lib/pages/forget_password/forget_password_binding.dart b/mobile-next/lib/pages/forget_password/forget_password_binding.dart new file mode 100644 index 0000000..0202955 --- /dev/null +++ b/mobile-next/lib/pages/forget_password/forget_password_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'forget_password_logic.dart'; + +class ForgetPasswordBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ForgetPasswordLogic()); + } +} diff --git a/mobile-next/lib/pages/forget_password/forget_password_logic.dart b/mobile-next/lib/pages/forget_password/forget_password_logic.dart new file mode 100644 index 0000000..2d7f2a5 --- /dev/null +++ b/mobile-next/lib/pages/forget_password/forget_password_logic.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../login/login_logic.dart'; + +class ForgetPasswordLogic extends GetxController { + final phoneCtrl = TextEditingController(); + final verificationCodeCtrl = TextEditingController(); + final areaCode = "+86".obs; + final enabled = false.obs; + final loginController = Get.find(); + String? get email => loginController.operateType == LoginType.email ? phoneCtrl.text.trim() : null; + String? get phone => loginController.operateType == LoginType.phone ? phoneCtrl.text.trim() : null; + @override + void onClose() { + phoneCtrl.dispose(); + verificationCodeCtrl.dispose(); + super.onClose(); + } + + @override + void onInit() { + phoneCtrl.addListener(_onChanged); + verificationCodeCtrl.addListener(_onChanged); + super.onInit(); + } + + _onChanged() { + enabled.value = phoneCtrl.text.trim().isNotEmpty && verificationCodeCtrl.text.trim().isNotEmpty; + } + + void openCountryCodePicker() async { + String? code = await IMViews.showCountryCodePicker(); + if (null != code) areaCode.value = code; + } + + Future getVerificationCode() async { + if (phone?.isNotEmpty == true && !IMUtils.isMobile(areaCode.value, phoneCtrl.text)) { + IMViews.showToast(StrRes.plsEnterRightPhone); + return false; + } + + if (email?.isNotEmpty == true && !phoneCtrl.text.isEmail) { + IMViews.showToast(StrRes.plsEnterRightEmail); + return false; + } + + final success = await sendVerificationCode(); + return success; + } + + Future sendVerificationCode() => LoadingView.singleton.wrap( + asyncFunction: () => Apis.requestVerificationCode( + areaCode: areaCode.value, + phoneNumber: phone, + email: email, + usedFor: 2, + )); + + checkVerificationCode() => LoadingView.singleton.wrap( + asyncFunction: () => Apis.checkVerificationCode( + areaCode: areaCode.value, + phoneNumber: phone, + email: email, + verificationCode: verificationCodeCtrl.text, + usedFor: 2, + )); + + void nextStep() async { + await checkVerificationCode(); + AppNavigator.startResetPassword( + areaCode: areaCode.value, + phoneNumber: phone, + email: email, + verificationCode: verificationCodeCtrl.text, + ); + } +} diff --git a/mobile-next/lib/pages/forget_password/forget_password_view.dart b/mobile-next/lib/pages/forget_password/forget_password_view.dart new file mode 100644 index 0000000..500b4e4 --- /dev/null +++ b/mobile-next/lib/pages/forget_password/forget_password_view.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/login/login_logic.dart'; +import 'package:openim/widgets/register_page_bg.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'forget_password_logic.dart'; + +class ForgetPasswordPage extends StatelessWidget { + final logic = Get.find(); + + ForgetPasswordPage({super.key}); + + @override + Widget build(BuildContext context) => RegisterBgView( + child: Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StrRes.forgetPassword.toText..style = Styles.ts_0089FF_22sp_semibold, + 29.verticalSpace, + InputBox.account( + label: logic.loginController.operateType.name, + hintText: logic.loginController.operateType.hintText, + code: logic.areaCode.value, + onAreaCode: logic.loginController.operateType == LoginType.phone ? logic.openCountryCodePicker : null, + controller: logic.phoneCtrl, + ), + 16.verticalSpace, + InputBox.verificationCode( + label: StrRes.verificationCode, + hintText: StrRes.plsEnterVerificationCode, + controller: logic.verificationCodeCtrl, + onSendVerificationCode: logic.getVerificationCode, + ), + 130.verticalSpace, + Button( + text: StrRes.nextStep, + enabled: logic.enabled.value, + onTap: logic.nextStep, + ), + ], + )), + ); +} diff --git a/mobile-next/lib/pages/forget_password/reset_password/reset_password_binding.dart b/mobile-next/lib/pages/forget_password/reset_password/reset_password_binding.dart new file mode 100644 index 0000000..554db8a --- /dev/null +++ b/mobile-next/lib/pages/forget_password/reset_password/reset_password_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'reset_password_logic.dart'; + +class ResetPasswordBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ResetPasswordLogic()); + } +} diff --git a/mobile-next/lib/pages/forget_password/reset_password/reset_password_logic.dart b/mobile-next/lib/pages/forget_password/reset_password/reset_password_logic.dart new file mode 100644 index 0000000..5bc5bca --- /dev/null +++ b/mobile-next/lib/pages/forget_password/reset_password/reset_password_logic.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +class ResetPasswordLogic extends GetxController { + final pwdCtrl = TextEditingController(); + final pwdAgainCtrl = TextEditingController(); + final enabled = false.obs; + String? phoneNumber; + String? email; + late String areaCode; + late int usedFor; + late String verificationCode; + String? invitationCode; + + @override + void onClose() { + pwdCtrl.dispose(); + pwdAgainCtrl.dispose(); + super.onClose(); + } + + @override + void onInit() { + phoneNumber = Get.arguments['phoneNumber']; + email = Get.arguments['email']; + areaCode = Get.arguments['areaCode']; + verificationCode = Get.arguments['verificationCode']; + pwdCtrl.addListener(_onChanged); + pwdAgainCtrl.addListener(_onChanged); + super.onInit(); + } + + _onChanged() { + enabled.value = pwdCtrl.text.trim().isNotEmpty && pwdAgainCtrl.text.trim().isNotEmpty; + } + + bool _checkingInput() { + if (!IMUtils.isValidPassword(pwdCtrl.text)) { + IMViews.showToast(StrRes.wrongPasswordFormat); + return false; + } else if (pwdCtrl.text != pwdAgainCtrl.text) { + IMViews.showToast(StrRes.twicePwdNoSame); + return false; + } + return true; + } + + resetPassword() => LoadingView.singleton.wrap( + asyncFunction: () => Apis.resetPassword( + areaCode: areaCode, + phoneNumber: phoneNumber, + email: email, + password: pwdCtrl.text, + verificationCode: verificationCode, + )); + + confirmTheChanges() async { + if (_checkingInput()) { + await resetPassword(); + IMViews.showToast(StrRes.changedSuccessfully); + AppNavigator.startBackLogin(); + } + } +} diff --git a/mobile-next/lib/pages/forget_password/reset_password/reset_password_view.dart b/mobile-next/lib/pages/forget_password/reset_password/reset_password_view.dart new file mode 100644 index 0000000..4aca783 --- /dev/null +++ b/mobile-next/lib/pages/forget_password/reset_password/reset_password_view.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../widgets/register_page_bg.dart'; +import 'reset_password_logic.dart'; + +class ResetPasswordPage extends StatelessWidget { + final logic = Get.find(); + + ResetPasswordPage({super.key}); + + @override + Widget build(BuildContext context) => RegisterBgView( + child: Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StrRes.forgetPassword.toText + ..style = Styles.ts_0089FF_22sp_semibold, + 29.verticalSpace, + InputBox.password( + label: StrRes.password, + hintText: StrRes.plsEnterPassword, + controller: logic.pwdCtrl, + formatHintText: StrRes.loginPwdFormat, + inputFormatters: [IMUtils.getPasswordFormatter()], + ), + 17.verticalSpace, + InputBox.password( + label: StrRes.confirmPassword, + hintText: StrRes.plsConfirmPasswordAgain, + controller: logic.pwdAgainCtrl, + inputFormatters: [IMUtils.getPasswordFormatter()], + ), + 129.verticalSpace, + Button( + text: StrRes.confirmTheChanges, + enabled: logic.enabled.value, + onTap: logic.confirmTheChanges, + ), + ], + )), + ); +} diff --git a/mobile-next/lib/pages/global_search/global_search_binding.dart b/mobile-next/lib/pages/global_search/global_search_binding.dart new file mode 100644 index 0000000..22264a7 --- /dev/null +++ b/mobile-next/lib/pages/global_search/global_search_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'global_search_logic.dart'; + +class GlobalSearchBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GlobalSearchLogic()); + } +} diff --git a/mobile-next/lib/pages/global_search/global_search_logic.dart b/mobile-next/lib/pages/global_search/global_search_logic.dart new file mode 100644 index 0000000..0b6afd0 --- /dev/null +++ b/mobile-next/lib/pages/global_search/global_search_logic.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pull_to_refresh_new/pull_to_refresh.dart'; + +import '../conversation/conversation_logic.dart'; + +class GlobalSearchLogic extends CommonSearchLogic { + final conversationLogic = Get.find(); + final textMessageRefreshCtrl = RefreshController(); + final fileMessageRefreshCtrl = RefreshController(); + final contactsList = [].obs; + final groupList = [].obs; + final textSearchResultItems = [].obs; + + final fileMessageList = [].obs; + final index = 0.obs; + final tabs = [ + StrRes.globalSearchAll, + StrRes.globalSearchContacts, + StrRes.globalSearchGroup, + StrRes.globalSearchChatHistory, + StrRes.globalSearchChatFile, + ]; + + int textMessagePageIndex = 1; + int fileMessagePageIndex = 1; + int count = 20; + + switchTab(int index) { + this.index.value = index; + } + + @override + void clearList() { + contactsList.clear(); + groupList.clear(); + textSearchResultItems.clear(); + fileMessageList.clear(); + } +} + +abstract class CommonSearchLogic extends GetxController { + final searchCtrl = TextEditingController(); + final focusNode = FocusNode(); + + void clearList(); + + @override + void onInit() { + searchCtrl.addListener(_clearInput); + super.onInit(); + } + + @override + void onClose() { + focusNode.dispose(); + searchCtrl.dispose(); + super.onClose(); + } + + _clearInput() { + if (searchKey.isEmpty) { + clearList(); + } + } + + String get searchKey => searchCtrl.text.trim(); + + Future> searchFriend() => Apis.searchFriendInfo(searchCtrl.text.trim()) + .then((list) => list.map((e) => FriendInfo.fromJson(e.toJson())).toList()); + + Future> searchGroup() => OpenIM.iMManager.groupManager + .searchGroups(keywordList: [searchCtrl.text.trim()], isSearchGroupName: true, isSearchGroupID: true); + + Future searchTextMessage({ + int pageIndex = 1, + int count = 20, + }) => + OpenIM.iMManager.messageManager.searchLocalMessages( + keywordList: [searchKey], + messageTypeList: [MessageType.text, MessageType.atText], + pageIndex: pageIndex, + count: count, + ); + + Future searchFileMessage({ + int pageIndex = 1, + int count = 20, + }) => + OpenIM.iMManager.messageManager.searchLocalMessages( + keywordList: [searchKey], + messageTypeList: [MessageType.file], + pageIndex: pageIndex, + count: count, + ); + + String? parseID(e) { + if (e is ConversationInfo) { + return e.isSingleChat ? e.userID : e.groupID; + } else if (e is GroupInfo) { + return e.groupID; + } else if (e is UserInfo) { + return e.userID; + } else if (e is FriendInfo) { + return e.userID; + } else { + return null; + } + } + + String? parseNickname(e) { + if (e is ConversationInfo) { + return e.showName; + } else if (e is GroupInfo) { + return e.groupName; + } else if (e is UserInfo) { + return e.nickname; + } else if (e is FriendInfo) { + return e.nickname; + } else { + return null; + } + } + + String? parseFaceURL(e) { + if (e is ConversationInfo) { + return e.faceURL; + } else if (e is GroupInfo) { + return e.faceURL; + } else if (e is UserInfo) { + return e.faceURL; + } else if (e is FriendInfo) { + return e.faceURL; + } else { + return null; + } + } +} diff --git a/mobile-next/lib/pages/global_search/global_search_view.dart b/mobile-next/lib/pages/global_search/global_search_view.dart new file mode 100644 index 0000000..6e69b96 --- /dev/null +++ b/mobile-next/lib/pages/global_search/global_search_view.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'global_search_logic.dart'; + +class GlobalSearchPage extends StatelessWidget { + final logic = Get.find(); + + GlobalSearchPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard(child: Scaffold()); + } +} diff --git a/mobile-next/lib/pages/home/home_binding.dart b/mobile-next/lib/pages/home/home_binding.dart new file mode 100644 index 0000000..a1cd1bf --- /dev/null +++ b/mobile-next/lib/pages/home/home_binding.dart @@ -0,0 +1,18 @@ +import 'package:get/get.dart'; + +import '../contacts/contacts_logic.dart'; +import '../conversation/conversation_logic.dart'; +import '../mine/mine_logic.dart'; +import '../discover/discover_logic.dart'; +import 'home_logic.dart'; + +class HomeBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => HomeLogic()); + Get.lazyPut(() => ConversationLogic()); + Get.lazyPut(() => ContactsLogic()); + Get.lazyPut(() => MineLogic()); + Get.lazyPut(() => DiscoverLogic()); + } +} diff --git a/mobile-next/lib/pages/home/home_logic.dart b/mobile-next/lib/pages/home/home_logic.dart new file mode 100644 index 0000000..51db304 --- /dev/null +++ b/mobile-next/lib/pages/home/home_logic.dart @@ -0,0 +1,198 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screen_lock/flutter_screen_lock.dart'; +import 'package:get/get.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../../core/controller/app_controller.dart'; +import '../../core/controller/im_controller.dart'; +import '../../core/im_callback.dart'; +import '../../routes/app_navigator.dart'; +import '../../widgets/screen_lock_title.dart'; + +class HomeLogic extends SuperController { + final pushLogic = Get.find(); + final imLogic = Get.find(); + final cacheLogic = Get.find(); + final initLogic = Get.find(); + final index = 0.obs; + final unreadMsgCount = 0.obs; + final unhandledFriendApplicationCount = 0.obs; + final unhandledGroupApplicationCount = 0.obs; + final unhandledCount = 0.obs; + String? _lockScreenPwd; + bool _isShowScreenLock = false; + bool? _isAutoLogin; + final auth = LocalAuthentication(); + final _errorController = PublishSubject(); + var conversationsAtFirstPage = []; + + Function()? onScrollToUnreadMessage; + + switchTab(index) { + this.index.value = index; + } + + scrollToUnreadMessage() { + onScrollToUnreadMessage?.call(); + } + + _getUnreadMsgCount() { + OpenIM.iMManager.conversationManager.getTotalUnreadMsgCount().then((count) { + unreadMsgCount.value = int.tryParse(count) ?? 0; + initLogic.showBadge(unreadMsgCount.value); + }); + } + + void getUnhandledFriendApplicationCount() async { + var i = 0; + var list = await OpenIM.iMManager.friendshipManager.getFriendApplicationListAsRecipient(); + var haveReadList = DataSp.getHaveReadUnHandleFriendApplication(); + haveReadList ??= []; + for (var info in list) { + var id = IMUtils.buildFriendApplicationID(info); + if (!haveReadList.contains(id)) { + if (info.handleResult == 0) i++; + } + } + unhandledFriendApplicationCount.value = i; + unhandledCount.value = unhandledGroupApplicationCount.value + i; + } + + void getUnhandledGroupApplicationCount() async { + var i = 0; + var list = await OpenIM.iMManager.groupManager.getGroupApplicationListAsRecipient(); + var haveReadList = DataSp.getHaveReadUnHandleGroupApplication(); + haveReadList ??= []; + for (var info in list) { + var id = IMUtils.buildGroupApplicationID(info); + if (!haveReadList.contains(id)) { + if (info.handleResult == 0) i++; + } + } + unhandledGroupApplicationCount.value = i; + unhandledCount.value = unhandledFriendApplicationCount.value + i; + } + + @override + void onInit() { + _isAutoLogin = Get.arguments != null ? Get.arguments['isAutoLogin'] : false; + if (_isAutoLogin == true) { + WidgetsBinding.instance.addPostFrameCallback((_) => _showLockScreenPwd()); + } + if (Get.arguments != null) { + conversationsAtFirstPage = Get.arguments['conversations'] ?? []; + } + imLogic.unreadMsgCountEventSubject.listen((value) { + unreadMsgCount.value = value; + }); + imLogic.friendApplicationChangedSubject.listen((value) { + getUnhandledFriendApplicationCount(); + }); + imLogic.groupApplicationChangedSubject.listen((value) { + getUnhandledGroupApplicationCount(); + }); + + imLogic.imSdkStatusPublishSubject.listen((value) { + if (value.status == IMSdkStatus.syncStart) { + _getRTCInvitationStart(); + } + }); + + Apis.kickoffController.stream.listen((event) { + DataSp.removeLoginCertificate(); + PushController.logout(); + AppNavigator.startLogin(); + }); + super.onInit(); + } + + @override + void onReady() { + _getRTCInvitationStart(); + _getUnreadMsgCount(); + getUnhandledFriendApplicationCount(); + getUnhandledGroupApplicationCount(); + cacheLogic.initCallRecords(); + cacheLogic.initFavoriteEmoji(); + super.onReady(); + } + + @override + void onClose() { + _errorController.close(); + super.onClose(); + } + + _localAuth() async { + final didAuthenticate = await IMUtils.checkingBiometric(auth); + if (didAuthenticate) { + Get.back(); + } + } + + _showLockScreenPwd() async { + if (_isShowScreenLock) return; + _lockScreenPwd = DataSp.getLockScreenPassword(); + if (null != _lockScreenPwd) { + final isEnabledBiometric = DataSp.isEnabledBiometric() == true; + bool enabled = false; + if (isEnabledBiometric) { + final isSupportedBiometrics = await auth.isDeviceSupported(); + final canCheckBiometrics = await auth.canCheckBiometrics; + enabled = isSupportedBiometrics && canCheckBiometrics; + } + _isShowScreenLock = true; + screenLock( + context: Get.context!, + correctString: _lockScreenPwd!, + maxRetries: 3, + title: ScreenLockTitle(stream: _errorController.stream), + canCancel: false, + customizedButtonChild: enabled ? const Icon(Icons.fingerprint) : null, + customizedButtonTap: enabled ? () async => await _localAuth() : null, + onUnlocked: () { + _isShowScreenLock = false; + Get.back(); + }, + onMaxRetries: (_) async { + Get.back(); + await LoadingView.singleton.wrap(asyncFunction: () async { + await imLogic.logout(); + await DataSp.removeLoginCertificate(); + await DataSp.clearLockScreenPassword(); + await DataSp.closeBiometric(); + PushController.logout(); + }); + AppNavigator.startLogin(); + }, + onError: (retries) { + _errorController.sink.add( + retries.toString(), + ); + }, + ); + } + } + + @override + void onDetached() {} + + @override + void onInactive() {} + + @override + void onPaused() {} + + @override + void onResumed() {} + + void _getRTCInvitationStart() async {} + + @override + void onHidden() {} +} diff --git a/mobile-next/lib/pages/home/home_view.dart b/mobile-next/lib/pages/home/home_view.dart new file mode 100644 index 0000000..e79176f --- /dev/null +++ b/mobile-next/lib/pages/home/home_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../contacts/contacts_view.dart'; +import '../conversation/conversation_view.dart'; +import '../mine/mine_view.dart'; +import '../discover/discover_view.dart'; +import 'home_logic.dart'; +import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; + +class HomePage extends StatelessWidget { + final logic = Get.find(); + HomePage({super.key}); + + List _tabs() => [ + PersistentTabConfig( + screen: ConversationPage(), + item: ItemConfig( + icon: GestureDetector( + onDoubleTap: () { + logic.scrollToUnreadMessage(); + }, + child: _setupIcon(ImageRes.homeTab1Sel.toImage, logic.unreadMsgCount.value), + ), + inactiveIcon: _setupIcon(ImageRes.homeTab1Nor.toImage, logic.unreadMsgCount.value), + title: StrRes.home, + textStyle: Styles.ts_0089FF_10sp_semibold, + ), + ), + PersistentTabConfig( + screen: ContactsPage(), + item: ItemConfig( + icon: _setupIcon(ImageRes.homeTab2Sel.toImage, logic.unhandledCount.value), + inactiveIcon: _setupIcon(ImageRes.homeTab2Nor.toImage, logic.unhandledCount.value), + title: StrRes.contacts, + textStyle: Styles.ts_0089FF_10sp_semibold, + ), + ), + PersistentTabConfig( + screen: DiscoverPage(), + item: ItemConfig( + icon: ImageRes.homeTab3Sel.toImage, + inactiveIcon: ImageRes.homeTab3Nor.toImage, + title: StrRes.workbench, + textStyle: Styles.ts_0089FF_10sp_semibold, + ), + ), + PersistentTabConfig( + screen: MinePage(), + item: ItemConfig( + icon: ImageRes.homeTab4Sel.toImage, + inactiveIcon: ImageRes.homeTab4Nor.toImage, + title: StrRes.mine, + textStyle: Styles.ts_0089FF_10sp_semibold, + ), + ), + ]; + + Widget _setupIcon(Widget icon, int unReadCount) { + return Stack( + alignment: Alignment.center, + children: [ + icon, + Positioned( + top: 0, + right: 0, + child: Transform.translate( + offset: const Offset(2, -2), + child: UnreadCountView(count: unReadCount), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Styles.c_FFFFFF, + body: Obx( + () => PersistentTabView( + tabs: _tabs(), + navBarBuilder: (navBarConfig) => Style1BottomNavBar( + navBarConfig: navBarConfig, + navBarDecoration: const NavBarDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow(color: Colors.black12, blurRadius: 0.5, spreadRadius: 0.5), + ], + ), + ), + navBarOverlap: const NavBarOverlap.none(), + screenTransitionAnimation: const ScreenTransitionAnimation.none(), + ), + ), + ); + } +} diff --git a/mobile-next/lib/pages/login/login_binding.dart b/mobile-next/lib/pages/login/login_binding.dart new file mode 100644 index 0000000..64200bb --- /dev/null +++ b/mobile-next/lib/pages/login/login_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'login_logic.dart'; + +class LoginBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => LoginLogic()); + } +} diff --git a/mobile-next/lib/pages/login/login_logic.dart b/mobile-next/lib/pages/login/login_logic.dart new file mode 100644 index 0000000..d9dce06 --- /dev/null +++ b/mobile-next/lib/pages/login/login_logic.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import '../../core/controller/im_controller.dart'; +import '../../routes/app_navigator.dart'; +import '../conversation/conversation_logic.dart'; + +enum LoginType { + phone(0), + email(1), + account(2); + + final int rawValue; + + const LoginType(this.rawValue); + + static LoginType fromRawValue(int rawValue) { + return values.firstWhere((e) => e.rawValue == rawValue); + } +} + +extension LoginTypeExt on LoginType { + String get name { + switch (this) { + case LoginType.phone: + return StrRes.phoneNumber; + case LoginType.email: + return StrRes.email; + case LoginType.account: + return StrRes.account; + } + } + + String get hintText { + switch (this) { + case LoginType.phone: + return StrRes.plsEnterPhoneNumber; + case LoginType.email: + return StrRes.plsEnterEmail; + case LoginType.account: + return StrRes.plsEnterAccount; + } + } + + String get exclusiveName { + switch (this) { + case LoginType.phone: + return StrRes.email; + case LoginType.email: + return StrRes.phoneNumber; + case LoginType.account: + return StrRes.account; + } + } +} + +class LoginLogic extends GetxController with GetTickerProviderStateMixin { + final imLogic = Get.find(); + final phoneCtrl = TextEditingController(); + final pwdCtrl = TextEditingController(); + final verificationCodeCtrl = TextEditingController(); + final obscureText = true.obs; + final enabled = false.obs; + final areaCode = "+86".obs; + final isPasswordLogin = true.obs; + final versionInfo = ''.obs; + final loginType = LoginType.phone.obs; + String? get email => loginType.value == LoginType.email ? phoneCtrl.text.trim() : null; + String? get phone => loginType.value == LoginType.phone ? phoneCtrl.text.trim() : null; + String? get account => loginType.value == LoginType.account ? phoneCtrl.text.trim() : null; + LoginType operateType = LoginType.phone; + + FocusNode? accountFocus = FocusNode(); + FocusNode? pwdFocus = FocusNode(); + + late TabController tabController; + + _initData() async { + var map = DataSp.getLoginAccount(); + if (map is Map) { + String? phoneNumber = map["phoneNumber"]; + String? areaCode = map["areaCode"]; + + if (phoneNumber != null && phoneNumber.isNotEmpty) { + phoneCtrl.text = phoneNumber; + } + if (areaCode != null && areaCode.isNotEmpty) { + this.areaCode.value = areaCode; + } + } + + loginType.value = LoginType.fromRawValue(DataSp.getLoginType()); + operateType = loginType.value; + tabController.index = loginType.value.rawValue; + } + + @override + void onClose() { + phoneCtrl.dispose(); + pwdCtrl.dispose(); + verificationCodeCtrl.dispose(); + tabController.dispose(); + super.onClose(); + } + + @override + void onInit() { + tabController = TabController(length: 3, vsync: this); + _initData(); + phoneCtrl.addListener(_onChanged); + pwdCtrl.addListener(_onChanged); + verificationCodeCtrl.addListener(_onChanged); + super.onInit(); + } + + @override + void onReady() { + super.onReady(); + getPackageInfo(); + } + + _onChanged() { + if (loginType.value == LoginType.account) { + enabled.value = phoneCtrl.text.trim().isNotEmpty && pwdCtrl.text.trim().isNotEmpty; + } else { + enabled.value = isPasswordLogin.value && phoneCtrl.text.trim().isNotEmpty && pwdCtrl.text.trim().isNotEmpty || + !isPasswordLogin.value && phoneCtrl.text.trim().isNotEmpty && verificationCodeCtrl.text.trim().isNotEmpty; + } + } + + login() { + DataSp.putLoginType(loginType.value.rawValue); + LoadingView.singleton.wrap(asyncFunction: () async { + var suc = await _login(); + if (suc) { + final result = await ConversationLogic.getConversationFirstPage(); + + Get.find().resetCache(); + AppNavigator.startMain(conversations: result); + } + }); + } + + Future _login() async { + try { + if (loginType.value == LoginType.phone) { + if (phone?.isNotEmpty == true && !IMUtils.isMobile(areaCode.value, phoneCtrl.text)) { + IMViews.showToast(StrRes.plsEnterRightPhone); + return false; + } + } else if (loginType.value == LoginType.email) { + if (email?.isNotEmpty == true && !phoneCtrl.text.isEmail) { + IMViews.showToast(StrRes.plsEnterRightEmail); + return false; + } + } else if (loginType.value == LoginType.account) { + if (this.account?.isNotEmpty != true) { + IMViews.showToast(StrRes.plsEnterRightAccount); + return false; + } + } + final password = IMUtils.emptyStrToNull(pwdCtrl.text); + final code = IMUtils.emptyStrToNull(verificationCodeCtrl.text); + final data = await Apis.login( + areaCode: areaCode.value, + phoneNumber: phone, + account: this.account, + email: email, + password: isPasswordLogin.value ? password : null, + verificationCode: isPasswordLogin.value ? null : code, + ); + final account = { + "areaCode": areaCode.value, + "phoneNumber": phoneCtrl.text, + 'loginType': loginType.value.rawValue, + }; + await DataSp.putLoginCertificate(data); + await DataSp.putLoginAccount(account); + Logger.print('login : ${data.userID}, token: ${data.imToken}'); + await imLogic.login(data.userID, data.imToken); + Logger.print('im login success'); + PushController.login( + data.userID, + onTokenRefresh: (token) { + OpenIM.iMManager.updateFcmToken( + fcmToken: token, expireTime: DateTime.now().add(Duration(days: 90)).millisecondsSinceEpoch); + }, + ); + Logger.print('push login success'); + return true; + } catch (e, s) { + Logger.print('login e: $e $s'); + } + return false; + } + + void togglePasswordType() { + isPasswordLogin.value = !isPasswordLogin.value; + } + + void toggleLoginType() { + if (loginType.value == LoginType.phone) { + loginType.value = LoginType.email; + } else { + loginType.value = LoginType.phone; + } + + phoneCtrl.text = ''; + } + + Future getVerificationCode() async { + if (phone?.isNotEmpty == true && !IMUtils.isMobile(areaCode.value, phoneCtrl.text)) { + IMViews.showToast(StrRes.plsEnterRightPhone); + return false; + } + + if (email?.isNotEmpty == true && !phoneCtrl.text.isEmail) { + IMViews.showToast(StrRes.plsEnterRightEmail); + return false; + } + + return sendVerificationCode(); + } + + Future sendVerificationCode() => LoadingView.singleton.wrap( + asyncFunction: () => Apis.requestVerificationCode( + areaCode: areaCode.value, + phoneNumber: phone, + email: email, + usedFor: 3, + )); + + void openCountryCodePicker() async { + String? code = await IMViews.showCountryCodePicker(); + if (null != code) areaCode.value = code; + } + + void registerNow() => AppNavigator.startRegister(); + + void forgetPassword() => AppNavigator.startForgetPassword(); + + void getPackageInfo() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final version = packageInfo.version; + final appName = packageInfo.appName; + final buildNumber = packageInfo.buildNumber; + + versionInfo.value = '$appName $version+$buildNumber SDK: ${OpenIM.version}'; + } +} diff --git a/mobile-next/lib/pages/login/login_view.dart b/mobile-next/lib/pages/login/login_view.dart new file mode 100644 index 0000000..7e09f5d --- /dev/null +++ b/mobile-next/lib/pages/login/login_view.dart @@ -0,0 +1,251 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import 'login_logic.dart'; + +class LoginPage extends StatelessWidget { + final logic = Get.find(); + + LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Material( + child: TouchCloseSoftKeyboard( + isGradientBg: true, + child: SingleChildScrollView( + child: Column( + children: [ + 88.verticalSpace, + ImageRes.loginLogo.toImage + ..width = 64.w + ..height = 64.h, + StrRes.welcome.toText..style = Styles.ts_0089FF_17sp_semibold, + 51.verticalSpace, + Padding( + padding: EdgeInsets.symmetric(horizontal: 32.w), + child: Column(children: [ + _buildInputView(), + 46.verticalSpace, + Obx(() => Button( + text: StrRes.login, + enabled: logic.enabled.value, + onTap: logic.login, + )), + ]), + ), + 100.verticalSpace, + Obx( + () => Visibility( + visible: logic.loginType.value != LoginType.account, + child: RichText( + text: TextSpan( + text: StrRes.noAccountYet, + style: Styles.ts_8E9AB0_12sp, + children: [ + TextSpan( + text: StrRes.registerNow, + style: Styles.ts_0089FF_12sp, + recognizer: TapGestureRecognizer()..onTap = _showRegisterBottomSheet, + ) + ], + ), + ), + ), + ), + 32.verticalSpace, + Obx(() => logic.versionInfo.value.toText..style = Styles.ts_0C1C33_14sp), + ], + ), + ), + ), + ); + } + + Widget _buildInputView() { + return Container( + height: 240.h, + width: 300.w, + child: Column( + children: [ + TabBar( + tabs: LoginType.values.map((e) => Tab(text: e.name)).toList(), + controller: logic.tabController, + isScrollable: true, + indicatorColor: Styles.c_0089FF, + labelColor: Styles.c_0089FF, + tabAlignment: TabAlignment.start, + labelPadding: const EdgeInsets.only(right: 16), + overlayColor: WidgetStateProperty.all(Colors.transparent), + dividerHeight: 0.1, + onTap: (index) { + logic.loginType.value = LoginType.fromRawValue(index); + logic.operateType = logic.loginType.value; + FocusScope.of(Get.context!).unfocus(); + logic.phoneCtrl.clear(); + logic.pwdCtrl.clear(); + }, + ), + Flexible( + child: Obx( + () => TabBarView( + controller: logic.tabController, + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildInputView1(LoginType.phone), + _buildInputView1(LoginType.email), + _buildInputView2(LoginType.account), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildInputView1(LoginType type) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InputBox.account( + label: '', + hintText: type.hintText, + code: logic.areaCode.value, + onAreaCode: type == LoginType.phone ? logic.openCountryCodePicker : null, + controller: logic.phoneCtrl, + focusNode: logic.accountFocus, + keyBoardType: type == LoginType.phone ? TextInputType.phone : TextInputType.text, + ), + 8.verticalSpace, + Offstage( + offstage: !logic.isPasswordLogin.value, + child: InputBox.password( + label: '', + hintText: StrRes.plsEnterPassword, + controller: logic.pwdCtrl, + focusNode: logic.pwdFocus, + ), + ), + Offstage( + offstage: logic.isPasswordLogin.value, + child: InputBox.verificationCode( + label: StrRes.verificationCode, + hintText: StrRes.plsEnterVerificationCode, + controller: logic.verificationCodeCtrl, + onSendVerificationCode: logic.getVerificationCode, + ), + ), + 10.verticalSpace, + Row( + children: [ + StrRes.forgetPassword.toText + ..style = Styles.ts_8E9AB0_12sp + ..onTap = logic.forgetPassword, + const Spacer(), + (logic.isPasswordLogin.value ? StrRes.verificationCodeLogin : StrRes.passwordLogin).toText + ..style = Styles.ts_0089FF_12sp + ..onTap = logic.togglePasswordType, + ], + ), + ], + ); + } + + Widget _buildInputView2(LoginType type) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InputBox.account( + label: '', + hintText: type.hintText, + code: logic.areaCode.value, + onAreaCode: null, + controller: logic.phoneCtrl, + focusNode: logic.accountFocus, + keyBoardType: TextInputType.text, + ), + 8.verticalSpace, + InputBox.password( + label: '', + hintText: StrRes.plsEnterPassword, + controller: logic.pwdCtrl, + focusNode: logic.pwdFocus, + ), + ], + ); + } + + void _showRegisterBottomSheet() { + showCupertinoModalPopup( + context: Get.context!, + builder: (BuildContext context) { + return CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context); + logic.operateType = LoginType.email; + logic.registerNow(); + }, + child: Text('${StrRes.email} ${StrRes.registerNow}'), + ), + CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context); + logic.operateType = LoginType.phone; + logic.registerNow(); + }, + child: Text('${StrRes.phoneNumber} ${StrRes.registerNow}'), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context); + }, + child: Text(StrRes.cancel), + ), + ); + }, + ); + } + + void _showForgetPasswordBottomSheet() { + showCupertinoModalPopup( + context: Get.context!, + builder: (BuildContext context) { + return CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context); + logic.operateType = LoginType.email; + logic.forgetPassword(); + }, + child: Text(sprintf(StrRes.through, [StrRes.email])), + ), + CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context); + logic.operateType = LoginType.phone; + logic.forgetPassword(); + }, + child: Text(sprintf(StrRes.through, [StrRes.phoneNumber])), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(context); + }, + child: Text(StrRes.cancel), + ), + ); + }, + ); + } +} diff --git a/mobile-next/lib/pages/mine/about_us/about_us_binding.dart b/mobile-next/lib/pages/mine/about_us/about_us_binding.dart new file mode 100644 index 0000000..4999783 --- /dev/null +++ b/mobile-next/lib/pages/mine/about_us/about_us_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'about_us_logic.dart'; + +class AboutUsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AboutUsLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/about_us/about_us_logic.dart b/mobile-next/lib/pages/mine/about_us/about_us_logic.dart new file mode 100644 index 0000000..1e09ff2 --- /dev/null +++ b/mobile-next/lib/pages/mine/about_us/about_us_logic.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import '../../../core/controller/app_controller.dart'; +import '../../../core/controller/im_controller.dart'; + +class AboutUsLogic extends GetxController { + final appLogic = Get.find(); + final imLogic = Get.find(); + final lineTextController = TextEditingController(text: '1000'); + final displayVersion = ''.obs; + + void getPackageInfo() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final version = packageInfo.version; + final appName = packageInfo.appName; + final buildNumber = packageInfo.buildNumber; + + displayVersion.value = '$appName $version+$buildNumber SDK: ${OpenIM.version}'; + } + + void checkUpdate() { + appLogic.checkUpdate(); + } + + void copyVersion() { + IMViews.showToast(StrRes.copySuccessfully); + Clipboard.setData(ClipboardData(text: displayVersion.value)); + } + + void uploadLogs([int line = 0]) async { + EasyLoading.showProgress(0); + await OpenIM.iMManager.uploadLogs(line: line); + EasyLoading.dismiss(); + } + + @override + void onReady() { + getPackageInfo(); + + imLogic.onUploadProgress = (current, size) { + final p = current / size; + final pStr = '${(p * 100.0).truncate()}%'; + EasyLoading.showProgress(p, status: pStr); + }; + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/mobile-next/lib/pages/mine/about_us/about_us_view.dart b/mobile-next/lib/pages/mine/about_us/about_us_view.dart new file mode 100644 index 0000000..6177418 --- /dev/null +++ b/mobile-next/lib/pages/mine/about_us/about_us_view.dart @@ -0,0 +1,132 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'about_us_logic.dart'; + +class AboutUsPage extends StatelessWidget { + final logic = Get.find(); + + AboutUsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.aboutUs), + backgroundColor: Styles.c_F8F9FA, + body: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(6.r), + ), + margin: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.h, + ), + child: Column( + children: [ + 23.verticalSpace, + ImageRes.splashLogo.toImage + ..width = 55.w + ..height = 78.h, + 10.verticalSpace, + Obx(() => '${logic.displayVersion}'.toText + ..style = Styles.ts_0C1C33_14sp + ..onTap = logic.copyVersion), + 16.verticalSpace, + Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + color: Styles.c_E8EAEF, + height: .5, + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.checkUpdate, + child: Container( + height: 57.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + StrRes.checkNewVersion.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.uploadLogs, + child: Container( + height: 57.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + StrRes.uploadErrorLog.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: _showInputDialog, + child: Container( + height: 57.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + StrRes.uploadLogWithLine.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + void _showInputDialog() { + showDialog( + context: Get.context!, + builder: (ctx) { + return CupertinoAlertDialog( + title: StrRes.setLines.toText..style = Styles.ts_0C1C33_17sp, + content: CupertinoTextField( + controller: logic.lineTextController, + placeholder: logic.lineTextController.text, + keyboardType: TextInputType.number, + ), + actions: [ + CupertinoButton( + child: StrRes.confirm.toText..style = Styles.ts_0C1C33_17sp, + onPressed: () { + navigator?.pop(); + final lineStr = logic.lineTextController.text.trim(); + logic.uploadLogs(int.parse(lineStr)); + }, + ) + ], + ); + }); + } +} diff --git a/mobile-next/lib/pages/mine/account_setup/account_setup_binding.dart b/mobile-next/lib/pages/mine/account_setup/account_setup_binding.dart new file mode 100644 index 0000000..ba213cf --- /dev/null +++ b/mobile-next/lib/pages/mine/account_setup/account_setup_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'account_setup_logic.dart'; + +class AccountSetupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AccountSetupLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/account_setup/account_setup_logic.dart b/mobile-next/lib/pages/mine/account_setup/account_setup_logic.dart new file mode 100644 index 0000000..92d304a --- /dev/null +++ b/mobile-next/lib/pages/mine/account_setup/account_setup_logic.dart @@ -0,0 +1,135 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; + +class AccountSetupLogic extends GetxController { + final imLogic = Get.find(); + final curLanguage = "".obs; + + @override + void onReady() { + _updateLanguage(); + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } + + @override + void onInit() { + _queryMyFullInfo(); + super.onInit(); + } + + bool get isGlobalNotDisturb => imLogic.userInfo.value.globalRecvMsgOpt == 2; + + bool get isAllowAddFriend => imLogic.userInfo.value.allowAddFriend == 1; + + bool get isAllowBeep => imLogic.userInfo.value.allowBeep == 1; + + bool get isAllowVibration => imLogic.userInfo.value.allowVibration == 1; + + void _queryMyFullInfo() async { + final data = await LoadingView.singleton.wrap( + asyncFunction: () => Apis.queryMyFullInfo(), + ); + if (data is UserFullInfo) { + final userInfo = UserFullInfo.fromJson(data.toJson()); + imLogic.userInfo.update((val) { + val?.allowAddFriend = userInfo.allowAddFriend; + val?.allowBeep = userInfo.allowBeep; + val?.allowVibration = userInfo.allowVibration; + }); + } + } + + void toggleNotDisturbMode() async { + var status = isGlobalNotDisturb ? 0 : 2; + await LoadingView.singleton + .wrap(asyncFunction: () => OpenIM.iMManager.userManager.setGlobalRecvMessageOpt(status: status)); + imLogic.userInfo.update((val) { + val?.globalRecvMsgOpt = status; + }); + } + + void toggleBeep() async { + final allowBeep = !isAllowBeep ? 1 : 2; + + await LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo( + allowBeep: allowBeep, + userID: OpenIM.iMManager.userID, + ), + ); + imLogic.userInfo.update((val) { + val?.allowBeep = allowBeep; + }); + } + + void toggleVibration() async { + final allowVibration = !isAllowVibration ? 1 : 2; + + await LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo( + allowVibration: allowVibration, + userID: OpenIM.iMManager.userID, + ), + ); + imLogic.userInfo.update((val) { + val?.allowVibration = allowVibration; + }); + } + + void toggleForbidAddMeToFriend() async { + final allowAddFriend = !isAllowAddFriend ? 1 : 2; + + final data = await LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo( + allowAddFriend: allowAddFriend, + userID: OpenIM.iMManager.userID, + ), + ); + imLogic.userInfo.update((val) { + val?.allowAddFriend = allowAddFriend; + }); + } + + void blacklist() => AppNavigator.startBlacklist(); + + void clearChatHistory() async { + var confirm = await Get.dialog(CustomDialog( + title: StrRes.confirmClearChatHistory, + )); + if (confirm == true) { + LoadingView.singleton.wrap(asyncFunction: () async { + await OpenIM.iMManager.messageManager.deleteAllMsgFromLocalAndSvr(); + }); + } + } + + void languageSetting() => AppNavigator.startLanguageSetup(); + + void _updateLanguage() { + var index = DataSp.getLanguage() ?? 0; + switch (index) { + case 1: + curLanguage.value = StrRes.chinese; + break; + case 2: + curLanguage.value = StrRes.english; + break; + default: + curLanguage.value = StrRes.followSystem; + break; + } + } + + void unlockSetup() => AppNavigator.startUnlockSetup(); + + void changePwd() => AppNavigator.startChangePassword(); +} diff --git a/mobile-next/lib/pages/mine/account_setup/account_setup_view.dart b/mobile-next/lib/pages/mine/account_setup/account_setup_view.dart new file mode 100644 index 0000000..7ffa3a6 --- /dev/null +++ b/mobile-next/lib/pages/mine/account_setup/account_setup_view.dart @@ -0,0 +1,139 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'account_setup_logic.dart'; + +class AccountSetupPage extends StatelessWidget { + final logic = Get.find(); + + AccountSetupPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: StrRes.accountSetup, + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => SingleChildScrollView( + child: Column( + children: [ + 10.verticalSpace, + _buildItemView( + label: StrRes.notDisturbMode, + switchOn: logic.isGlobalNotDisturb, + onChanged: (_) => logic.toggleNotDisturbMode(), + showSwitchButton: true, + isTopRadius: true, + ), + _buildItemView( + label: StrRes.allowRing, + switchOn: logic.isAllowBeep, + onChanged: (_) => logic.toggleBeep(), + showSwitchButton: true, + ), + _buildItemView( + label: StrRes.allowVibrate, + switchOn: logic.isAllowVibration, + onChanged: (_) => logic.toggleVibration(), + showSwitchButton: true, + isBottomRadius: true, + ), + 10.verticalSpace, + _buildItemView( + label: StrRes.forbidAddMeToFriend, + switchOn: !logic.isAllowAddFriend, + onChanged: (_) => logic.toggleForbidAddMeToFriend(), + showSwitchButton: true, + isTopRadius: true, + ), + _buildItemView( + label: StrRes.blacklist, + onTap: logic.blacklist, + showRightArrow: true, + ), + _buildItemView( + label: StrRes.languageSetup, + value: logic.curLanguage.value, + onTap: logic.languageSetting, + showRightArrow: true, + isBottomRadius: true, + ), + 10.verticalSpace, + _buildItemView( + label: StrRes.unlockSettings, + onTap: logic.unlockSetup, + showRightArrow: true, + isTopRadius: true, + ), + _buildItemView( + label: StrRes.changePassword, + showRightArrow: true, + onTap: logic.changePwd, + ), + _buildItemView( + label: StrRes.clearChatHistory, + textStyle: Styles.ts_FF381F_17sp, + onTap: logic.clearChatHistory, + showRightArrow: true, + isBottomRadius: true, + ), + ], + ), + )), + ); + } + + Widget _buildItemView({ + required String label, + TextStyle? textStyle, + String? value, + bool switchOn = false, + bool isTopRadius = false, + bool isBottomRadius = false, + bool showRightArrow = false, + bool showSwitchButton = false, + ValueChanged? onChanged, + Function()? onTap, + }) => + Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + child: Ink( + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topRight: Radius.circular(isTopRadius ? 6.r : 0), + topLeft: Radius.circular(isTopRadius ? 6.r : 0), + bottomLeft: Radius.circular(isBottomRadius ? 6.r : 0), + bottomRight: Radius.circular(isBottomRadius ? 6.r : 0), + ), + ), + child: InkWell( + onTap: onTap, + child: Container( + height: 46.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + label.toText..style = textStyle ?? Styles.ts_0C1C33_17sp, + const Spacer(), + if (showSwitchButton) + CupertinoSwitch( + value: switchOn, + activeColor: Styles.c_0089FF, + onChanged: onChanged, + ), + if (showRightArrow) + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/mine/blacklist/blacklist_binding.dart b/mobile-next/lib/pages/mine/blacklist/blacklist_binding.dart new file mode 100644 index 0000000..00ee778 --- /dev/null +++ b/mobile-next/lib/pages/mine/blacklist/blacklist_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'blacklist_logic.dart'; + +class BlacklistBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => BlacklistLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/blacklist/blacklist_logic.dart b/mobile-next/lib/pages/mine/blacklist/blacklist_logic.dart new file mode 100644 index 0000000..d72be7b --- /dev/null +++ b/mobile-next/lib/pages/mine/blacklist/blacklist_logic.dart @@ -0,0 +1,29 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; + +class BlacklistLogic extends GetxController { + final blacklist = [].obs; + + void _getBlacklist() async { + var list = await OpenIM.iMManager.friendshipManager.getBlacklist(); + blacklist.addAll(list); + } + + remove(BlacklistInfo info) async { + await OpenIM.iMManager.friendshipManager.removeBlacklist( + userID: info.userID!, + ); + blacklist.remove(info); + } + + @override + void onReady() { + _getBlacklist(); + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/mobile-next/lib/pages/mine/blacklist/blacklist_view.dart b/mobile-next/lib/pages/mine/blacklist/blacklist_view.dart new file mode 100644 index 0000000..1786b8e --- /dev/null +++ b/mobile-next/lib/pages/mine/blacklist/blacklist_view.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'blacklist_logic.dart'; + +class BlacklistPage extends StatelessWidget { + final logic = Get.find(); + + BlacklistPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.blacklist), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => logic.blacklist.isEmpty + ? _emptyListView + : ListView.builder( + padding: EdgeInsets.only(top: 10.h), + itemCount: logic.blacklist.length, + itemBuilder: (_, index) => _buildItemView( + logic.blacklist[index], + underline: index != logic.blacklist.length - 1, + ), + )), + ); + } + + Widget _buildItemView( + BlacklistInfo info, { + bool underline = true, + }) => + Ink( + height: 62.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + border: underline + ? BorderDirectional( + bottom: BorderSide( + color: Styles.c_E8EAEF, + width: 1, + ), + ) + : null, + ), + child: InkWell( + onTap: () => logic.remove(info), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + AvatarView( + width: 44.w, + height: 44.h, + text: info.nickname, + url: info.faceURL, + ), + 12.horizontalSpace, + Expanded( + child: (info.nickname ?? '').toText..style = Styles.ts_0C1C33_17sp, + ), + StrRes.remove.toText..style = Styles.ts_0089FF_17sp, + ], + ), + ), + ), + ); + + Widget get _emptyListView => SizedBox( + width: 1.sw, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 157.verticalSpace, + ImageRes.blacklistEmpty.toImage + ..width = 120.w + ..height = 120.h, + 22.verticalSpace, + StrRes.blacklistEmpty.toText..style = Styles.ts_8E9AB0_16sp, + ], + ), + ); +} diff --git a/mobile-next/lib/pages/mine/change_pwd/change_pwd_binding.dart b/mobile-next/lib/pages/mine/change_pwd/change_pwd_binding.dart new file mode 100644 index 0000000..9cec2bf --- /dev/null +++ b/mobile-next/lib/pages/mine/change_pwd/change_pwd_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'change_pwd_logic.dart'; + +class ChangePwdBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ChangePwdLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/change_pwd/change_pwd_logic.dart b/mobile-next/lib/pages/mine/change_pwd/change_pwd_logic.dart new file mode 100644 index 0000000..bf29b94 --- /dev/null +++ b/mobile-next/lib/pages/mine/change_pwd/change_pwd_logic.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../routes/app_navigator.dart'; + +class ChangePwdLogic extends GetxController { + final oldPwdCtrl = TextEditingController(); + final newPwdCtrl = TextEditingController(); + final againPwdCtrl = TextEditingController(); + + @override + void onClose() { + oldPwdCtrl.dispose(); + newPwdCtrl.dispose(); + againPwdCtrl.dispose(); + super.onClose(); + } + + void confirm() async { + if (oldPwdCtrl.text.isEmpty) { + IMViews.showToast(StrRes.plsEnterOldPwd); + return; + } + if (!IMUtils.isValidPassword(newPwdCtrl.text)) { + IMViews.showToast(StrRes.wrongPasswordFormat); + return; + } + if (newPwdCtrl.text.isEmpty) { + IMViews.showToast(StrRes.plsEnterNewPwd); + return; + } + if (againPwdCtrl.text.isEmpty) { + IMViews.showToast(StrRes.plsEnterConfirmPwd); + return; + } + if (newPwdCtrl.text != againPwdCtrl.text) { + IMViews.showToast(StrRes.twicePwdNoSame); + return; + } + + final result = await LoadingView.singleton.wrap( + asyncFunction: () => Apis.changePassword( + userID: OpenIM.iMManager.userID, + newPassword: newPwdCtrl.text, + currentPassword: oldPwdCtrl.text, + ), + ); + if (result) { + IMViews.showToast(StrRes.changedSuccessfully); + await LoadingView.singleton.wrap(asyncFunction: () async { + await OpenIM.iMManager.logout(); + await DataSp.removeLoginCertificate(); + PushController.logout(); + }); + AppNavigator.startLogin(); + } + } +} diff --git a/mobile-next/lib/pages/mine/change_pwd/change_pwd_view.dart b/mobile-next/lib/pages/mine/change_pwd/change_pwd_view.dart new file mode 100644 index 0000000..42fe59e --- /dev/null +++ b/mobile-next/lib/pages/mine/change_pwd/change_pwd_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'change_pwd_logic.dart'; + +class ChangePwdPage extends StatelessWidget { + final logic = Get.find(); + + ChangePwdPage({super.key}); + + @override + Widget build(BuildContext context) { + return TouchCloseSoftKeyboard( + child: Scaffold( + appBar: TitleBar.back( + title: StrRes.changePassword, + right: StrRes.determine.toText + ..style = Styles.ts_0C1C33_17sp + ..onTap = logic.confirm, + ), + backgroundColor: Styles.c_F8F9FA, + body: SingleChildScrollView( + child: Column( + children: [ + 12.verticalSpace, + _buildItemView( + label: StrRes.oldPwd, + controller: logic.oldPwdCtrl, + autofocus: true, + isTopRadius: true, + ), + Container( + margin: EdgeInsets.only(left: 26.w, right: 10.w), + color: Styles.c_E8EAEF, + height: .5, + ), + _buildItemView( + label: StrRes.newPwd, + controller: logic.newPwdCtrl, + ), + Container( + margin: EdgeInsets.only(left: 26.w, right: 10.w), + color: Styles.c_E8EAEF, + height: .5, + ), + _buildItemView( + label: StrRes.confirmNewPwd, + controller: logic.againPwdCtrl, + isBottomRadius: true, + ), + ], + ), + ), + ), + ); + } + + Widget _buildItemView({ + required String label, + TextEditingController? controller, + bool autofocus = false, + bool isTopRadius = false, + bool isBottomRadius = false, + }) => + Container( + height: 60.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + margin: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(isTopRadius ? 6.r : 0), + topRight: Radius.circular(isTopRadius ? 6.r : 0), + bottomRight: Radius.circular(isBottomRadius ? 6.r : 0), + bottomLeft: Radius.circular(isBottomRadius ? 6.r : 0), + ), + ), + child: Row( + children: [ + label.toText..style = Styles.ts_0C1C33_17sp, + Expanded( + child: TextField( + controller: controller, + autofocus: autofocus, + textInputAction: TextInputAction.next, + textAlign: TextAlign.end, + style: Styles.ts_0C1C33_17sp, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + ), + ), + ), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_binding.dart b/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_binding.dart new file mode 100644 index 0000000..cf3d94c --- /dev/null +++ b/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'edit_my_info_logic.dart'; + +class EditMyInfoBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => EditMyInfoLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_logic.dart b/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_logic.dart new file mode 100644 index 0000000..a8b3de4 --- /dev/null +++ b/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_logic.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; + +enum EditAttr { + nickname, + englishName, + telephone, + mobile, + email, +} + +class EditMyInfoLogic extends GetxController { + final imLogic = Get.find(); + late TextEditingController inputCtrl; + late EditAttr editAttr; + late int maxLength; + String? title; + String? defaultValue; + TextInputType? keyboardType; + + @override + void onInit() { + editAttr = Get.arguments['editAttr']; + maxLength = Get.arguments['maxLength'] ?? 16; + _initAttr(); + inputCtrl = TextEditingController(text: defaultValue); + super.onInit(); + } + + _initAttr() { + switch (editAttr) { + case EditAttr.nickname: + title = StrRes.name; + defaultValue = imLogic.userInfo.value.nickname; + keyboardType = TextInputType.text; + break; + case EditAttr.englishName: + break; + case EditAttr.telephone: + break; + case EditAttr.mobile: + title = StrRes.mobile; + defaultValue = imLogic.userInfo.value.phoneNumber; + keyboardType = TextInputType.phone; + break; + case EditAttr.email: + title = StrRes.email; + defaultValue = imLogic.userInfo.value.email; + keyboardType = TextInputType.emailAddress; + break; + } + } + + void save() async { + final value = inputCtrl.text.trim(); + if (editAttr == EditAttr.nickname) { + await LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo( + userID: OpenIM.iMManager.userID, + nickname: value, + ), + ); + imLogic.userInfo.update((val) { + val?.nickname = value; + }); + } else if (editAttr == EditAttr.mobile) { + await LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo( + userID: OpenIM.iMManager.userID, + phoneNumber: value, + ), + ); + imLogic.userInfo.update((val) { + val?.phoneNumber = value; + }); + } else if (editAttr == EditAttr.email) { + if (defaultValue?.isNotEmpty == true && value.isEmpty) { + IMViews.showToast(StrRes.plsEnterEmail); + return; + } + await LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo( + userID: OpenIM.iMManager.userID, + email: value, + ), + ); + imLogic.userInfo.update((val) { + val?.email = value; + }); + } + Get.back(); + } +} diff --git a/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_view.dart b/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_view.dart new file mode 100644 index 0000000..6a177af --- /dev/null +++ b/mobile-next/lib/pages/mine/edit_my_info/edit_my_info_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'edit_my_info_logic.dart'; + +class EditMyInfoPage extends StatelessWidget { + final logic = Get.find(); + EditMyInfoPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: logic.title, + right: StrRes.save.toText + ..style = Styles.ts_0C1C33_17sp + ..onTap = logic.save, + ), + backgroundColor: Styles.c_FFFFFF, + body: Column( + children: [ + 22.verticalSpace, + Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Styles.c_E8EAEF, + borderRadius: BorderRadius.circular(4.r), + ), + child: TextField( + controller: logic.inputCtrl, + style: Styles.ts_0C1C33_17sp, + autofocus: true, + keyboardType: logic.keyboardType, + inputFormatters: [LengthLimitingTextInputFormatter(logic.maxLength)], + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric( + vertical: 10.h, + horizontal: 12.w, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile-next/lib/pages/mine/language_setup/language_setup_binding.dart b/mobile-next/lib/pages/mine/language_setup/language_setup_binding.dart new file mode 100644 index 0000000..15252c7 --- /dev/null +++ b/mobile-next/lib/pages/mine/language_setup/language_setup_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'language_setup_logic.dart'; + +class LanguageSetupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => LanguageSetupLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/language_setup/language_setup_logic.dart b/mobile-next/lib/pages/mine/language_setup/language_setup_logic.dart new file mode 100644 index 0000000..1bb19f3 --- /dev/null +++ b/mobile-next/lib/pages/mine/language_setup/language_setup_logic.dart @@ -0,0 +1,61 @@ +import 'dart:ui'; + +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +class LanguageSetupLogic extends GetxController { + final isFollowSystem = false.obs; + final isChinese = false.obs; + final isEnglish = false.obs; + + @override + void onInit() { + _initLanguageSetting(); + super.onInit(); + } + + void _initLanguageSetting() { + var language = DataSp.getLanguage() ?? 0; + switch (language) { + case 1: + isFollowSystem.value = false; + isChinese.value = true; + isEnglish.value = false; + break; + case 2: + isFollowSystem.value = false; + isChinese.value = false; + isEnglish.value = true; + break; + default: + isFollowSystem.value = true; + isChinese.value = false; + isEnglish.value = false; + break; + } + } + + void switchLanguage(index) async { + await DataSp.putLanguage(index); + switch (index) { + case 1: + isFollowSystem.value = false; + isChinese.value = true; + isEnglish.value = false; + Get.updateLocale(const Locale('zh', 'CN')); + break; + case 2: + isFollowSystem.value = false; + isChinese.value = false; + isEnglish.value = true; + Get.updateLocale(const Locale('en', 'US')); + break; + default: + isFollowSystem.value = true; + isChinese.value = false; + isEnglish.value = false; + Get.updateLocale(window.locale); + break; + } + } +} diff --git a/mobile-next/lib/pages/mine/language_setup/language_setup_view.dart b/mobile-next/lib/pages/mine/language_setup/language_setup_view.dart new file mode 100644 index 0000000..aa9935b --- /dev/null +++ b/mobile-next/lib/pages/mine/language_setup/language_setup_view.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'language_setup_logic.dart'; + +class LanguageSetupPage extends StatelessWidget { + final logic = Get.find(); + + LanguageSetupPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.languageSetup), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => Column( + children: [ + 12.verticalSpace, + _buildItemView( + label: StrRes.followSystem, + isChecked: logic.isFollowSystem.value, + onTap: () => logic.switchLanguage(0), + isTopRadius: true, + ), + Container( + margin: EdgeInsets.only(left: 26.w, right: 10.w), + color: Styles.c_E8EAEF, + height: .5, + ), + _buildItemView( + label: StrRes.chinese, + isChecked: logic.isChinese.value, + onTap: () => logic.switchLanguage(1), + ), + Container( + margin: EdgeInsets.only(left: 26.w, right: 10.w), + color: Styles.c_E8EAEF, + height: .5, + ), + _buildItemView( + label: StrRes.english, + isChecked: logic.isEnglish.value, + onTap: () => logic.switchLanguage(2), + isBottomRadius: true, + ), + ], + )), + ); + } + + Widget _buildItemView({ + required String label, + bool isChecked = false, + bool isTopRadius = false, + bool isBottomRadius = false, + Function()? onTap, + }) => + Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + child: Ink( + height: 60.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(isTopRadius ? 6.r : 0), + topRight: Radius.circular(isTopRadius ? 6.r : 0), + bottomRight: Radius.circular(isBottomRadius ? 6.r : 0), + bottomLeft: Radius.circular(isBottomRadius ? 6.r : 0), + ), + ), + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + label.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + if (isChecked) + ImageRes.checked.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/mine/mine_binding.dart b/mobile-next/lib/pages/mine/mine_binding.dart new file mode 100644 index 0000000..4cad69c --- /dev/null +++ b/mobile-next/lib/pages/mine/mine_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'mine_logic.dart'; + +class MineBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => MineLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/mine_logic.dart b/mobile-next/lib/pages/mine/mine_logic.dart new file mode 100644 index 0000000..af65c8d --- /dev/null +++ b/mobile-next/lib/pages/mine/mine_logic.dart @@ -0,0 +1,73 @@ +import 'dart:async'; + +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; +import 'package:openim/core/im_callback.dart'; +import 'package:openim/pages/home/home_logic.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../core/controller/im_controller.dart'; +import '../../routes/app_navigator.dart'; + +class MineLogic extends GetxController { + final imLogic = Get.find(); + + late StreamSubscription kickedOfflineSub; + + void viewMyQrcode() => AppNavigator.startMyQrcode(); + + void viewMyInfo() => AppNavigator.startMyInfo(); + + void copyID() { + IMUtils.copy(text: imLogic.userInfo.value.userID!); + } + + void accountSetup() => AppNavigator.startAccountSetup(); + + void aboutUs() => AppNavigator.startAboutUs(); + + void logout() async { + var confirm = await Get.dialog(CustomDialog(title: StrRes.logoutHint)); + if (confirm == true) { + try { + await LoadingView.singleton.wrap(asyncFunction: () async { + await imLogic.logout(); + await DataSp.removeLoginCertificate(); + PushController.logout(); + Get.find().conversationsAtFirstPage.clear(); + }); + AppNavigator.startLogin(); + } catch (e) { + IMViews.showToast('e:$e'); + } + } + } + + void kickedOffline({String? tips}) async { + if (EasyLoading.isShow) { + EasyLoading.dismiss(); + } + Get.snackbar(StrRes.accountWarn, tips ?? StrRes.accountException); + await DataSp.removeLoginCertificate(); + PushController.logout(); + AppNavigator.startLogin(); + } + + @override + void onInit() { + kickedOfflineSub = imLogic.onKickedOfflineSubject.listen((value) { + if (value == KickoffType.userTokenInvalid) { + kickedOffline(tips: StrRes.tokenInvalid); + } else { + kickedOffline(); + } + }); + super.onInit(); + } + + @override + void onClose() { + kickedOfflineSub.cancel(); + super.onClose(); + } +} diff --git a/mobile-next/lib/pages/mine/mine_view.dart b/mobile-next/lib/pages/mine/mine_view.dart new file mode 100644 index 0000000..b9398a1 --- /dev/null +++ b/mobile-next/lib/pages/mine/mine_view.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'mine_logic.dart'; + +class MinePage extends StatelessWidget { + final logic = Get.find(); + + MinePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Styles.c_F8F9FA, + body: SingleChildScrollView( + child: Column( + children: [ + Stack( + children: [ + Container( + height: 138.h, + width: 1.sw, + color: Styles.c_0089FF, + child: ImageRes.mineHeaderBg.toImage, + ), + Obx(() => _buildMyInfoView()), + ], + ), + 10.verticalSpace, + _buildItemView( + icon: ImageRes.myInfo, + label: StrRes.myInfo, + onTap: logic.viewMyInfo, + isTopRadius: true, + ), + _buildItemView( + icon: ImageRes.accountSetup, + label: StrRes.accountSetup, + onTap: logic.accountSetup, + ), + _buildItemView( + icon: ImageRes.aboutUs, + label: StrRes.aboutUs, + onTap: logic.aboutUs, + ), + _buildItemView( + icon: ImageRes.logout, + label: StrRes.logout, + onTap: logic.logout, + isBottomRadius: true, + ), + ], + ), + ), + ); + } + + Widget _buildMyInfoView() => Container( + height: 98.h, + margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 90.h), + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(6.r), + ), + child: Row( + children: [ + AvatarView( + url: logic.imLogic.userInfo.value.faceURL, + text: logic.imLogic.userInfo.value.nickname, + width: 48.w, + height: 48.h, + textStyle: Styles.ts_FFFFFF_14sp, + ), + 10.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + (logic.imLogic.userInfo.value.nickname ?? '').toText..style = Styles.ts_0C1C33_17sp_medium, + 4.verticalSpace, + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.copyID, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + (logic.imLogic.userInfo.value.userID ?? '').toText..style = Styles.ts_8E9AB0_14sp, + ImageRes.mineCopy.toImage + ..width = 16.w + ..height = 16.h, + ], + ), + ), + ], + ), + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: logic.viewMyQrcode, + child: Row( + children: [ + ImageRes.mineQr.toImage + ..width = 18.w + ..height = 18.h, + 8.horizontalSpace, + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ) + ], + ), + ); + + Widget _buildItemView({ + required String icon, + required String label, + bool isTopRadius = false, + bool isBottomRadius = false, + Function()? onTap, + }) => + Container( + margin: EdgeInsets.symmetric(horizontal: 16.w), + child: Ink( + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topRight: Radius.circular(isTopRadius ? 6.r : 0), + topLeft: Radius.circular(isTopRadius ? 6.r : 0), + bottomLeft: Radius.circular(isBottomRadius ? 6.r : 0), + bottomRight: Radius.circular(isBottomRadius ? 6.r : 0), + ), + ), + child: InkWell( + onTap: onTap, + child: Container( + height: 56.h, + padding: EdgeInsets.only(left: 12.w, right: 16.w), + child: Row( + children: [ + icon.toImage + ..width = 24.w + ..height = 24.h, + 11.horizontalSpace, + label.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ), + ), + ); +} diff --git a/mobile-next/lib/pages/mine/my_info/my_info_binding.dart b/mobile-next/lib/pages/mine/my_info/my_info_binding.dart new file mode 100644 index 0000000..26d3df9 --- /dev/null +++ b/mobile-next/lib/pages/mine/my_info/my_info_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'my_info_logic.dart'; + +class MyInfoBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => MyInfoLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/my_info/my_info_logic.dart b/mobile-next/lib/pages/mine/my_info/my_info_logic.dart new file mode 100644 index 0000000..aea2a2e --- /dev/null +++ b/mobile-next/lib/pages/mine/my_info/my_info_logic.dart @@ -0,0 +1,130 @@ +import 'package:flutter_datetime_picker_plus/flutter_datetime_picker_plus.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/login/login_logic.dart'; +import 'package:openim/pages/mine/edit_my_info/edit_my_info_logic.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; + +class MyInfoLogic extends GetxController { + final imLogic = Get.find(); + final loginType = LoginType.fromRawValue(DataSp.getLoginType()); + + void editMyName() => AppNavigator.startEditMyInfo(); + + void editEnglishName() => AppNavigator.startEditMyInfo( + attr: EditAttr.englishName, + ); + + void editTel() => AppNavigator.startEditMyInfo( + attr: EditAttr.telephone, + ); + + void editMobile() => AppNavigator.startEditMyInfo( + attr: EditAttr.mobile, + ); + + void editEmail() => AppNavigator.startEditMyInfo(attr: EditAttr.email, maxLength: 30); + + void openPhotoSheet() { + IMViews.openPhotoSheet( + onData: (path, url) async { + if (url != null) { + LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo(userID: OpenIM.iMManager.userID, faceURL: url) + .then((value) => imLogic.userInfo.update((val) { + val?.faceURL = url; + })), + ); + } + }, + quality: 15); + } + + void openDatePicker() { + var appLocale = Get.locale; + var isZh = appLocale!.languageCode.toLowerCase().contains("zh"); + DatePicker.showDatePicker( + Get.context!, + locale: isZh ? LocaleType.zh : LocaleType.en, + maxTime: DateTime.now(), + currentTime: DateTime.fromMillisecondsSinceEpoch(imLogic.userInfo.value.birth ?? 0), + theme: DatePickerTheme( + cancelStyle: Styles.ts_0C1C33_17sp, + doneStyle: Styles.ts_0089FF_17sp, + itemStyle: Styles.ts_0C1C33_17sp, + ), + onConfirm: (dateTime) { + _updateBirthday(dateTime.millisecondsSinceEpoch ~/ 1000); + }, + ); + } + + void selectGender() { + Get.bottomSheet( + BottomSheetView( + items: [ + SheetItem( + label: StrRes.man, + onTap: () => _updateGender(1), + ), + SheetItem( + label: StrRes.woman, + onTap: () => _updateGender(2), + ), + ], + ), + ); + } + + void _updateGender(int gender) { + LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo(userID: OpenIM.iMManager.userID, gender: gender) + .then((value) => imLogic.userInfo.update((val) { + val?.gender = gender; + })), + ); + } + + void _updateBirthday(int birthday) { + LoadingView.singleton.wrap( + asyncFunction: () => Apis.updateUserInfo( + userID: OpenIM.iMManager.userID, + birth: birthday * 1000, + ).then((value) => imLogic.userInfo.update((val) { + val?.birth = birthday * 1000; + })), + ); + } + + @override + void onReady() { + _queryMyFullIno(); + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } + + void _queryMyFullIno() async { + final info = await LoadingView.singleton.wrap( + asyncFunction: () => Apis.queryMyFullInfo(), + ); + if (null != info) { + imLogic.userInfo.update((val) { + val?.nickname = info.nickname; + val?.faceURL = info.faceURL; + val?.gender = info.gender; + val?.phoneNumber = info.phoneNumber; + val?.birth = info.birth; + val?.email = info.email; + }); + } + } + + static _trimNullStr(String? value) => IMUtils.emptyStrToNull(value); +} diff --git a/mobile-next/lib/pages/mine/my_info/my_info_view.dart b/mobile-next/lib/pages/mine/my_info/my_info_view.dart new file mode 100644 index 0000000..88d1c13 --- /dev/null +++ b/mobile-next/lib/pages/mine/my_info/my_info_view.dart @@ -0,0 +1,133 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; +import 'my_info_logic.dart'; + +class MyInfoPage extends StatelessWidget { + final logic = Get.find(); + final imLogic = Get.find(); + + MyInfoPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back( + title: StrRes.myInfo, + ), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => SingleChildScrollView( + child: Column( + children: [ + 10.verticalSpace, + _buildCornerBgView( + children: [ + _buildItemView( + label: StrRes.avatar, + isAvatar: true, + value: imLogic.userInfo.value.nickname, + url: imLogic.userInfo.value.faceURL, + onTap: logic.openPhotoSheet, + ), + _buildItemView( + label: StrRes.name, + value: imLogic.userInfo.value.nickname, + onTap: logic.editMyName, + ), + _buildItemView( + label: StrRes.gender, + value: imLogic.userInfo.value.isMale ? StrRes.man : StrRes.woman, + onTap: logic.selectGender, + ), + _buildItemView( + label: StrRes.birthDay, + value: DateUtil.formatDateMs( + imLogic.userInfo.value.birth ?? 0, + format: IMUtils.getTimeFormat1(), + ), + onTap: logic.openDatePicker, + ), + ], + ), + 10.verticalSpace, + _buildCornerBgView( + children: [ + _buildItemView( + label: StrRes.mobile, + value: imLogic.userInfo.value.phoneNumber, + showRightArrow: false, + ), + _buildItemView( + label: StrRes.email, + value: imLogic.userInfo.value.email, + onTap: logic.editEmail, + ), + ], + ), + ], + ), + )), + ); + } + + Widget _buildCornerBgView({required List children}) => Container( + padding: EdgeInsets.symmetric(horizontal: 16.w), + margin: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(6.r), + topRight: Radius.circular(6.r), + bottomLeft: Radius.circular(6.r), + bottomRight: Radius.circular(6.r), + ), + ), + child: Column(children: children), + ); + + Widget _buildItemView({ + required String label, + String? value, + String? url, + bool isAvatar = false, + bool showRightArrow = true, + Function()? onTap, + }) => + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: showRightArrow ? onTap : null, + child: SizedBox( + height: 46.h, + child: Row( + children: [ + label.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + if (isAvatar) + AvatarView( + width: 32.w, + height: 32.h, + url: url, + text: value, + textStyle: Styles.ts_FFFFFF_10sp, + ) + else + Expanded( + flex: 3, + child: (IMUtils.emptyStrToNull(value) ?? '').toText + ..style = Styles.ts_0C1C33_17sp + ..maxLines = 1 + ..overflow = TextOverflow.ellipsis + ..textAlign = TextAlign.right), + if (showRightArrow) + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ); +} diff --git a/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_binding.dart b/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_binding.dart new file mode 100644 index 0000000..c192969 --- /dev/null +++ b/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'my_qrcode_logic.dart'; + +class MyQrcodeBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => MyQrcodeLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_logic.dart b/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_logic.dart new file mode 100644 index 0000000..e3a67d9 --- /dev/null +++ b/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_logic.dart @@ -0,0 +1,12 @@ +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; + +class MyQrcodeLogic extends GetxController { + final imLogic = Get.find(); + + String buildQRContent() { + return '${Config.friendScheme}${imLogic.userInfo.value.userID}'; + } +} diff --git a/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_view.dart b/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_view.dart new file mode 100644 index 0000000..90325c5 --- /dev/null +++ b/mobile-next/lib/pages/mine/my_qrcode/my_qrcode_view.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +import 'my_qrcode_logic.dart'; + +class MyQrcodePage extends StatelessWidget { + final logic = Get.find(); + + MyQrcodePage({super.key}); + + @override + Widget build(BuildContext context) { + return Obx( + () => Scaffold( + appBar: TitleBar.back( + title: StrRes.qrcode, + ), + backgroundColor: Styles.c_F8F9FA, + body: Container( + alignment: Alignment.topCenter, + child: Container( + margin: EdgeInsets.only(top: 22.h), + padding: EdgeInsets.symmetric(horizontal: 30.w), + width: 331.w, + height: 460.h, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.circular(10.r), + boxShadow: [ + BoxShadow( + blurRadius: 7.r, + spreadRadius: 1.r, + color: Styles.c_000000.withOpacity(.08), + ), + ], + ), + child: Stack( + children: [ + Positioned( + top: 30.h, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AvatarView( + width: 48.w, + height: 48.h, + url: logic.imLogic.userInfo.value.faceURL, + text: logic.imLogic.userInfo.value.nickname, + textStyle: Styles.ts_FFFFFF_14sp, + ), + 12.horizontalSpace, + (logic.imLogic.userInfo.value.nickname ?? '').toText..style = Styles.ts_0C1C33_20sp, + ], + ), + ), + Positioned( + top: 140.h, + width: 272.w, + child: Container( + alignment: Alignment.center, + child: Column( + children: [ + StrRes.qrcodeHint.toText + ..style = Styles.ts_8E9AB0_15sp + ..textAlign = TextAlign.center, + 20.verticalSpace, + Container( + width: 180.w, + height: 180.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + border: Border.all(color: Styles.c_E8EAEF, width: 4.w), + ), + child: QrImageView( + data: logic.buildQRContent(), + size: 140.w, + backgroundColor: Styles.c_FFFFFF, + ), + ), + ], + ), + ), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_binding.dart b/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_binding.dart new file mode 100644 index 0000000..3c5483a --- /dev/null +++ b/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'unlock_setup_logic.dart'; + +class UnlockSetupBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => UnlockSetupLogic()); + } +} diff --git a/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_logic.dart b/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_logic.dart new file mode 100644 index 0000000..0b1ffbb --- /dev/null +++ b/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_logic.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screen_lock/flutter_screen_lock.dart'; +import 'package:get/get.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:openim_common/openim_common.dart'; + +class UnlockSetupLogic extends GetxController { + final passwordEnabled = false.obs; + final fingerprintEnabled = false.obs; + final gestureEnabled = false.obs; + final biometricsEnabled = false.obs; + final isSupportedBiometric = false.obs; + final canCheckBiometrics = false.obs; + String? lockScreenPwd; + final auth = LocalAuthentication(); + late List availableBiometrics; + + @override + void onInit() { + checkingSupported(); + lockScreenPwd = DataSp.getLockScreenPassword(); + biometricsEnabled.value = DataSp.isEnabledBiometric() == true; + passwordEnabled.value = lockScreenPwd != null; + super.onInit(); + } + + void checkingSupported() async { + isSupportedBiometric.value = await auth.isDeviceSupported(); + canCheckBiometrics.value = await auth.canCheckBiometrics; + availableBiometrics = await auth.getAvailableBiometrics(); + + if (availableBiometrics.isNotEmpty) {} + + if (availableBiometrics.contains(BiometricType.strong) || availableBiometrics.contains(BiometricType.weak)) {} + } + + void toggleBiometricLock() async { + if (biometricsEnabled.value) { + await DataSp.closeBiometric(); + biometricsEnabled.value = false; + } else { + final didAuthenticate = await IMUtils.checkingBiometric(auth); + if (didAuthenticate) { + await DataSp.openBiometric(); + biometricsEnabled.value = true; + } + } + } + + void togglePwdLock() { + if (passwordEnabled.value) { + closePwdLock(); + } else { + openPwdLock(); + } + } + + void closePwdLock() { + screenLock( + context: Get.context!, + correctString: lockScreenPwd!, + title: StrRes.plsEnterPwd.toText..style = Styles.ts_FFFFFF_17sp, + onUnlocked: () async { + await DataSp.clearLockScreenPassword(); + await DataSp.closeBiometric(); + passwordEnabled.value = false; + biometricsEnabled.value = false; + Get.back(); + }, + ); + } + + void openPwdLock() { + final controller = InputController(); + screenLockCreate( + context: Get.context!, + inputController: controller, + title: StrRes.plsEnterNewPwd.toText..style = Styles.ts_FFFFFF_17sp, + confirmTitle: StrRes.plsConfirmNewPwd.toText..style = Styles.ts_FFFFFF_17sp, + cancelButton: StrRes.cancel.toText..style = Styles.ts_FFFFFF_17sp, + onConfirmed: (matchedText) async { + lockScreenPwd = matchedText; + await DataSp.putLockScreenPassword(matchedText); + passwordEnabled.value = true; + Get.back(); + }, + footer: TextButton( + onPressed: () { + controller.unsetConfirmed(); + }, + child: StrRes.reset.toText..style = Styles.ts_0089FF_17sp, + ), + ); + } +} diff --git a/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_view.dart b/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_view.dart new file mode 100644 index 0000000..e3e3eb4 --- /dev/null +++ b/mobile-next/lib/pages/mine/unlock_setup/unlock_setup_view.dart @@ -0,0 +1,82 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'unlock_setup_logic.dart'; + +class UnlockSetupPage extends StatelessWidget { + final logic = Get.find(); + + UnlockSetupPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TitleBar.back(title: StrRes.unlockSettings), + backgroundColor: Styles.c_F8F9FA, + body: Obx(() => Column( + children: [ + 12.verticalSpace, + _buildItemView( + label: StrRes.password, + switchOn: logic.passwordEnabled.value, + onChanged: (_) => logic.togglePwdLock(), + isTopRadius: true, + ), + if (logic.passwordEnabled.value && + (logic.isSupportedBiometric.value && + logic.canCheckBiometrics.value)) + Container( + margin: EdgeInsets.only(left: 26.w, right: 10.w), + color: Styles.c_E8EAEF, + height: .5, + ), + if (logic.passwordEnabled.value && + (logic.isSupportedBiometric.value && + logic.canCheckBiometrics.value)) + _buildItemView( + label: StrRes.biometrics, + switchOn: logic.biometricsEnabled.value, + onChanged: (_) => logic.toggleBiometricLock(), + isBottomRadius: true, + ), + ], + )), + ); + } + + Widget _buildItemView({ + required String label, + bool isTopRadius = false, + bool isBottomRadius = false, + bool switchOn = false, + ValueChanged? onChanged, + }) => + Container( + height: 60.h, + padding: EdgeInsets.symmetric(horizontal: 16.w), + margin: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(isTopRadius ? 6.r : 0), + topRight: Radius.circular(isTopRadius ? 6.r : 0), + bottomRight: Radius.circular(isBottomRadius ? 6.r : 0), + bottomLeft: Radius.circular(isBottomRadius ? 6.r : 0), + ), + ), + child: Row( + children: [ + label.toText..style = Styles.ts_0C1C33_17sp, + const Spacer(), + CupertinoSwitch( + value: switchOn, + activeColor: Styles.c_0089FF, + onChanged: onChanged, + ), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/register/register_binding.dart b/mobile-next/lib/pages/register/register_binding.dart new file mode 100644 index 0000000..a715e84 --- /dev/null +++ b/mobile-next/lib/pages/register/register_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'register_logic.dart'; + +class RegisterBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => RegisterLogic()); + } +} diff --git a/mobile-next/lib/pages/register/register_logic.dart b/mobile-next/lib/pages/register/register_logic.dart new file mode 100644 index 0000000..15aa213 --- /dev/null +++ b/mobile-next/lib/pages/register/register_logic.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/login/login_logic.dart'; +import 'package:openim/routes/app_navigator.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../core/controller/app_controller.dart'; + +class RegisterLogic extends GetxController { + final appLogic = Get.find(); + final phoneCtrl = TextEditingController(); + final invitationCodeCtrl = TextEditingController(); + final areaCode = "+86".obs; + final enabled = false.obs; + final loginController = Get.find(); + String? get email => loginController.operateType == LoginType.email ? phoneCtrl.text.trim() : null; + String? get phone => + (loginController.operateType == LoginType.phone || loginController.operateType == LoginType.account) + ? phoneCtrl.text.trim() + : null; + + @override + void onClose() { + phoneCtrl.dispose(); + invitationCodeCtrl.dispose(); + super.onClose(); + } + + @override + void onInit() { + phoneCtrl.addListener(_onChanged); + invitationCodeCtrl.addListener(_onChanged); + super.onInit(); + } + + _onChanged() { + enabled.value = needInvitationCodeRegister + ? phoneCtrl.text.trim().isNotEmpty && invitationCodeCtrl.text.trim().isNotEmpty + : phoneCtrl.text.trim().isNotEmpty; + } + + bool get needInvitationCodeRegister => + /*null != appLogic.clientConfigMap['needInvitationCodeRegister'] && appLogic.clientConfigMap['needInvitationCodeRegister'] != '0'*/ false; + + String? get invitationCode => IMUtils.emptyStrToNull(invitationCodeCtrl.text); + + void openCountryCodePicker() async { + String? code = await IMViews.showCountryCodePicker(); + if (null != code) areaCode.value = code; + } + + Future requestVerificationCode() => Apis.requestVerificationCode( + areaCode: areaCode.value, + phoneNumber: phone, + email: email, + usedFor: 1, + invitationCode: invitationCode, + ); + + void next() async { + if ((loginController.operateType == LoginType.phone || loginController.operateType == LoginType.account) && + !IMUtils.isMobile(areaCode.value, phoneCtrl.text)) { + IMViews.showToast(StrRes.plsEnterRightPhone); + return; + } + + if (loginController.operateType == LoginType.email && !phoneCtrl.text.isEmail) { + IMViews.showToast(StrRes.plsEnterRightEmail); + return; + } + final success = await LoadingView.singleton.wrap( + asyncFunction: () => requestVerificationCode(), + ); + if (success) { + AppNavigator.startVerifyPhone( + areaCode: areaCode.value, + phoneNumber: phone, + email: email, + usedFor: 1, + invitationCode: invitationCode, + ); + } + } +} diff --git a/mobile-next/lib/pages/register/register_view.dart b/mobile-next/lib/pages/register/register_view.dart new file mode 100644 index 0000000..6cc4f04 --- /dev/null +++ b/mobile-next/lib/pages/register/register_view.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/login/login_logic.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +import '../../widgets/register_page_bg.dart'; +import 'register_logic.dart'; + +class RegisterPage extends StatelessWidget { + final logic = Get.find(); + + RegisterPage({super.key}); + + @override + Widget build(BuildContext context) => RegisterBgView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StrRes.newUserRegister.toText..style = Styles.ts_0089FF_22sp_semibold, + 29.verticalSpace, + Obx(() => InputBox.account( + label: logic.loginController.operateType.name, + hintText: logic.loginController.operateType.hintText, + code: logic.areaCode.value, + onAreaCode: logic.loginController.operateType == LoginType.email ? null : logic.openCountryCodePicker, + controller: logic.phoneCtrl, + )), + 16.verticalSpace, + if (logic.needInvitationCodeRegister) + InputBox.invitationCode( + label: StrRes.invitationCode, + hintText: sprintf( + StrRes.plsEnterInvitationCode, [logic.needInvitationCodeRegister ? '' : '(${StrRes.optional})']), + controller: logic.invitationCodeCtrl, + ), + 130.verticalSpace, + Obx(() => Button( + text: StrRes.nextStep, + enabled: logic.enabled.value, + onTap: logic.next, + )), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/register/set_password/set_password_binding.dart b/mobile-next/lib/pages/register/set_password/set_password_binding.dart new file mode 100644 index 0000000..8256f1a --- /dev/null +++ b/mobile-next/lib/pages/register/set_password/set_password_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'set_password_logic.dart'; + +class SetPasswordBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SetPasswordLogic()); + } +} diff --git a/mobile-next/lib/pages/register/set_password/set_password_logic.dart b/mobile-next/lib/pages/register/set_password/set_password_logic.dart new file mode 100644 index 0000000..615825b --- /dev/null +++ b/mobile-next/lib/pages/register/set_password/set_password_logic.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/login/login_logic.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; +import '../../../routes/app_navigator.dart'; + +class SetPasswordLogic extends GetxController { + final imLogic = Get.find(); + final nicknameCtrl = TextEditingController(); + final pwdCtrl = TextEditingController(); + final pwdAgainCtrl = TextEditingController(); + final enabled = false.obs; + String? phoneNumber; + String? email; + late String areaCode; + late int usedFor; + late String verificationCode; + String? invitationCode; + + @override + void onClose() { + nicknameCtrl.dispose(); + pwdCtrl.dispose(); + pwdAgainCtrl.dispose(); + super.onClose(); + } + + @override + void onInit() { + phoneNumber = Get.arguments['phoneNumber']; + email = Get.arguments['email']; + areaCode = Get.arguments['areaCode']; + usedFor = Get.arguments['usedFor']; + verificationCode = Get.arguments['verificationCode']; + invitationCode = Get.arguments['invitationCode']; + nicknameCtrl.addListener(_onChanged); + pwdCtrl.addListener(_onChanged); + pwdAgainCtrl.addListener(_onChanged); + super.onInit(); + } + + _onChanged() { + enabled.value = + nicknameCtrl.text.trim().isNotEmpty && pwdCtrl.text.trim().isNotEmpty && pwdAgainCtrl.text.trim().isNotEmpty; + } + + bool _checkingInput() { + if (nicknameCtrl.text.trim().isEmpty) { + IMViews.showToast(StrRes.plsEnterYourNickname); + return false; + } + if (!IMUtils.isValidPassword(pwdCtrl.text)) { + IMViews.showToast(StrRes.wrongPasswordFormat); + return false; + } else if (pwdCtrl.text != pwdAgainCtrl.text) { + IMViews.showToast(StrRes.twicePwdNoSame); + return false; + } + return true; + } + + void nextStep() { + if (_checkingInput()) { + register(); + } + } + + void register() async { + final operateType = Get.find().operateType; + await LoadingView.singleton.wrap(asyncFunction: () async { + final data = await Apis.register( + nickname: nicknameCtrl.text.trim(), + areaCode: areaCode, + phoneNumber: operateType == LoginType.phone ? phoneNumber : null, + email: email, + account: operateType == LoginType.account ? phoneNumber : null, + password: pwdCtrl.text, + verificationCode: verificationCode, + invitationCode: invitationCode, + ); + if (null == IMUtils.emptyStrToNull(data.imToken) || null == IMUtils.emptyStrToNull(data.chatToken)) { + AppNavigator.startLogin(); + return; + } + final account = {"areaCode": areaCode, "phoneNumber": phoneNumber, 'email': email}; + await DataSp.putLoginCertificate(data); + await DataSp.putLoginAccount(account); + DataSp.putLoginType(email != null ? 1 : 0); + await imLogic.login(data.userID, data.imToken); + Logger.print('---------im login success-------'); + PushController.login(data.userID); + Logger.print('---------jpush login success----'); + }); + AppNavigator.startMain(); + } +} diff --git a/mobile-next/lib/pages/register/set_password/set_password_view.dart b/mobile-next/lib/pages/register/set_password/set_password_view.dart new file mode 100644 index 0000000..8a1e892 --- /dev/null +++ b/mobile-next/lib/pages/register/set_password/set_password_view.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../widgets/register_page_bg.dart'; +import 'set_password_logic.dart'; + +class SetPasswordPage extends StatelessWidget { + final logic = Get.find(); + + SetPasswordPage({super.key}); + + @override + Widget build(BuildContext context) => RegisterBgView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StrRes.setInfo.toText..style = Styles.ts_0089FF_22sp_semibold, + 29.verticalSpace, + InputBox( + label: StrRes.nickname, + hintText: StrRes.plsEnterYourNickname, + controller: logic.nicknameCtrl, + ), + 17.verticalSpace, + InputBox.password( + label: StrRes.password, + hintText: StrRes.plsEnterPassword, + controller: logic.pwdCtrl, + formatHintText: StrRes.loginPwdFormat, + inputFormatters: [IMUtils.getPasswordFormatter()], + ), + 17.verticalSpace, + InputBox.password( + label: StrRes.confirmPassword, + hintText: StrRes.plsConfirmPasswordAgain, + controller: logic.pwdAgainCtrl, + inputFormatters: [IMUtils.getPasswordFormatter()], + ), + 129.verticalSpace, + Obx(() => Button( + text: StrRes.registerNow, + enabled: logic.enabled.value, + onTap: logic.nextStep, + )), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/register/set_self_info/set_self_info_binding.dart b/mobile-next/lib/pages/register/set_self_info/set_self_info_binding.dart new file mode 100644 index 0000000..fff18dc --- /dev/null +++ b/mobile-next/lib/pages/register/set_self_info/set_self_info_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'set_self_info_logic.dart'; + +class SetSelfInfoBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SetSelfInfoLogic()); + } +} diff --git a/mobile-next/lib/pages/register/set_self_info/set_self_info_logic.dart b/mobile-next/lib/pages/register/set_self_info/set_self_info_logic.dart new file mode 100644 index 0000000..05b27ec --- /dev/null +++ b/mobile-next/lib/pages/register/set_self_info/set_self_info_logic.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../../core/controller/im_controller.dart'; + +class SetSelfInfoLogic extends GetxController { + final imLogic = Get.find(); + final nicknameCtrl = TextEditingController(); + late String phoneNumber; + late String areaCode; + late String password; + late String verificationCode; + String? invitationCode; + final nickname = ''.obs; + final faceURL = "".obs; + final gender = 1.obs; + + @override + void onClose() { + nicknameCtrl.dispose(); + super.onClose(); + } + + @override + void onInit() { + phoneNumber = Get.arguments['phoneNumber']; + areaCode = Get.arguments['areaCode']; + password = Get.arguments['password']; + verificationCode = Get.arguments['verificationCode']; + invitationCode = Get.arguments['invitationCode']; + nicknameCtrl.addListener(_onChanged); + super.onInit(); + } + + _onChanged() { + nickname.value = nicknameCtrl.text.trim(); + } + + void openPhotoSheet() { + IMViews.openPhotoSheet(onData: (path, url) async { + if (url != null) { + faceURL.value = url; + } + }); + } +} diff --git a/mobile-next/lib/pages/register/set_self_info/set_self_info_view.dart b/mobile-next/lib/pages/register/set_self_info/set_self_info_view.dart new file mode 100644 index 0000000..58a8f18 --- /dev/null +++ b/mobile-next/lib/pages/register/set_self_info/set_self_info_view.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim/widgets/register_page_bg.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'set_self_info_logic.dart'; + +class SetSelfInfoPage extends StatelessWidget { + final logic = Get.find(); + + SetSelfInfoPage({super.key}); + + @override + Widget build(BuildContext context) => RegisterBgView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StrRes.plsCompleteInfo.toText..style = Styles.ts_0089FF_22sp_semibold, + _buildInputItemView( + label: StrRes.nickname, + controller: logic.nicknameCtrl, + ), + Obx(() => _buildItemView( + label: StrRes.avatar, + isAvatar: true, + nickname: logic.nickname.value, + onTap: logic.openPhotoSheet, + )), + _buildItemView(label: StrRes.gender), + ], + ), + ); + + Widget _buildItemView({ + required String label, + String? value, + bool isAvatar = false, + String? faceURL, + String? nickname, + Function()? onTap, + }) => + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: onTap, + child: Container( + height: 72.h, + padding: EdgeInsets.only(bottom: 6.h), + alignment: Alignment.bottomCenter, + decoration: BoxDecoration( + border: BorderDirectional( + bottom: BorderSide( + color: Styles.c_E8EAEF, + width: 1, + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + label.toText..style = Styles.ts_8E9AB0_17sp, + const Spacer(), + if (null != value && !isAvatar) value.toText..style = Styles.ts_0C1C33_17sp, + if (isAvatar) + AvatarView( + width: 42.w, + height: 42.h, + text: nickname, + url: faceURL, + ), + 4.horizontalSpace, + ImageRes.rightArrow.toImage + ..width = 24.w + ..height = 24.h, + ], + ), + ), + ); + + Widget _buildInputItemView({ + required String label, + TextEditingController? controller, + }) => + Container( + height: 72.h, + padding: EdgeInsets.only(bottom: 6.h), + alignment: Alignment.bottomCenter, + decoration: BoxDecoration( + border: BorderDirectional( + bottom: BorderSide( + color: Styles.c_E8EAEF, + width: 1, + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + label.toText..style = Styles.ts_8E9AB0_17sp, + Expanded( + child: SizedBox( + height: 36.h, + child: TextField( + controller: controller, + textAlign: TextAlign.end, + maxLength: 16, + style: Styles.ts_0C1C33_17sp, + maxLines: 1, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + counter: SizedBox(), + ), + ), + ), + ), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/register/verify_phone/verify_phone_binding.dart b/mobile-next/lib/pages/register/verify_phone/verify_phone_binding.dart new file mode 100644 index 0000000..b4e499a --- /dev/null +++ b/mobile-next/lib/pages/register/verify_phone/verify_phone_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'verify_phone_logic.dart'; + +class VerifyPhoneBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => VerifyPhoneLogic()); + } +} diff --git a/mobile-next/lib/pages/register/verify_phone/verify_phone_logic.dart b/mobile-next/lib/pages/register/verify_phone/verify_phone_logic.dart new file mode 100644 index 0000000..ced6641 --- /dev/null +++ b/mobile-next/lib/pages/register/verify_phone/verify_phone_logic.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; + +import '../../../routes/app_navigator.dart'; + +class VerifyPhoneLogic extends GetxController { + final codeErrorCtrl = StreamController(); + final codeEditCtrl = TextEditingController(); + final enabled = false.obs; + String? phoneNumber; + String? email; + late String areaCode; + late int usedFor; + String? invitationCode; + + String get account => phoneNumber?.isNotEmpty == true ? (areaCode + phoneNumber!) : email!; + @override + void onInit() { + phoneNumber = Get.arguments['phoneNumber']; + email = Get.arguments['email']; + areaCode = Get.arguments['areaCode']; + usedFor = Get.arguments['usedFor']; + invitationCode = Get.arguments['invitationCode']; + codeEditCtrl.addListener(_onChanged); + super.onInit(); + } + + @override + void onClose() { + codeErrorCtrl.close(); + super.onClose(); + } + + void _onChanged() { + enabled.value = codeEditCtrl.text.length == 6; + } + + void shake() { + codeErrorCtrl.add(ErrorAnimationType.shake); + } + + Future requestVerificationCode() => LoadingView.singleton.wrap( + asyncFunction: () => Apis.requestVerificationCode( + areaCode: areaCode, + phoneNumber: phoneNumber, + email: email, + usedFor: usedFor, + invitationCode: invitationCode, + )); + + Future checkVerificationCode(String verificationCode) => Apis.checkVerificationCode( + areaCode: areaCode, + phoneNumber: phoneNumber, + email: email, + verificationCode: verificationCode, + usedFor: usedFor, + invitationCode: invitationCode, + ); + + void completed(value) async { + try { + await LoadingView.singleton.wrap( + asyncFunction: () => checkVerificationCode(value), + ); + AppNavigator.startSetPassword( + areaCode: areaCode, + phoneNumber: phoneNumber, + email: email, + verificationCode: value, + usedFor: usedFor, + invitationCode: invitationCode, + ); + } catch (e, s) { + shake(); + } + } +} diff --git a/mobile-next/lib/pages/register/verify_phone/verify_phone_view.dart b/mobile-next/lib/pages/register/verify_phone/verify_phone_view.dart new file mode 100644 index 0000000..8eb6bc3 --- /dev/null +++ b/mobile-next/lib/pages/register/verify_phone/verify_phone_view.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; +import 'package:sprintf/sprintf.dart'; + +import '../../../widgets/register_page_bg.dart'; +import 'verify_phone_logic.dart'; + +class VerifyPhonePage extends StatelessWidget { + final logic = Get.find(); + + VerifyPhonePage({super.key}); + + @override + Widget build(BuildContext context) => RegisterBgView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + sprintf(StrRes.enterVerificationCode, [logic.email?.isNotEmpty == true ? StrRes.email : StrRes.phoneNumber]) + .toText + ..style = Styles.ts_0089FF_22sp_semibold, + 10.verticalSpace, + '${logic.account} ${sprintf(StrRes.defaultVerificationCode, ['666666'])}'.toText + ..style = Styles.ts_8E9AB0_12sp, + 35.verticalSpace, + PinCodeTextField( + appContext: context, + controller: logic.codeEditCtrl, + autoFocus: true, + textStyle: Styles.ts_0C1C33_20sp_semibold, + length: 6, + obscureText: false, + blinkWhenObscuring: true, + animationType: AnimationType.fade, + validator: (v) { + return null; + }, + pinTheme: PinTheme( + shape: PinCodeFieldShape.box, + activeColor: Styles.c_0089FF, + selectedColor: Styles.c_0089FF, + inactiveColor: Styles.c_E8EAEF, + disabledColor: Styles.c_E8EAEF, + activeFillColor: Styles.c_E8EAEF, + selectedFillColor: Styles.c_E8EAEF, + inactiveFillColor: Styles.c_E8EAEF, + borderRadius: BorderRadius.circular(8.r), + borderWidth: 1, + fieldHeight: 42.w, + fieldWidth: 42.h, + ), + cursorColor: Colors.black, + animationDuration: 300.milliseconds, + errorAnimationController: logic.codeErrorCtrl, + keyboardType: TextInputType.number, + onCompleted: (v) { + logic.completed(v); + }, + onSubmitted: (v) { + logic.completed(v); + }, + onChanged: (v) {}, + beforeTextPaste: (text) { + return true; + }, + ), + VerifyCodeSendButton( + sec: 300, + onTapCallback: () => logic.requestVerificationCode(), + ), + 170.verticalSpace, + Obx(() => Button( + text: StrRes.nextStep, + enabled: logic.enabled.value, + onTap: () => logic.completed(logic.codeEditCtrl.text), + )), + ], + ), + ); +} diff --git a/mobile-next/lib/pages/splash/splash_binding.dart b/mobile-next/lib/pages/splash/splash_binding.dart new file mode 100644 index 0000000..6df202c --- /dev/null +++ b/mobile-next/lib/pages/splash/splash_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'splash_logic.dart'; + +class SplashBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SplashLogic()); + } +} diff --git a/mobile-next/lib/pages/splash/splash_logic.dart b/mobile-next/lib/pages/splash/splash_logic.dart new file mode 100644 index 0000000..afc749c --- /dev/null +++ b/mobile-next/lib/pages/splash/splash_logic.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim/pages/conversation/conversation_logic.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../../core/controller/im_controller.dart'; +import '../../routes/app_navigator.dart'; + +class SplashLogic extends GetxController { + final imLogic = Get.find(); + final pushLogic = Get.find(); + + String? get userID => DataSp.userID; + + String? get token => DataSp.imToken; + + late StreamSubscription initializedSub; + + @override + void onInit() { + initializedSub = imLogic.initializedSubject.listen((value) { + Logger.print('---------------------initialized---------------------'); + if (null != userID && null != token) { + _login(); + } else { + AppNavigator.startLogin(); + } + }); + super.onInit(); + } + + _login() async { + try { + Logger.print('---------login---------- userID: $userID, token: $token'); + await imLogic.login(userID!, token!); + Logger.print('---------im login success-------'); + PushController.login( + userID!, + onTokenRefresh: (token) { + OpenIM.iMManager.updateFcmToken( + fcmToken: token, expireTime: DateTime.now().add(Duration(days: 90)).millisecondsSinceEpoch); + }, + ); + Logger.print('---------push login success----'); + final result = await ConversationLogic.getConversationFirstPage(); + + AppNavigator.startSplashToMain(isAutoLogin: true, conversations: result); + } catch (e, s) { + IMViews.showToast('$e $s'); + await DataSp.removeLoginCertificate(); + AppNavigator.startLogin(); + } + } + + @override + void onClose() { + initializedSub.cancel(); + super.onClose(); + } +} diff --git a/mobile-next/lib/pages/splash/splash_view.dart b/mobile-next/lib/pages/splash/splash_view.dart new file mode 100644 index 0000000..9ae49a3 --- /dev/null +++ b/mobile-next/lib/pages/splash/splash_view.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import 'splash_logic.dart'; + +class SplashPage extends StatelessWidget { + final logic = Get.find(); + + SplashPage({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Styles.c_0089FF_opacity10, Styles.c_FFFFFF_opacity0], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + bottom: 130.h, + child: ImageRes.splashLogo.toImage + ..width = 55.61.w + ..height = 78.91.h, + ), + ], + ), + ); + } +} diff --git a/mobile-next/lib/routes/app_navigator.dart b/mobile-next/lib/routes/app_navigator.dart new file mode 100644 index 0000000..c9fb0e2 --- /dev/null +++ b/mobile-next/lib/routes/app_navigator.dart @@ -0,0 +1,382 @@ +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../pages/chat/group_setup/edit_name/edit_name_logic.dart'; +import '../pages/chat/group_setup/group_member_list/group_member_list_logic.dart'; +import '../pages/contacts/add_by_search/add_by_search_logic.dart'; +import '../pages/contacts/group_profile_panel/group_profile_panel_logic.dart'; +import '../pages/contacts/select_contacts/select_contacts_logic.dart'; +import '../pages/mine/edit_my_info/edit_my_info_logic.dart'; +import 'app_pages.dart'; + +class AppNavigator { + AppNavigator._(); + + static void startLogin() { + Get.offAllNamed(AppRoutes.login); + } + + static void startBackLogin() { + Get.until((route) => Get.currentRoute == AppRoutes.login); + } + + static void startMain({bool isAutoLogin = false, List? conversations}) { + Get.offAllNamed( + AppRoutes.home, + arguments: {'isAutoLogin': isAutoLogin, 'conversations': conversations}, + ); + } + + static void startSplashToMain({bool isAutoLogin = false, List? conversations}) { + Get.offAndToNamed( + AppRoutes.home, + arguments: {'isAutoLogin': isAutoLogin, 'conversations': conversations}, + ); + } + + static void startBackMain() { + Get.until((route) => Get.currentRoute == AppRoutes.home); + } + + static startOANtfList({required ConversationInfo info}) { + return Get.toNamed(AppRoutes.oaNotificationList, arguments: info); + } + + static Future? startChat({ + required ConversationInfo conversationInfo, + bool offUntilHome = true, + String? draftText, + Message? searchMessage, + }) async { + GetTags.createChatTag(); + + final arguments = { + 'draftText': draftText, + 'conversationInfo': conversationInfo, + 'searchMessage': searchMessage, + }; + + return offUntilHome + ? Get.offNamedUntil( + AppRoutes.chat, + (route) => route.settings.name == AppRoutes.home, + arguments: arguments, + ) + : Get.toNamed( + AppRoutes.chat, + arguments: arguments, + preventDuplicates: false, + ); + } + + static startMyQrcode() => Get.toNamed(AppRoutes.myQrcode); + + static startFavoriteMange() => Get.toNamed(AppRoutes.favoriteManage); + + static startAddContactsMethod() => Get.toNamed(AppRoutes.addContactsMethod); + + static startScan() => Permissions.camera(() => Get.to( + () => const QrcodeView(), + transition: Transition.cupertino, + popGesture: true, + )); + + static startAddContactsBySearch({required SearchType searchType}) => Get.toNamed( + AppRoutes.addContactsBySearch, + arguments: {"searchType": searchType}, + ); + + static startUserProfilePane({ + required String userID, + String? groupID, + String? nickname, + String? faceURL, + bool offAllWhenDelFriend = false, + bool offAndToNamed = false, + bool forceCanAdd = false, + }) { + GetTags.createUserProfileTag(); + + final arguments = { + 'groupID': groupID, + 'userID': userID, + 'nickname': nickname, + 'faceURL': faceURL, + 'offAllWhenDelFriend': offAllWhenDelFriend, + 'forceCanAdd': forceCanAdd, + }; + + return offAndToNamed + ? Get.offAndToNamed(AppRoutes.userProfilePanel, arguments: arguments) + : Get.toNamed( + AppRoutes.userProfilePanel, + arguments: arguments, + preventDuplicates: false, + ); + } + + static startPersonalInfo({ + required String userID, + }) => + Get.toNamed(AppRoutes.personalInfo, arguments: { + 'userID': userID, + }); + + static startFriendSetup({ + required String userID, + }) => + Get.toNamed(AppRoutes.friendSetup, arguments: { + 'userID': userID, + }); + + static startSetFriendRemark() => Get.toNamed(AppRoutes.setFriendRemark, arguments: {}); + + static startSendVerificationApplication({ + String? userID, + String? groupID, + JoinGroupMethod? joinGroupMethod, + }) => + Get.toNamed(AppRoutes.sendVerificationApplication, arguments: { + 'joinGroupMethod': joinGroupMethod, + 'userID': userID, + 'groupID': groupID, + }); + + static startGroupProfilePanel({ + required String groupID, + required JoinGroupMethod joinGroupMethod, + bool offAndToNamed = false, + }) => + offAndToNamed + ? Get.offAndToNamed(AppRoutes.groupProfilePanel, arguments: { + 'joinGroupMethod': joinGroupMethod, + 'groupID': groupID, + }) + : Get.toNamed(AppRoutes.groupProfilePanel, arguments: { + 'joinGroupMethod': joinGroupMethod, + 'groupID': groupID, + }); + + static startMyInfo() => Get.toNamed(AppRoutes.myInfo); + + static startEditMyInfo({EditAttr attr = EditAttr.nickname, int? maxLength}) => + Get.toNamed(AppRoutes.editMyInfo, arguments: {'editAttr': attr, 'maxLength': maxLength}); + + static startAccountSetup() => Get.toNamed(AppRoutes.accountSetup); + + static startBlacklist() => Get.toNamed(AppRoutes.blacklist); + + static startLanguageSetup() => Get.toNamed(AppRoutes.languageSetup); + + static startUnlockSetup() => Get.toNamed(AppRoutes.unlockSetup); + + static startChangePassword() => Get.toNamed(AppRoutes.changePassword); + + static startAboutUs() => Get.toNamed(AppRoutes.aboutUs); + + static startChatSetup({ + required ConversationInfo conversationInfo, + }) => + Get.toNamed(AppRoutes.chatSetup, arguments: { + 'conversationInfo': conversationInfo, + }); + + static startGroupChatSetup({ + required ConversationInfo conversationInfo, + }) => + Get.toNamed(AppRoutes.groupChatSetup, arguments: { + 'conversationInfo': conversationInfo, + }); + + static startGroupManage({ + required GroupInfo groupInfo, + }) => + Get.toNamed(AppRoutes.groupManage, arguments: { + 'groupInfo': groupInfo, + }); + + static startEditGroupName({required EditNameType type, String? faceUrl}) => + Get.toNamed(AppRoutes.editGroupName, arguments: { + 'type': type, + 'faceUrl': faceUrl, + }); + + static Future? startGroupMemberList({ + required GroupInfo groupInfo, + GroupMemberOpType opType = GroupMemberOpType.view, + }) => + Get.toNamed(AppRoutes.groupMemberList, preventDuplicates: false, arguments: { + 'groupInfo': groupInfo, + 'opType': opType, + }); + + static startSearchGroupMember({ + required GroupInfo groupInfo, + GroupMemberOpType opType = GroupMemberOpType.view, + }) => + Get.toNamed(AppRoutes.searchGroupMember, arguments: { + 'groupInfo': groupInfo, + 'opType': opType, + }); + + static startGroupQrcode() => Get.toNamed(AppRoutes.groupQrcode); + + static startFriendRequests() => Get.toNamed(AppRoutes.friendRequests); + + static startProcessFriendRequests({ + required FriendApplicationInfo applicationInfo, + }) => + Get.toNamed(AppRoutes.processFriendRequests, arguments: { + 'applicationInfo': applicationInfo, + }); + + static startGroupRequests() => Get.toNamed(AppRoutes.groupRequests); + + static startProcessGroupRequests({ + required GroupApplicationInfo applicationInfo, + }) => + Get.toNamed(AppRoutes.processGroupRequests, arguments: { + 'applicationInfo': applicationInfo, + }); + + static startFriendList() => Get.toNamed(AppRoutes.friendList); + + static startGroupList() => Get.toNamed(AppRoutes.groupList); + + static startSearchFriend() => Get.toNamed(AppRoutes.searchFriend); + + static startSearchGroup() => Get.toNamed(AppRoutes.searchGroup); + + static startSelectContacts({ + required SelAction action, + List? defaultCheckedIDList, + List? checkedList, + List? excludeIDList, + bool openSelectedSheet = false, + String? groupID, + String? ex, + }) => + Get.toNamed(AppRoutes.selectContacts, arguments: { + 'action': action, + 'defaultCheckedIDList': defaultCheckedIDList, + 'checkedList': IMUtils.convertCheckedListToMap(checkedList), + 'excludeIDList': excludeIDList, + 'openSelectedSheet': openSelectedSheet, + 'groupID': groupID, + 'ex': ex, + }); + + static startSelectContactsFromFriends() => Get.toNamed(AppRoutes.selectContactsFromFriends); + + static startSelectContactsFromGroup() => Get.toNamed(AppRoutes.selectContactsFromGroup); + + static startSelectContactsFromSearchFriends() => Get.toNamed(AppRoutes.selectContactsFromSearchFriends); + + static startSelectContactsFromSearchGroup() => Get.toNamed(AppRoutes.selectContactsFromSearchGroup); + + static startSelectContactsFromSearch() => Get.toNamed(AppRoutes.selectContactsFromSearch); + + static startCreateGroup({ + List defaultCheckedList = const [], + }) async { + final result = await startSelectContacts( + action: SelAction.crateGroup, + defaultCheckedIDList: defaultCheckedList.map((e) => e.userID!).toList(), + ); + final list = IMUtils.convertSelectContactsResultToUserInfo(result); + if (list is List) { + return Get.toNamed( + AppRoutes.createGroup, + arguments: {'checkedList': list, 'defaultCheckedList': defaultCheckedList}, + ); + } + return null; + } + + static startGlobalSearch() => Get.toNamed(AppRoutes.globalSearch); + + static startExpandChatHistory({ + required SearchResultItems searchResultItems, + required String defaultSearchKey, + }) => + Get.toNamed(AppRoutes.expandChatHistory, arguments: { + 'searchResultItems': searchResultItems, + 'defaultSearchKey': defaultSearchKey, + }); + + static startRegister() => Get.toNamed(AppRoutes.register); + + static void startVerifyPhone({ + String? phoneNumber, + String? email, + required String areaCode, + required int usedFor, + String? invitationCode, + }) => + Get.toNamed(AppRoutes.verifyPhone, arguments: { + 'phoneNumber': phoneNumber, + 'email': email, + 'areaCode': areaCode, + 'usedFor': usedFor, + 'invitationCode': invitationCode, + }); + + static void startSetPassword({ + String? phoneNumber, + String? email, + required String areaCode, + required int usedFor, + required String verificationCode, + String? invitationCode, + }) => + Get.toNamed(AppRoutes.setPassword, arguments: { + 'phoneNumber': phoneNumber, + 'email': email, + 'areaCode': areaCode, + 'usedFor': usedFor, + 'verificationCode': verificationCode, + 'invitationCode': invitationCode + }); + + static void startSetSelfInfo({ + String? phoneNumber, + String? email, + String? account, + required String areaCode, + required password, + required int usedFor, + required String verificationCode, + String? invitationCode, + }) => + Get.toNamed(AppRoutes.setSelfInfo, arguments: { + 'phoneNumber': phoneNumber, + 'email': email, + 'account': account, + 'areaCode': areaCode, + 'password': password, + 'usedFor': usedFor, + 'verificationCode': verificationCode, + 'invitationCode': invitationCode + }); + + static startForgetPassword() => Get.toNamed(AppRoutes.forgetPassword); + + static void startResetPassword({ + String? phoneNumber, + String? email, + String? account, + required String areaCode, + required String verificationCode, + }) => + Get.toNamed(AppRoutes.resetPassword, arguments: { + 'phoneNumber': phoneNumber, + 'email': email, + 'account': account, + 'areaCode': areaCode, + 'usedFor': 2, + 'verificationCode': verificationCode, + }); + + static startSelectContactsFromTag() => Get.toNamed(AppRoutes.selectContactsFromTag); +} diff --git a/mobile-next/lib/routes/app_pages.dart b/mobile-next/lib/routes/app_pages.dart new file mode 100644 index 0000000..353c83f --- /dev/null +++ b/mobile-next/lib/routes/app_pages.dart @@ -0,0 +1,391 @@ +import 'package:get/get.dart'; + +import '../pages/chat/chat_binding.dart'; +import '../pages/chat/chat_setup/chat_setup_binding.dart'; +import '../pages/chat/chat_setup/chat_setup_view.dart'; +import '../pages/chat/chat_setup/favorite_manage/favorite_manage_binding.dart'; +import '../pages/chat/chat_setup/favorite_manage/favorite_manage_view.dart'; +import '../pages/chat/chat_view.dart'; +import '../pages/chat/group_setup/edit_name/edit_name_binding.dart'; +import '../pages/chat/group_setup/edit_name/edit_name_view.dart'; +import '../pages/chat/group_setup/group_manage/group_manage_binding.dart'; +import '../pages/chat/group_setup/group_manage/group_manage_view.dart'; +import '../pages/chat/group_setup/group_member_list/group_member_list_binding.dart'; +import '../pages/chat/group_setup/group_member_list/group_member_list_view.dart'; +import '../pages/chat/group_setup/group_member_list/search_group_member/search_group_member_binding.dart'; +import '../pages/chat/group_setup/group_member_list/search_group_member/search_group_member_view.dart'; +import '../pages/chat/group_setup/group_qrcode/group_qrcode_binding.dart'; +import '../pages/chat/group_setup/group_qrcode/group_qrcode_view.dart'; +import '../pages/chat/group_setup/group_setup_binding.dart'; +import '../pages/chat/group_setup/group_setup_view.dart'; +import '../pages/chat/oa_notification/oa_notification_binding.dart'; +import '../pages/chat/oa_notification/oa_notification_view.dart'; +import '../pages/contacts/add_by_search/add_by_search_binding.dart'; +import '../pages/contacts/add_by_search/add_by_search_view.dart'; +import '../pages/contacts/add_method/add_method_binding.dart'; +import '../pages/contacts/add_method/add_method_view.dart'; +import '../pages/contacts/create_group/create_group_binding.dart'; +import '../pages/contacts/create_group/create_group_view.dart'; +import '../pages/contacts/friend_list/friend_list_binding.dart'; +import '../pages/contacts/friend_list/friend_list_view.dart'; +import '../pages/contacts/friend_list/search_friend/search_friend_binding.dart'; +import '../pages/contacts/friend_list/search_friend/search_friend_view.dart'; +import '../pages/contacts/friend_requests/friend_requests_binding.dart'; +import '../pages/contacts/friend_requests/friend_requests_view.dart'; +import '../pages/contacts/friend_requests/process_friend_requests/process_friend_requests_binding.dart'; +import '../pages/contacts/friend_requests/process_friend_requests/process_friend_requests_view.dart'; +import '../pages/contacts/group_list/group_list_binding.dart'; +import '../pages/contacts/group_list/group_list_view.dart'; +import '../pages/contacts/group_list/search_group/search_group_binding.dart'; +import '../pages/contacts/group_list/search_group/search_group_view.dart'; +import '../pages/contacts/group_profile_panel/group_profile_panel_binding.dart'; +import '../pages/contacts/group_profile_panel/group_profile_panel_view.dart'; +import '../pages/contacts/group_requests/group_requests_binding.dart'; +import '../pages/contacts/group_requests/group_requests_view.dart'; +import '../pages/contacts/group_requests/process_group_requests/process_group_requests_binding.dart'; +import '../pages/contacts/group_requests/process_group_requests/process_group_requests_view.dart'; +import '../pages/contacts/select_contacts/friend_list/friend_list_binding.dart'; +import '../pages/contacts/select_contacts/friend_list/friend_list_view.dart'; +import '../pages/contacts/select_contacts/friend_list/search_friend/search_friend_binding.dart'; +import '../pages/contacts/select_contacts/friend_list/search_friend/search_friend_view.dart'; +import '../pages/contacts/select_contacts/group_list/group_list_binding.dart'; +import '../pages/contacts/select_contacts/group_list/group_list_view.dart'; +import '../pages/contacts/select_contacts/group_list/search_group/search_group_binding.dart'; +import '../pages/contacts/select_contacts/group_list/search_group/search_group_view.dart'; +import '../pages/contacts/select_contacts/search_contacts/search_contacts_binding.dart'; +import '../pages/contacts/select_contacts/search_contacts/search_contacts_view.dart'; +import '../pages/contacts/select_contacts/select_contacts_binding.dart'; +import '../pages/contacts/select_contacts/select_contacts_view.dart'; +import '../pages/contacts/send_verification_application/send_verification_application_binding.dart'; +import '../pages/contacts/send_verification_application/send_verification_application_view.dart'; +import '../pages/contacts/user_profile_panel/friend_setup/friend_setup_binding.dart'; +import '../pages/contacts/user_profile_panel/friend_setup/friend_setup_view.dart'; +import '../pages/contacts/user_profile_panel/personal_info/personal_info_binding.dart'; +import '../pages/contacts/user_profile_panel/personal_info/personal_info_view.dart'; +import '../pages/contacts/user_profile_panel/set_remark/set_remark_binding.dart'; +import '../pages/contacts/user_profile_panel/set_remark/set_remark_view.dart'; +import '../pages/contacts/user_profile_panel/user_profile _panel_binding.dart'; +import '../pages/contacts/user_profile_panel/user_profile _panel_view.dart'; +import '../pages/forget_password/forget_password_binding.dart'; +import '../pages/forget_password/forget_password_view.dart'; +import '../pages/forget_password/reset_password/reset_password_binding.dart'; +import '../pages/forget_password/reset_password/reset_password_view.dart'; +import '../pages/global_search/global_search_binding.dart'; +import '../pages/global_search/global_search_view.dart'; +import '../pages/home/home_binding.dart'; +import '../pages/home/home_view.dart'; +import '../pages/login/login_binding.dart'; +import '../pages/login/login_view.dart'; +import '../pages/mine/about_us/about_us_binding.dart'; +import '../pages/mine/about_us/about_us_view.dart'; +import '../pages/mine/account_setup/account_setup_binding.dart'; +import '../pages/mine/account_setup/account_setup_view.dart'; +import '../pages/mine/blacklist/blacklist_binding.dart'; +import '../pages/mine/blacklist/blacklist_view.dart'; +import '../pages/mine/change_pwd/change_pwd_binding.dart'; +import '../pages/mine/change_pwd/change_pwd_view.dart'; +import '../pages/mine/edit_my_info/edit_my_info_binding.dart'; +import '../pages/mine/edit_my_info/edit_my_info_view.dart'; +import '../pages/mine/language_setup/language_setup_binding.dart'; +import '../pages/mine/language_setup/language_setup_view.dart'; +import '../pages/mine/my_info/my_info_binding.dart'; +import '../pages/mine/my_info/my_info_view.dart'; +import '../pages/mine/my_qrcode/my_qrcode_binding.dart'; +import '../pages/mine/my_qrcode/my_qrcode_view.dart'; +import '../pages/mine/unlock_setup/unlock_setup_binding.dart'; +import '../pages/mine/unlock_setup/unlock_setup_view.dart'; +import '../pages/register/register_binding.dart'; +import '../pages/register/register_view.dart'; +import '../pages/register/set_password/set_password_binding.dart'; +import '../pages/register/set_password/set_password_view.dart'; +import '../pages/register/set_self_info/set_self_info_binding.dart'; +import '../pages/register/set_self_info/set_self_info_view.dart'; +import '../pages/register/verify_phone/verify_phone_binding.dart'; +import '../pages/register/verify_phone/verify_phone_view.dart'; +import '../pages/splash/splash_binding.dart'; +import '../pages/splash/splash_view.dart'; + +part 'app_routes.dart'; + +class AppPages { + static _pageBuilder({ + required String name, + required GetPageBuilder page, + Bindings? binding, + bool preventDuplicates = true, + bool popGesture = true, + }) => + GetPage( + name: name, + page: page, + binding: binding, + preventDuplicates: preventDuplicates, + transition: Transition.cupertino, + popGesture: popGesture, + ); + + static final routes = [ + _pageBuilder( + name: AppRoutes.splash, + page: () => SplashPage(), + binding: SplashBinding(), + ), + _pageBuilder( + name: AppRoutes.login, + page: () => LoginPage(), + binding: LoginBinding(), + ), + _pageBuilder( + name: AppRoutes.home, + page: () => HomePage(), + binding: HomeBinding(), + ), + _pageBuilder( + name: AppRoutes.chat, + page: () => ChatPage(), + binding: ChatBinding(), + preventDuplicates: false, + ), + _pageBuilder( + name: AppRoutes.myQrcode, + page: () => MyQrcodePage(), + binding: MyQrcodeBinding(), + ), + _pageBuilder( + name: AppRoutes.chatSetup, + page: () => ChatSetupPage(), + binding: ChatSetupBinding(), + popGesture: false, + ), + _pageBuilder( + name: AppRoutes.favoriteManage, + page: () => FavoriteManagePage(), + binding: FavoriteManageBinding(), + ), + _pageBuilder( + name: AppRoutes.addContactsMethod, + page: () => AddContactsMethodPage(), + binding: AddContactsMethodBinding(), + ), + _pageBuilder( + name: AppRoutes.addContactsBySearch, + page: () => AddContactsBySearchPage(), + binding: AddContactsBySearchBinding(), + ), + _pageBuilder( + name: AppRoutes.userProfilePanel, + page: () => UserProfilePanelPage(), + binding: UserProfilePanelBinding(), + ), + _pageBuilder( + name: AppRoutes.personalInfo, + page: () => PersonalInfoPage(), + binding: PersonalInfoBinding(), + ), + _pageBuilder( + name: AppRoutes.friendSetup, + page: () => FriendSetupPage(), + binding: FriendSetupBinding(), + ), + _pageBuilder( + name: AppRoutes.setFriendRemark, + page: () => SetFriendRemarkPage(), + binding: SetFriendRemarkBinding(), + ), + _pageBuilder( + name: AppRoutes.sendVerificationApplication, + page: () => SendVerificationApplicationPage(), + binding: SendVerificationApplicationBinding(), + ), + _pageBuilder( + name: AppRoutes.groupProfilePanel, + page: () => GroupProfilePanelPage(), + binding: GroupProfilePanelBinding(), + ), + _pageBuilder( + name: AppRoutes.myInfo, + page: () => MyInfoPage(), + binding: MyInfoBinding(), + ), + _pageBuilder( + name: AppRoutes.editMyInfo, + page: () => EditMyInfoPage(), + binding: EditMyInfoBinding(), + ), + _pageBuilder( + name: AppRoutes.accountSetup, + page: () => AccountSetupPage(), + binding: AccountSetupBinding(), + ), + _pageBuilder( + name: AppRoutes.blacklist, + page: () => BlacklistPage(), + binding: BlacklistBinding(), + ), + _pageBuilder( + name: AppRoutes.languageSetup, + page: () => LanguageSetupPage(), + binding: LanguageSetupBinding(), + ), + _pageBuilder( + name: AppRoutes.unlockSetup, + page: () => UnlockSetupPage(), + binding: UnlockSetupBinding(), + ), + _pageBuilder( + name: AppRoutes.changePassword, + page: () => ChangePwdPage(), + binding: ChangePwdBinding(), + ), + _pageBuilder( + name: AppRoutes.aboutUs, + page: () => AboutUsPage(), + binding: AboutUsBinding(), + ), + _pageBuilder( + name: AppRoutes.groupChatSetup, + page: () => GroupSetupPage(), + binding: GroupSetupBinding(), + ), + _pageBuilder( + name: AppRoutes.groupManage, + page: () => GroupManagePage(), + binding: GroupManageBinding(), + ), + _pageBuilder( + name: AppRoutes.editGroupName, + page: () => EditGroupNamePage(), + binding: EditGroupNameBinding(), + ), + _pageBuilder( + name: AppRoutes.groupMemberList, + page: () => GroupMemberListPage(), + binding: GroupMemberListBinding(), + ), + _pageBuilder( + name: AppRoutes.searchGroupMember, + page: () => SearchGroupMemberPage(), + binding: SearchGroupMemberBinding(), + ), + _pageBuilder( + name: AppRoutes.groupQrcode, + page: () => GroupQrcodePage(), + binding: GroupQrcodeBinding(), + ), + _pageBuilder( + name: AppRoutes.friendRequests, + page: () => FriendRequestsPage(), + binding: FriendRequestsBinding(), + ), + _pageBuilder( + name: AppRoutes.processFriendRequests, + page: () => ProcessFriendRequestsPage(), + binding: ProcessFriendRequestsBinding(), + ), + _pageBuilder( + name: AppRoutes.groupRequests, + page: () => GroupRequestsPage(), + binding: GroupRequestsBinding(), + ), + _pageBuilder( + name: AppRoutes.processGroupRequests, + page: () => ProcessGroupRequestsPage(), + binding: ProcessGroupRequestsBinding(), + ), + _pageBuilder( + name: AppRoutes.friendList, + page: () => FriendListPage(), + binding: FriendListBinding(), + ), + _pageBuilder( + name: AppRoutes.groupList, + page: () => GroupListPage(), + binding: GroupListBinding(), + ), + _pageBuilder( + name: AppRoutes.searchFriend, + page: () => SearchFriendPage(), + binding: SearchFriendBinding(), + ), + _pageBuilder( + name: AppRoutes.searchGroup, + page: () => SearchGroupPage(), + binding: SearchGroupBinding(), + ), + _pageBuilder( + name: AppRoutes.selectContacts, + page: () => SelectContactsPage(), + binding: SelectContactsBinding(), + ), + _pageBuilder( + name: AppRoutes.selectContactsFromFriends, + page: () => SelectContactsFromFriendsPage(), + binding: SelectContactsFromFriendsBinding(), + ), + _pageBuilder( + name: AppRoutes.selectContactsFromGroup, + page: () => SelectContactsFromGroupPage(), + binding: SelectContactsFromGroupBinding(), + ), + _pageBuilder( + name: AppRoutes.selectContactsFromSearchFriends, + page: () => SelectContactsFromSearchFriendsPage(), + binding: SelectContactsFromSearchFriendsBinding(), + ), + _pageBuilder( + name: AppRoutes.selectContactsFromSearchGroup, + page: () => SelectContactsFromSearchGroupPage(), + binding: SelectContactsFromSearchGroupBinding(), + ), + _pageBuilder( + name: AppRoutes.selectContactsFromSearch, + page: () => SelectContactsFromSearchPage(), + binding: SelectContactsFromSearchBinding(), + ), + _pageBuilder( + name: AppRoutes.createGroup, + page: () => CreateGroupPage(), + binding: CreateGroupBinding(), + ), + _pageBuilder( + name: AppRoutes.globalSearch, + page: () => GlobalSearchPage(), + binding: GlobalSearchBinding(), + ), + _pageBuilder( + name: AppRoutes.register, + page: () => RegisterPage(), + binding: RegisterBinding(), + ), + _pageBuilder( + name: AppRoutes.verifyPhone, + page: () => VerifyPhonePage(), + binding: VerifyPhoneBinding(), + ), + _pageBuilder( + name: AppRoutes.setPassword, + page: () => SetPasswordPage(), + binding: SetPasswordBinding(), + ), + _pageBuilder( + name: AppRoutes.setSelfInfo, + page: () => SetSelfInfoPage(), + binding: SetSelfInfoBinding(), + ), + _pageBuilder( + name: AppRoutes.forgetPassword, + page: () => ForgetPasswordPage(), + binding: ForgetPasswordBinding(), + ), + _pageBuilder( + name: AppRoutes.resetPassword, + page: () => ResetPasswordPage(), + binding: ResetPasswordBinding(), + ), + _pageBuilder( + name: AppRoutes.oaNotificationList, + page: () => OANotificationPage(), + binding: OANotificationBinding(), + ), + ]; +} diff --git a/mobile-next/lib/routes/app_routes.dart b/mobile-next/lib/routes/app_routes.dart new file mode 100644 index 0000000..7c7d6a1 --- /dev/null +++ b/mobile-next/lib/routes/app_routes.dart @@ -0,0 +1,63 @@ +part of 'app_pages.dart'; + +abstract class AppRoutes { + static const notFound = '/not-found'; + static const splash = '/splash'; + static const login = '/login'; + static const home = '/home'; + static const chat = '/chat'; + static const myQrcode = '/my_qrcode'; + static const chatSetup = '/chat_setup'; + static const favoriteManage = '/favorite_manage'; + static const addContactsMethod = '/add_contacts_method'; + static const addContactsBySearch = '/add_contacts_by_search'; + static const userProfilePanel = '/user_profile_panel'; + static const personalInfo = '/personal_info'; + static const friendSetup = '/friend_setup'; + static const setFriendRemark = '/set_friend_remark'; + static const sendVerificationApplication = '/send_verification_application'; + static const groupProfilePanel = '/group_profile_panel'; + static const myInfo = '/my_info'; + static const editMyInfo = '/edit_my_info'; + static const accountSetup = '/account_setup'; + static const blacklist = '/blacklist'; + static const languageSetup = '/language_setup'; + static const unlockSetup = '/unlock_setup'; + static const changePassword = '/change_password'; + static const aboutUs = '/about_us'; + static const groupChatSetup = '/group_chat_setup'; + static const groupManage = '/group_manage'; + static const editGroupName = '/edit_group_name'; + static const groupMemberList = '/group_member_list'; + static const searchGroupMember = '/search_group_member'; + static const groupQrcode = '/group_qrcode'; + static const friendRequests = '/friend_requests'; + static const processFriendRequests = '/process_friend_requests'; + static const groupRequests = '/group_requests'; + static const processGroupRequests = '/process_group_requests'; + static const friendList = '/friend_list'; + static const groupList = '/group_list'; + static const searchFriend = '/search_friend'; + static const searchGroup = '/search_group'; + static const selectContacts = '/select_contacts'; + static const selectContactsFromFriends = '/select_contacts_from_friends'; + static const selectContactsFromSearchFriends = '/select_contacts_from_search_friends'; + static const selectContactsFromGroup = '/select_contacts_from_group'; + static const selectContactsFromSearchGroup = '/select_contacts_from_search_group'; + static const selectContactsFromSearch = '/select_contacts_from_search'; + static const createGroup = '/create_group'; + static const globalSearch = '/global_search'; + static const expandChatHistory = '/expand_chat_history'; + static const register = '/register'; + static const verifyPhone = '/verify_phone'; + static const setPassword = '/set_password'; + static const setSelfInfo = '/set_self_info'; + static const forgetPassword = '/forget_password'; + static const resetPassword = '/reset_password'; + static const selectContactsFromTag = '/select_contacts_from_tag'; + static const oaNotificationList = "/oa_notification_list"; +} + +extension RoutesExtension on String { + String toRoute() => '/${toLowerCase()}'; +} diff --git a/mobile-next/lib/utils/upgrade_manager.dart b/mobile-next/lib/utils/upgrade_manager.dart new file mode 100644 index 0000000..953d8a1 --- /dev/null +++ b/mobile-next/lib/utils/upgrade_manager.dart @@ -0,0 +1,132 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../widgets/upgrade_view.dart'; + +mixin UpgradeManger { + PackageInfo? packageInfo; + UpgradeInfoV2? upgradeInfoV2; + var isShowUpgradeDialog = false; + var isNowIgnoreUpdate = false; + final subject = PublishSubject(); + final notificationService = NotificationService(); + + void closeSubject() { + subject.close(); + } + + void ignoreUpdate() { + DataSp.putIgnoreVersion(upgradeInfoV2!.buildVersion! + upgradeInfoV2!.buildVersionNo!); + Get.back(); + } + + void laterUpdate() { + isNowIgnoreUpdate = true; + Get.back(); + } + + getAppInfo() async { + packageInfo ??= await PackageInfo.fromPlatform(); + } + + void nowUpdate() async { + final appUrl = upgradeInfoV2?.appURl; + + if (appUrl != null && await canLaunchUrlString(appUrl)) { + launchUrlString(appUrl); + return; + } + } + + void checkUpdate() async { + LoadingView.singleton.wrap(asyncFunction: () async { + await getAppInfo(); + return Apis.checkUpgradeV2(); + }).then((value) { + upgradeInfoV2 = value; + if (!canUpdate) { + IMViews.showToast('已是最新版本'); + return; + } + Get.dialog( + UpgradeViewV2( + upgradeInfo: upgradeInfoV2!, + packageInfo: packageInfo!, + onNow: nowUpdate, + subject: subject, + ), + routeSettings: const RouteSettings(name: 'upgrade_dialog'), + ); + }); + } + + autoCheckVersionUpgrade() async { + if (isShowUpgradeDialog || isNowIgnoreUpdate) return; + await getAppInfo(); + upgradeInfoV2 = await Apis.checkUpgradeV2(); + + if (!canUpdate) return; + isShowUpgradeDialog = true; + Get.dialog( + UpgradeViewV2( + upgradeInfo: upgradeInfoV2!, + packageInfo: packageInfo!, + onLater: laterUpdate, + onIgnore: ignoreUpdate, + onNow: nowUpdate, + subject: subject, + ), + routeSettings: const RouteSettings(name: 'upgrade_dialog'), + ).whenComplete(() => isShowUpgradeDialog = false); + } + + bool get canUpdate => + packageInfo!.version + packageInfo!.buildNumber != upgradeInfoV2!.buildVersion! + upgradeInfoV2!.buildVersionNo!; +} + +class NotificationService { + static final NotificationService _notificationService = NotificationService._internal(); + final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + final AndroidInitializationSettings _androidInitializationSettings = + const AndroidInitializationSettings('@mipmap/ic_launcher'); + + factory NotificationService() { + return _notificationService; + } + + NotificationService._internal() { + if (Platform.isAndroid) { + init(); + } + } + + void init() async { + final InitializationSettings initializationSettings = InitializationSettings( + android: _androidInitializationSettings, + ); + await _flutterLocalNotificationsPlugin.initialize(initializationSettings); + } + + Future createNotification(int count, int i, int id, String status) async { + var androidPlatformChannelSpecifics = AndroidNotificationDetails('progress channel', 'progress channel', + channelDescription: 'progress channel description', + channelShowBadge: false, + importance: Importance.max, + priority: Priority.high, + onlyAlertOnce: true, + showProgress: true, + maxProgress: count, + progress: i); + var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); + await _flutterLocalNotificationsPlugin.show(id, status, '$i%', platformChannelSpecifics, payload: 'item x'); + + return; + } +} diff --git a/mobile-next/lib/widgets/app_view.dart b/mobile-next/lib/widgets/app_view.dart new file mode 100644 index 0000000..768018f --- /dev/null +++ b/mobile-next/lib/widgets/app_view.dart @@ -0,0 +1,57 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:focus_detector_v2/focus_detector_v2.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +import '../core/controller/app_controller.dart'; + +class AppView extends StatelessWidget { + const AppView({Key? key, required this.builder}) : super(key: key); + final Widget Function(Locale? locale, TransitionBuilder builder) builder; + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: AppController(), + builder: (ctrl) => FocusDetector( + onForegroundGained: () => ctrl.runningBackground(false), + onForegroundLost: () => ctrl.runningBackground(true), + child: ScreenUtilInit( + designSize: const Size(Config.uiW, Config.uiH), + minTextAdapt: true, + splitScreenMode: true, + fontSizeResolver: (fontSize, _) => fontSize.toDouble(), + builder: (_, child) => builder(ctrl.getLocale(), _builder()), + ), + ), + ); + } + + static TransitionBuilder _builder() { + final builder = EasyLoading.init( + builder: (context, widget) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: Config.textScaleFactor, + ), + child: widget!, + ); + }, + ); + + EasyLoading.instance + ..userInteractions = false + ..indicatorSize = 50 + ..backgroundColor = Styles.c_0C1C33 + ..indicatorColor = CupertinoColors.systemGrey2 + ..progressColor = CupertinoColors.systemGrey2 + ..progressWidth = 6.0 + ..textColor = Colors.white + ..loadingStyle = EasyLoadingStyle.custom + ..indicatorType = EasyLoadingIndicatorType.fadingCircle; + return builder; + } +} diff --git a/mobile-next/lib/widgets/file_download_progress.dart b/mobile-next/lib/widgets/file_download_progress.dart new file mode 100644 index 0000000..73c1858 --- /dev/null +++ b/mobile-next/lib/widgets/file_download_progress.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_download_manager/flutter_download_manager.dart'; +import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +class FileDownloadProgressView extends StatelessWidget { + FileDownloadProgressView(this.message, {Key? key}) : super(key: key); + final Message message; + final logic = Get.find(); + + @override + Widget build(BuildContext context) { + final url = message.fileElem?.sourceUrl; + return Obx(() => SizedBox( + width: 38.w, + height: 44.h, + child: message.isFileType && null != logic.downloadTaskList[url] + ? ValueListenableBuilder( + valueListenable: logic.downloadTaskList[url]!.status, + builder: (_, status, child) { + return ValueListenableBuilder( + valueListenable: logic.downloadTaskList[url]!.progress, + builder: (_, progress, child) { + return (status == DownloadStatus.downloading || status == DownloadStatus.paused || status == DownloadStatus.queued) + ? Stack( + alignment: Alignment.center, + children: [ + ImageRes.fileMask.toImage + ..width = 38.w + ..height = 44.h, + SizedBox( + width: 22.w, + height: 22.w, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + backgroundColor: Styles.c_FFFFFF, + color: Styles.c_0089FF, + strokeWidth: 1.5, + value: progress, + ), + if (status == DownloadStatus.downloading) + ImageRes.progressPause.toImage + ..width = 16.w + ..height = 16.h, + if (status == DownloadStatus.paused) + ImageRes.progressGoing.toImage + ..width = 16.w + ..height = 16.h, + ], + ), + ) + ], + ) + : const SizedBox(); + }, + ); + }, + ) + : null, + )); + } +} diff --git a/mobile-next/lib/widgets/register_page_bg.dart b/mobile-next/lib/widgets/register_page_bg.dart new file mode 100644 index 0000000..ca38719 --- /dev/null +++ b/mobile-next/lib/widgets/register_page_bg.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; + +class RegisterBgView extends StatelessWidget { + const RegisterBgView({ + Key? key, + required this.child, + }) : super(key: key); + final Widget child; + + @override + Widget build(BuildContext context) => Material( + child: TouchCloseSoftKeyboard( + isGradientBg: true, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 54.verticalSpace, + Padding( + padding: EdgeInsets.only(left: 22.w), + child: ImageRes.backBlack.toImage + ..width = 24.w + ..height = 24.h + ..onTap = () => Get.back(), + ), + 38.verticalSpace, + Padding( + padding: EdgeInsets.symmetric(horizontal: 32.w), + child: child, + ), + ], + ), + ), + ), + ); +} diff --git a/mobile-next/lib/widgets/screen_lock_title.dart b/mobile-next/lib/widgets/screen_lock_title.dart new file mode 100644 index 0000000..aab66f8 --- /dev/null +++ b/mobile-next/lib/widgets/screen_lock_title.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:sprintf/sprintf.dart'; + +class ScreenLockTitle extends StatelessWidget { + const ScreenLockTitle({ + Key? key, + required this.stream, + }) : super(key: key); + + final Stream stream; + + @override + Widget build(BuildContext context) { + return Column(children: [ + Text(StrRes.plsEnterPassword, style: Styles.ts_FFFFFF_17sp), + StreamBuilder( + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return Text( + sprintf(StrRes.lockPwdErrorHint, [snapshot.data]), + style: Styles.ts_FF381F_17sp, + ); + } + return const SizedBox(); + }, + stream: stream, + ), + ]); + } +} diff --git a/mobile-next/lib/widgets/upgrade_view.dart b/mobile-next/lib/widgets/upgrade_view.dart new file mode 100644 index 0000000..f4e63d9 --- /dev/null +++ b/mobile-next/lib/widgets/upgrade_view.dart @@ -0,0 +1,194 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:openim_common/openim_common.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:sprintf/sprintf.dart'; + +class UpgradeViewV2 extends StatefulWidget { + final Function()? onLater; + final Function()? onIgnore; + final Function() onNow; + final UpgradeInfoV2 upgradeInfo; + final PackageInfo packageInfo; + final PublishSubject? subject; + + const UpgradeViewV2({ + super.key, + this.onLater, + this.onIgnore, + required this.onNow, + required this.upgradeInfo, + required this.packageInfo, + this.subject, + }); + + @override + State createState() => _UpgradeViewV2State(); +} + +class _UpgradeViewV2State extends State { + double _progress = 0.0; + bool _showProgress = false; + + @override + void initState() { + widget.subject?.stream.listen((progress) { + if (!mounted) return; + setState(() { + _progress = progress; + }); + }); + super.initState(); + } + + void _startDownload() { + if (Platform.isAndroid) { + Get.back(); + } else { + if (!widget.upgradeInfo.needForceUpdate!) { + Get.back(); + } + } + widget.onNow.call(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + return !widget.upgradeInfo.needForceUpdate!; + }, + child: Center( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 46.w), + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + StrRes.upgradeFind, + style: TextStyle( + color: const Color(0xFF333333), + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + height: 10.h, + ), + Text( + sprintf(StrRes.upgradeVersion, [ + '${widget.upgradeInfo.buildVersion!} + ${widget.upgradeInfo.buildVersionNo!}', + '${widget.packageInfo.version} + ${widget.packageInfo.buildNumber}' + ]), + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF333333), + ), + ), + SizedBox( + height: 10.h, + ), + Text( + StrRes.upgradeDescription, + style: TextStyle( + color: const Color(0xFF333333), + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + height: 4.h, + ), + Text( + widget.upgradeInfo.buildUpdateDescription ?? '', + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF333333), + ), + ), + SizedBox( + height: 10.h, + ), + if (!widget.upgradeInfo.needForceUpdate! && !_showProgress) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildButton( + text: StrRes.upgradeIgnore, + onTap: widget.onIgnore ?? () => Get.back(), + ), + _buildButton( + text: StrRes.upgradeLater, + onTap: widget.onLater ?? () => Get.back(), + ), + _buildButton( + text: StrRes.upgradeNow, + onTap: _startDownload, + ), + ], + ), + if (widget.upgradeInfo.needForceUpdate! && !_showProgress) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildButton( + text: StrRes.upgradeNow, + onTap: _startDownload, + ) + ], + ), + if (_showProgress) + SizedBox( + height: 44.h, + child: Center( + child: LinearPercentIndicator( + lineHeight: 20.h, + percent: _progress, + center: "${(_progress * 100).toInt()}%".toText..style = TextStyle(fontSize: 12.sp), + linearStrokeCap: LinearStrokeCap.roundAll, + backgroundColor: Colors.grey.withOpacity(0.5), + progressColor: Colors.blueAccent, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildButton({required String text, Function()? onTap}) => Ink( + height: 44.h, + child: InkWell( + onTap: onTap, + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + horizontal: 14.w, + ), + child: Text( + text, + style: TextStyle( + color: const Color(0xFF1B72EC), + fontSize: 16.sp, + ), + ), + ), + ), + ); +} diff --git a/mobile-next/local_plugin/flutter_download_manager/CHANGELOG.md b/mobile-next/local_plugin/flutter_download_manager/CHANGELOG.md new file mode 100644 index 0000000..ecbe5c8 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/CHANGELOG.md @@ -0,0 +1,24 @@ +## [0.5.5] - Bug Fixes + +* Update dio to 5.0.1 #10 mbfakourii +* Replace slash with Platform.pathSeparator on downloader #12 (Paulo Ortolan) +* Fixed download task list #4 (arjundevlucid) +* Fixed initial progress error during resume, after multiple pause resumes (YanRui) + +## [0.5.4] - Bug Fixes + +* Improve batch progress notification to notify more granular + +## [0.5.3] - Bug Fixes + +* Disable Streams temporarily to fix bad state bugs +* Bug Fix: Stop Same Download Requests if already downloading +* Return DownloadTask when addDownload + +## [0.5.2] - Added Stream to expose events for all task when download status changes + +* Added Stream to expose events for all task when download status changes + +## [0.5.0] - Initial Release + +* Introducing Flutter Download Manager. \ No newline at end of file diff --git a/mobile-next/local_plugin/flutter_download_manager/LICENSE b/mobile-next/local_plugin/flutter_download_manager/LICENSE new file mode 100644 index 0000000..fd130e2 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Nabil Mosharraf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/mobile-next/local_plugin/flutter_download_manager/README.md b/mobile-next/local_plugin/flutter_download_manager/README.md new file mode 100644 index 0000000..d3b427d --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/README.md @@ -0,0 +1,211 @@ +Flutter Download Manager +=========== + +[![pub package](https://img.shields.io/pub/v/flutter_download_manager.svg)](https://pub.dev/packages/flutter_download_manager) +[![pub points](https://badges.bar/flutter_download_manager/pub%20points)](https://pub.dev/packages/flutter_download_manager/score) +[![popularity](https://badges.bar/flutter_download_manager/popularity)](https://pub.dev/packages/flutter_download_manager/score) +[![likes](https://badges.bar/flutter_download_manager/likes)](https://pub.dev/packages/flutter_download_manager/score) | + +Overview +======== +Flutter Download Manager is a Cross-Platform file downloader with Parallel and Batch Download support. Manage download tasks by url and be notified of status and their progress. Pause, Cancel, Queue and Resume Downloads. + +This package was made as I felt like there are no download managers in flutter, specially with desktop support. + +![alt Example](files/screenshot1.png "Download Manager Example") + +## Features + +* Manage download tasks by url +* Ability to notified of status and progress changes +* Partial Download Feature +* Queue Downloads +* Pause, Cancel or Resume Downloads +* Parallel File Downloads (2 or can change) +* Support Batch download + +## Platforms Supported + +- Linux +- MacOS +- Windows +- Android +- iOS + +There are a few caveats about this package: +- On desktop it saves the file in absolute or relative path. +- On mobile it saves the file in absolute or relative path, but we should ask/ensure if the app has the required permissions. +- It does not run in a background process, so when the dart application closes the manager will also shut down. + +## Getting Started + +In your `pubspec.yaml` file add: + +```dart +dependencies: + flutter_download_manager: any +``` +Then, in your code import: +```dart +import 'package:flutter_download_manager/flutter_download_manager.dart'; +``` + +## Usage + +Please refer to `/example` folder for a working example. + +### Simply Download a file + +```dart +var dl = DownloadManager(); +var url = "adasdad.com/asda.sdas"; +dl.addDownload(url, "./test.sdas"); + +DownloadTask? task = dl.getDownload(url4); + +task?.status.addListener(() { + print(task.status.value); +}); + +task?.progress.addListener(() { + print(task.progress.value); +}); + +await dl.whenDownloadComplete(url4); +``` + +### Get Download Status + + +```dart +DownloadTask? task = dl.getDownload(url4); + +task?.status.addListener(() { + print(task.status.value); +}); +``` +### Get Download Progress + + +```dart +DownloadTask? task = dl.getDownload(url4); + +task?.progress.addListener(() { + print(task.progress.value); +}); +``` + +### Await for a task to be complete + +```dart +DownloadTask? task = dl.getDownload(url4); + +await task.whenDownloadComplete(); +``` + +### Cancel a task + + ```dart +var dl = DownloadManager(); + +dl.cancelDownload(url5); +``` + +### Pause a task + + ```dart +var dl = DownloadManager(); + +dl.pauseDownload(url5); +``` + +### Resume a task + + ```dart +var dl = DownloadManager(); + +dl.resumeDownload(url5); +``` + +### Download in Batch + +```dart +var dl = DownloadManager(); + +var urls = []; +urls.add(url2); +urls.add(url3); +urls.add(url); + +dl.addDownload(url2, "./test2.ipa"); +dl.addDownload(url3, "./test3.ipa"); +dl.addDownload(url, "./test.ipa"); + +var downloadProgress = dl.getDownloadProgress(urls); + +downloadProgress.addListener(() { + print(downloadProgress.value); +}); + +await dl.whenDownloadsComplete(urls); +``` + +```dart +var dl = DownloadManager(); + +var urls = []; +urls.add(url2); +urls.add(url3); +urls.add(url); + +dl.addBatchDownloads(urls, "./"); +``` + +### Cancel a Batch Download + + ```dart +var dl = DownloadManager(); + +var urls = []; +urls.add(url6); +urls.add(url5); +urls.add(url); + +dl.cancelDownloads(urls); +``` +### Get Batch Download Progress + +```dart +var dl = DownloadManager(); + +var urls = []; +urls.add(url2); +urls.add(url3); + +var downloadProgress = dl.getDownloadProgress(urls); + +downloadProgress.addListener(() { + print(downloadProgress.value); +}); +``` + +### Await for Batch Download to complete + +```dart +var dl = DownloadManager(); + +var urls = []; +urls.add(url2); +urls.add(url3); +urls.add(url); + +await dl.whenDownloadsComplete(urls); +``` + +## Future Work + +[] Add in Shared Preference to survive app shutdown + +# DownloadStatus + +enum DownloadStatus { queued, downloading, completed, failed, paused, canceled } diff --git a/mobile-next/local_plugin/flutter_download_manager/example/analysis_options.yaml b/mobile-next/local_plugin/flutter_download_manager/example/analysis_options.yaml new file mode 100644 index 0000000..7ed880a --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:lints/recommended.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/dependencies-accessors/dependencies-accessors.lock b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/dependencies-accessors/dependencies-accessors.lock new file mode 100644 index 0000000..c350af0 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/dependencies-accessors/dependencies-accessors.lock differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/fileChanges/last-build.bin b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/fileChanges/last-build.bin differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/fileHashes/fileHashes.lock b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/fileHashes/fileHashes.lock new file mode 100644 index 0000000..210fd2e Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/7.4/fileHashes/fileHashes.lock differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..cc64594 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/buildOutputCleanup/cache.properties b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..af71c7d --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Mon Dec 23 19:29:42 HKT 2024 +gradle.version=7.4 diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/build.gradle b/mobile-next/local_plugin/flutter_download_manager/example/android/app/build.gradle new file mode 100644 index 0000000..0833ecf --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/debug/AndroidManifest.xml b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..45d523a --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/AndroidManifest.xml b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3f41384 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 0000000..e793a00 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/drawable-v21/launch_background.xml b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/drawable/launch_background.xml b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/values-night/styles.xml b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/values/styles.xml b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/profile/AndroidManifest.xml b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..45d523a --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/build.gradle b/mobile-next/local_plugin/flutter_download_manager/example/android/build.gradle new file mode 100644 index 0000000..83ae220 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/gradle.properties b/mobile-next/local_plugin/flutter_download_manager/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/gradle/wrapper/gradle-wrapper.properties b/mobile-next/local_plugin/flutter_download_manager/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cb24abd --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/mobile-next/local_plugin/flutter_download_manager/example/android/settings.gradle b/mobile-next/local_plugin/flutter_download_manager/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/AppFrameworkInfo.plist b/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/Debug.xcconfig b/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/Release.xcconfig b/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Podfile b/mobile-next/local_plugin/flutter_download_manager/example/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/project.pbxproj b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..55b9457 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,484 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UD534233NF; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UD534233NF; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UD534233NF; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/AppDelegate.swift b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Base.lproj/Main.storyboard b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Info.plist b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Info.plist new file mode 100644 index 0000000..7f55346 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Runner-Bridging-Header.h b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/mobile-next/local_plugin/flutter_download_manager/example/lib/main.dart b/mobile-next/local_plugin/flutter_download_manager/example/lib/main.dart new file mode 100644 index 0000000..b97cb5e --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/lib/main.dart @@ -0,0 +1,427 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_download_manager/flutter_download_manager.dart'; +import 'package:path_provider/path_provider.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + var url2 = "http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg"; + + var url3 = + 'https://cdn.jsdelivr.net/gh/flutterchina/flutter-in-action@1.0/docs/imgs/book.jpg'; + var url = "http://app01.78x56.com/Xii_2021-03-13%2010%EF%BC%9A41.ipa"; + var url4 = + "https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/sample-mp4-file.mp4"; + var url5 = + "https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/Sample-Video-File-For-Testing.mp4"; + + var downloadManager = DownloadManager(); + var savedDir = ""; + + @override + void initState() { + super.initState(); + getApplicationSupportDirectory().then((value) => savedDir = value.path); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("Flutter Download Manager")), + body: SingleChildScrollView( + child: Column( + children: [ + ListItem( + onDownloadPlayPausedPressed: (url) async { + setState(() { + var task = downloadManager.getDownload(url); + + if (task != null && !task.status.value.isCompleted) { + switch (task.status.value) { + case DownloadStatus.downloading: + downloadManager.pauseDownload(url); + break; + case DownloadStatus.paused: + downloadManager.resumeDownload(url); + break; + } + } else { + downloadManager.addDownload(url, + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"); + } + }); + }, + onDelete: (url) { + var fileName = + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"; + var file = File(fileName); + file.delete(); + + downloadManager.removeDownload(url); + setState(() {}); + }, + url: url, + downloadTask: downloadManager.getDownload(url)), + ListItem( + onDownloadPlayPausedPressed: (url) async { + setState(() { + var task = downloadManager.getDownload(url); + + if (task != null && !task.status.value.isCompleted) { + switch (task.status.value) { + case DownloadStatus.downloading: + downloadManager.pauseDownload(url); + break; + case DownloadStatus.paused: + downloadManager.resumeDownload(url); + break; + } + } else { + downloadManager.addDownload(url, + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"); + } + }); + }, + onDelete: (url) { + var fileName = + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"; + var file = File(fileName); + file.delete(); + + downloadManager.removeDownload(url); + setState(() {}); + }, + url: url2, + downloadTask: downloadManager.getDownload(url2)), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text("Batch Downloads"), + ), + ListItem( + onDownloadPlayPausedPressed: (url) async { + setState(() { + var task = downloadManager.getDownload(url); + + if (task != null && !task.status.value.isCompleted) { + switch (task.status.value) { + case DownloadStatus.downloading: + downloadManager.pauseDownload(url); + break; + case DownloadStatus.paused: + downloadManager.resumeDownload(url); + break; + } + } else { + downloadManager.addDownload(url, + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"); + } + }); + }, + onDelete: (url) { + var fileName = + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"; + var file = File(fileName); + file.delete(); + + downloadManager.removeDownload(url); + setState(() {}); + }, + url: url3, + downloadTask: downloadManager.getDownload(url3)), + ListItem( + onDownloadPlayPausedPressed: (url) async { + setState(() { + var task = downloadManager.getDownload(url); + + if (task != null && !task.status.value.isCompleted) { + switch (task.status.value) { + case DownloadStatus.downloading: + downloadManager.pauseDownload(url); + break; + case DownloadStatus.paused: + downloadManager.resumeDownload(url); + break; + } + } else { + downloadManager.addDownload(url, + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"); + } + }); + }, + onDelete: (url) { + var fileName = + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"; + var file = File(fileName); + file.delete(); + + downloadManager.removeDownload(url); + setState(() {}); + }, + url: url4, + downloadTask: downloadManager.getDownload(url4)), + ListItem( + onDownloadPlayPausedPressed: (url) async { + setState(() { + var task = downloadManager.getDownload(url); + + if (task != null && !task.status.value.isCompleted) { + switch (task.status.value) { + case DownloadStatus.downloading: + downloadManager.pauseDownload(url); + break; + case DownloadStatus.paused: + downloadManager.resumeDownload(url); + break; + } + } else { + downloadManager.addDownload(url, + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"); + } + }); + }, + onDelete: (url) { + var fileName = + "$savedDir/${downloadManager.getFileNameFromUrl(url)}"; + var file = File(fileName); + file.delete(); + + downloadManager.removeDownload(url); + setState(() {}); + }, + url: url5, + downloadTask: downloadManager.getDownload(url5)), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + downloadManager.addBatchDownloads( + [url3, url4, url5], savedDir); + setState(() {}); + }, + child: Text("Download All")), + TextButton( + onPressed: () { + downloadManager + .pauseBatchDownloads([url3, url4, url5]); + }, + child: Text("Pause All")), + TextButton( + onPressed: () { + downloadManager + .cancelBatchDownloads([url3, url4, url5]); + }, + child: Text("Cancel All")), + ], + ), + ValueListenableBuilder( + valueListenable: downloadManager + .getBatchDownloadProgress([url3, url4, url5]), + builder: (context, value, child) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 4), + child: LinearProgressIndicator( + value: value, + ), + ); + }), + FutureBuilder?>( + future: downloadManager + .whenBatchDownloadsComplete([url3, url4, url5]), + builder: (BuildContext context, + AsyncSnapshot?> snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return Text( + 'I will wait till the batch downloads have been completed'); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return snapshot.data != null + ? Column(children: [ + Text("Result"), + for (var e in snapshot.data!) + e != null + ? Padding( + padding: + const EdgeInsets.all(8.0), + child: Text( + "${downloadManager.getFileNameFromUrl(e.request.url)}: ${e.status.value}"), + ) + : Text("Not found"), + ]) + : Text("No Downloads have been found"); + } + } + }) + ], + ), + ) + ], + ), + ), + ); + } +} + +class ListItem extends StatelessWidget { + final Function(String) onDownloadPlayPausedPressed; + final Function(String) onDelete; + DownloadTask? downloadTask; + String url = ""; + + ListItem( + {Key? key, + required this.url, + required this.onDownloadPlayPausedPressed, + required this.onDelete, + this.downloadTask}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.amber, + ), + borderRadius: BorderRadius.all(Radius.circular(20))), + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + url, + overflow: TextOverflow.ellipsis, + ), + if (downloadTask != null) + ValueListenableBuilder( + valueListenable: downloadTask!.status, + builder: (context, value, child) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text("$value", + style: TextStyle(fontSize: 16)), + ); + }), + ], + )), + downloadTask != null + ? ValueListenableBuilder( + valueListenable: downloadTask!.status, + builder: (context, value, child) { + switch (downloadTask!.status.value) { + case DownloadStatus.downloading: + return IconButton( + onPressed: () { + onDownloadPlayPausedPressed(url); + }, + icon: const Icon(Icons.pause)); + case DownloadStatus.paused: + return IconButton( + onPressed: () { + onDownloadPlayPausedPressed(url); + }, + icon: const Icon(Icons.play_arrow)); + case DownloadStatus.completed: + return IconButton( + onPressed: () { + onDelete(url); + }, + icon: const Icon(Icons.delete)); + case DownloadStatus.failed: + case DownloadStatus.canceled: + return IconButton( + onPressed: () { + onDownloadPlayPausedPressed(url); + }, + icon: const Icon(Icons.download)); + } + return Text("$value", style: TextStyle(fontSize: 16)); + }) + : IconButton( + onPressed: () { + onDownloadPlayPausedPressed(url); + }, + icon: const Icon(Icons.download)) + ], + ), // if (widget.item.isDownloadingOrPaused) + if (downloadTask != null && !downloadTask!.status.value.isCompleted) + ValueListenableBuilder( + valueListenable: downloadTask!.progress, + builder: (context, value, child) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 4), + child: LinearProgressIndicator( + value: value, + color: + downloadTask!.status.value == DownloadStatus.paused + ? Colors.grey + : Colors.amber, + ), + ); + }), + if (downloadTask != null) + FutureBuilder( + future: downloadTask!.whenDownloadComplete(), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return Text( + 'I will wait till this download has been completed'); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return Text('Result: ${snapshot.data}'); + } + } + }) + ], + ), + ), + ); + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/linux/CMakeLists.txt b/mobile-next/local_plugin/flutter_download_manager/example/linux/CMakeLists.txt new file mode 100644 index 0000000..74c66dd --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/CMakeLists.txt b/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugin_registrant.cc b/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugin_registrant.h b/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugins.cmake b/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/mobile-next/local_plugin/flutter_download_manager/example/linux/main.cc b/mobile-next/local_plugin/flutter_download_manager/example/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/linux/my_application.cc b/mobile-next/local_plugin/flutter_download_manager/example/linux/my_application.cc new file mode 100644 index 0000000..0ba8f43 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/linux/my_application.h b/mobile-next/local_plugin/flutter_download_manager/example/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/Flutter-Debug.xcconfig b/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/Flutter-Release.xcconfig b/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/GeneratedPluginRegistrant.swift b/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..e777c67 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import path_provider_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Podfile b/mobile-next/local_plugin/flutter_download_manager/example/macos/Podfile new file mode 100644 index 0000000..dade8df --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Podfile.lock b/mobile-next/local_plugin/flutter_download_manager/example/macos/Podfile.lock new file mode 100644 index 0000000..c21cdc7 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - FlutterMacOS (1.0.0) + - path_provider_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 + path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + +PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c + +COCOAPODS: 1.11.3 diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner.xcodeproj/project.pbxproj b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ba62462 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 13273F3A47F9BA64C39744C3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34AD632B3819122E7B1EE690 /* Pods_Runner.framework */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 09A9E8DED8279A6E340A0326 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 34AD632B3819122E7B1EE690 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3CFE4CD1DD51790124074DB0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A9FF21029585A92E61319F4E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 13273F3A47F9BA64C39744C3 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 93B8D220809078DB4BEFB739 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 93B8D220809078DB4BEFB739 /* Pods */ = { + isa = PBXGroup; + children = ( + A9FF21029585A92E61319F4E /* Pods-Runner.debug.xcconfig */, + 3CFE4CD1DD51790124074DB0 /* Pods-Runner.release.xcconfig */, + 09A9E8DED8279A6E340A0326 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 34AD632B3819122E7B1EE690 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 3DA5FE460F8646DA5CCCFE9A /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 5D48CCBE43BF75C4A535A1EE /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 3DA5FE460F8646DA5CCCFE9A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 5D48CCBE43BF75C4A535A1EE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..fb7259e --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/AppDelegate.swift b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Base.lproj/MainMenu.xib b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/AppInfo.xcconfig b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..8b42559 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Debug.xcconfig b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Release.xcconfig b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Warnings.xcconfig b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/DebugProfile.entitlements b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..c946719 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Info.plist b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/MainFlutterWindow.swift b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Release.entitlements b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/pubspec.yaml b/mobile-next/local_plugin/flutter_download_manager/example/pubspec.yaml new file mode 100644 index 0000000..6c779d7 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/pubspec.yaml @@ -0,0 +1,94 @@ +name: example +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=2.18.0-271.2.beta <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + path_provider: + flutter_download_manager: + path: ../ + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/CMakeLists.txt b/mobile-next/local_plugin/flutter_download_manager/example/windows/CMakeLists.txt new file mode 100644 index 0000000..c027074 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/CMakeLists.txt b/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..930d207 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugin_registrant.cc b/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugin_registrant.h b/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugins.cmake b/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/CMakeLists.txt b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..17411a8 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/Runner.rc b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/Runner.rc new file mode 100644 index 0000000..0f5c085 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/flutter_window.cpp b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..b43b909 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/flutter_window.h b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/main.cpp b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/main.cpp new file mode 100644 index 0000000..bcb57b0 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/resource.h b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/resources/app_icon.ico b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/resources/app_icon.ico differ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/runner.exe.manifest b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/utils.cpp b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/utils.cpp new file mode 100644 index 0000000..f5bf9fa --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/utils.h b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/win32_window.cpp b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..c10f08d --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/win32_window.h b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/win32_window.h new file mode 100644 index 0000000..17ba431 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/mobile-next/local_plugin/flutter_download_manager/files/screenshot1.png b/mobile-next/local_plugin/flutter_download_manager/files/screenshot1.png new file mode 100644 index 0000000..579f4d4 Binary files /dev/null and b/mobile-next/local_plugin/flutter_download_manager/files/screenshot1.png differ diff --git a/mobile-next/local_plugin/flutter_download_manager/lib/flutter_download_manager.dart b/mobile-next/local_plugin/flutter_download_manager/lib/flutter_download_manager.dart new file mode 100644 index 0000000..6bf8b0c --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/lib/flutter_download_manager.dart @@ -0,0 +1,6 @@ +library flutter_download_manager; + +export 'src/downloader.dart'; +export 'src/download_request.dart'; +export 'src/download_status.dart'; +export 'src/download_task.dart'; diff --git a/mobile-next/local_plugin/flutter_download_manager/lib/src/download_request.dart b/mobile-next/local_plugin/flutter_download_manager/lib/src/download_request.dart new file mode 100644 index 0000000..80c4af3 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/lib/src/download_request.dart @@ -0,0 +1,24 @@ +import 'package:dio/dio.dart'; + +class DownloadRequest { + final String url; + final String path; + var cancelToken = CancelToken(); + var forceDownload = false; + + DownloadRequest( + this.url, + this.path, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DownloadRequest && + runtimeType == other.runtimeType && + url == other.url && + path == other.path; + + @override + int get hashCode => url.hashCode ^ path.hashCode; +} diff --git a/mobile-next/local_plugin/flutter_download_manager/lib/src/download_status.dart b/mobile-next/local_plugin/flutter_download_manager/lib/src/download_status.dart new file mode 100644 index 0000000..4d7ef0a --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/lib/src/download_status.dart @@ -0,0 +1,21 @@ +enum DownloadStatus { queued, downloading, completed, failed, paused, canceled } + +extension DownloadStatusExtension on DownloadStatus { + bool get isCompleted { + switch (this) { + case DownloadStatus.queued: + return false; + case DownloadStatus.downloading: + return false; + case DownloadStatus.paused: + return false; + case DownloadStatus.completed: + return true; + case DownloadStatus.failed: + return true; + + case DownloadStatus.canceled: + return true; + } + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/lib/src/download_task.dart b/mobile-next/local_plugin/flutter_download_manager/lib/src/download_task.dart new file mode 100644 index 0000000..99ceccd --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/lib/src/download_task.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_download_manager/flutter_download_manager.dart'; + +class DownloadTask { + final DownloadRequest request; + ValueNotifier status = ValueNotifier(DownloadStatus.queued); + ValueNotifier progress = ValueNotifier(0); + + DownloadTask( + this.request, + ); + + Future whenDownloadComplete( + {Duration timeout = const Duration(hours: 2)}) async { + var completer = Completer(); + + if (status.value.isCompleted) { + completer.complete(status.value); + } + + var listener; + listener = () { + if (status.value.isCompleted) { + completer.complete(status.value); + status.removeListener(listener); + } + }; + + status.addListener(listener); + + return completer.future.timeout(timeout); + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/lib/src/downloader.dart b/mobile-next/local_plugin/flutter_download_manager/lib/src/downloader.dart new file mode 100644 index 0000000..b7f8b49 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/lib/src/downloader.dart @@ -0,0 +1,361 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_download_manager/flutter_download_manager.dart'; + +class DownloadManager { + final Map _cache = {}; + final Queue _queue = Queue(); + var dio = Dio(); + static const partialExtension = ".partial"; + static const tempExtension = ".temp"; + + static final DownloadManager _dm = new DownloadManager._internal(); + + DownloadManager._internal(); + + factory DownloadManager() { + return _dm; + } + + void Function(int, int) createCallback(url, int partialFileLength) => (int received, int total) { + getDownload(url)?.progress.value = (received + partialFileLength) / (total + partialFileLength); + + if (total == -1) {} + }; + + Future download(String url, String savePath, cancelToken, {forceDownload = false}) async { + late String partialFilePath; + late File partialFile; + try { + var task = getDownload(url); + + if (task == null || task.status.value == DownloadStatus.canceled) { + return; + } + setStatus(task, DownloadStatus.downloading); + + if (kDebugMode) { + print(url); + } + var file = File(savePath.toString()); + partialFilePath = savePath + partialExtension; + partialFile = File(partialFilePath); + + var fileExist = await file.exists(); + var partialFileExist = await partialFile.exists(); + + if (fileExist) { + if (kDebugMode) { + print("File Exists"); + } + setStatus(task, DownloadStatus.completed); + } else if (partialFileExist) { + if (kDebugMode) { + print("Partial File Exists"); + } + + var partialFileLength = await partialFile.length(); + + var response = await dio.download(url, partialFilePath + tempExtension, + onReceiveProgress: createCallback(url, partialFileLength), + options: Options( + headers: {HttpHeaders.rangeHeader: 'bytes=$partialFileLength-'}, + ), + cancelToken: cancelToken, + deleteOnError: false); + + if (response.statusCode == HttpStatus.partialContent) { + var ioSink = partialFile.openWrite(mode: FileMode.writeOnlyAppend); + var _f = File(partialFilePath + tempExtension); + await ioSink.addStream(_f.openRead()); + await _f.delete(); + await ioSink.close(); + await partialFile.rename(savePath); + + setStatus(task, DownloadStatus.completed); + } + } else { + var response = await dio.download(url, partialFilePath, + onReceiveProgress: createCallback(url, 0), cancelToken: cancelToken, deleteOnError: false); + + if (response.statusCode == HttpStatus.ok) { + await partialFile.rename(savePath); + setStatus(task, DownloadStatus.completed); + } + } + } catch (e) { + var task = getDownload(url)!; + + if (task.status.value != DownloadStatus.canceled && task.status.value != DownloadStatus.paused) { + setStatus(task, DownloadStatus.failed); + + if (_queue.isNotEmpty) { + _startExecution(); + } + rethrow; + } else if (task.status.value == DownloadStatus.paused) { + final ioSink = partialFile.openWrite(mode: FileMode.writeOnlyAppend); + final f = File(partialFilePath + tempExtension); + if (await f.exists()) { + await ioSink.addStream(f.openRead()); + } + await ioSink.close(); + } + } + + if (_queue.isNotEmpty) { + _startExecution(); + } + } + + void disposeNotifiers(DownloadTask task) {} + + void setStatus(DownloadTask? task, DownloadStatus status) { + if (task != null) { + task.status.value = status; + + if (status.isCompleted) { + disposeNotifiers(task); + } + } + } + + Future addDownload(String url, String savedDir) async { + if (url.isNotEmpty) { + if (savedDir.isEmpty) { + savedDir = "."; + } + + var isDirectory = await Directory(savedDir).exists(); + var downloadFilename = isDirectory ? savedDir + Platform.pathSeparator + getFileNameFromUrl(url) : savedDir; + + return _addDownloadRequest(DownloadRequest(url, downloadFilename)); + } + return null; + } + + Future _addDownloadRequest( + DownloadRequest downloadRequest, + ) async { + if (_cache[downloadRequest.url] != null) { + if (!_cache[downloadRequest.url]!.status.value.isCompleted && + _cache[downloadRequest.url]!.request == downloadRequest) { + return _cache[downloadRequest.url]!; + } else { + _queue.remove(_cache[downloadRequest.url]); + } + } + + _queue.add(DownloadRequest(downloadRequest.url, downloadRequest.path)); + var task = DownloadTask(_queue.last); + + _cache[downloadRequest.url] = task; + + _startExecution(); + + return task; + } + + Future pauseDownload(String url) async { + if (kDebugMode) { + print("Pause Download"); + } + var task = getDownload(url)!; + setStatus(task, DownloadStatus.paused); + task.request.cancelToken.cancel(); + + _queue.remove(task.request); + } + + Future cancelDownload(String url) async { + if (kDebugMode) { + print("Cancel Download"); + } + var task = getDownload(url)!; + setStatus(task, DownloadStatus.canceled); + _queue.remove(task.request); + task.request.cancelToken.cancel(); + } + + Future resumeDownload(String url) async { + if (kDebugMode) { + print("Resume Download"); + } + var task = getDownload(url)!; + setStatus(task, DownloadStatus.downloading); + task.request.cancelToken = CancelToken(); + _queue.add(task.request); + + _startExecution(); + } + + Future removeDownload(String url) async { + cancelDownload(url); + _cache.remove(url); + } + + DownloadTask? getDownload(String url) { + return _cache[url]; + } + + Future whenDownloadComplete(String url, {Duration timeout = const Duration(hours: 2)}) async { + DownloadTask? task = getDownload(url); + + if (task != null) { + return task.whenDownloadComplete(timeout: timeout); + } else { + return Future.error("Not found"); + } + } + + List getAllDownloads() { + return _cache.values.toList(); + } + + Future addBatchDownloads(List urls, String savedDir) async { + urls.forEach((url) { + addDownload(url, savedDir); + }); + } + + List getBatchDownloads(List urls) { + return urls.map((e) => _cache[e]).toList(); + } + + Future pauseBatchDownloads(List urls) async { + urls.forEach((element) { + pauseDownload(element); + }); + } + + Future cancelBatchDownloads(List urls) async { + urls.forEach((element) { + cancelDownload(element); + }); + } + + Future resumeBatchDownloads(List urls) async { + urls.forEach((element) { + resumeDownload(element); + }); + } + + ValueNotifier getBatchDownloadProgress(List urls) { + ValueNotifier progress = ValueNotifier(0); + var total = urls.length; + + if (total == 0) { + return progress; + } + + if (total == 1) { + return getDownload(urls.first)?.progress ?? progress; + } + + var progressMap = Map(); + + urls.forEach((url) { + DownloadTask? task = getDownload(url); + + if (task != null) { + progressMap[url] = 0.0; + + if (task.status.value.isCompleted) { + progressMap[url] = 1.0; + progress.value = progressMap.values.sum / total; + } + + var progressListener; + progressListener = () { + progressMap[url] = task.progress.value; + progress.value = progressMap.values.sum / total; + }; + + task.progress.addListener(progressListener); + + var listener; + listener = () { + if (task.status.value.isCompleted) { + progressMap[url] = 1.0; + progress.value = progressMap.values.sum / total; + task.status.removeListener(listener); + task.progress.removeListener(progressListener); + } + }; + + task.status.addListener(listener); + } else { + total--; + } + }); + + return progress; + } + + Future?> whenBatchDownloadsComplete(List urls, + {Duration timeout = const Duration(hours: 2)}) async { + var completer = Completer?>(); + + var completed = 0; + var total = urls.length; + + urls.forEach((url) { + DownloadTask? task = getDownload(url); + + if (task != null) { + if (task.status.value.isCompleted) { + completed++; + + if (completed == total) { + completer.complete(getBatchDownloads(urls)); + } + } + + var listener; + listener = () { + if (task.status.value.isCompleted) { + completed++; + + if (completed == total) { + completer.complete(getBatchDownloads(urls)); + task.status.removeListener(listener); + } + } + }; + + task.status.addListener(listener); + } else { + total--; + + if (total == 0) { + completer.complete(null); + } + } + }); + + return completer.future.timeout(timeout); + } + + void _startExecution() async { + if (_queue.isEmpty) { + return; + } + + while (_queue.isNotEmpty) { + var currentRequest = _queue.removeFirst(); + + download(currentRequest.url, currentRequest.path, currentRequest.cancelToken); + + await Future.delayed(Duration(milliseconds: 500), null); + } + } + + String getFileNameFromUrl(String url) { + return url.split('/').last; + } +} diff --git a/mobile-next/local_plugin/flutter_download_manager/pubspec.lock b/mobile-next/local_plugin/flutter_download_manager/pubspec.lock new file mode 100644 index 0000000..ca385bc --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/pubspec.lock @@ -0,0 +1,237 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + dio: + dependency: "direct main" + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/mobile-next/local_plugin/flutter_download_manager/pubspec.yaml b/mobile-next/local_plugin/flutter_download_manager/pubspec.yaml new file mode 100644 index 0000000..5c35243 --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/pubspec.yaml @@ -0,0 +1,22 @@ +name: flutter_download_manager +description: Flutter Download Manager is a Cross-Platform file downloader with resumeable, parallel and batch Download support. Also be notified of download status and progress updates +version: 0.5.5 +homepage: https://github.com/nabil6391/flutter_download_manager + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + dio: + collection: + +dev_dependencies: + flutter_test: + sdk: flutter + lints: ^3.0.0 + +flutter: + diff --git a/mobile-next/local_plugin/flutter_download_manager/test/flutter_download_manager_test.dart b/mobile-next/local_plugin/flutter_download_manager/test/flutter_download_manager_test.dart new file mode 100644 index 0000000..f6a0edd --- /dev/null +++ b/mobile-next/local_plugin/flutter_download_manager/test/flutter_download_manager_test.dart @@ -0,0 +1,214 @@ +import 'package:flutter_download_manager/flutter_download_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + var url2 = "http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg"; + + var url3 = + 'https://cdn.jsdelivr.net/gh/flutterchina/flutter-in-action@1.0/docs/imgs/book.jpg'; + var url = "http://app01.78x56.com/Xii_2021-03-13%2010%EF%BC%9A41.ipa"; + var url4 = + "https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/sample-mp4-file.mp4"; + var url5 = + "https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/Sample-Video-File-For-Testing.mp4"; + var url6 = "https://static.gtaf.org/v1/quran/static/translation-db/10"; + + test('future download', () async { + var dl = DownloadManager(); + + DownloadTask? task = await dl.addDownload(url4, "./test.mp4"); + + task?.status.addListener(() { + print(task.status.value); + }); + + task?.progress.addListener(() { + print(task.progress.value); + }); + + await dl.whenDownloadComplete(url4); + }); + + test('parallel download', () async { + var dl = DownloadManager(); + + DownloadTask? task = await dl.addDownload(url2, "./test2.ipa"); + DownloadTask? task2 = await dl.addDownload(url3, "./test3.ipa"); + DownloadTask? task3 = await dl.addDownload(url, "./test.ipa"); + + task?.status.addListener(() { + print(task.status.value); + }); + + task2?.status.addListener(() { + print(task2.status.value); + }); + + task3?.status.addListener(() { + print(task3.status.value); + }); + + await dl.whenBatchDownloadsComplete([url, url2, url3], timeout: Duration(seconds: 20)); + }); + + test('cancel download', () async { + var dl = DownloadManager(); + + DownloadTask? task = await dl.addDownload(url5, "./test2.mp4"); + + Future.delayed(Duration(milliseconds: 500), () { + dl.cancelDownload(url5); + }); + + task?.status.addListener(() { + print(task.status.value); + }); + + await Future.delayed(Duration(seconds: 10), null); + + assert(task?.status.value == DownloadStatus.canceled); + }); + + test('pause and resume download', () async { + var dl = DownloadManager(); + + DownloadTask? task = await dl.addDownload(url5, "./test2.mp4"); + + Future.delayed(Duration(milliseconds: 500), () { + dl.pauseDownload(url5); + }); + + Future.delayed(Duration(milliseconds: 1000), () { + dl.resumeDownload(url5); + }); + + task?.status.addListener(() { + print(task.status.value); + }); + + await Future.delayed(Duration(seconds: 20), null); + + assert(task?.status.value == DownloadStatus.completed); + }); + + test('handle empty url', () async { + var dl = DownloadManager(); + + var url = ""; + DownloadTask? task = await dl.addDownload(url, ""); + + task?.status.addListener(() { + print(task.status.value); + }); + + var error = dl.whenDownloadComplete(url); + }); + + test('handle empty path', () async { + var dl = DownloadManager(); + + DownloadTask? task = await dl.addDownload(url3, ""); + + task?.status.addListener(() { + print(task.status.value); + }); + + await dl.whenDownloadComplete(url3); + }); + + test('handle url with empty extension', () async { + var dl = DownloadManager(); + + DownloadTask? task = await dl.addDownload(url6, ""); + + task?.status.addListener(() { + print(task.status.value); + }); + + await dl.whenDownloadComplete(url6); + }); + + test('download in batch', () async { + var dl = DownloadManager(); + + var urls = []; + urls.add(url2); + urls.add(url3); + urls.add(url); + + await dl.addDownload(url2, "./test2.ipa"); + await dl.addDownload(url3, "./test3.ipa"); + await dl.addDownload(url, "./test.ipa"); + + var downloadProgress = dl.getBatchDownloadProgress(urls); + + downloadProgress.addListener(() { + print(downloadProgress.value); + }); + + await dl.whenBatchDownloadsComplete(urls); + }); + + test('download in batch by setting the savedDirectory only', () async { + var dl = DownloadManager(); + + var urls = []; + urls.add(url2); + urls.add(url3); + urls.add(url); + + dl.addBatchDownloads(urls, "./"); + + var downloadProgress = dl.getBatchDownloadProgress(urls); + + downloadProgress.addListener(() { + print(downloadProgress.value); + }); + + await dl.whenBatchDownloadsComplete(urls); + }); + + test('cancel a batched download', () async { + var dl = DownloadManager(); + + var urls = []; + urls.add(url6); + urls.add(url5); + urls.add(url); + dl.addBatchDownloads(urls, "."); + + var downloads = dl.getBatchDownloads(urls); + + downloads.forEach((task) { + task?.status.addListener(() { + print(task.request.url + ", " + task.status.value.toString()); + }); + }); + + dl.cancelBatchDownloads(urls); + + await dl.whenBatchDownloadsComplete(urls); + }); + + test('cancel a single item in a batched download', () async { + var dl = DownloadManager(); + + var urls = []; + urls.add(url4); + urls.add(url3); + urls.add(url); + dl.addBatchDownloads(urls, ""); + + var downloads = dl.getBatchDownloads(urls); + + downloads.forEach((task) { + task?.status.addListener(() { + print(task.status.value); + }); + }); + + dl.cancelDownload(url3); + + await dl.whenBatchDownloadsComplete(urls); + }); +} diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/.metadata b/mobile-next/local_plugin/flutter_openim_live_alert/.metadata new file mode 100644 index 0000000..a23172b --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 7e9793dee1b85a243edd0e06cb1658e98b077561 + channel: stable + +project_type: plugin diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/CHANGELOG.md b/mobile-next/local_plugin/flutter_openim_live_alert/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/LICENSE b/mobile-next/local_plugin/flutter_openim_live_alert/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/README.md b/mobile-next/local_plugin/flutter_openim_live_alert/README.md new file mode 100644 index 0000000..d69e05b --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/README.md @@ -0,0 +1,5 @@ +# flutter_openim_live_alert + +#### 仅支持Android端,当app处于后台收到音视频通话邀请,这时候会在桌面弹窗一个接听对话框。 + + diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/analysis_options.yaml b/mobile-next/local_plugin/flutter_openim_live_alert/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/8.9/fileChanges/last-build.bin b/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/8.9/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/8.9/fileChanges/last-build.bin differ diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/8.9/fileHashes/fileHashes.lock b/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/8.9/fileHashes/fileHashes.lock new file mode 100644 index 0000000..4e9feca Binary files /dev/null and b/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/8.9/fileHashes/fileHashes.lock differ diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..802f772 Binary files /dev/null and b/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/buildOutputCleanup/cache.properties b/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..2521fde --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Thu Jan 02 10:14:31 HKT 2025 +gradle.version=8.9 diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/build.gradle b/mobile-next/local_plugin/flutter_openim_live_alert/android/build.gradle new file mode 100644 index 0000000..335438f --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/build.gradle @@ -0,0 +1,41 @@ +group 'io.openim.live.alert.flutter_openim_live_alert' +version '1.0' + +buildscript { + repositories { + maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + } +} + +rootProject.allprojects { + repositories { + maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } + maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 16 + } +} diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/settings.gradle b/mobile-next/local_plugin/flutter_openim_live_alert/android/settings.gradle new file mode 100644 index 0000000..c511632 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_openim_live_alert' diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/AndroidManifest.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f985f5e --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/FlutterOpenimLiveAlertPlugin.java b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/FlutterOpenimLiveAlertPlugin.java new file mode 100644 index 0000000..533ba1b --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/FlutterOpenimLiveAlertPlugin.java @@ -0,0 +1,95 @@ +package io.openim.live.alert.flutter_openim_live_alert; + +import android.app.Activity; +import android.content.Context; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.openim.live.alert.flutter_openim_live_alert.services.CallService; +import io.openim.live.alert.flutter_openim_live_alert.utils.Utils; + +/** + * FlutterOpenimLiveAlertPlugin + */ +public class FlutterOpenimLiveAlertPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + public static MethodChannel channel; + private Context context; + private Activity activity; + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + context = flutterPluginBinding.getApplicationContext(); + channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "flutter_openim_live_alert"); + channel.setMethodCallHandler(this); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + switch (call.method) { + case "showLiveAlert": + String title = call.argument("title"); + String rejectText = call.argument("rejectText"); + String acceptText = call.argument("acceptText"); + CallService.startService(activity, title, rejectText, acceptText); + break; + case "closeLiveAlert": + CallService.stopService(activity); + break; + case "closeLiveAlertAndMoveTaskToFront": { + CallService.stopService(activity); + String packageName = call.argument("packageName"); + String activityName = call.argument("activityName"); + if (null == packageName) { + packageName = activity.getPackageName(); + } + if (null == activityName) { + activityName = packageName + ".MainActivity"; + } + Utils.moveTaskToFront(activity, activity.getPackageName(), activityName); + break; + } + default: + result.notImplemented(); + break; + } + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + activity = null; + context = null; + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + activity = binding.getActivity(); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + activity = null; + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + activity = binding.getActivity(); + } + + @Override + public void onDetachedFromActivity() { + activity = null; + } + +} diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/services/CallService.java b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/services/CallService.java new file mode 100644 index 0000000..191a502 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/services/CallService.java @@ -0,0 +1,128 @@ +package io.openim.live.alert.flutter_openim_live_alert.services; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.os.Build; +import android.os.IBinder; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import io.openim.live.alert.flutter_openim_live_alert.FlutterOpenimLiveAlertPlugin; +import io.openim.live.alert.flutter_openim_live_alert.R; + +public class CallService extends Service { + private WindowManager mWindowManager; + private View mFloatingView; + private TextView mTitleView; + private Button mRejectButton; + private TextView mAcceptButton; + private final Point szWindow = new Point(); + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + // init WindowManager + mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + getWindowManagerDefaultDisplay(); + + // Init LayoutInflater + LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + + // Inflate the floating view layout we created + mFloatingView = inflater.inflate(R.layout.call, null); + + mTitleView = mFloatingView.findViewById(R.id.tv_title); + mRejectButton = mFloatingView.findViewById(R.id.btn_reject); + mAcceptButton = mFloatingView.findViewById(R.id.btn_accept); + mRejectButton.setOnClickListener(view -> { + FlutterOpenimLiveAlertPlugin.channel.invokeMethod("reject", null); + }); + mAcceptButton.setOnClickListener(view -> { + FlutterOpenimLiveAlertPlugin.channel.invokeMethod("accept", null); + }); + // Add the view to the window. + WindowManager.LayoutParams params; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + } else { + params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + } + + // Specify the view position + params.gravity = Gravity.TOP | Gravity.CENTER; + + // Initially view will be added to top-left corner, you change x-y coordinates according to your need + params.x = 0; + params.y = 20; + + // Add the view to the window + mWindowManager.addView(mFloatingView, params); + } + + private void getWindowManagerDefaultDisplay() { + mWindowManager.getDefaultDisplay().getSize(szWindow); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mFloatingView != null) { + mWindowManager.removeView(mFloatingView); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String title = intent.getStringExtra("title"); + String rejectText = intent.getStringExtra("rejectText"); + String acceptText = intent.getStringExtra("acceptText"); + if (null != title) { + mTitleView.setText(title); + } + if (null != rejectText) { + mRejectButton.setText(rejectText); + } + if (null != acceptText) { + mAcceptButton.setText(acceptText); + } + return START_STICKY; + } + + public static void startService(Context activity, String title, String rejectText, String acceptText) { + Intent intent = new Intent(activity, CallService.class); + intent.putExtra("title", title); + intent.putExtra("rejectText", rejectText); + intent.putExtra("acceptText", acceptText); + activity.startService(intent); + } + + public static void stopService(Context activity) { + Intent intent = new Intent(activity, CallService.class); + activity.stopService(intent); + } +} diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/utils/Utils.java b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/utils/Utils.java new file mode 100644 index 0000000..1cd00cd --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/java/io/openim/live/alert/flutter_openim_live_alert/utils/Utils.java @@ -0,0 +1,79 @@ +package io.openim.live.alert.flutter_openim_live_alert.utils; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import java.util.List; + +public class Utils { + + public static boolean isInstalled(Context context, String packageName) { + try { + final PackageManager packageManager = context.getPackageManager(); + // 获取所有已安装程序的包信息 + List infoList = packageManager.getInstalledPackages(0); + for (int i = 0; i < infoList.size(); i++) { + if (infoList.get(i).packageName.equalsIgnoreCase(packageName)) + return true; + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + + public static boolean isRunningForeground(Context context) { + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (null != activityManager) { + List appProcessInfos = activityManager.getRunningAppProcesses(); + // 枚举进程 + for (ActivityManager.RunningAppProcessInfo appProcessInfo : appProcessInfos) { + if (appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + if (appProcessInfo.processName.equals(context.getApplicationInfo().processName)) { + return true; + } + } + } + } + return false; + } + + public static void moveTaskToFront(Context context, String packageName, String activityName) { + //如果APP是在后台运行 + if (!isRunningForeground(context)) { + //获取ActivityManager + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (null != activityManager) { + //获得当前运行的task + List taskList = activityManager.getRunningTasks(Integer.MAX_VALUE); + for (ActivityManager.RunningTaskInfo rti : taskList) { + //找到当前应用的task,并启动task的栈顶activity,达到程序切换到前台 + if (rti.topActivity.getPackageName().equals(packageName)) { + activityManager.moveTaskToFront(rti.id, 0); +// return; + } + } + } + //若没有找到运行的task,用户结束了task或被系统释放,则重新启动 + if (null != activityName) { + startActivity(context, activityName); + } + + } + } + + public static void startActivity(Context context, String activityName) { + try { + Class clazz = Class.forName(activityName); + Intent intent = new Intent(context, clazz); +// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + context.startActivity(intent); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button.xml new file mode 100644 index 0000000..948403b --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button_nor.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button_nor.xml new file mode 100644 index 0000000..d30833a --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button_nor.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button_sel.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button_sel.xml new file mode 100644 index 0000000..e2b3ad5 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/accept_button_sel.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/call_background.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/call_background.xml new file mode 100644 index 0000000..ef9e28c --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/call_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button.xml new file mode 100644 index 0000000..c84ad14 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button_nor.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button_nor.xml new file mode 100644 index 0000000..c4b5507 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button_nor.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button_sel.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button_sel.xml new file mode 100644 index 0000000..9a1e18e --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/drawable/reject_button_sel.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/layout/call.xml b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/layout/call.xml new file mode 100644 index 0000000..c38fbb0 --- /dev/null +++ b/mobile-next/local_plugin/flutter_openim_live_alert/android/src/main/res/layout/call.xml @@ -0,0 +1,53 @@ + + + + + + + +