From 09b892c3d2d6e8b70899418d21521f066a4cefcb Mon Sep 17 00:00:00 2001 From: "sky.min" Date: Thu, 20 Aug 2026 09:25:05 +0800 Subject: [PATCH] Initial commit --- .gitignore | 20 + README.md | 380 ++++++ example/.gitignore | 43 + example/.metadata | 30 + example/README.md | 119 ++ example/analysis_options.yaml | 28 + example/android/.gitignore | 13 + example/android/app/build.gradle | 62 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 56 + .../com/example/example/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + example/android/build.gradle | 21 + example/android/gradle.properties | 8 + .../gradle/wrapper/gradle-wrapper.properties | 5 + example/android/settings.gradle | 26 + example/ios/.gitignore | 34 + example/ios/Flutter/AppFrameworkInfo.plist | 24 + example/ios/Flutter/Debug.xcconfig | 2 + example/ios/Flutter/Release.xcconfig | 2 + example/ios/Podfile | 43 + example/ios/Runner.xcodeproj/project.pbxproj | 623 ++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 101 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + example/ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 ++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 13 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + example/ios/Runner/Base.lproj/Main.storyboard | 26 + example/ios/Runner/Info.plist | 78 ++ example/ios/Runner/Runner-Bridging-Header.h | 1 + example/ios/Runner/SceneDelegate.swift | 6 + example/ios/RunnerTests/RunnerTests.swift | 12 + example/lib/global.dart | 26 + example/lib/l10n.dart | 125 ++ example/lib/main.dart | 107 ++ .../lib/services/cat_eye_monitor_service.dart | 151 +++ .../lib/services/video_talk_initializer.dart | 46 + example/lib/tabs/auth_tab.dart | 233 ++++ example/lib/tabs/device_scan_tab.dart | 181 +++ example/lib/tabs/lock_control_tab.dart | 89 ++ example/lib/tabs/video_talk_tab.dart | 1044 +++++++++++++++++ example/lib/tabs/wifi_config_tab.dart | 279 +++++ example/lib/widgets/common_widgets.dart | 63 + example/pubspec.lock | 749 ++++++++++++ example/pubspec.yaml | 45 + .../advanced/models/lock_setting_models.dart | 57 + .../advanced/models/lock_state_models.dart | 15 + lib/src/advanced/models/wifi_models.dart | 91 ++ .../starcloud_protocol_exception.dart | 10 + .../protocol/starcloud_protocol_executor.dart | 9 + .../starcloud_protocol_executor_impl.dart | 45 + .../starcloud_lock_setting_service.dart | 417 +++++++ .../starcloud_lock_state_service.dart | 64 + lib/src/advanced/starcloud_wifi_service.dart | 656 +++++++++++ lib/src/auth/starcloud_auth_service.dart | 97 ++ lib/src/core/callback_bridge.dart | 59 + lib/src/core/starcloud_exception.dart | 9 + lib/src/core/starcloud_lock_context.dart | 80 ++ .../starcloud_biometric_service.dart | 91 ++ .../credential/starcloud_card_service.dart | 51 + .../starcloud_electric_key_service.dart | 124 ++ .../starcloud_password_service.dart | 94 ++ lib/src/device/starcloud_device_service.dart | 174 +++ lib/src/record/starcloud_record_service.dart | 72 ++ lib/src/starcloud_client.dart | 49 + lib/src/talk/starcloud_talk_service.dart | 271 +++++ lib/starcloud_flutter_sdk.dart | 31 + pubspec.lock | 551 +++++++++ pubspec.yaml | 28 + scripts/package_sdk.sh | 77 ++ 104 files changed, 8459 insertions(+) create mode 100755 .gitignore create mode 100644 README.md create mode 100755 example/.gitignore create mode 100755 example/.metadata create mode 100644 example/README.md create mode 100755 example/analysis_options.yaml create mode 100755 example/android/.gitignore create mode 100755 example/android/app/build.gradle create mode 100755 example/android/app/src/debug/AndroidManifest.xml create mode 100755 example/android/app/src/main/AndroidManifest.xml create mode 100755 example/android/app/src/main/kotlin/com/example/example/MainActivity.kt create mode 100755 example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100755 example/android/app/src/main/res/drawable/launch_background.xml create mode 100755 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100755 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100755 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100755 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100755 example/android/app/src/main/res/values-night/styles.xml create mode 100755 example/android/app/src/main/res/values/styles.xml create mode 100755 example/android/app/src/profile/AndroidManifest.xml create mode 100755 example/android/build.gradle create mode 100755 example/android/gradle.properties create mode 100755 example/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 example/android/settings.gradle create mode 100755 example/ios/.gitignore create mode 100755 example/ios/Flutter/AppFrameworkInfo.plist create mode 100755 example/ios/Flutter/Debug.xcconfig create mode 100755 example/ios/Flutter/Release.xcconfig create mode 100755 example/ios/Podfile create mode 100755 example/ios/Runner.xcodeproj/project.pbxproj create mode 100755 example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100755 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100755 example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100755 example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100755 example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100755 example/ios/Runner/AppDelegate.swift create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100755 example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100755 example/ios/Runner/Base.lproj/Main.storyboard create mode 100755 example/ios/Runner/Info.plist create mode 100755 example/ios/Runner/Runner-Bridging-Header.h create mode 100755 example/ios/Runner/SceneDelegate.swift create mode 100755 example/ios/RunnerTests/RunnerTests.swift create mode 100755 example/lib/global.dart create mode 100755 example/lib/l10n.dart create mode 100755 example/lib/main.dart create mode 100644 example/lib/services/cat_eye_monitor_service.dart create mode 100644 example/lib/services/video_talk_initializer.dart create mode 100755 example/lib/tabs/auth_tab.dart create mode 100755 example/lib/tabs/device_scan_tab.dart create mode 100755 example/lib/tabs/lock_control_tab.dart create mode 100755 example/lib/tabs/video_talk_tab.dart create mode 100755 example/lib/tabs/wifi_config_tab.dart create mode 100755 example/lib/widgets/common_widgets.dart create mode 100755 example/pubspec.lock create mode 100755 example/pubspec.yaml create mode 100755 lib/src/advanced/models/lock_setting_models.dart create mode 100755 lib/src/advanced/models/lock_state_models.dart create mode 100755 lib/src/advanced/models/wifi_models.dart create mode 100755 lib/src/advanced/protocol/starcloud_protocol_exception.dart create mode 100755 lib/src/advanced/protocol/starcloud_protocol_executor.dart create mode 100755 lib/src/advanced/protocol/starcloud_protocol_executor_impl.dart create mode 100755 lib/src/advanced/starcloud_lock_setting_service.dart create mode 100755 lib/src/advanced/starcloud_lock_state_service.dart create mode 100755 lib/src/advanced/starcloud_wifi_service.dart create mode 100755 lib/src/auth/starcloud_auth_service.dart create mode 100755 lib/src/core/callback_bridge.dart create mode 100755 lib/src/core/starcloud_exception.dart create mode 100755 lib/src/core/starcloud_lock_context.dart create mode 100755 lib/src/credential/starcloud_biometric_service.dart create mode 100755 lib/src/credential/starcloud_card_service.dart create mode 100755 lib/src/credential/starcloud_electric_key_service.dart create mode 100755 lib/src/credential/starcloud_password_service.dart create mode 100755 lib/src/device/starcloud_device_service.dart create mode 100755 lib/src/record/starcloud_record_service.dart create mode 100755 lib/src/starcloud_client.dart create mode 100755 lib/src/talk/starcloud_talk_service.dart create mode 100755 lib/starcloud_flutter_sdk.dart create mode 100755 pubspec.lock create mode 100755 pubspec.yaml create mode 100755 scripts/package_sdk.sh diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..07fb728 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Flutter/Dart build artifacts +build/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies + +# SDK delivery output +dist/ + +# IDE +.idea/ +*.iml +.vscode/ + +# macOS +.DS_Store + +# FVM Version Cache +.fvm/ +.fvmrc diff --git a/README.md b/README.md new file mode 100644 index 0000000..4492789 --- /dev/null +++ b/README.md @@ -0,0 +1,380 @@ +# StarCloud Flutter SDK + +Flutter 智能锁接入 SDK。SDK 封装了星云账户、蓝牙扫描和配对、锁管理、凭证管理、网关 WiFi 配网、锁状态/设置,以及视频对讲信令与媒体数据事件。 + +> 当前交付面向 Android 和 iOS;不提供 macOS、Windows、Linux 或 Web 的桌面端支持。 + +## 交付内容 + +实际交付根目录包含下列五个同级目录。`flutter_sdk` 通过相对路径依赖前三个底层包;`video_decode_plugin` 是示例工程的视频渲染可选组件: + +```text +vendor/ +├── flutter_sdk/ # 本 SDK +├── starlock_ble/ # 蓝牙协议与设备连接层 +├── starcloud/ # 云端、设备与领域模型 +├── starchart/ # 视频对讲信令与传输层 +└── video_decode_plugin/ # 可选:示例使用的 H264 FFmpeg 渲染插件 +``` + +`example/` 是可运行的集成示例,覆盖常用的接入路径。不要将示例中的测试账号、日志或 UI 直接用于生产环境。 + +## 环境与平台要求 + +| 项目 | 要求 | +| --- | --- | +| Flutter | 3.19.6 或更高版本 | +| Dart | 3.3.0 或更高版本 | +| Android | `minSdkVersion 23`,真机蓝牙调试 | +| iOS | iOS 12.0 或更高版本,真机调试 | +| 网络 | 可访问已获授权的星云服务和视频对讲服务 | + +蓝牙扫描、配对、门锁控制、WiFi 配网、麦克风和相机相关能力均需要在真机验证。模拟器不具备完整的蓝牙能力。 + +## 安装 + +将 `flutter_sdk`、`starlock_ble`、`starcloud`、`starchart` 放入宿主 App 的 `vendor/`。需要使用示例同款 FFmpeg 视频渲染时,再一并放入 `video_decode_plugin`。 + +宿主 `pubspec.yaml` 的最小依赖只需要声明 `flutter_sdk`: + +```yaml +dependencies: + flutter: + sdk: flutter + + flutter_sdk: + path: vendor/flutter_sdk +``` + +`flutter_sdk` 的 `pubspec.yaml` 已通过 `path: ../starlock_ble`、`../starcloud`、`../starchart` 引用同级底层包,因此宿主执行一次依赖安装即可递归解析全部核心包: + +```bash +fvm flutter pub get +``` + +如需将示例的 H264 FFmpeg 解码渲染带入宿主 App,额外声明: + +```yaml + video_decode_plugin: + path: vendor/video_decode_plugin +``` + +然后仍只需要在宿主 App 根目录再执行一次 `fvm flutter pub get`。**不需要**为正常集成专门进入 `video_decode_plugin`、`starlock_ble`、`starcloud` 或 `starchart` 逐个执行 `pub get`;只有开发/测试这些包自身或运行它们的独立 example 时才需要。 + +如果项目不使用 FVM,请使用与本 SDK 兼容的 Flutter 版本执行 `flutter pub get`。 + +## 平台配置 + +### Android + +确认 `android/app/build.gradle` 的最小版本不低于 23,并在 `android/app/src/main/AndroidManifest.xml` 添加按业务实际需要的权限: + +```xml + + + + + + + + + + +``` + +- 仅使用锁管理时,麦克风、相机权限可不声明。 +- Android 12 及以上需要在运行时申请 `BLUETOOTH_SCAN`、`BLUETOOTH_CONNECT`。 +- 若视频对讲服务使用 `http://` 地址,Android 需要允许明文流量;生产环境建议使用 HTTPS。 + +### iOS + +在 `ios/Podfile` 中设置: + +```ruby +platform :ios, '12.0' +``` + +在 `ios/Runner/Info.plist` 添加适合实际业务的用途说明: + +```xml +NSBluetoothAlwaysUsageDescription +用于扫描和连接智能锁。 +NSBluetoothPeripheralUsageDescription +用于连接智能锁设备。 +NSLocationWhenInUseUsageDescription +用于发现附近的蓝牙智能锁。 +NSMicrophoneUsageDescription +用于视频对讲。 +NSCameraUsageDescription +用于视频对讲和扫码功能。 +``` + +完成后执行: + +```bash +cd ios && pod install && cd .. +``` + +## 接入顺序 + +常规业务建议严格按以下顺序执行: + +```text +bootstrap + → auth.init + → createCloudUser / setCloudAccounts + → 扫描、配对或获取锁列表 + → 构建 StarCloudLockContext + → 锁控制、凭证、WiFi 配网或视频对讲 +``` + +`CloudUserInfo` 是扫描、配对及云端锁管理的前提;`lock.skychip.top` 的 WiFi 授权登录令牌仅用于网关配网,不能替代 SDK 云账户。 + +## 快速接入 + +### 1. 创建客户端并初始化认证 + +应用启动时只调用一次 `bootstrap()`: + +```dart +import 'package:flutter/widgets.dart'; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + StarCloudClient.bootstrap(); + runApp(const MyApp()); +} +``` + +从星云后台注册应用并获取 `clientId`、`clientSecret`。不要把密钥提交到代码仓库;建议由业务后端下发或通过构建参数注入: + +```dart +const clientId = String.fromEnvironment('STARCLOUD_CLIENT_ID'); +const clientSecret = String.fromEnvironment('STARCLOUD_CLIENT_SECRET'); +const environmentUrl = String.fromEnvironment('STARCLOUD_ENV_URL'); + +StarCloudClient.instance.auth.init( + clientId: clientId, + clientSecret: clientSecret, + environmentUrl: environmentUrl, +); +``` + +例如: + +```bash +fvm flutter run \ + --dart-define=STARCLOUD_CLIENT_ID=your_client_id \ + --dart-define=STARCLOUD_CLIENT_SECRET=your_client_secret \ + --dart-define=STARCLOUD_ENV_URL=https://your-authorized-host +``` + +### 2. 设置 SDK 云账户 + +生产环境应由业务后台保存、恢复并下发合法的 `CloudUserInfo`。仅在联调或示例中,可创建临时云账户: + +```dart +final cloudUser = await StarCloudClient.instance.auth.createCloudUser(); +StarCloudClient.instance.auth.setCloudAccounts([cloudUser]); +``` + +若有多个云账户,使用 `setCloudAccounts` 一次性设置,并在设备业务 API 中按需传入 `cloudUid`。 + +### 3. 扫描、配对与锁控制 + +```dart +final devices = await StarCloudClient.instance.device.scanDeviceList( + timeout: 10, + onScan: (list) { + // 可持续刷新扫描列表 + }, +); + +if (devices.isEmpty) return; + +final lock = await StarCloudClient.instance.device.pairDevice( + scanResult: devices.first, +); + +await StarCloudClient.instance.device.openLock(lockId: lock.lockId); +await StarCloudClient.instance.device.closeLock(lockId: lock.lockId); +``` + +扫描页应直接为每个扫描结果提供“配对”操作;若本地已配对锁的 `mac` 或蓝牙设备名与扫描结果一致,应展示“已配对”而非再次配对。离开页面或用户取消时调用: + +```dart +await StarCloudClient.instance.device.stopScan(); +await StarCloudClient.instance.device.disconnect(); +``` + +已配对设备可从云端获取: + +```dart +final list = await StarCloudClient.instance.device.getDeviceList( + pageNo: 1, + pageSize: 20, +); +final latestLock = await StarCloudClient.instance.device.getDevice( + lockId: lock.lockId, +); +``` + +### 4. 使用 `StarCloudLockContext` + +高级 BLE 接口(状态、功能位、网关配网)需要锁身份、蓝牙密钥和本地握手 token。请从已配对或已查询到的 `StarCloudLock` 构建上下文,不要自行拼装密钥: + +```dart +final context = await StarCloudLockContext.fromLock(latestLock); + +final state = await StarCloudClient.instance.lockState + .readLockStateByContext(context); + +await StarCloudClient.instance.lockSetting.setRemoteUnlockByContext( + context, + enabled: true, +); +``` + +上下文依赖已经完成的蓝牙认证握手;token 缺失或过期时,应重新连接/配对并获取最新锁信息后重试。 + +### 5. 网关 WiFi 配网 + +网关配网需要:已配对的网关锁、SDK 云账户、锁主账号或管理员账号的 WiFi 授权令牌,以及 App 视频对讲 PeerID。 + +先登录 WiFi 授权服务: + +```dart +final accessToken = await StarCloudClient.instance.auth.loginLockService( + username: username, + password: password, + deviceBrand: 'android', + deviceModel: 'model-name', + deviceVersion: 'os-version', +); +``` + +初始化视频对讲以取得 App PeerID(见下一节),随后执行: + +```dart +final context = await StarCloudLockContext.fromLock(lock); +final reply = await StarCloudClient.instance.wifi.configureWifiByContext( + context, + ssid: ssid, + password: password, + lockServiceAccessToken: accessToken, + deviceName: lock.bluetooth.bluetoothDeviceName, + mac: lock.mac, +); + +// 配网响应包含锁端 peerId;仍需轮询云端确认锁上线。 +final status = await StarCloudClient.instance.wifi.getGatewayStatus( + lockId: lock.bluetooth.bluetoothDeviceName, + userId: context.userId, + deviceName: lock.bluetooth.bluetoothDeviceName, + mac: lock.mac, +); +``` + +`configureWifiByContext` 会获取网关配置、通过 BLE 下发,并向云端提交设备网络信息。BLE 返回成功只表示流程已接受,不代表锁已联网;请使用 `getDevice(lockId: ...)` 轮询 `isOnline` 与 `peerId`。 + +### 6. 视频对讲 + +视频对讲服务只负责注册、呼叫和事件分发;H264 解码渲染、G711 编解码、麦克风采集与权限处理由宿主 App 集成。示例工程使用本地 `video_decode_plugin` 的 FFmpeg 后端进行 H264 解码渲染。 + +```dart +await StarCloudClient.instance.talk.init( + productName: 'YourApp', + unique: deviceUniqueId, + baseUrl: 'http://scd.skychip.top:8710', +); + +final appPeerId = StarCloudClient.instance.talk.myPeerId; +``` + +先订阅事件,再响应来电、视频协商与状态: + +```dart +final incomingSub = StarCloudClient.instance.talk.onIncomingCall.listen((call) { + // 展示接听/拒绝 UI +}); + +final mediaSub = StarCloudClient.instance.talk.onTalkData.listen((event) { + // event.isAudio:G711 音频;否则为视频数据,交给宿主解码器 +}); + +final expectSub = StarCloudClient.instance.talk.onTalkExpectResp.listen((event) { + // 根据 width、height、rotate 与 videoType 重建/调整渲染器 +}); + +final statusSub = StarCloudClient.instance.talk.onTalkStatus.listen((event) { + // 根据 TalkStatus 更新 UI 与资源生命周期 +}); +``` + +主动监控、接听和挂断: + +```dart +StarCloudClient.instance.talk.requestCall( + lockPeerId: lockPeerId, + callerName: 'YourApp Monitor', + options: const TalkOptions( + preferredVideoTypes: [VideoTypeE.H264], + preferredAudioTypes: [], + ), +); + +StarCloudClient.instance.talk.accept(lockPeerId: incomingCall.lockPeerId); +StarCloudClient.instance.talk.hangup(lockPeerId: lockPeerId); +``` + +停止使用时取消订阅并释放宿主侧解码器、录音和播放器;在应用彻底退出视频对讲模块时调用 `StarCloudClient.instance.talk.dispose()`。 + +## 服务一览 + +| 服务 | 入口 | 典型能力 | +| --- | --- | --- | +| 认证 | `auth` | 初始化、SDK 云账户、WiFi 授权登录 | +| 设备 | `device` | 扫描、配对、锁列表、开关锁、重置 | +| 密码 | `password` | 查询、在线/离线密码、重置 | +| 卡片/生物识别 | `card`、`biometric` | IC 卡、指纹、人脸管理 | +| 电子钥匙 | `electricKey` | 查询、新增、编辑、冻结、删除 | +| 记录 | `record` | 查询与上报锁记录 | +| 状态/设置 | `lockState`、`lockSetting` | 锁状态、功能位读取与设置 | +| WiFi | `wifi` | WiFi 搜索、网关/锁板配网、网关状态 | +| 视频对讲 | `talk` | 注册、来电、呼叫、媒体事件、挂断 | + +高级 BLE 同类请求应串行发送,避免多个相同 Reply 类型的命令同时在途导致响应匹配错误。 + +## 示例工程 + +示例位于 [example](example/README.md),当前操作顺序为: + +1. 登录页注册星云应用,填写 `client_id` 与 `client_secret` 并初始化 SDK;创建测试云账户或接入业务账户。 +2. 在设备扫描页扫描、直接配对门锁。 +3. 在 WiFi 配网页完成“第 1 步:初始化视频对讲”,然后输入 WiFi 名称/密码并执行“第 2 步:WiFi 配网”。 +4. 在视频对讲页对已联网且有 `peerId` 的锁发起监控,或处理来电。 + +## 故障排查 + +| 现象 | 优先检查 | +| --- | --- | +| `auth.init` 或 `createCloudUser` 失败 | `clientId`、`clientSecret`、授权环境地址与网络连通性 | +| 扫描不到锁 | 真机蓝牙/定位已开启,运行时权限已授予,锁处于可发现状态 | +| 配对或高级 BLE 命令超时 | 设备距离、蓝牙连接、锁的认证握手及 `StarCloudLockContext` token | +| WiFi 配网成功但无法监控 | 配网后等待锁上线,轮询锁详情确认 `isOnline == true` 且 `peerId` 非空 | +| 视频无画面 | 确认对讲已初始化、锁端协商到 H264、宿主解码器已处理 SPS/PPS/关键帧与旋转比例 | +| iOS 编译失败 | Podfile 最低版本、权限声明和 `pod install` 是否完成 | + +提交支持请求时,请提供 Flutter 版本、设备型号/系统版本、操作时间、脱敏后的完整日志、锁型号和最小复现步骤。请不要提交 `clientSecret`、访问令牌、私钥或完整用户信息。 + +## 安全与发布 + +- `clientSecret`、WiFi 授权令牌、蓝牙密钥和视频对讲凭据均为敏感数据,不得写入日志或提交版本库。 +- 生产环境应由业务后端管理应用凭据与用户身份,不建议在客户端内置长期密钥。 +- 上线前请使用正式包名、签名、隐私政策和实际权限用途说明;按应用商店要求审核蓝牙、定位、相机与麦克风权限。 +- SDK 功能与门锁固件、账号权限和已开通服务相关;请在量产前完成真实锁、弱网和异常断连测试。 + +## 支持 + +联系技术支持时请附上项目名称、SDK 版本、目标平台、问题模块和上述脱敏诊断信息。 diff --git a/example/.gitignore b/example/.gitignore new file mode 100755 index 0000000..29a3a50 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.metadata b/example/.metadata new file mode 100755 index 0000000..faee040 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# 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: "54e66469a933b60ddf175f858f82eaeb97e48c8d" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: android + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..ebc0dfc --- /dev/null +++ b/example/README.md @@ -0,0 +1,119 @@ +# StarCloud Flutter SDK 示例 + +本示例用于验证 `flutter_sdk` 的移动端集成流程,包含 SDK 认证、测试云账户、蓝牙扫描/配对、基础锁控制、网关 WiFi 配网,以及视频对讲监控与来电处理。 + +> 示例用于联调和能力演示。生产项目必须自行实现账号体系、密钥保护、错误提示、权限说明与日志脱敏。 + +## 环境 + +- Flutter 3.19.6(项目通过 FVM 管理) +- Android 真机或 iOS 真机 +- 已按根目录 [README](../README.md) 配置蓝牙、定位、网络、麦克风和相机权限 +- 需要 `flutter_sdk`、`starlock_ble`、`starcloud`、`starchart` 及 `video_decode_plugin` 位于当前相对路径所指向的位置 + +安装依赖并运行: + +```bash +fvm use 3.19.6 +fvm flutter pub get +fvm flutter run +``` + +不需要先进入 `../../video_decode_plugin` 执行 `fvm flutter pub get`:本示例的 `pubspec.yaml` 已以 `path: ../../video_decode_plugin` 声明该插件,执行本目录的 `fvm flutter pub get` 会递归解析其依赖。只有修改插件源码或运行插件自身示例时,才进入插件目录单独执行该命令。 + +若只做代码检查: + +```bash +fvm dart analyze +``` + +## 使用流程 + +### 1. 登录 + +在“登录”页填写在星云平台注册后获得的 `client_id` 与 `client_secret`,点击: + +```text +第1步:注册星云账号得到client_id和client_secret初始化SDK +``` + +环境地址已写在示例代码中: + +```text +https://cloud.skychip.top/sdk +``` + +随后点击“第2步:仅测试创建新的 SDK 星云账户”。生产环境请替换为业务后台下发的 `CloudUserInfo`,不要将测试账户创建逻辑直接发布。 + +如需网关配网,再输入锁主账号/管理员账号并点击: + +```text +第3步:填写app登录账号支持WIFI授权账号 +``` + +该账号的授权令牌仅用于 WiFi 网关配网,不能代替 SDK 云账户。 + +### 2. 设备扫描与配对 + +进入“设备扫描”页,点击“扫描”。每条扫描结果会直接显示: + +- `配对`:可对该设备发起配对; +- `已配对`:当前设备已匹配本地配对锁,不会重复配对。 + +配对成功后可在“蓝牙控锁”页验证基础开锁、关锁能力。 + +### 3. WiFi 配网 + +进入“WiFi配网”页,按顺序操作: + +1. 点击“第1步:初始化视频对讲”,生成 App PeerID; +2. 输入 WiFi SSID、密码; +3. 点击“第2步:wifi配网”。 + +配网下发成功后,锁仍可能需要 30–60 秒联网。示例会查询网关状态并轮询云端锁信息;在锁的 `isOnline` 为 `true` 且 `peerId` 非空之前,不能保证监控可用。 + +### 4. 视频对讲 + +“视频对讲”页不再提供初始化入口;请先在“WiFi配网”页完成第 1 步。该页提供: + +- 锁 PeerID 自动回填与手动输入; +- `监控`、`挂断`; +- 来电接听/拒绝卡片; +- H264 视频 Texture 渲染、状态与诊断日志。 + +示例通过 `video_decode_plugin` 选择 FFmpeg 解码后端。启动监控时日志应包含: + +```text +[DIAG] recreateDecoder preferred=ffmpeg selected=ffmpeg +[DIAG] decoder active engine=ffmpeg +``` + +若日志显示 `mediacodec`,说明运行的不是当前 FFmpeg 配置;请检查本地 `video_decode_plugin` 路径、重新构建 APK,并确认未使用旧产物。 + +## 示例配置位置 + +| 配置 | 文件 | 说明 | +| --- | --- | --- | +| 星云环境地址 | `lib/tabs/auth_tab.dart` | 示例固定为测试环境地址;按已授权环境修改 | +| 视频对讲服务地址 | `lib/services/video_talk_initializer.dart` | 示例固定地址;生产按服务分配修改 | +| WiFi SSID/密码 | 页面输入 | 不设默认值,也不会保存 | +| FFmpeg 视频渲染 | `lib/tabs/video_talk_tab.dart` | 初始化前后校验活动引擎为 `ffmpeg` | + +## 重要说明 + +- 请不要向代码仓库提交真实 `client_secret`、账号密码、访问令牌、蓝牙密钥或视频凭据。 +- Android 模拟器不适合测试蓝牙扫描、配对和 WiFi 配网;请使用真机。 +- 监控超时通常由锁未联网、锁端 `peerId` 未上报、网关未完成配网或锁端未回复对讲协商造成。 +- 示例目前不包含生产级账号持久化、异常恢复、埋点或 UI 设计,应按业务要求实现。 + +## 目录 + +```text +lib/ +├── tabs/ # 登录、扫描、锁控、WiFi、视频对讲页面 +├── services/ +│ ├── cat_eye_monitor_service.dart +│ └── video_talk_initializer.dart +├── global.dart # 跨页面的锁、PeerID、授权状态 +└── main.dart # 页面导航与 SDK bootstrap +``` diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100755 index 0000000..0d29021 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# 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.dev/lints. + # + # 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/example/android/.gitignore b/example/android/.gitignore new file mode 100755 index 0000000..6f56801 --- /dev/null +++ b/example/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/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100755 index 0000000..ba825bd --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,62 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.example" + compileSdk 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 { + applicationId "com.example.example" + minSdkVersion 23 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} \ No newline at end of file diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100755 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..309eb84 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100755 index 0000000..70f8f08 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100755 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100755 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100755 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100755 index 0000000..cb1ef88 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100755 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100755 index 0000000..279fd1f --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,21 @@ +allprojects { + repositories { + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/central' } + maven { url 'https://maven.aliyun.com/repository/public' } + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100755 index 0000000..780941c --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,8 @@ +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.jvmargs=-Xmx4g -XX:+UseG1GC + +# AndroidX 迷离 +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000..7bb2df6 --- /dev/null +++ b/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-8.3-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100755 index 0000000..ea1cefc --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.1.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.0" apply false +} + +include ":app" diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100755 index 0000000..7a7f987 --- /dev/null +++ b/example/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/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100755 index 0000000..391a902 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100755 index 0000000..ec97fc6 --- /dev/null +++ b/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/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100755 index 0000000..c4855bf --- /dev/null +++ b/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/example/ios/Podfile b/example/ios/Podfile new file mode 100755 index 0000000..a20f12b --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,43 @@ +# 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! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100755 index 0000000..3dbc591 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,623 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.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 */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.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 */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + 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 */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + 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 */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 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 = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 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 */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* 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 */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 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"; + }; + 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"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency 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; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 13.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 = NAQ5PL2DYC; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 13.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; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 13.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 = NAQ5PL2DYC; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp; + 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 = NAQ5PL2DYC; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp; + 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 */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* 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/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100755 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100755 index 0000000..e3773d4 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100755 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100755 index 0000000..c30b367 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..d36b1fa --- /dev/null +++ b/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/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100755 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_xN#0001NP)t-s|Ns9~ z#rXRE|M&d=0au&!`~QyF`q}dRnBDt}*!qXo`c{v z{Djr|@Adh0(D_%#_&mM$D6{kE_x{oE{l@J5@%H*?%=t~i_`ufYOPkAEn!pfkr2$fs z652Tz0001XNklqeeKN4RM4i{jKqmiC$?+xN>3Apn^ z0QfuZLym_5b<*QdmkHjHlj811{If)dl(Z2K0A+ekGtrFJb?g|wt#k#pV-#A~bK=OT ts8>{%cPtyC${m|1#B1A6#u!Q;umknL1chzTM$P~L002ovPDHLkV1lTfnu!1a literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100755 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 zcmV;526g#~P){YQnis^a@{&-nmRmq)<&%Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7b zE>q9${Lb$D&h7k=+4=!ek^n+`0zq>LL1O?lVyea53S5x`Nqqo2YyeuIrQrJj9XjOp z{;T5qbj3}&1vg1VK~#9!?b~^C5-}JC@Pyrv-6dSEqJqT}#j9#dJ@GzT@B8}x zU&J@bBI>f6w6en+CeI)3^kC*U?}X%OD8$Fd$H&LV$H&LV$H&LV#|K5~mLYf|VqzOc zkc7qL~0sOYuM{tG`rYEDV{DWY`Z8&)kW*hc2VkBuY+^Yx&92j&StN}Wp=LD zxoGxXw6f&8sB^u})h@b@z0RBeD`K7RMR9deyL(ZJu#39Z>rT)^>v}Khq8U-IbIvT> z?4pV9qGj=2)TNH3d)=De<+^w;>S7m_eFKTvzeaBeir45xY!^m!FmxnljbSS_3o=g( z->^wC9%qkR{kbGnW8MfFew_o9h3(r55Is`L$8KI@d+*%{=Nx+FXJ98L0PjFIu;rGnnfY zn1R5Qnp<{Jq0M1vX=X&F8gtLmcWv$1*M@4ZfF^9``()#hGTeKeP`1!iED ztNE(TN}M5}3Bbc*d=FIv`DNv&@|C6yYj{sSqUj5oo$#*0$7pu|Dd2TLI>t5%I zIa4Dvr(iayb+5x=j*Vum9&irk)xV1`t509lnPO0%skL8_1c#Xbamh(2@f?4yUI zhhuT5<#8RJhGz4%b$`PJwKPAudsm|at?u;*hGgnA zU1;9gnxVBC)wA(BsB`AW54N{|qmikJR*%x0c`{LGsSfa|NK61pYH(r-UQ4_JXd!Rsz)=k zL{GMc5{h138)fF5CzHEDM>+FqY)$pdN3}Ml+riTgJOLN0F*Vh?{9ESR{SVVg>*>=# zix;VJHPtvFFCRY$Ks*F;VX~%*r9F)W`PmPE9F!(&s#x07n2<}?S{(ygpXgX-&B&OM zONY&BRQ(#%0%jeQs?oJ4P!p*R98>qCy5p8w>_gpuh39NcOlp)(wOoz0sY-Qz55eB~ z7OC-fKBaD1sE3$l-6QgBJO!n?QOTza`!S_YK z_v-lm^7{VO^8Q@M_^8F)09Ki6%=s?2_5eupee(w1FB%aqSweusQ-T+CH0Xt{` zFjMvW{@C&TB)k25()nh~_yJ9coBRL(0oO@HK~z}7?bm5j;y@69;bvlHb2tf!$ReA~x{22wTq550 z?f?Hnw(;m3ip30;QzdV~7pi!wyMYhDtXW#cO7T>|f=bdFhu+F!zMZ2UFj;GUKX7tI z;hv3{q~!*pMj75WP_c}>6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FaF{8 z;u`Mw0ly(uE>*CgQYv{be6ab2LWhlaH1^iLIM{olnag$78^Fd}%dR7;JECQ+hmk|o z!u2&!3MqPfP5ChDSkFSH8F2WVOEf0(E_M(JL17G}Y+fg0_IuW%WQ zG(mG&u?|->YSdk0;8rc{yw2@2Z&GA}z{Wb91Ooz9VhA{b2DYE7RmG zjL}?eq#iX%3#k;JWMx_{^2nNax`xPhByFiDX+a7uTGU|otOvIAUy|dEKkXOm-`aWS z27pUzD{a)Ct<6p{{3)+lq@i`t@%>-wT4r?*S}k)58e09WZYP0{{R3FC5Sl00039P)t-s|Ns9~ z#rP?<_5oL$Q^olD{r_0T`27C={r>*`|Nj71npVa5OTzc(_WfbW_({R{p56NV{r*M2 z_xt?)2V0#0NsfV0u>{42ctGP(8vQj-Btk1n|O0ZD=YLwd&R{Ko41Gr9H= zY@z@@bOAMB5Ltl$E>bJJ{>JP30ZxkmI%?eW{k`b?Wy<&gOo;dS`~CR$Vwb@XWtR|N zi~t=w02?-0&j0TD{>bb6sNwsK*!p?V`RMQUl(*DVjk-9Cx+-z1KXab|Ka2oXhX5f% z`$|e!000AhNklrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`? zTG`AHia671e^vgmp!llKp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?hz91 z7p83F3%LVu9;S$tSI$C^%^yud1dfTM_6p2|+5Ejp$bd`GDvbR|xit>i!ZD&F>@CJrPmu*UjD&?DfZs=$@e3FQA(vNiU+$A*%a} z?`XcG2jDxJ_ZQ#Md`H{4Lpf6QBDp81_KWZ6Tk#yCy1)32zO#3<7>b`eT7UyYH1eGz z;O(rH$=QR*L%%ZcBpc=eGua?N55nD^K(8<#gl2+pN_j~b2MHs4#mcLmv%DkspS-3< zpI1F=^9siI0s-;IN_IrA;5xm~3?3!StX}pUv0vkxMaqm+zxrg7X7(I&*N~&dEd0kD z-FRV|g=|QuUsuh>-xCI}vD2imzYIOIdcCVV=$Bz@*u0+Bs<|L^)32nN*=wu3n%Ynw z@1|eLG>!8ruU1pFXUfb`j>(=Gy~?Rn4QJ-c3%3T|(Frd!bI`9u&zAnyFYTqlG#&J7 zAkD(jpw|oZLNiA>;>hgp1KX7-wxC~31II47gc zHcehD6Uxlf%+M^^uN5Wc*G%^;>D5qT{>=uxUhX%WJu^Z*(_Wq9y}npFO{Hhb>s6<9 zNi0pHXWFaVZnb)1+RS&F)xOv6&aeILcI)`k#0YE+?e)5&#r7J#c`3Z7x!LpTc01dx zrdC3{Z;joZ^KN&))zB_i)I9fWedoN>Zl-6_Iz+^G&*ak2jpF07*qoM6N<$f;w%0(f|Me literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100755 index 0000000..0bedcf2 --- /dev/null +++ b/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/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100755 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100755 index 0000000..1aa7e36 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,13 @@ +# iOS 启动页资源 + +此目录用于存放 iOS 启动页的图片资源。当前 Flutter 项目使用 `LaunchScreen.storyboard`;如需自定义品牌启动页,请优先在 Xcode 中维护资源目录,而不是在 Dart 页面中延迟加载图片。 + +## 修改方式 + +1. 在 macOS 上执行 `open ios/Runner.xcworkspace`; +2. 在 Xcode 的 Project Navigator 中选择 `Runner/Assets.xcassets`; +3. 选择 `LaunchImage` 或新建对应 Image Set,并按 `Contents.json` 中要求填入各尺寸图片; +4. 确认 `ios/Runner/Info.plist` 的 `UILaunchStoryboardName` 仍为 `LaunchScreen`; +5. 使用真机和模拟器分别验证暗色模式、不同屏幕尺寸与横竖屏显示。 + +不要在启动页展示账号密码、设备信息、动态网络内容或需要异步加载的图片。启动页应保持静态、简洁,并符合 Apple 审核规范。 diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100755 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100755 index 0000000..cb88d79 --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,78 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + NSBluetoothPeripheralUsageDescription + 该应用需要蓝牙权限以连接智能锁设备 + NSBluetoothCentralUsageDescription + 该应用需要蓝牙权限来扫描和连接智能锁 + NSLocationWhenInUseUsageDescription + 该应用需要位置权限以扫描周围蓝牙设备 + NSMicrophoneUsageDescription + 该应用需要麦克风权限 + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + example_demo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example_demo + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100755 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/Runner/SceneDelegate.swift b/example/ios/Runner/SceneDelegate.swift new file mode 100755 index 0000000..b9ce8ea --- /dev/null +++ b/example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100755 index 0000000..86a7c3b --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/lib/global.dart b/example/lib/global.dart new file mode 100755 index 0000000..01727b9 --- /dev/null +++ b/example/lib/global.dart @@ -0,0 +1,26 @@ +import 'package:flutter/foundation.dart'; +import 'package:starcloud/entity/star_cloud_lock_list.dart' as scl; + +// 全局共享状态 +final ValueNotifier pairedLockId = ValueNotifier(null); +final ValueNotifier pairedLock = + ValueNotifier(null); + +// 关键:App 的星图 PeerID(视频对讲初始化后产生,用于配网时告知锁端) +final ValueNotifier appPeerId = ValueNotifier(null); + +// 视频对讲服务由 WiFi 配网页的第 1 步初始化,供配网和视频对讲页共用。 +final ValueNotifier videoTalkReady = ValueNotifier(false); +final ValueNotifier videoTalkInitializing = ValueNotifier(false); + +// lock.skychip.top 授权登录返回的动态 token,仅用于 WiFi 网关配网。 +// 它与 cloud.skychip.top 的 OAuth token 属于不同认证域。 +final ValueNotifier lockServiceAccessToken = + ValueNotifier(null); + +// SDK 云账户是扫描、绑定和锁密钥操作的前置条件。生产环境应由业务后台 +// 按用户保存并下发 CloudUserInfo;Demo 可设置后台账户或创建测试账户。 +final ValueNotifier sdkCloudAccountReady = ValueNotifier(false); + +// 当前完成 lock.skychip.top 授权的账号,仅供 WiFi 网关业务使用。 +final ValueNotifier signedInAccount = ValueNotifier(null); diff --git a/example/lib/l10n.dart b/example/lib/l10n.dart new file mode 100755 index 0000000..db51259 --- /dev/null +++ b/example/lib/l10n.dart @@ -0,0 +1,125 @@ +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class S { + static const _languagePreferenceKey = 'is_english'; + static final ValueNotifier langNotifier = ValueNotifier(false); + + static bool get isEn => langNotifier.value; + static set isEn(bool value) => langNotifier.value = value; + + static Future loadLanguagePreference() async { + try { + final preferences = await SharedPreferences.getInstance(); + isEn = preferences.getBool(_languagePreferenceKey) ?? false; + } catch (error) { + debugPrint('Failed to load language preference: $error'); + isEn = false; + } + } + + static Future setLanguagePreference(bool value) async { + isEn = value; + try { + final preferences = await SharedPreferences.getInstance(); + await preferences.setBool(_languagePreferenceKey, value); + } catch (error) { + debugPrint('Failed to save language preference: $error'); + } + } + + // App Title + static String get title => isEn ? 'Demo' : '示例'; + + // Tabs + static String get tabInit => isEn ? 'Login' : '登录'; + static String get tabScan => isEn ? 'Scan' : '设备扫描'; + static String get tabLock => isEn ? 'Lock' : '蓝牙控锁'; + static String get tabWifi => isEn ? 'WiFi' : 'WiFi配网'; + static String get tabTalk => isEn ? 'Talk' : '视频对讲'; + + // Common Logs & Buttons + static String get logFailed => isEn ? 'Failed' : '失败'; + static String get logSuccess => isEn ? 'Success' : '成功'; + static String get btnProcessing => isEn ? 'Processing...' : '处理中...'; + + // Auth Tab + static String get sdkStatus => isEn ? 'SDK Status' : 'SDK 状态'; + static String get cloudUserStatus => isEn ? 'Signed-in Account' : '登录账户'; + static String get clientId => isEn ? 'Client ID *' : '客户ID *'; + static String get clientSecret => isEn ? 'Client Secret' : '客户端密钥'; + static String get envUrl => isEn ? 'Env URL' : '环境地址'; + static String get step1Init => isEn + ? 'Step 1: Register a cloud account to get client_id and client_secret, then initialize the SDK' + : '第1步:注册星云账号填写后初始化SDK'; + static String get step2User => isEn ? 'Step 2: Sign In' : '第 2 步:登录账号'; + static String get btnCreateTestCloudAccount => isEn + ? 'Step 2: Create a new SDK cloud account (testing only)' + : '第2步:仅测试创建新的 SDK 星云账户'; + static String get btnLoginForWifiAuthorization => isEn + ? 'Step 3: Sign in with the app account for WiFi authorization' + : '第3步:填写 app 登录账号,支持 WIFI 授权账号'; + static String get accountLabel => isEn ? 'Account' : '账号'; + static String get passwordLabel => isEn ? 'Password' : '密码'; + static String get logFillId => + isEn ? 'Please fill Client ID' : '请先填写 Client ID'; + static String get logInitSuccess => isEn ? 'SDK Initialized' : 'SDK 初始化成功'; + static String get logUserReady => isEn ? 'Account ready' : '账号登录成功'; + + // Scan Tab + static String get btnScan => isEn ? 'Scan' : '扫描'; + static String get btnScanning => isEn ? 'Scanning...' : '扫描中...'; + static String get btnStop => isEn ? 'Stop' : '停止'; + static String get scanHint => isEn ? 'Tap "Scan" to start' : '点击"扫描设备"开始'; + static String get pairedStatus => isEn ? 'Paired' : '已配对'; + static String get btnPair => isEn ? 'Pair' : '配对'; + static String get logScanning => + isEn ? 'Scanning (10s)...' : '开始扫描(10 秒超时)...'; + static String get logScanDone => isEn ? 'Scan done' : '扫描完成'; + + // Lock Tab + static String get lockIdLabel => isEn ? 'Lock ID' : '锁定ID'; + static String get notPaired => isEn ? 'Not Paired' : '未配对'; + static String get btnOpen => isEn ? 'Open' : '开锁'; + static String get btnClose => isEn ? 'Close' : '关锁'; + + // WiFi Tab + static String get wifiTitle => isEn ? 'WiFi Config' : 'WiFi配网'; + static String get wifiModeDirect => + isEn ? 'Direct Mode (Lock Board)' : '锁板直连模式'; + static String get wifiModeGateway => isEn ? 'Gateway Mode' : '网关模式'; + static String get wifiConfigSec => isEn ? 'WiFi Settings' : 'WiFi 配置'; + static String get btnStartConfig => + isEn ? 'Step 2: WiFi Setup' : '第2步:wifi配网'; + static String get logConfigSent => isEn ? 'Config Sent' : '指令已发送'; + static String get currentLoggedInAccountPrefix => + isEn ? 'Current signed-in account: ' : '当前登录账号:'; + static String get wifiUsesAuthorizedSession => isEn + ? 'WiFi setup will use this account\'s authorized session.' + : 'WIFI 配网将使用此账号的授权会话。'; + static String get logConfigCancelledNeedTalkInitialization => isEn + ? 'WiFi setup cancelled: Please tap “Step 1: Initialize Video Talk” on this page first.' + : '已取消配网:请先点击本页“第1步:初始化视频对讲”。'; + static String get logUsingLoginSessionForGatewayConfig => isEn + ? 'Using the signed-in session to get the gateway configuration and send it via BLE...' + : '已使用登录会话,正在获取网关配置并通过 BLE 下发...'; + static String get logWifiConfigCompleted => isEn + ? 'SUCCESS: BLE WiFi provisioning and deviceNetwork/setting are complete.' + : 'SUCCESS: BLE 配网及 deviceNetwork/setting 均已完成。'; + + // Talk Tab + static String get talkServer => isEn ? 'Server Address' : '服务器地址'; + static String get btnInitTalk => + isEn ? 'Step 1: Initialize Video Talk' : '第1步:初始化视频对讲'; + static String get initialized => isEn ? 'Initialized' : '已初始化'; + static String get completeWifiTalkInitialization => isEn + ? 'Please complete Step 1: Initialize Video Talk on the WiFi Setup tab.' + : '请先在“WiFi配网”页完成第1步:初始化视频对讲。'; + static String get incomingCall => isEn ? 'Incoming Call' : '来电'; + static String get btnAccept => isEn ? 'Accept' : '接听'; + static String get btnReject => isEn ? 'Reject' : '拒绝'; + static String get btnHangup => isEn ? 'Hang up' : '挂断'; + static String get btnMonitor => isEn ? 'Monitor' : '监控'; + static String get logTalkReady => isEn ? 'Talk Ready' : '视频对讲就绪'; + static String get deviceIdLabel => isEn ? 'Device ID' : '设备ID'; +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100755 index 0000000..c4f92aa --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; + +import 'l10n.dart'; +import 'tabs/auth_tab.dart'; +import 'tabs/device_scan_tab.dart'; +import 'tabs/lock_control_tab.dart'; +import 'tabs/video_talk_tab.dart'; +import 'tabs/wifi_config_tab.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await S.loadLanguagePreference(); + StarCloudClient.bootstrap(); + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + Future _toggleLanguage() async { + // 切换 ValueNotifier 的值,这会触发下方的 ValueListenableBuilder + await S.setLanguagePreference(!S.isEn); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: S.langNotifier, + builder: (context, isEn, _) { + return MaterialApp( + // 关键:使用 ValueKey 强制整个 App 在语言切换时重新构建,确保所有 S.xxx 文字刷新 + key: ValueKey(isEn), + title: S.title, + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: + ColorScheme.fromSeed(seedColor: const Color(0xFF0066CC)), + useMaterial3: true, + ), + home: MainScreen(onToggleLang: _toggleLanguage), + ); + }, + ); + } +} + +class MainScreen extends StatefulWidget { + final VoidCallback onToggleLang; + const MainScreen({super.key, required this.onToggleLang}); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + int _currentIndex = 0; + + final _tabs = [ + const AuthTab(), + const DeviceScanTab(), + const LockControlTab(), + const WifiConfigTab(), + const VideoTalkTab(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(S.title), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + actions: [ + TextButton.icon( + onPressed: widget.onToggleLang, + icon: const Icon(Icons.language), + label: Text(S.isEn ? '中文' : 'EN'), + ) + ], + ), + body: IndexedStack( + index: _currentIndex, + children: _tabs, + ), + bottomNavigationBar: NavigationBar( + selectedIndex: _currentIndex, + onDestinationSelected: (index) => setState(() => _currentIndex = index), + destinations: [ + NavigationDestination( + icon: const Icon(Icons.settings), label: S.tabInit), + NavigationDestination( + icon: const Icon(Icons.bluetooth_searching), label: S.tabScan), + NavigationDestination( + icon: const Icon(Icons.lock_outline), label: S.tabLock), + NavigationDestination(icon: const Icon(Icons.wifi), label: S.tabWifi), + NavigationDestination( + icon: const Icon(Icons.videocam), label: S.tabTalk), + ], + ), + ); + } +} diff --git a/example/lib/services/cat_eye_monitor_service.dart b/example/lib/services/cat_eye_monitor_service.dart new file mode 100644 index 0000000..ffa319c --- /dev/null +++ b/example/lib/services/cat_eye_monitor_service.dart @@ -0,0 +1,151 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:crypto/crypto.dart' as crypto; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; +import 'package:starcloud/blue/blue_features.dart'; +import 'package:starcloud/blue/blue_read_write.dart'; +import 'package:starcloud/blue/entity/blue_message.dart'; +import 'package:starcloud/device/ble/base/base_sender.dart'; +import 'package:starcloud/device/ble/ble_reply.dart'; +import 'package:starcloud/device/ble/business/ble_type.dart'; +import 'package:starcloud/device/ble/tool/ble_tool.dart'; +import 'package:starcloud/entity/star_cloud_lock_list.dart'; +import 'package:starcloud/tool/sm4_encipher/sm4.dart'; + +/// 主动监控前将猫眼从省电模式切换到实时监控模式。 +/// +/// 实时模式由 BLE feature bit 64 写入锁端。 +/// 省电模式(catEyeMode=0)下锁固件会忽略主动 TalkRequest。 +/// +/// 公开 Flutter SDK 文档未定义 cat-eye 云端设置 API;因此这里不能调用 +/// 非公开的 `updateCatEyeConfig` 并把其服务端权限错误当作 BLE 操作失败。 +class CatEyeMonitorService { + static const int _catEyeFeatureBit = 64; + static const int _realtimeMode = 2; + + Future enableRealtimeMode({ + required StarCloudLock lock, + }) async { + final context = await StarCloudLockContext.fromLock(lock); + final featureData = [ + _realtimeMode, + 0, 0, 0, 0, 0, 0, // 非自定义模式的录像参数 + 0, 0, // detectionDistance / realTimePicture + 1, 10, 1, 1, // 自动亮屏、亮屏时长、逗留/异常告警 + ]; + + final blue = BlueReadWrite.shareManager(); + if (blue == null) { + throw const StarCloudException('Bluetooth service is unavailable'); + } + blue.deviceName = lock.bluetooth.bluetoothDeviceName; + if (!Platform.isAndroid) blue.mac = lock.mac; + blue.type = BlueFeaturesType.lock; + blue.clearCallback(); + + final reply = await blue.writeAndReturn( + _SetCatEyeModeCommand( + keyId: context.keyId, + userId: context.userId, + featureData: featureData, + token: context.token, + needAuthor: context.needAuthor, + publicKey: context.publicKey, + privateKey: context.privateKey, + ), + getPrivateKey: () => context.privateKey, + timeout: const Duration(seconds: 20), + ); + if (reply.status != 0) { + throw StarCloudException( + 'Failed to enable cat-eye realtime mode over BLE', + raw: 'status=0x${reply.status.toRadixString(16)}', + ); + } + } +} + +class _SetCatEyeModeCommand extends SenderProtocol { + _SetCatEyeModeCommand({ + required this.keyId, + required this.userId, + required this.featureData, + required List token, + required this.needAuthor, + required this.publicKey, + required this.privateKey, + }) : token = List.from(token), + super(CommandType.generalExtendedCommond); + + final String keyId; + final String userId; + final List featureData; + final int needAuthor; + final List publicKey; + final List privateKey; + + @override + List? token; + + @override + List messageDetail() { + final data = [0x30, 0x30, 72]; + var subData = []; + + final keyBytes = utf8.encode(keyId); + subData.addAll(keyBytes); + subData = BLETool.getFixedLengthList(subData, 40 - keyBytes.length); + + final userBytes = utf8.encode(userId); + subData.addAll(userBytes); + subData = BLETool.getFixedLengthList(subData, 20 - userBytes.length); + subData + ..add(CatEyeMonitorService._catEyeFeatureBit) + ..add(featureData.length) + ..addAll(featureData); + + if (needAuthor == 0) { + subData.add(0); + } else { + final authData = [ + ...userBytes, + ...keyBytes, + ...?token, + ...publicKey, + ]; + final authCode = crypto.md5.convert(authData).bytes; + subData + ..add(authCode.length) + ..addAll(authCode); + } + + data + ..add(subData.length) + ..addAll(subData); + while (data.length % 16 != 0) { + data.add(0); + } + return SM4.encrypt(data, key: privateKey, mode: SM4CryptoMode.ECB); + } + + @override + BLEReply blueMessageToReply(BlueMessage message) { + return _SetCatEyeModeReply.parseData(commandType, message.data); + } +} + +class _SetCatEyeModeReply extends BLEReply { + _SetCatEyeModeReply.parseData( + CommandType commandType, + List data, + ) : super.parseData(commandType, data) { + // 通用扩展应答:[0x30, 0x30, mainStatus, subCommand, subStatus, ...] + // 原 App 以 mainStatus 为本次设置结果。 + status = data.length > 2 ? data[2] : 0xff; + errorWithStatus(status); + if (status == 0x06 && data.length >= 9) { + token = data.sublist(5, 9); + } + } +} diff --git a/example/lib/services/video_talk_initializer.dart b/example/lib/services/video_talk_initializer.dart new file mode 100644 index 0000000..aa08df2 --- /dev/null +++ b/example/lib/services/video_talk_initializer.dart @@ -0,0 +1,46 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; + +import '../global.dart'; + +const _talkServerUrl = 'http://scd.skychip.top:8710'; +Future? _initialization; + +/// 初始化后生成的 App PeerID 会同时供 WiFi 配网及监控使用。 +Future initializeVideoTalk() { + if (videoTalkReady.value) return Future.value(); + final pending = _initialization; + if (pending != null) return pending; + + final future = _initialize(); + _initialization = future; + future.whenComplete(() { + if (identical(_initialization, future)) _initialization = null; + }); + return future; +} + +Future _initialize() async { + videoTalkInitializing.value = true; + try { + var deviceId = 'dev_user_123'; + final deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid) { + deviceId = (await deviceInfo.androidInfo).id; + } else if (Platform.isIOS) { + deviceId = (await deviceInfo.iosInfo).identifierForVendor ?? deviceId; + } + + await StarCloudClient.instance.talk.init( + productName: 'Demo', + unique: deviceId, + baseUrl: _talkServerUrl, + ); + appPeerId.value = StarCloudClient.instance.talk.myPeerId; + videoTalkReady.value = true; + } finally { + videoTalkInitializing.value = false; + } +} diff --git a/example/lib/tabs/auth_tab.dart b/example/lib/tabs/auth_tab.dart new file mode 100755 index 0000000..378df50 --- /dev/null +++ b/example/lib/tabs/auth_tab.dart @@ -0,0 +1,233 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; +import '../global.dart'; +import '../l10n.dart'; +import '../widgets/common_widgets.dart'; + +class AuthTab extends StatefulWidget { + const AuthTab({super.key}); + + @override + State createState() => _AuthTabState(); +} + +class _AuthTabState extends State { + final _clientIdCtrl = TextEditingController(); + final _clientSecretCtrl = TextEditingController(); + static const _environmentUrl = 'https://cloud.skychip.top/sdk'; + final _usernameCtrl = TextEditingController(); + final _passwordCtrl = TextEditingController(); + final List _log = []; + bool _sdkReady = false; + bool _cloudUserReady = false; + bool _loggingIn = false; + + void _log_(String msg) { + final ts = DateTime.now().toIso8601String().substring(11, 19); + setState(() => _log.add('[$ts] $msg')); + } + + void _initSdk() { + final id = _clientIdCtrl.text.trim(); + if (id.isEmpty) { + _log_(S.isEn ? 'Please fill Client ID' : '请先填写 Client ID'); + return; + } + try { + StarCloudClient.instance.auth.init( + clientId: id, + clientSecret: _clientSecretCtrl.text.trim(), + environmentUrl: _environmentUrl, + ); + setState(() => _sdkReady = true); + _log_(S.isEn ? 'SDK Init Success' : 'SDK 初始化成功'); + } catch (e) { + _log_('${S.isEn ? "Failed" : "失败"}: $e'); + } + } + + Future _createDemoCloudAccount() async { + setState(() => _loggingIn = true); + _log_(S.isEn ? 'Creating SDK cloud account...' : '正在创建 SDK 星云测试账户...'); + try { + final cloudUser = await StarCloudClient.instance.auth.createCloudUser(); + StarCloudClient.instance.auth.setCloudAccounts([cloudUser]); + sdkCloudAccountReady.value = true; + pairedLockId.value = null; + pairedLock.value = null; + setState(() => _cloudUserReady = true); + _log_(S.isEn + ? 'SDK cloud account is ready. You can scan and pair.' + : 'SDK 星云账户已就绪。现在可扫描并绑定锁。'); + } catch (e) { + _log_('${S.isEn ? "Failed" : "失败"}: $e'); + } finally { + if (mounted) setState(() => _loggingIn = false); + } + } + + Future _loginLockService() async { + final username = _usernameCtrl.text.trim(); + final password = _passwordCtrl.text; + if (username.isEmpty || password.isEmpty) { + _log_(S.isEn ? 'Please fill username and password' : '请填写用户名和密码'); + return; + } + setState(() => _loggingIn = true); + _log_(S.isEn ? 'Signing in...' : '正在登录...'); + try { + String deviceBrand = Platform.isAndroid ? 'android' : 'ios'; + String deviceModel = 'unknown'; + String deviceVersion = 'unknown'; + try { + final deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final android = await deviceInfo.androidInfo; + deviceBrand = android.brand; + deviceModel = android.model; + deviceVersion = android.version.release; + } else if (Platform.isIOS) { + final ios = await deviceInfo.iosInfo; + deviceBrand = 'Apple'; + deviceModel = ios.utsname.machine; + deviceVersion = ios.systemVersion; + } + } catch (_) { + // 设备信息只用于服务端风控,无法读取时使用默认值继续登录。 + } + + // lock.skychip.top token 只用于 WiFi 网关配置,不能替代 SDK 云账户。 + final lockServiceToken = + await StarCloudClient.instance.auth.loginLockService( + username: username, + password: password, + deviceBrand: deviceBrand, + deviceModel: deviceModel, + deviceVersion: deviceVersion, + ); + + lockServiceAccessToken.value = lockServiceToken; + signedInAccount.value = username; + _log_(S.isEn ? 'WiFi authorization is ready.' : 'WiFi 网关授权登录成功。'); + } catch (e) { + _log_('${S.isEn ? "Failed" : "失败"}: $e'); + } finally { + if (mounted) setState(() => _loggingIn = false); + } + } + + @override + void dispose() { + _clientIdCtrl.dispose(); + _clientSecretCtrl.dispose(); + _usernameCtrl.dispose(); + _passwordCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: S.langNotifier, + builder: (context, isEn, _) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Wrap( + spacing: 8, + children: [ + StatusChip(label: S.sdkStatus, ready: _sdkReady), + StatusChip( + label: S.cloudUserStatus, ready: _cloudUserReady), + ], + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _clientIdCtrl, + decoration: InputDecoration( + labelText: S.clientId, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.badge_outlined)), + ), + const SizedBox(height: 12), + TextField( + controller: _clientSecretCtrl, + obscureText: true, + decoration: InputDecoration( + labelText: S.clientSecret, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.key_outlined)), + ), + const SizedBox(height: 12), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _initSdk, + icon: const Icon(Icons.settings), + label: Text(S.step1Init)), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: + _sdkReady && !_loggingIn ? _createDemoCloudAccount : null, + icon: const Icon(Icons.person_add_outlined), + label: Text( + _loggingIn ? S.btnProcessing : S.btnCreateTestCloudAccount, + ), + ), + const SizedBox(height: 16), + TextField( + controller: _usernameCtrl, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: S.accountLabel, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.person_outline), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _passwordCtrl, + obscureText: true, + onSubmitted: (_) => + _sdkReady && !_loggingIn ? _loginLockService() : null, + decoration: InputDecoration( + labelText: S.passwordLabel, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.password), + ), + ), + const SizedBox(height: 8), + ElevatedButton.icon( + onPressed: + _sdkReady && !_loggingIn ? _loginLockService : null, + icon: _loggingIn + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.login), + label: Text( + _loggingIn + ? S.btnProcessing + : S.btnLoginForWifiAuthorization, + )), + const SizedBox(height: 16), + LogArea(log: _log), + ], + ), + ); + }, + ); + } +} diff --git a/example/lib/tabs/device_scan_tab.dart b/example/lib/tabs/device_scan_tab.dart new file mode 100755 index 0000000..c05b8ca --- /dev/null +++ b/example/lib/tabs/device_scan_tab.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; +import 'package:starcloud/entity/star_cloud_lock_list.dart' as scl; +import '../l10n.dart'; +import '../global.dart'; +import '../widgets/common_widgets.dart'; + +class DeviceScanTab extends StatefulWidget { + const DeviceScanTab({super.key}); + + @override + State createState() => _DeviceScanTabState(); +} + +class _DeviceScanTabState extends State { + bool _scanning = false; + bool _pairing = false; + List _devices = []; + final List _log = []; + + void _log_(String msg) { + final ts = DateTime.now().toIso8601String().substring(11, 19); + setState(() => _log.add('[$ts] $msg')); + } + + Future _scan() async { + if (!sdkCloudAccountReady.value) { + _log_(S.isEn + ? 'Set an SDK cloud account before scanning and pairing a lock.' + : '请先在“登录”页创建或设置 SDK 星云账户,再扫描绑定锁。'); + return; + } + setState(() { + _scanning = true; + _devices = []; + }); + _log_(S.isEn ? 'Scan start (10s)...' : '开始扫描(10 秒超时)...'); + try { + await StarCloudClient.instance.device.scanDeviceList( + timeout: 10, + onScan: (results) { + setState(() => _devices = results); + }); + _log_(S.isEn + ? 'Done. Found ${_devices.length}' + : '扫描完成,发现 ${_devices.length} 个设备'); + } catch (e) { + _log_('${S.isEn ? "Failed" : "失败"}: $e'); + } finally { + setState(() => _scanning = false); + } + } + + Future _pair(int idx) async { + if (!sdkCloudAccountReady.value) { + _log_( + S.isEn ? 'Please set an SDK cloud account first.' : '请先设置 SDK 星云账户。'); + return; + } + setState(() => _pairing = true); + _log_(S.isEn ? 'Pairing...' : '正在配对...'); + try { + final lock = await StarCloudClient.instance.device + .pairDevice(scanResult: _devices[idx]); + // 更新 ValueNotifier 的值 + pairedLockId.value = lock.lockId; + pairedLock.value = lock; + + _log_('${S.isEn ? "Success" : "成功"}! ID: ${lock.lockId}'); + } catch (e) { + _log_('${S.isEn ? "Failed" : "失败"}: $e'); + } finally { + setState(() => _pairing = false); + } + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: S.langNotifier, + builder: (context, isEn, _) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _scanning ? null : _scan, + icon: _scanning + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2)) + : const Icon(Icons.bluetooth_searching), + label: Text(_scanning ? S.btnScanning : S.btnScan), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _scanning + ? () => StarCloudClient.instance.device + .stopScan() + .then((_) => setState(() => _scanning = false)) + : null, + child: Text(S.btnStop)), + ], + ), + ), + // 使用 ValueListenableBuilder 监听配对状态 + ValueListenableBuilder( + valueListenable: pairedLockId, + builder: (context, id, _) { + if (id == null) return const SizedBox.shrink(); + return Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + alignment: Alignment.centerLeft, + child: Chip( + label: Text('${S.pairedStatus}: #$id'), + backgroundColor: Colors.green.shade50, + onDeleted: () { + pairedLockId.value = null; + pairedLock.value = null; + }, + ), + ); + }, + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: pairedLock, + builder: (context, paired, _) { + if (_devices.isEmpty) { + return Center( + child: Text( + S.scanHint, + style: const TextStyle(color: Colors.grey), + ), + ); + } + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 12), + itemCount: _devices.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, i) { + final device = _devices[i] as StarCloudScanResult; + final isPaired = _isPairedDevice(device, paired); + return ListTile( + title: Text(device.advName.isNotEmpty + ? device.advName + : (S.isEn ? 'Unknown' : '未知设备')), + subtitle: Text('MAC: ${device.id}'), + trailing: ElevatedButton( + onPressed: + _pairing || isPaired ? null : () => _pair(i), + child: Text(isPaired ? S.pairedStatus : S.btnPair), + ), + ); + }, + ); + }, + ), + ), + LogArea(log: _log), + ], + ); + }, + ); + } + + bool _isPairedDevice( + StarCloudScanResult device, + scl.StarCloudLock? paired, + ) { + if (paired == null) return false; + return paired.mac == device.id || + paired.bluetooth.bluetoothDeviceName == device.advName; + } +} diff --git a/example/lib/tabs/lock_control_tab.dart b/example/lib/tabs/lock_control_tab.dart new file mode 100755 index 0000000..3ea6139 --- /dev/null +++ b/example/lib/tabs/lock_control_tab.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; +import '../l10n.dart'; +import '../global.dart'; +import '../widgets/common_widgets.dart'; + +class LockControlTab extends StatefulWidget { + const LockControlTab({super.key}); + + @override + State createState() => _LockControlTabState(); +} + +class _LockControlTabState extends State { + final List _log = []; + bool _loading = false; + + void _log_(String msg) { + final ts = DateTime.now().toIso8601String().substring(11, 19); + setState(() => _log.add('[$ts] $msg')); + } + + Future _run(String desc, Future Function() fn) async { + if (pairedLockId.value == null) return; + _log_('$desc...'); + setState(() => _loading = true); + try { + await fn(); + _log_('${S.isEn ? "Success" : "成功"}'); + } catch (e) { + _log_('${S.isEn ? "Failed" : "失败"}: $e'); + } finally { + setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + // 监听语言切换通知 + return ValueListenableBuilder( + valueListenable: S.langNotifier, + builder: (context, isEn, _) { + return Padding( + padding: const EdgeInsets.all(16), + child: ValueListenableBuilder( + valueListenable: pairedLockId, + builder: (context, id, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('${S.lockIdLabel}: ${id ?? S.notPaired}', + style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ActionButton( + label: S.btnOpen, + icon: Icons.lock_open, + color: Colors.green, + loading: _loading || id == null, + onTap: () => _run(S.btnOpen, + () => StarCloudClient.instance.device.openLock(lockId: id!)), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ActionButton( + label: S.btnClose, + icon: Icons.lock, + color: Colors.orange, + loading: _loading || id == null, + onTap: () => _run(S.btnClose, + () => StarCloudClient.instance.device.closeLock(lockId: id!)), + ), + ), + ], + ), + const SizedBox(height: 16), + LogArea(log: _log), + ], + ); + }, + ), + ); + }, + ); + } +} diff --git a/example/lib/tabs/video_talk_tab.dart b/example/lib/tabs/video_talk_tab.dart new file mode 100755 index 0000000..a5cbcb2 --- /dev/null +++ b/example/lib/tabs/video_talk_tab.dart @@ -0,0 +1,1044 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; +import 'package:video_decode_plugin/video_decode_plugin.dart'; +import '../l10n.dart'; +import '../global.dart'; +import '../widgets/common_widgets.dart'; +import '../services/cat_eye_monitor_service.dart'; + +class VideoTalkTab extends StatefulWidget { + const VideoTalkTab({super.key}); + + @override + State createState() => _VideoTalkTabState(); +} + +class _VideoTalkTabState extends State { + // 与参考项目保持一致:为 UDP 帧保留一个很短的乱序窗口,避免 P 帧 + // 在前序帧尚未到达时被过早提交给解码器。 + static const _maxVideoReorderDistance = 3; + static const _videoReorderWait = Duration(milliseconds: 15); + + // 参考锁端原生 App:主动“监控”只请求 H264 视频。部分固件在首个 + // TalkExpect 同时携带 G711 音频时不会回复协商包,最终导致请求超时。 + static const TalkOptions _monitorOptions = TalkOptions( + preferredVideoTypes: [VideoTypeE.H264], + preferredAudioTypes: [], + ); + + final List _log = []; + + IncomingCallEvent? _call; + TalkStatus _status = TalkStatus.idle; + String? _activeLockPeerId; + + // 视频解码器 + int? _textureId; + bool _decoderReady = false; + Future? _decoderInitFuture; + bool _frameRendered = false; + int _videoCount = 0; + int _videoWidth = 640; + int _videoHeight = 480; + int _videoQuarterTurns = 3; + int? _framePtsBaseWallTimeMs; + int _lastFramePtsMs = -1; + Timer? _videoProcessTimer; + final List<_QueuedVideoFrame> _videoBuffer = <_QueuedVideoFrame>[]; + _QueuedVideoFrame? _pendingVideoFrame; + int _videoSendGeneration = 0; + bool _sendingVideoFrame = false; + int _highestVideoFrameSeq = -1; + int? _lastDecodedIFrameSeq; + bool _waitingForIFrame = true; + bool _isCallMode = false; + bool _preparingMonitor = false; + Timer? _videoFrameTimeoutTimer; + + StreamSubscription? _callSub; + StreamSubscription? _statusSub; + StreamSubscription? _dataSub; + StreamSubscription? _expectSub; + Timer? _peerIdPollTimer; + bool _resolvingPeerId = false; + + void _log_(String msg) { + final ts = DateTime.now().toIso8601String().substring(11, 19); + if (mounted) setState(() => _log.add('[$ts] $msg')); + } + + @override + void initState() { + super.initState(); + _initDeviceAndListeners(); + } + + @override + void dispose() { + _callSub?.cancel(); + _statusSub?.cancel(); + _dataSub?.cancel(); + _expectSub?.cancel(); + _peerIdPollTimer?.cancel(); + _videoProcessTimer?.cancel(); + _videoFrameTimeoutTimer?.cancel(); + _videoBuffer.clear(); + VideoDecodePlugin.setOnMissingSpsPpsListener(() {}); + _releaseDecoder(); + pairedLock.removeListener(_onPairedLockChanged); + super.dispose(); + } + + Future _initDeviceAndListeners() async { + // 监听对讲事件。服务在 WiFi 配网页的第 1 步初始化。 + _callSub = StarCloudClient.instance.talk.onIncomingCall.listen((e) { + _log_('${S.incomingCall}: ${e.callerName}'); + print('${S.incomingCall}: ${e.callerName}'); + if (mounted) + setState(() { + _call = e; + _status = TalkStatus.incomingCall; + _activeLockPeerId = e.lockPeerId; + }); + }); + + _statusSub = StarCloudClient.instance.talk.onTalkStatus.listen((e) { + _log_('Talk status: ${e.status}'); + print('Talk status: ${e.status}, lockPeerId=${e.lockPeerId}'); + if (mounted) { + setState(() { + _status = e.status; + if (e.status == TalkStatus.inCall) { + _call = null; + } + if (e.status == TalkStatus.timeout) { + _log_(S.isEn + ? 'Monitor timed out. Check if lock is online and connected to WiFi.' + : '监控超时,请确认锁已联网并在线。'); + } else if (e.status == TalkStatus.rejected) { + _log_(S.isEn ? 'Monitor rejected by lock.' : '监控被锁端拒绝。'); + } else if (e.status == TalkStatus.hangup) { + _log_(S.isEn ? 'Call ended.' : '通话已结束。'); + } else if (e.status == TalkStatus.inCall) { + _log_(S.isEn + ? 'Monitor connected, waiting for video...' + : '监控已连接,等待视频数据...'); + _startVideoFrameTimeout(); + } + if (e.status == TalkStatus.idle || + e.status == TalkStatus.hangup || + e.status == TalkStatus.timeout || + e.status == TalkStatus.rejected) { + _call = null; + _activeLockPeerId = null; + _cancelVideoFrameTimeout(); + _releaseDecoder(); + } + }); + } + }); + + _dataSub = StarCloudClient.instance.talk.onTalkData.listen((e) { + if (!mounted) return; + if (e.isAudio) { + return; + } else { + _videoCount++; + _cancelVideoFrameTimeout(); + if (_videoCount == 1) { + final preview = e.content + .take(12) + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(' '); + _log_( + 'First video frame received (type=${e.contentType}, size=${e.content.length})'); + print( + '[VideoTalk] first frame from=${e.fromPeerId} type=${e.contentType} size=${e.content.length} head=$preview'); + } else if (_videoCount % 120 == 0) { + print( + '[VideoTalk] video frames=$_videoCount, latestSize=${e.content.length}'); + } + _feedVideoFrame(e); + } + }); + + _expectSub = StarCloudClient.instance.talk.onTalkExpectResp.listen((e) { + _log_('TalkExpect response from ${e.fromPeerId}: ' + '${e.width}x${e.height}, rotate=${e.rotate}, ' + 'video=${e.videoType.name}, audio=${e.audioType.name}'); + print('[VideoTalk] TalkExpect response from ${e.fromPeerId}: ' + '${e.width}x${e.height}, rotate=${e.rotate}, ' + 'video=${e.videoType.name}, audio=${e.audioType.name}'); + if (e.videoType != VideoTypeE.H264 || e.width <= 0 || e.height <= 0) { + _log_('TalkExpect did not negotiate usable H264 video.'); + return; + } + final quarterTurns = ((e.rotate % 360) + 360) % 360 ~/ 90; + final sizeChanged = e.width != _videoWidth || e.height != _videoHeight; + _videoWidth = e.width; + _videoHeight = e.height; + _videoQuarterTurns = quarterTurns; + if (mounted) setState(() {}); + if (sizeChanged) { + _log_('Video format negotiated: ${e.width}x${e.height}'); + print('[VideoTalk] video format negotiated: ${e.width}x${e.height}'); + unawaited(_recreateDecoderForResolution()); + } + }); + + pairedLock.addListener(_onPairedLockChanged); + _onPairedLockChanged(); + } + + // ==================== 视频解码器 ==================== + + Future _initDecoder() { + if (_decoderReady) return Future.value(true); + final pending = _decoderInitFuture; + if (pending != null) return pending; + + final future = _doInitDecoder(); + _decoderInitFuture = future; + future.whenComplete(() { + if (identical(_decoderInitFuture, future)) { + _decoderInitFuture = null; + } + }); + return future; + } + + Future _doInitDecoder() async { + if (_decoderReady) return true; + try { + // 参考项目 Android 端的 video_decode_plugin 默认使用 FFmpeg;这里显式 + // 保持同一后端,避免插件默认值变化时两端走出不同的解码流程。 + final engineState = await VideoDecodePlugin.setDecodeEngine( + VideoDecodePlugin.engineFfmpeg, + applyImmediately: false, + ); + final activeEngine = engineState['activeEngine']?.toString(); + _log_('Video decoder engine: $activeEngine'); + if (activeEngine != VideoDecodePlugin.engineFfmpeg) { + _log_('FFmpeg video decoder is unavailable; monitor was not started.'); + return false; + } + final config = VideoDecoderConfig( + width: _videoWidth, + height: _videoHeight, + codecType: 'h264', + ); + final tid = await VideoDecodePlugin.initDecoder(config); + if (tid == null) { + _log_('Video decoder init failed: null textureId'); + print('Video decoder init failed: null textureId'); + return false; + } + // initDecoder 后再次确认后端,保证整个会话都与参考项目一致。 + final initializedEngine = + (await VideoDecodePlugin.getDecodeEngine())['activeEngine'] + ?.toString(); + if (initializedEngine != VideoDecodePlugin.engineFfmpeg) { + await VideoDecodePlugin.releaseDecoder(); + _log_( + 'FFmpeg video decoder failed to start (active=$initializedEngine).'); + return false; + } + + _textureId = tid; + _decoderReady = true; + _frameRendered = false; + _videoCount = 0; + _framePtsBaseWallTimeMs = null; + _lastFramePtsMs = -1; + _highestVideoFrameSeq = -1; + _lastDecodedIFrameSeq = null; + _waitingForIFrame = true; + _videoProcessTimer?.cancel(); + _videoProcessTimer = Timer.periodic( + const Duration(milliseconds: 5), + (_) => _processNextVideoFrame(), + ); + VideoDecodePlugin.setOnFrameRenderedListener((textureId) { + _frameRendered = true; + _log_('First decoded video frame rendered (textureId=$textureId)'); + if (mounted) setState(() {}); + }); + VideoDecodePlugin.setOnMissingSpsPpsListener(() { + if (!mounted || _status != TalkStatus.inCall) return; + _log_(S.isEn + ? 'Missing H264 SPS/PPS, requesting a key frame...' + : '缺少 H264 SPS/PPS,正在重新请求关键帧...'); + StarCloudClient.instance.talk.renegotiate( + options: _isCallMode ? TalkOptions.defaultOptions : _monitorOptions, + ); + }); + _log_('Video decoder ready (textureId=$tid)'); + print('Video decoder ready (textureId=$tid)'); + if (mounted) setState(() {}); + return true; + } catch (e) { + _log_('Video decoder init error: $e'); + print('Video decoder init error: $e'); + return false; + } + } + + Future _releaseDecoder() async { + if (!_decoderReady) return; + try { + await VideoDecodePlugin.releaseDecoder(); + } catch (_) {} + _textureId = null; + _decoderReady = false; + _frameRendered = false; + _decoderInitFuture = null; + _videoSendGeneration++; + _pendingVideoFrame = null; + _sendingVideoFrame = false; + _videoProcessTimer?.cancel(); + _videoProcessTimer = null; + _videoFrameTimeoutTimer?.cancel(); + _videoFrameTimeoutTimer = null; + _videoBuffer.clear(); + _highestVideoFrameSeq = -1; + _lastDecodedIFrameSeq = null; + _waitingForIFrame = true; + _videoCount = 0; + _framePtsBaseWallTimeMs = null; + _lastFramePtsMs = -1; + if (mounted) setState(() {}); + } + + Future _recreateDecoderForResolution() async { + await _releaseDecoder(); + if (mounted) await _initDecoder(); + } + + void _startVideoFrameTimeout() { + _cancelVideoFrameTimeout(); + _videoFrameTimeoutTimer = Timer(const Duration(seconds: 10), () { + if (_videoCount == 0 && mounted) { + _log_(S.isEn + ? 'No video frames received within 10s. Check camera and video settings.' + : '10秒内未收到视频帧,请检查摄像头及视频设置。'); + print('Video frame timeout: no frames received in 10s'); + } + }); + } + + void _cancelVideoFrameTimeout() { + _videoFrameTimeoutTimer?.cancel(); + _videoFrameTimeoutTimer = null; + } + + /// 将 TalkDataEvent 中的视频帧送入解码器 + void _feedVideoFrame(TalkDataEvent e) { + if (!_decoderReady || _textureId == null) return; + try { + final Uint8List frameData = e.content; + if (e.isJpeg) { + return; + } + if (!e.isH264) return; + if (frameData.isEmpty) return; + + final metadata = e.h264Frame; + final bool hasValidMetadata = metadata != null && + (metadata.frameType.value == 1 || metadata.frameType.value == 2); + if (!hasValidMetadata) { + _log_('Invalid H264 metadata; dropping frame'); + print('[VideoTalk] invalid H264 metadata, dropping frame'); + return; + } + + final bool isIFrame = metadata.frameType.value == 1; + + final int frameType = isIFrame ? 0 : 1; + + final int frameSeq = metadata.frameSeq; + final int frameSeqI = metadata.frameSeqI; + + // 对齐参考项目:检测到锁端视频流重启(I 帧序号大幅回退)时, + // 清除旧 GOP 的待处理帧,立即从新的 I 帧恢复。 + if (isIFrame && + _highestVideoFrameSeq >= 0 && + frameSeq < _highestVideoFrameSeq) { + final rolloverThreshold = + _getFrameSeqRolloverThreshold(_highestVideoFrameSeq); + if (_highestVideoFrameSeq - frameSeq > rolloverThreshold) { + _log_( + 'Video stream reset detected; resuming from I-frame $frameSeq.'); + _videoBuffer.clear(); + _pendingVideoFrame = null; + _lastDecodedIFrameSeq = null; + _waitingForIFrame = false; + _highestVideoFrameSeq = frameSeq; + } else { + return; + } + } else if (_highestVideoFrameSeq >= 0 && + frameSeq <= _highestVideoFrameSeq) { + if (_videoBuffer.any((frame) => frame.frameSeq == frameSeq)) { + return; + } + if (_highestVideoFrameSeq - frameSeq > _maxVideoReorderDistance) { + return; + } + } + if (frameSeq > _highestVideoFrameSeq) { + _highestVideoFrameSeq = frameSeq; + } + + if (isIFrame) { + _waitingForIFrame = false; + _videoBuffer.removeWhere((f) => f.frameType == 1); + } else if (_waitingForIFrame || + (_lastDecodedIFrameSeq != null && + frameSeqI != 0 && + frameSeqI != _lastDecodedIFrameSeq)) { + if (_lastDecodedIFrameSeq != null && frameSeqI != 0) { + _waitingForIFrame = true; + _videoBuffer.removeWhere((f) => f.frameType == 1); + } + return; + } + + _videoBuffer.add(_QueuedVideoFrame( + frameData: frameData, + frameType: frameType, + frameSeq: frameSeq, + frameSeqI: frameSeqI, + arrivalWallTimeMs: DateTime.now().millisecondsSinceEpoch, + )); + _videoBuffer.sort((a, b) => a.frameSeq.compareTo(b.frameSeq)); + while (_videoBuffer.length > 6) { + final index = _videoBuffer.indexWhere((f) => f.frameType == 1); + _videoBuffer.removeAt(index >= 0 ? index : 0); + } + } catch (err) { + _log_('Frame feed error: $err'); + print('Frame feed error: $err'); + } + } + + void _processNextVideoFrame() { + if (_videoBuffer.isEmpty || !_decoderReady) return; + var index = _videoBuffer.indexWhere((f) => f.frameType == 0); + if (index >= 0) { + final keyFrameSeq = _videoBuffer[index].frameSeq; + _videoBuffer + .removeWhere((f) => f.frameType == 1 && f.frameSeq < keyFrameSeq); + index = _videoBuffer + .indexWhere((f) => f.frameType == 0 && f.frameSeq == keyFrameSeq); + final frame = _videoBuffer[index]; + if (_lastDecodedIFrameSeq != null && + _shouldWaitForReorderedFrame(frame)) { + return; + } + } else if (_lastDecodedIFrameSeq != null) { + _videoBuffer.removeWhere((f) => + f.frameType == 1 && + f.frameSeqI != 0 && + f.frameSeqI != _lastDecodedIFrameSeq); + index = _videoBuffer.indexWhere((f) => f.frameType == 1); + if (index >= 0 && _shouldWaitForReorderedFrame(_videoBuffer[index])) { + return; + } + } + if (index < 0) { + return; + } + final frame = _videoBuffer.removeAt(index); + _submitVideoFrame(frame); + } + + /// 参考项目 VideoPipelineV2:解码器忙碌时只保留最新帧,关键帧优先。 + /// 这样不会让过期 P 帧持续堆积,画面能尽快追上实时流。 + void _submitVideoFrame(_QueuedVideoFrame frame) { + if (frame.frameType == 0) { + _lastDecodedIFrameSeq = frame.frameSeq; + } + if (!_sendingVideoFrame) { + _sendingVideoFrame = true; + unawaited(_drainVideoFrames(frame, _videoSendGeneration)); + return; + } + + final pending = _pendingVideoFrame; + if (frame.frameType == 0 || pending == null || pending.frameType != 0) { + _pendingVideoFrame = frame; + } + } + + Future _drainVideoFrames( + _QueuedVideoFrame firstFrame, + int generation, + ) async { + var frame = firstFrame; + while (_decoderReady && generation == _videoSendGeneration) { + try { + await VideoDecodePlugin.sendFrame( + frameData: frame.frameData, + frameType: frame.frameType, + timestamp: _nextFrameTimestamp(frame.arrivalWallTimeMs), + ingressWallTimeMs: frame.arrivalWallTimeMs, + frameSeq: frame.frameSeq, + refIFrameSeq: frame.frameSeqI, + // TalkDataH264Frame.frameData 已经是完整 Annex-B 数据。 + // 原生解码器会自行缓存 SPS/PPS 并处理 I/P NALU;这里不能再次 + // 在 Dart 层拆分 I 帧,否则会丢失设备发送的有效切片格式。 + splitNalFromIFrame: false, + ); + } catch (err) { + _log_('Frame decode error: $err'); + } + + if (!_decoderReady || generation != _videoSendGeneration) { + return; + } + + final nextFrame = _pendingVideoFrame; + _pendingVideoFrame = null; + if (nextFrame == null) { + _sendingVideoFrame = false; + return; + } + frame = nextFrame; + } + if (generation != _videoSendGeneration) { + return; + } + _pendingVideoFrame = null; + _sendingVideoFrame = false; + } + + int _nextFrameTimestamp(int arrivalWallTimeMs) { + _framePtsBaseWallTimeMs ??= arrivalWallTimeMs; + final candidate = arrivalWallTimeMs - _framePtsBaseWallTimeMs!; + final timestamp = + candidate > _lastFramePtsMs ? candidate : _lastFramePtsMs + 1; + _lastFramePtsMs = timestamp; + return timestamp; + } + + bool _shouldWaitForReorderedFrame(_QueuedVideoFrame frame) { + return _highestVideoFrameSeq - frame.frameSeq < _maxVideoReorderDistance && + DateTime.now().millisecondsSinceEpoch - frame.arrivalWallTimeMs < + _videoReorderWait.inMilliseconds; + } + + int _getFrameSeqRolloverThreshold(int lastSeq) { + return lastSeq > 2000 ? 1000 : (lastSeq / 2).round(); + } + + // ==================== PeerID 管理 ==================== + + void _onPairedLockChanged() { + _startPeerIdPolling(); + } + + /// 锁版 peerId 为空时定时轮询云端(每 5s),出现即回填并停止 —— 锁上线监控 + void _startPeerIdPolling() { + _peerIdPollTimer?.cancel(); + final paired = pairedLock.value; + if (paired == null) return; + if ((paired.peerId ?? '').trim().isNotEmpty) return; + _peerIdPollTimer = Timer.periodic( + const Duration(seconds: 5), + (_) => _pollLockPeerId(), + ); + } + + Future _pollLockPeerId() async { + final current = pairedLock.value; + if (current == null) { + _peerIdPollTimer?.cancel(); + return; + } + if ((current.peerId ?? '').trim().isNotEmpty) { + _peerIdPollTimer?.cancel(); + return; + } + if (_resolvingPeerId) return; + _resolvingPeerId = true; + try { + final fresh = await StarCloudClient.instance.device + .getDevice(lockId: current.lockId); + final freshPeerId = (fresh.peerId ?? '').trim(); + if (freshPeerId.isNotEmpty) { + pairedLock.value = fresh; + _log_('Lock PeerID detected (auto-poll): $freshPeerId'); + print('Lock PeerID detected (auto-poll): $freshPeerId'); + _peerIdPollTimer?.cancel(); + } + } catch (_) { + // 锁可能尚未上报,下个周期重试 + } finally { + _resolvingPeerId = false; + } + } + + Future _resolveLockPeerId() async { + final paired = pairedLock.value; + if (paired == null) { + _log_(S.isEn ? 'No paired lock. Please pair first.' : '未配对锁,请先在设备页配对。'); + print(S.isEn ? 'No paired lock. Please pair first.' : '未配对锁,请先在设备页配对。'); + return ''; + } + + if (_resolvingPeerId) return ''; + _resolvingPeerId = true; + try { + final fresh = await StarCloudClient.instance.device + .getDevice(lockId: paired.lockId); + final freshPeerId = (fresh.peerId ?? '').trim(); + final cachedPeerId = (paired.peerId ?? '').trim(); + final online = fresh.isOnline == true; + _log_('Lock status: lockId=${paired.lockId}, cloudOnline=$online, ' + 'peerId=${freshPeerId.isEmpty ? "" : freshPeerId}'); + print('[VideoTalk] Lock status: lockId=${paired.lockId}, ' + 'cloudOnline=$online, peerId=${freshPeerId.isEmpty ? "" : freshPeerId}'); + + if (freshPeerId.isNotEmpty) { + pairedLock.value = fresh; + if (!online) { + _log_(S.isEn + ? 'Cloud status is offline; trying the monitor request with the latest lock PeerID.' + : '云端状态显示离线,但锁已有最新 PeerID,继续尝试监控请求。'); + } + return freshPeerId; + } + + if (cachedPeerId.isNotEmpty) { + if (!online) { + _log_(S.isEn + ? 'Cloud status is offline; trying the cached lock PeerID.' + : '云端状态显示离线,使用已缓存的锁 PeerID 尝试监控请求。'); + } + return cachedPeerId; + } + _log_(S.isEn + ? 'Lock PeerID is empty. Ensure lock is online and WiFi configured.' + : '锁版PeerID为空,请确认锁已联网并完成WiFi配网。'); + print(S.isEn + ? 'Lock PeerID is empty. Ensure lock is online and WiFi configured.' + : '锁版PeerID为空,请确认锁已联网并完成WiFi配网。'); + return ''; + } catch (e) { + final cachedPeerId = (paired.peerId ?? '').trim(); + if (cachedPeerId.isNotEmpty) return cachedPeerId; + _log_('${S.isEn ? "Fetch lock detail failed" : "获取锁详情失败"}: $e'); + return ''; + } finally { + _resolvingPeerId = false; + } + } + + void _requestCall() { + _requestCallAsync(); + } + + Future _requestCallAsync() async { + if (_preparingMonitor) return; + if (_status == TalkStatus.outgoingCall || + _status == TalkStatus.inCall || + _status == TalkStatus.incomingCall) { + _log_(S.isEn ? 'Monitor request already active.' : '监控请求已在进行中。'); + return; + } + setState(() => _preparingMonitor = true); + try { + final target = await _resolveLockPeerId(); + if (target.isEmpty) return; + + final paired = pairedLock.value; + final online = paired?.isOnline == true; + if (!online) { + _log_(S.isEn + ? 'Lock appears offline. Monitor may fail if lock has no network access.' + : '锁当前显示离线,若锁未联网则监控会失败,仍尝试发起...'); + print('Lock is offline, attempting monitor anyway'); + } + + if (paired != null) { + _log_(S.isEn + ? 'Enabling cat-eye realtime mode over BLE...' + : '正在通过 BLE 切换猫眼实时监控模式...'); + await CatEyeMonitorService().enableRealtimeMode( + lock: paired, + ); + _log_(S.isEn + ? 'Cat-eye realtime mode enabled over BLE.' + : '猫眼实时监控模式已通过 BLE 启用。'); + } + + _log_('Requesting Monitor to $target...'); + print('Requesting Monitor to $target...'); + final decoderReady = await _initDecoder(); + if (!decoderReady) { + _log_(S.isEn + ? 'Monitor cancelled: decoder unavailable.' + : '监控已取消:视频解码器初始化失败。'); + return; + } + _log_('Decoder initialized, sending monitor request...'); + _isCallMode = false; + _activeLockPeerId = target; + StarCloudClient.instance.talk.requestCall( + lockPeerId: target, + callerName: 'DemoApp Monitor', + options: _monitorOptions, + ); + } catch (e) { + _log_('${S.isEn ? "Failed to prepare monitor" : "监控准备失败"}: $e'); + print('[VideoTalk] monitor preparation failed: $e'); + } finally { + if (mounted) setState(() => _preparingMonitor = false); + } + } + + Future _acceptCall() async { + if (_call == null) return; + if (!await _initDecoder()) return; + _isCallMode = true; + _activeLockPeerId = _call!.lockPeerId; + StarCloudClient.instance.talk.accept(lockPeerId: _call!.lockPeerId); + } + + void _hangup() { + final target = (_call?.lockPeerId ?? _activeLockPeerId)?.trim() ?? ''; + if (target.isEmpty) return; + _log_('Hanging up...'); + print('Hanging up...'); + StarCloudClient.instance.talk.hangup(lockPeerId: target); + _releaseDecoder(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: S.langNotifier, + builder: (context, isEn, _) { + return Stack( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ValueListenableBuilder( + valueListenable: pairedLock, + builder: (context, lock, _) { + if (lock == null) return const SizedBox.shrink(); + final online = lock.isOnline == true; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Text( + isEn ? 'Lock status' : '锁状态', + style: const TextStyle( + fontSize: 10, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: online + ? Colors.green.withOpacity(0.1) + : Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: + online ? Colors.green : Colors.orange, + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: online + ? Colors.green + : Colors.orange, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + Text( + online + ? (isEn ? 'Online' : '在线') + : (isEn ? 'Offline' : '离线'), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: online + ? Colors.green + : Colors.orange, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 3), + ValueListenableBuilder( + valueListenable: videoTalkReady, + builder: (context, ready, _) { + if (!ready) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text(S.completeWifiTalkInitialization), + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: + _preparingMonitor ? null : _requestCall, + child: _preparingMonitor + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Text(S.btnMonitor), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + onPressed: (_status == TalkStatus.inCall || + _status == TalkStatus.incomingCall) + ? _hangup + : null, + icon: const Icon(Icons.call_end), + label: Text(S.btnHangup), + ), + ), + ], + ), + // 视频画面渲染区域 + const SizedBox(height: 3), + _buildVideoPreview(), + ], + ); + }, + ), + const SizedBox(height: 3), + LogArea(log: _log), + ], + ), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + child: IgnorePointer( + ignoring: _call == null, + child: AnimatedSlide( + offset: _call == null ? const Offset(0, -1.2) : Offset.zero, + duration: const Duration(milliseconds: 280), + curve: Curves.easeOutCubic, + child: AnimatedOpacity( + opacity: _call == null ? 0 : 1, + duration: const Duration(milliseconds: 180), + child: _call == null + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: _buildIncomingCallCard(), + ), + ), + ), + ), + ), + ), + ], + ); + }, + ); + } + + Widget _buildIncomingCallCard() { + final call = _call!; + return Card( + color: Colors.blue.shade50, + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Text( + S.incomingCall, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + ), + Text(call.callerName, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + onPressed: _acceptCall, + icon: const Icon(Icons.call), + label: Text(S.btnAccept), + ), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + onPressed: () { + StarCloudClient.instance.talk + .reject(lockPeerId: call.lockPeerId); + setState(() => _call = null); + }, + icon: const Icon(Icons.call_end), + label: Text(S.btnReject), + ), + ], + ), + ], + ), + ), + ); + } + + /// 视频画面渲染区域 + Widget _buildVideoPreview() { + final isSideways = _videoQuarterTurns.isOdd; + final sourceAspectRatio = _videoWidth / _videoHeight; + final displayAspectRatio = + isSideways ? 1 / sourceAspectRatio : sourceAspectRatio; + + // 未初始化解码器或无画面时显示占位 + if (!_decoderReady || _textureId == null) { + return AspectRatio( + aspectRatio: displayAspectRatio, + child: Container( + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade600, width: 1), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.videocam_off, size: 36, color: Colors.grey.shade400), + const SizedBox(height: 8), + Text( + S.isEn ? 'No video stream' : '暂无视频画面', + style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + ), + if (_status == TalkStatus.inCall) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + S.isEn ? 'Waiting for frames...' : '等待视频帧...', + style: + TextStyle(color: Colors.grey.shade500, fontSize: 10), + ), + ), + ], + ), + ), + ), + ); + } + + return AspectRatio( + aspectRatio: displayAspectRatio, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: Colors.black, + border: Border.all(color: Colors.grey.shade600, width: 1), + ), + child: Stack( + fit: StackFit.expand, + children: [ + // 旋转角由锁端 TalkExpect 协商结果决定。布局比例必须使用 + // 旋转后的宽高,否则会被固定高度的容器压缩。 + RotatedBox( + quarterTurns: _videoQuarterTurns, + child: RepaintBoundary( + child: Texture( + textureId: _textureId!, + filterQuality: FilterQuality.medium, + ), + ), + ), + // 无帧时显示加载指示 + if (!_frameRendered) + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(Colors.white70), + strokeWidth: 2, + ), + const SizedBox(height: 8), + Text( + S.isEn ? 'Initializing...' : '初始化中...', + style: const TextStyle( + color: Colors.white70, fontSize: 12), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _QueuedVideoFrame { + const _QueuedVideoFrame({ + required this.frameData, + required this.frameType, + required this.frameSeq, + required this.frameSeqI, + required this.arrivalWallTimeMs, + }); + + final Uint8List frameData; + final int frameType; + final int frameSeq; + final int frameSeqI; + final int arrivalWallTimeMs; +} diff --git a/example/lib/tabs/wifi_config_tab.dart b/example/lib/tabs/wifi_config_tab.dart new file mode 100755 index 0000000..0debf24 --- /dev/null +++ b/example/lib/tabs/wifi_config_tab.dart @@ -0,0 +1,279 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_sdk/starcloud_flutter_sdk.dart'; +import '../l10n.dart'; +import '../global.dart'; +import '../services/video_talk_initializer.dart'; +import '../widgets/common_widgets.dart'; +import 'package:starcloud/entity/star_cloud_lock_list.dart' as scl; + +class WifiConfigTab extends StatefulWidget { + const WifiConfigTab({super.key}); + + @override + State createState() => _WifiConfigTabState(); +} + +class _WifiConfigTabState extends State { + final List _log = []; + final _ssidCtrl = TextEditingController(); + final _pwdCtrl = TextEditingController(); + bool _loading = false; + + @override + void dispose() { + _ssidCtrl.dispose(); + _pwdCtrl.dispose(); + super.dispose(); + } + + void _log_(String msg) { + final ts = DateTime.now().toIso8601String().substring(11, 19); + if (mounted) setState(() => _log.add('[$ts] $msg')); + debugPrint('WiFi_DEBUG: $msg'); + } + + Future _initVideoTalk() async { + try { + _log_(S.isEn ? 'Initializing video talk...' : '正在初始化视频对讲...'); + await initializeVideoTalk(); + _log_(S.isEn + ? 'Video talk is ready. App PeerID: ${appPeerId.value}' + : '视频对讲已初始化。App PeerID: ${appPeerId.value}'); + } catch (e) { + _log_('${S.logFailed}: $e'); + } + } + + Future _config() async { + final lock = pairedLock.value; + if (lock == null) { + _log_('ERROR: No device paired.'); + return; + } + + setState(() => _loading = true); + + final String lockServiceToken = lockServiceAccessToken.value?.trim() ?? ''; + if (signedInAccount.value == null || lockServiceToken.isEmpty) { + _log_('请先在“登录”页使用锁主账号/管理员账号登录。'); + setState(() => _loading = false); + return; + } + _log_('>>> START CONFIG | HTTP + BLE 0x30F4/0x30F5'); + + try { + final ctx = await StarCloudLockContext.fromLock(lock); + final String myId = appPeerId.value ?? ''; + if (myId.isEmpty) { + _log_(S.logConfigCancelledNeedTalkInitialization); + return; + } + + _log_(S.logUsingLoginSessionForGatewayConfig); + + final reply = await StarCloudClient.instance.wifi.configureWifiByContext( + ctx, + ssid: _ssidCtrl.text.trim(), + password: _pwdCtrl.text.trim(), + lockServiceAccessToken: lockServiceToken, + deviceName: lock.bluetooth.bluetoothDeviceName, + mac: lock.mac, + ); + _log_(S.logWifiConfigCompleted); + + try { + final gatewayStatus = + await StarCloudClient.instance.wifi.getGatewayStatus( + lockId: lock.bluetooth.bluetoothDeviceName, + userId: ctx.userId, + deviceName: lock.bluetooth.bluetoothDeviceName, + mac: lock.mac, + ); + _log_( + 'GATEWAY STATUS: 0x${gatewayStatus.status.toRadixString(16).toUpperCase()}'); + } catch (e) { + _log_('GATEWAY STATUS QUERY FAILED: $e'); + } + + final blePeerId = reply.peerId?.trim() ?? ''; + if (blePeerId.isNotEmpty) { + _log_('Lock PeerID from BLE: $blePeerId'); + lock.peerId = blePeerId; + pairedLock.value = null; + pairedLock.value = lock; + unawaited(_waitForLockOnline(lock.lockId)); + } else { + await _refreshLockPeerId(lock); + } + } catch (e) { + _log_('${S.logFailed}: $e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + /// 配网成功后自动回查云端锁详情,回填锁版 peerId,并等待锁上线 + Future _refreshLockPeerId(scl.StarCloudLock lock) async { + try { + final fresh = + await StarCloudClient.instance.device.getDevice(lockId: lock.lockId); + pairedLock.value = fresh; + final p = (fresh.peerId ?? '').trim(); + _log_(p.isNotEmpty + ? 'Lock PeerID after config: $p' + : 'Lock not reported PeerID yet (may take a while).'); + // 等待锁上线 + unawaited(_waitForLockOnline(lock.lockId)); + } catch (e) { + _log_('Refresh lock detail failed: $e (retry in Talk tab)'); + } + } + + /// 配网后轮询等待锁上线(最长等待 120 秒) + Future _waitForLockOnline(int lockId) async { + const maxSeconds = 120; + const interval = Duration(seconds: 5); + int waited = 0; + _log_(S.isEn + ? 'Waiting for lock to come online (this may take 30-60s)...' + : '等待锁上线(可能需要 30-60 秒)...'); + while (waited < maxSeconds) { + await Future.delayed(interval); + waited += 5; + if (!mounted) return; + try { + final fresh = + await StarCloudClient.instance.device.getDevice(lockId: lockId); + final bool online = fresh.isOnline ?? false; + final String peerId = (fresh.peerId ?? '').trim(); + if (online && peerId.isNotEmpty) { + pairedLock.value = fresh; + _log_(S.isEn + ? 'SUCCESS: Lock is ONLINE! peerId=$peerId. You can start monitoring now.' + : '锁已上线!peerId=$peerId。现在可以开始监控了。'); + return; + } + _log_( + '[${waited}s] ${S.isEn ? "still offline" : "仍未上线"} (peerId=${peerId.isNotEmpty ? peerId : "none"})'); + } catch (e) { + _log_('Poll online status failed: $e'); + } + } + _log_(S.isEn + ? 'WARNING: Lock did not come online within 120s. Check WiFi signal and try restarting the lock.' + : '警告:120 秒内锁未上线。请检查 WiFi 信号,或尝试重启锁。'); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: S.langNotifier, + builder: (context, isEn, _) { + return ValueListenableBuilder( + valueListenable: pairedLock, + builder: (context, lock, _) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ValueListenableBuilder( + valueListenable: videoTalkReady, + builder: (context, ready, _) { + return ValueListenableBuilder( + valueListenable: videoTalkInitializing, + builder: (context, initializing, _) { + return ElevatedButton.icon( + onPressed: + ready || initializing ? null : _initVideoTalk, + icon: initializing + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.videocam), + label: Text(ready + ? '${S.btnInitTalk} (${S.initialized})' + : S.btnInitTalk), + ); + }, + ); + }, + ), + const SizedBox(height: 16), + Text( + lock != null + ? '${S.pairedStatus}: ${lock.lockName.isNotEmpty ? lock.lockName : "ID:${lock.lockId}"}' + : S.notPaired, + style: TextStyle( + color: lock != null ? Colors.green : Colors.red, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: signedInAccount, + builder: (context, account, _) => Card( + elevation: 0, + color: account == null + ? Colors.red.shade50 + : Colors.green.shade50, + child: ListTile( + leading: Icon(account == null + ? Icons.error_outline + : Icons.verified_user_outlined), + title: Text(account == null + ? '未登录' + : '${S.currentLoggedInAccountPrefix}$account'), + subtitle: Text(account == null + ? '请先在“登录”页登录锁主账号/管理员账号。' + : S.wifiUsesAuthorizedSession), + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _ssidCtrl, + decoration: InputDecoration( + labelText: isEn ? 'WiFi SSID' : 'WIFI名称', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.wifi))), + const SizedBox(height: 12), + TextField( + controller: _pwdCtrl, + obscureText: true, + decoration: InputDecoration( + labelText: isEn ? 'WiFi Password' : 'WIFI密码', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.lock_outline))), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: _loading || lock == null ? null : _config, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + minimumSize: const Size.fromHeight(50)), + icon: _loading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 2)) + : const Icon(Icons.send), + label: Text(_loading ? S.btnProcessing : S.btnStartConfig), + ), + const SizedBox(height: 20), + LogArea(log: _log), + ], + ), + ); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/common_widgets.dart b/example/lib/widgets/common_widgets.dart new file mode 100755 index 0000000..4f3498c --- /dev/null +++ b/example/lib/widgets/common_widgets.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class StatusChip extends StatelessWidget { + final String label; + final bool ready; + const StatusChip({super.key, required this.label, required this.ready}); + @override + Widget build(BuildContext context) { + return Chip( + label: Text(label), + avatar: Icon(ready ? Icons.check_circle : Icons.radio_button_unchecked, + color: ready ? Colors.green : Colors.grey, size: 18), + backgroundColor: ready ? Colors.green.shade50 : Colors.grey.shade100); + } +} + +class ActionButton extends StatelessWidget { + final String label; + final IconData icon; + final Color color; + final bool loading; + final VoidCallback onTap; + const ActionButton( + {super.key, + required this.label, + required this.icon, + required this.color, + required this.loading, + required this.onTap}); + @override + Widget build(BuildContext context) { + return ElevatedButton.icon( + onPressed: loading ? null : onTap, + icon: Icon(icon, color: color), + label: Text(label), + style: ElevatedButton.styleFrom(backgroundColor: color.withAlpha(25))); + } +} + +class LogArea extends StatelessWidget { + final List log; + const LogArea({super.key, required this.log}); + @override + Widget build(BuildContext context) { + if (log.isEmpty) return const SizedBox.shrink(); + return Container( + height: 120, + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF1E1E1E), + borderRadius: BorderRadius.circular(8)), + child: ListView.builder( + reverse: true, + itemCount: log.length, + itemBuilder: (_, i) => Text(log[log.length - 1 - i], + style: const TextStyle( + color: Color(0xFF4EC9B0), + fontSize: 11, + fontFamily: 'monospace'))), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100755 index 0000000..4807d98 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,749 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.5" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.3" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.2" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.3" + date_format: + dependency: transitive + description: + name: date_format + sha256: a48254e60bdb7f1d5a15cac7f86e37491808056c0a99dbdc850841def4754ddc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.9" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.1.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.2" + dio: + dependency: transitive + description: + name: dio + sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.10.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: + dependency: transitive + description: + name: flutter_blue_plus + sha256: "59fded880d769859e9e2fd6e5599555575174f642d758cc846988c86c4341131" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.33.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + flutter_pcm_sound: + dependency: "direct main" + description: + name: flutter_pcm_sound + sha256: e963b3a0c7658d57255f6e44fceddff6c7751dff82b7bcd561cfc1d5478d4e20 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.7" + flutter_sdk: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.1.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.7" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "12.0.13" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.4.10" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.3" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.9.1" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + record: + dependency: "direct main" + description: + name: record + sha256: "8cb57763d954624fbc673874930c6f1ceca3baaf9bfee24b25da6fd451362394" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.0" + record_android: + dependency: transitive + description: + name: record_android + sha256: "0b4739a2502fff402b0ac0ff1d6b2740854d116d78e06a4a16b3989821f84446" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + record_darwin: + dependency: transitive + description: + name: record_darwin + sha256: e487eccb19d82a9a39cd0126945cfc47b9986e0df211734e2788c95e3f63c82c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + record_linux: + dependency: transitive + description: + name: record_linux + sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.2" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + sha256: "8a575828733d4c3cb5983c914696f40db8667eab3538d4c41c50cbb79e722ef4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + record_web: + dependency: transitive + description: + name: record_web + sha256: "10cb041349024ce4256e11dd35874df26d8b45b800678f2f51fd1318901adc64" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.4" + record_windows: + dependency: transitive + description: + name: record_windows + sha256: "7bce0ac47454212ca8bfa72791d8b6a951f2fb0d4b953b64443c014227f035b4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + 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.flutter-io.cn" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.1" + starchart: + dependency: transitive + description: + path: "../../starchart" + relative: true + source: path + version: "0.1.0" + starcloud: + dependency: "direct main" + description: + path: "../../starcloud" + relative: true + source: path + version: "0.1.2" + starlock_ble: + dependency: "direct main" + description: + path: "../../starlock_ble" + relative: true + source: path + version: "0.1.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0+1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + video_decode_plugin: + dependency: "direct main" + description: + path: "../../video_decode_plugin" + relative: true + source: path + version: "0.0.1" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.flutter-io.cn" + source: hosted + version: "13.0.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.5.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.3.4 <4.0.0" + flutter: ">=3.19.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100755 index 0000000..175d79c --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,45 @@ +name: flutter_sdk_example +description: Example application for flutter_sdk. +publish_to: 'none' + +version: 0.1.0+1 + +environment: + sdk: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" + +dependencies: + flutter: + sdk: flutter + + flutter_sdk: + path: .. + starlock_ble: + path: ../../starlock_ble + starcloud: + path: ../../starcloud + crypto: ^3.0.3 + shared_preferences: ^2.2.3 + + # 设备信息(用于视频对讲初始化) + device_info_plus: ^10.0.0 + + # 视频对讲:铃声(来电/呼叫/挂断) + audioplayers: ^5.2.1 + # 视频对讲:PCM 音频播放(G711 解码后的 PCM 流) + flutter_pcm_sound: ^1.2.7 + # 视频对讲:麦克风录音(PCM 流采集) + record: ^5.1.2 + # 视频对讲:权限申请 + permission_handler: ^11.3.1 + # 视频对讲:H264 裸流解码渲染(本地插件,含 ffmpeg-kit) + video_decode_plugin: + path: ../../video_decode_plugin + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: + uses-material-design: true diff --git a/lib/src/advanced/models/lock_setting_models.dart b/lib/src/advanced/models/lock_setting_models.dart new file mode 100755 index 0000000..6e7bd7c --- /dev/null +++ b/lib/src/advanced/models/lock_setting_models.dart @@ -0,0 +1,57 @@ +enum StarCloudLockFeatureBit { + remoteUnlock(28), + autoLock(29), + antiPryAlarm(30), + resetButton(31), + lockSound(33), + passageMode(50), + appUnlockOnline(55), + bluetoothBroadcast(56), + stayWarn(61), + abnormalWarn(62); + + final int value; + const StarCloudLockFeatureBit(this.value); +} + +class StarCloudLockSettingRequest { + final String lockId; + final String keyId; + final String userId; + final List token; + final int needAuthor; + final List signKey; + final List privateKey; + + const StarCloudLockSettingRequest({ + required this.lockId, + required this.keyId, + required this.userId, + required this.token, + required this.needAuthor, + required this.signKey, + required this.privateKey, + }); +} + +class StarCloudFeatureToggleRequest { + final String keyId; + final String userId; + final int featureBit; + final int featureEnable; + final List token; + final int needAuthor; + final List publicKey; + final List privateKey; + + const StarCloudFeatureToggleRequest({ + required this.keyId, + required this.userId, + required this.featureBit, + required this.featureEnable, + required this.token, + required this.needAuthor, + required this.publicKey, + required this.privateKey, + }); +} \ No newline at end of file diff --git a/lib/src/advanced/models/lock_state_models.dart b/lib/src/advanced/models/lock_state_models.dart new file mode 100755 index 0000000..d37730a --- /dev/null +++ b/lib/src/advanced/models/lock_state_models.dart @@ -0,0 +1,15 @@ +class StarCloudLockStateRequest { + final String lockId; + final String userId; + final int utcTimeStamp; + final int unixTimeStamp; + final List privateKey; + + const StarCloudLockStateRequest({ + required this.lockId, + required this.userId, + required this.utcTimeStamp, + required this.unixTimeStamp, + required this.privateKey, + }); +} \ No newline at end of file diff --git a/lib/src/advanced/models/wifi_models.dart b/lib/src/advanced/models/wifi_models.dart new file mode 100755 index 0000000..acb624e --- /dev/null +++ b/lib/src/advanced/models/wifi_models.dart @@ -0,0 +1,91 @@ +class StarCloudWifiSearchRequest { + final String keyId; + final String userId; + final List token; + final int needAuthor; + final List signKey; + + /// 锁公钥。WiFi 直连配网认证摘要使用此字段。 + final List? publicKey; + final List privateKey; + + /// 锁的蓝牙设备名(如锁名),用于连接前定位设备 + final String deviceName; + + /// 锁的蓝牙 MAC 地址,Android 优先使用 + final String mac; + + const StarCloudWifiSearchRequest({ + required this.keyId, + required this.userId, + required this.token, + required this.needAuthor, + required this.signKey, + this.publicKey, + required this.privateKey, + this.deviceName = '', + this.mac = '', + }); +} + +class StarCloudWifiConfigRequest { + final String keyId; + final String userId; + final String ssid; + final String password; + final String peerId; + final int numberOfServers; + final List listOfServers; + final int numberOfPhone; + final List listOfPhone; + final List token; + final int needAuthor; + final List signKey; + + /// 锁公钥。WiFi 直连配网认证摘要使用此字段。 + final List? publicKey; + final List privateKey; + final String? gatewayConfigurationStr; + + /// 锁的蓝牙设备名(如锁名),用于连接前定位设备 + final String deviceName; + + /// 锁的蓝牙 MAC 地址,Android 优先使用 + final String mac; + + const StarCloudWifiConfigRequest({ + required this.keyId, + required this.userId, + required this.ssid, + required this.password, + required this.peerId, + required this.numberOfServers, + required this.listOfServers, + required this.numberOfPhone, + required this.listOfPhone, + required this.token, + required this.needAuthor, + required this.signKey, + this.publicKey, + required this.privateKey, + this.gatewayConfigurationStr, + this.deviceName = '', + this.mac = '', + }); +} + +class WifiInfo { + final String ssid; + final int signal; + final int security; + + const WifiInfo({ + required this.ssid, + required this.signal, + required this.security, + }); + + @override + String toString() => + 'WifiInfo{ssid: $ssid, signal: $signal, security: $security}'; +} diff --git a/lib/src/advanced/protocol/starcloud_protocol_exception.dart b/lib/src/advanced/protocol/starcloud_protocol_exception.dart new file mode 100755 index 0000000..fc8d783 --- /dev/null +++ b/lib/src/advanced/protocol/starcloud_protocol_exception.dart @@ -0,0 +1,10 @@ +class StarCloudProtocolException implements Exception { + final String message; + final Object? raw; + + const StarCloudProtocolException(this.message, {this.raw}); + + @override + String toString() => + 'StarCloudProtocolException(message: $message, raw: $raw)'; +} \ No newline at end of file diff --git a/lib/src/advanced/protocol/starcloud_protocol_executor.dart b/lib/src/advanced/protocol/starcloud_protocol_executor.dart new file mode 100755 index 0000000..9a83a5e --- /dev/null +++ b/lib/src/advanced/protocol/starcloud_protocol_executor.dart @@ -0,0 +1,9 @@ +import 'package:starlock_ble/starlock_ble.dart'; + +abstract class StarCloudProtocolExecutor { + Future executeForReply( + SenderProtocol command, { + Duration timeout, + bool Function(T reply)? test, + }); +} \ No newline at end of file diff --git a/lib/src/advanced/protocol/starcloud_protocol_executor_impl.dart b/lib/src/advanced/protocol/starcloud_protocol_executor_impl.dart new file mode 100755 index 0000000..63615f9 --- /dev/null +++ b/lib/src/advanced/protocol/starcloud_protocol_executor_impl.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:starlock_ble/starlock_ble.dart'; + +import 'starcloud_protocol_executor.dart'; + +class StarCloudProtocolExecutorImpl implements StarCloudProtocolExecutor { + @override + Future executeForReply( + SenderProtocol command, { + Duration? timeout, + bool Function(T reply)? test, + }) async { + final sdk = StarLockBLE.instance; + + // send the command + await sdk.send(command); + + // listen for replies on the shared EventBus and apply optional predicate + final bus = EventBusManager().bus; + if (bus == null) { + throw StateError('EventBus is not initialized'); + } + + final completer = Completer(); + final sub = bus.on().listen((reply) { + if (reply is T) { + final tReply = reply as T; + if (test == null || test(tReply)) { + if (!completer.isCompleted) { + completer.complete(tReply); + } + } + } + }); + + try { + return await completer.future.timeout(timeout ?? sdk.timeout, onTimeout: () { + throw TimeoutException('Timeout waiting for reply: ${T.toString()}'); + }); + } finally { + await sub.cancel(); + } + } +} diff --git a/lib/src/advanced/starcloud_lock_setting_service.dart b/lib/src/advanced/starcloud_lock_setting_service.dart new file mode 100755 index 0000000..188b7db --- /dev/null +++ b/lib/src/advanced/starcloud_lock_setting_service.dart @@ -0,0 +1,417 @@ +import 'package:starlock_ble/starlock_ble.dart'; + +import '../core/starcloud_lock_context.dart'; +import 'models/lock_setting_models.dart'; +import 'protocol/starcloud_protocol_executor.dart'; + +class StarCloudLockSettingService { + StarCloudLockSettingService(this._executor); + + final StarCloudProtocolExecutor _executor; + + UpdataLockSetCommand buildUploadLockSettingCommand( + StarCloudLockSettingRequest request, + ) { + return UpdataLockSetCommand( + lockID: request.lockId, + keyID: request.keyId, + userID: request.userId, + token: request.token, + needAuthor: request.needAuthor, + signKey: request.signKey, + privateKey: request.privateKey, + ); + } + + Future uploadLockSetting( + StarCloudLockSettingRequest request, { + Duration timeout = const Duration(seconds: 15), + }) { + return _executor.executeForReply( + buildUploadLockSettingCommand(request), + timeout: timeout, + ); + } + + ReadSupportFunctionsNoParametersCommand buildReadFeatureCommand( + StarCloudFeatureToggleRequest request, + ) { + return ReadSupportFunctionsNoParametersCommand( + keyID: request.keyId, + userID: request.userId, + featureBit: request.featureBit, + token: request.token, + needAuthor: request.needAuthor, + publicKey: request.publicKey, + privateKey: request.privateKey, + ); + } + + Future readFeature( + StarCloudFeatureToggleRequest request, { + Duration timeout = const Duration(seconds: 15), + }) { + return _executor.executeForReply( + buildReadFeatureCommand(request), + timeout: timeout, + ); + } + + SetSupportFunctionsNoParametersCommand buildSetFeatureCommand( + StarCloudFeatureToggleRequest request, + ) { + return SetSupportFunctionsNoParametersCommand( + keyID: request.keyId, + userID: request.userId, + featureBit: request.featureBit, + featureEnable: request.featureEnable, + token: request.token, + needAuthor: request.needAuthor, + publicKey: request.publicKey, + privateKey: request.privateKey, + ); + } + + Future setFeature( + StarCloudFeatureToggleRequest request, { + Duration timeout = const Duration(seconds: 15), + }) { + return _executor.executeForReply( + buildSetFeatureCommand(request), + timeout: timeout, + ); + } + + ReadSupportFunctionsWithParametersCommand + buildReadFeatureWithParametersCommand( + StarCloudFeatureToggleRequest request, + ) { + return ReadSupportFunctionsWithParametersCommand( + keyID: request.keyId, + userID: request.userId, + featureBit: request.featureBit, + token: request.token, + needAuthor: request.needAuthor, + publicKey: request.publicKey, + privateKey: request.privateKey, + ); + } + + Future readFeatureWithParameters( + StarCloudFeatureToggleRequest request, { + Duration timeout = const Duration(seconds: 15), + }) { + return _executor.executeForReply( + buildReadFeatureWithParametersCommand(request), + timeout: timeout, + ); + } + + SetSupportFunctionsWithParametersCommand + buildSetFeatureWithParametersCommand({ + required String keyId, + required String userId, + required int featureBit, + required int featureParaLength, + required List featureData, + required List token, + required int needAuthor, + required List publicKey, + required List privateKey, + }) { + return SetSupportFunctionsWithParametersCommand( + keyID: keyId, + userID: userId, + featureBit: featureBit, + featureParaLength: featureParaLength, + featureData: featureData, + token: token, + needAuthor: needAuthor, + publicKey: publicKey, + privateKey: privateKey, + ); + } + + Future setFeatureWithParameters({ + required String keyId, + required String userId, + required int featureBit, + required int featureParaLength, + required List featureData, + required List token, + required int needAuthor, + required List publicKey, + required List privateKey, + Duration timeout = const Duration(seconds: 15), + }) { + return _executor.executeForReply( + buildSetFeatureWithParametersCommand( + keyId: keyId, + userId: userId, + featureBit: featureBit, + featureParaLength: featureParaLength, + featureData: featureData, + token: token, + needAuthor: needAuthor, + publicKey: publicKey, + privateKey: privateKey, + ), + timeout: timeout, + ); + } + + Future setRemoteUnlock({ + required String keyId, + required String userId, + required bool enabled, + required List token, + required int needAuthor, + required List publicKey, + required List privateKey, + Duration timeout = const Duration(seconds: 15), + }) { + return setFeature( + StarCloudFeatureToggleRequest( + keyId: keyId, + userId: userId, + featureBit: StarCloudLockFeatureBit.remoteUnlock.value, + featureEnable: enabled ? 1 : 0, + token: token, + needAuthor: needAuthor, + publicKey: publicKey, + privateKey: privateKey, + ), + timeout: timeout, + ); + } + + Future setAppUnlockOnline({ + required String keyId, + required String userId, + required bool enabled, + required List token, + required int needAuthor, + required List publicKey, + required List privateKey, + Duration timeout = const Duration(seconds: 15), + }) { + return setFeature( + StarCloudFeatureToggleRequest( + keyId: keyId, + userId: userId, + featureBit: StarCloudLockFeatureBit.appUnlockOnline.value, + featureEnable: enabled ? 1 : 0, + token: token, + needAuthor: needAuthor, + publicKey: publicKey, + privateKey: privateKey, + ), + timeout: timeout, + ); + } + + Future setBluetoothBroadcast({ + required String keyId, + required String userId, + required bool enabled, + required List token, + required int needAuthor, + required List publicKey, + required List privateKey, + Duration timeout = const Duration(seconds: 15), + }) { + return setFeature( + StarCloudFeatureToggleRequest( + keyId: keyId, + userId: userId, + featureBit: StarCloudLockFeatureBit.bluetoothBroadcast.value, + featureEnable: enabled ? 1 : 0, + token: token, + needAuthor: needAuthor, + publicKey: publicKey, + privateKey: privateKey, + ), + timeout: timeout, + ); + } + + // --------------------------------------------------------------------------- + // ByContext helpers — auth fields are filled automatically from the context. + // --------------------------------------------------------------------------- + + /// Simplified variant — auth fields are filled from [context]. + /// + /// ```dart + /// await StarCloudClient.instance.lockSetting + /// .uploadLockSettingByContext(context); + /// ``` + Future uploadLockSettingByContext( + StarCloudLockContext context, { + Duration timeout = const Duration(seconds: 15), + }) { + return uploadLockSetting( + StarCloudLockSettingRequest( + lockId: context.lockId.toString(), + keyId: context.keyId, + userId: context.userId, + token: context.token, + needAuthor: context.needAuthor, + signKey: context.signKey, + privateKey: context.privateKey, + ), + timeout: timeout, + ); + } + + /// Simplified variant — auth fields are filled from [context]. + /// + /// ```dart + /// final reply = await StarCloudClient.instance.lockSetting + /// .readFeatureByContext(context, featureBit: StarCloudLockFeatureBit.remoteUnlock); + /// ``` + Future readFeatureByContext( + StarCloudLockContext context, { + required StarCloudLockFeatureBit featureBit, + Duration timeout = const Duration(seconds: 15), + }) { + return readFeature( + StarCloudFeatureToggleRequest( + keyId: context.keyId, + userId: context.userId, + featureBit: featureBit.value, + featureEnable: 0, + token: context.token, + needAuthor: context.needAuthor, + publicKey: context.publicKey, + privateKey: context.privateKey, + ), + timeout: timeout, + ); + } + + /// Simplified variant — auth fields are filled from [context]. + /// + /// ```dart + /// await StarCloudClient.instance.lockSetting.setFeatureByContext( + /// context, + /// featureBit: StarCloudLockFeatureBit.remoteUnlock, + /// enabled: true, + /// ); + /// ``` + Future setFeatureByContext( + StarCloudLockContext context, { + required StarCloudLockFeatureBit featureBit, + required bool enabled, + Duration timeout = const Duration(seconds: 15), + }) { + return setFeature( + StarCloudFeatureToggleRequest( + keyId: context.keyId, + userId: context.userId, + featureBit: featureBit.value, + featureEnable: enabled ? 1 : 0, + token: context.token, + needAuthor: context.needAuthor, + publicKey: context.publicKey, + privateKey: context.privateKey, + ), + timeout: timeout, + ); + } + + /// Simplified variant — auth fields are filled from [context]. + Future + readFeatureWithParametersByContext( + StarCloudLockContext context, { + required StarCloudLockFeatureBit featureBit, + Duration timeout = const Duration(seconds: 15), + }) { + return readFeatureWithParameters( + StarCloudFeatureToggleRequest( + keyId: context.keyId, + userId: context.userId, + featureBit: featureBit.value, + featureEnable: 0, + token: context.token, + needAuthor: context.needAuthor, + publicKey: context.publicKey, + privateKey: context.privateKey, + ), + timeout: timeout, + ); + } + + /// Simplified variant — auth fields are filled from [context]. + /// + /// ```dart + /// await StarCloudClient.instance.lockSetting.setFeatureWithParametersByContext( + /// context, + /// featureBit: StarCloudLockFeatureBit.passageMode, + /// featureParaLength: 2, + /// featureData: [1, 0], + /// ); + /// ``` + Future + setFeatureWithParametersByContext( + StarCloudLockContext context, { + required StarCloudLockFeatureBit featureBit, + required int featureParaLength, + required List featureData, + Duration timeout = const Duration(seconds: 15), + }) { + return setFeatureWithParameters( + keyId: context.keyId, + userId: context.userId, + featureBit: featureBit.value, + featureParaLength: featureParaLength, + featureData: featureData, + token: context.token, + needAuthor: context.needAuthor, + publicKey: context.publicKey, + privateKey: context.privateKey, + timeout: timeout, + ); + } + + /// Simplified variant — auth fields are filled from [context]. + Future setRemoteUnlockByContext( + StarCloudLockContext context, { + required bool enabled, + Duration timeout = const Duration(seconds: 15), + }) { + return setFeatureByContext( + context, + featureBit: StarCloudLockFeatureBit.remoteUnlock, + enabled: enabled, + timeout: timeout, + ); + } + + /// Simplified variant — auth fields are filled from [context]. + Future setAppUnlockOnlineByContext( + StarCloudLockContext context, { + required bool enabled, + Duration timeout = const Duration(seconds: 15), + }) { + return setFeatureByContext( + context, + featureBit: StarCloudLockFeatureBit.appUnlockOnline, + enabled: enabled, + timeout: timeout, + ); + } + + /// Simplified variant — auth fields are filled from [context]. + Future setBluetoothBroadcastByContext( + StarCloudLockContext context, { + required bool enabled, + Duration timeout = const Duration(seconds: 15), + }) { + return setFeatureByContext( + context, + featureBit: StarCloudLockFeatureBit.bluetoothBroadcast, + enabled: enabled, + timeout: timeout, + ); + } +} diff --git a/lib/src/advanced/starcloud_lock_state_service.dart b/lib/src/advanced/starcloud_lock_state_service.dart new file mode 100755 index 0000000..83730c7 --- /dev/null +++ b/lib/src/advanced/starcloud_lock_state_service.dart @@ -0,0 +1,64 @@ +import 'package:starcloud/blue/blue_read_write.dart'; +import 'package:starcloud/device/ble/protocol/public/lock_status_info.dart'; + +import '../core/starcloud_exception.dart'; +import '../core/starcloud_lock_context.dart'; +import 'models/lock_state_models.dart'; + +class StarCloudLockStateService { + StarCloudLockStateService(); + + LockStatusInfoCommand _buildReadLockStateCommand( + StarCloudLockStateRequest request, + ) { + return LockStatusInfoCommand( + lockID: request.lockId, + userID: request.userId, + utcTimeStamp: request.utcTimeStamp, + unixTimeStamp: request.unixTimeStamp, + privateKey: request.privateKey, + ); + } + + List buildReadLockStatePayload( + StarCloudLockStateRequest request, + ) { + return _buildReadLockStateCommand(request).packageData(); + } + + Future readLockState( + StarCloudLockStateRequest request, { + Duration timeout = const Duration(seconds: 15), + }) async { + final command = _buildReadLockStateCommand(request); + try { + final reply = await BlueReadWrite.shareManager()!.writeAndReturn( + command, + getPrivateKey: () => request.privateKey, + ); + return reply as GetStarLockStatusInfoReply; + } catch (e) { + throw StarCloudException( + 'Read lock state failed', + raw: e, + ); + } + } + + Future readLockStateByContext( + StarCloudLockContext context, { + Duration timeout = const Duration(seconds: 15), + }) { + final now = DateTime.now(); + return readLockState( + StarCloudLockStateRequest( + lockId: context.lockId.toString(), + userId: context.userId, + utcTimeStamp: now.toUtc().millisecondsSinceEpoch ~/ 1000, + unixTimeStamp: now.millisecondsSinceEpoch ~/ 1000, + privateKey: context.privateKey, + ), + timeout: timeout, + ); + } +} diff --git a/lib/src/advanced/starcloud_wifi_service.dart b/lib/src/advanced/starcloud_wifi_service.dart new file mode 100755 index 0000000..3a39dd8 --- /dev/null +++ b/lib/src/advanced/starcloud_wifi_service.dart @@ -0,0 +1,656 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:starcloud/blue/blue_features.dart'; +import 'package:starcloud/blue/blue_read_write.dart'; +import 'package:starcloud/blue/entity/blue_message.dart'; +import 'package:starcloud/device/ble/business/ble_type.dart'; +import 'package:starcloud/device/ble/protocol/public/configuring_wifi.dart'; +import 'package:starcloud/device/ble/protocol/public/gateway_configuring_wifi.dart'; +import 'package:starcloud/device/ble/protocol/public/gateway_get_status.dart'; +import 'package:starcloud/device/ble/protocol/public/get_wifi_list.dart'; +import 'package:starcloud/net/entity/api_response.dart'; +import 'package:starcloud/sdk/business/sdk_common.dart'; +import 'package:starcloud/sdk/business/sdk_wrapper.dart'; +import 'package:starchart/src/discovery/discovery_service.dart'; + +import '../core/starcloud_exception.dart'; +import '../core/starcloud_lock_context.dart'; +import '../starcloud_client.dart'; +import 'models/wifi_models.dart' as wifi_models; + +class StarCloudWifiService { + StarCloudWifiService(); + + /// 配网/搜 WiFi 前必须设置锁的蓝牙设备名与 MAC, + /// 否则 BlueReadWrite 单例为空会走空名搜索 → 15s 超时 → 10004 蓝牙请求超时。 + /// + /// 注意:Android 12+ 蓝牙使用随机地址,云端锁详情里的静态 MAC 无法 + /// `BluetoothDevice.fromId()` 直连(实测 10s connect 超时),因此 + /// Android 一律只设置设备名、走扫描路径(_searchByDeviceName); + /// 仅 iOS 使用 MAC 直连。 + BlueReadWrite _prepareBlueRW({String deviceName = '', String mac = ''}) { + final blueRW = BlueReadWrite.shareManager(); + if (blueRW == null) { + throw StarCloudException( + 'Bluetooth service is unavailable', + raw: 'BlueReadWrite manager is null', + ); + } + if (deviceName.isNotEmpty) blueRW.deviceName = deviceName; + if (!Platform.isAndroid && mac.isNotEmpty) blueRW.mac = mac; + blueRW.type = BlueFeaturesType.lock; + return blueRW; + } + + GetWifiListCommand _buildSearchCommand( + wifi_models.StarCloudWifiSearchRequest request) { + return GetWifiListCommand( + userID: request.userId, + privateKey: request.privateKey, + ); + } + + GatewayConfiguringWifiCommand _buildConfigCommand( + wifi_models.StarCloudWifiConfigRequest request) { + return GatewayConfiguringWifiCommand( + ssid: request.ssid, + password: request.password, + gatewayConfigurationStr: request.gatewayConfigurationStr ?? '', + ); + } + + Future> searchWifiAndGetList( + wifi_models.StarCloudWifiSearchRequest request, { + Duration timeout = const Duration(seconds: 20), + }) async { + final blueRW = _prepareBlueRW( + deviceName: request.deviceName, + mac: request.mac, + ); + final command = _buildSearchCommand(request); + final completer = Completer>(); + final List accumulated = []; + + Timer? timeoutTimer; + Timer? settleTimer; + StreamSubscription? messageSub; + + void complete() { + timeoutTimer?.cancel(); + settleTimer?.cancel(); + messageSub?.cancel(); + if (!completer.isCompleted) { + completer.complete(List.unmodifiable(accumulated)); + } + } + + void cleanup() { + timeoutTimer?.cancel(); + settleTimer?.cancel(); + messageSub?.cancel(); + } + + messageSub = blueRW.messageStream.listen((BlueMessage message) { + if (message.data.length < 2) return; + final int cmdType = message.data[0] * 256 + message.data[1]; + if (cmdType == CommandType.gatewayGetWifiListResult.typeValue) { + final resultReply = GetWifiListResultReply.parseData( + CommandType.gatewayGetWifiListResult, + message.data, + ); + final entries = resultReply.wifiList + .map((m) => wifi_models.WifiInfo( + ssid: m['ssid'] as String, + signal: m['signal'] as int, + security: m['security'] as int, + )) + .where((w) => w.ssid.isNotEmpty) + .toList(); + accumulated.addAll(entries); + + // 重置结算计时器:在最后一条结果消息到达后 1.5 秒完成 + settleTimer?.cancel(); + settleTimer = Timer(const Duration(milliseconds: 1500), complete); + } + }); + + timeoutTimer = Timer(timeout, () { + cleanup(); + if (!completer.isCompleted) { + if (accumulated.isNotEmpty) { + completer.complete(List.unmodifiable(accumulated)); + } else { + completer.completeError( + StarCloudException( + 'WiFi list search timeout', + raw: 'timeout', + ), + ); + } + } + }); + + try { + await blueRW.writeAndReturn( + command, + getPrivateKey: () => request.privateKey, + ); + } catch (e) { + cleanup(); + if (!completer.isCompleted) { + completer.completeError( + StarCloudException('WiFi search failed', raw: e), + ); + } + } + + return completer.future; + } + + Future configureWifi( + wifi_models.StarCloudWifiConfigRequest request, { + Duration timeout = const Duration(seconds: 120), + }) async { + print( + '[WifiConfig] start ssid=${request.ssid} deviceName=${request.deviceName} mac=${request.mac} configLen=${request.gatewayConfigurationStr?.length ?? 0}'); + final blueRW = _prepareBlueRW( + deviceName: request.deviceName, + mac: request.mac, + ); + final command = _buildConfigCommand(request); + // 清除上一个业务残留的回调,防止 0x30F4/0x30F5 数据被错误格式解析(RangeError / 误识别) + blueRW.clearCallback(); + final completer = Completer(); + + Timer? timer; + Timer? reconnectTimer; + StreamSubscription? messageSub; + + void cleanup() { + timer?.cancel(); + reconnectTimer?.cancel(); + messageSub?.cancel(); + blueRW.clearCallback(); + } + + // 监听配网结果(0x30F5) + messageSub = blueRW.messageStream.listen((BlueMessage message) { + if (message.data.length < 2) return; + final int cmdType = message.data[0] * 256 + message.data[1]; + if (cmdType == CommandType.gatewayConfiguringWifiResult.typeValue) { + cleanup(); + if (!completer.isCompleted) { + final reply = GatewayConfiguringWifiResultReply.parseData( + CommandType.gatewayConfiguringWifiResult, + message.data, + ); + completer.complete(reply); + } + } + }); + + timer = Timer(timeout, () { + cleanup(); + if (!completer.isCompleted) { + completer.completeError( + StarCloudException( + 'WiFi configuration timeout after ${timeout.inSeconds}s', + raw: 'timeout', + ), + ); + } + }); + + try { + final ackReply = await blueRW.writeAndReturn( + command, + getPrivateKey: () => request.privateKey, + ); + if (ackReply.status != 0x00) { + cleanup(); + if (!completer.isCompleted) { + completer.completeError( + StarCloudException( + 'WiFi configuration rejected by device', + raw: 'status=0x${ackReply.status.toRadixString(16)}', + ), + ); + } + } + + void scheduleReconnect() { + reconnectTimer = Timer(const Duration(seconds: 5), () async { + if (completer.isCompleted) return; + try { + await blueRW.reconnect(isNeedSeacrch: true); + scheduleReconnect(); + } catch (_) { + scheduleReconnect(); + } + }); + } + + scheduleReconnect(); + } catch (e) { + print('[WifiConfig] writeAndReturn failed: $e'); + cleanup(); + if (!completer.isCompleted) { + completer.completeError( + StarCloudException('WiFi configuration failed', raw: e), + ); + } + } + + return completer.future; + } + + Future configureWifiByContext( + StarCloudLockContext context, { + required String ssid, + required String password, + required String lockServiceAccessToken, + String? gatewayConfigurationStr, + String deviceName = '', + String mac = '', + Duration timeout = const Duration(seconds: 120), + }) async { + final String configStr = + (gatewayConfigurationStr != null && gatewayConfigurationStr.isNotEmpty) + ? gatewayConfigurationStr + : await _fetchGatewayConfiguration( + context.userId, + lockServiceAccessToken, + ); + final GatewayConfiguringWifiResultReply reply = await configureWifi( + wifi_models.StarCloudWifiConfigRequest( + keyId: context.keyId, + userId: context.userId, + ssid: ssid, + password: password, + peerId: '', + numberOfServers: 0, + listOfServers: [], + numberOfPhone: 0, + listOfPhone: [], + token: context.token, + needAuthor: context.needAuthor, + signKey: context.signKey, + privateKey: context.privateKey, + gatewayConfigurationStr: configStr, + deviceName: deviceName, + mac: mac, + ), + timeout: timeout, + ); + if (reply.status != 0x00) { + throw StarCloudException( + 'Gateway WiFi configuration rejected by device', + raw: 'status=0x${reply.status.toRadixString(16)}', + ); + } + final String peerId = reply.peerId?.trim() ?? ''; + final String wifiName = (reply.wifiName?.trim().isNotEmpty ?? false) + ? reply.wifiName!.trim() + : ssid; + final String secretKey = reply.secretKey?.trim() ?? ''; + final String deviceMac = (reply.deviceMac?.trim().isNotEmpty ?? false) + ? reply.deviceMac!.trim() + : mac; + final String networkMac = reply.networkMac?.trim() ?? ''; + // 与原始 App 保持一致:设备固件只保证 peerId 和 secretKey 必有值。 + // 其余字段在部分固件中会缺失,提交空值或客户端已知值即可。 + if (peerId.isEmpty || secretKey.isEmpty) { + throw StarCloudException( + 'BLE 0x30F5 response is missing required peerId or secretKey', + raw: { + 'payload': reply.payloadJson, + 'peerId': peerId, + 'wifiName': wifiName, + 'secretKeyPresent': secretKey.isNotEmpty, + 'deviceMac': deviceMac, + 'networkMac': networkMac, + 'rssi': reply.rssi, + 'bytes': reply.data, + }, + ); + } + print('[WifiConfig] 0x30F5 parsed peerId=$peerId wifiName=$wifiName ' + 'deviceMac=$deviceMac networkMac=$networkMac rssi=${reply.rssi ?? 0}'); + final ApiResponse settingResponse = + await SDKCommon.instance.netApi.deviceApi.settingDeviceNetwork( + lockServiceAccessToken: lockServiceAccessToken, + deviceType: 2, + deviceMac: deviceMac, + wifiName: wifiName, + networkMac: networkMac, + secretKey: secretKey, + peerId: peerId, + rssi: reply.rssi ?? 0, + ); + if (settingResponse.errcode != 0) { + throw StarCloudException( + 'WiFi succeeded over BLE, but deviceNetwork/setting failed: ' + '${settingResponse.errmsg}', + raw: settingResponse, + ); + } + return reply; + } + + /// 查询网关是否已接受网络配置并完成联网初始化。 + /// + /// 0x30F4/0x30F5 只表示配网流程被接受,不能作为网关已上线的依据; + /// 参考项目在此之后使用 0x30F8 查询状态。 + Future getGatewayStatus({ + required String lockId, + required String userId, + String deviceName = '', + String mac = '', + }) async { + final blueRW = _prepareBlueRW(deviceName: deviceName, mac: mac); + blueRW.clearCallback(); + try { + final reply = await blueRW.writeAndReturn( + GatewayGetStatusCommand(lockId: lockId, userId: userId), + ); + return reply as GatewayGetStatusReply; + } catch (e) { + throw StarCloudException('Gateway status query failed', raw: e); + } finally { + blueRW.clearCallback(); + } + } + + Future _fetchGatewayConfiguration( + String userId, + String lockServiceAccessToken, + ) async { + final String userPeerId = await _getStarChartPeerId(userId); + try { + final ApiResponse response = await SDKCommon.instance.netApi.deviceApi + .getGatewayConfig(lockServiceAccessToken: lockServiceAccessToken); + if (response.errcode == 0 && response.data != null) { + final Map jsonMap; + if (response.data is Map) { + jsonMap = Map.from(response.data as Map); + } else { + jsonMap = + json.decode(response.data.toString()) as Map; + } + print('[WifiConfig] gateway config keys: ${jsonMap.keys.toList()}'); + // 保留所有云端返回的 URL/服务器配置字段(cloud/starCloudUrl 等), + // 锁端需要这些地址才能连接中继服务器上线。 + // 仅移除锁端不应直接使用的锁 Peer 字段。 + jsonMap.remove('starLockPeerId'); + + // 修复:部分云端返回的 URL 字段值被反引号包裹(如 `http://xxx`), + // 锁端无法正确解析,需要清理掉这些反引号。 + // 同时处理常见的 URL 字段:starcloudUrl, starCloudUrl, scdUrl 等。 + const urlKeys = [ + 'starcloudUrl', + 'starCloudUrl', + 'scdUrl', + 'cloudUrl', + 'discoveryUrl', + 'starchartUrl', + ]; + for (final key in urlKeys) { + final dynamic value = jsonMap[key]; + if (value is String && value.isNotEmpty) { + String cleaned = value; + // 移除首尾的反引号 + if (cleaned.startsWith('`')) { + cleaned = cleaned.substring(1); + } + if (cleaned.endsWith('`')) { + cleaned = cleaned.substring(0, cleaned.length - 1); + } + if (cleaned != value) { + jsonMap[key] = cleaned; + print( + '[WifiConfig] cleaned URL field $key: "$value" -> "$cleaned"'); + } + } + } + + // 云端返回的 userPeerId 当前可能为空,统一设置为 App 的 peerId, + // 确保锁端知道该呼叫谁。 + jsonMap['userPeerId'] = userPeerId; + jsonMap['userPeerld'] = userPeerId; + final bool hasServerConfig = jsonMap.keys.any((key) { + final String normalized = key.toLowerCase(); + return normalized.contains('url') || + normalized.contains('server') || + normalized.contains('relay'); + }); + if (!hasServerConfig) { + throw StarCloudException( + 'Gateway configuration is incomplete: no cloud or relay server fields', + ); + } + final String configStr = json.encode(jsonMap); + print( + '[WifiConfig] getGatewayConfig OK, configLen=${configStr.length}, ' + 'keys=${jsonMap.keys.toList()}'); + return configStr; + } else { + print( + '[WifiConfig] getGatewayConfig errcode=${response.errcode} msg=${response.errmsg}'); + } + } catch (e, s) { + print('[WifiConfig] getGatewayConfig failed: $e\n$s'); + } + throw StarCloudException( + 'Unable to obtain complete gateway configuration from cloud', + ); + } + + Future _getStarChartPeerId(String userId) async { + // 优先复用已初始化对讲的稳定 peerId,避免注册出第二个星图节点 + // (新实例 StarChartClient().talk.fromPeerId 在未 init 时恒为空,必须走已初始化的单例) + try { + final String myPeerId = StarCloudClient.instance.talk.myPeerId; + if (myPeerId.isNotEmpty) return myPeerId; + } catch (_) {} + try { + final cred = await DiscoveryService().register( + productName: 'StarCloudSDK', + unique: 'uid_$userId', + ); + return cred.id; + } catch (_) { + return userId; + } + } + + // ── 锁板直连WiFi配网 ── + + Future configureLockWifi( + wifi_models.StarCloudWifiConfigRequest request, { + Duration timeout = const Duration(seconds: 120), + }) async { + _prepareBlueRW(deviceName: request.deviceName, mac: request.mac); + final command = ConfiguringWifiCommand( + keyID: request.keyId, + userID: request.userId, + ssid: request.ssid, + password: request.password, + peerId: request.peerId, + numberOfServers: request.numberOfServers, + listOfServers: request.listOfServers, + numberOfPhone: request.numberOfPhone, + listOfPhone: request.listOfPhone, + token: request.token, + needAuthor: request.needAuthor, + // WiFi 配网认证摘要按协议使用 publicKey;request.signKey 是历史字段名, + // 为保持 API 兼容仍保留该字段,但这里传入锁的 publicKey。 + publicKey: request.publicKey ?? request.signKey, + privateKey: request.privateKey, + ); + final blueRW = BlueReadWrite.shareManager()!; + try { + final reply = await blueRW.writeAndReturn( + command, + getPrivateKey: () => request.privateKey, + timeout: timeout, + ); + final result = reply as ConfiguringWifiReply; + if (result.status != 0x00) { + throw StarCloudException( + 'Lock WiFi configuration rejected by device', + raw: 'status=0x${result.status.toRadixString(16)}', + ); + } + return result; + } catch (e) { + throw StarCloudException('Lock WiFi configuration failed', raw: e); + } + } + + /// 通过蓝牙直接给锁板配置WiFi。 + /// [peerId] 为 App 的星图 ID,必须传入,否则门铃无法呼叫。 + /// [deviceName] / [mac] 为锁的蓝牙设备名与 MAC,必须传入,否则连接超时(10004)。 + Future configureLockWifiByContext( + StarCloudLockContext context, { + required String ssid, + required String password, + + /// Token returned by `https://lock.skychip.top/api/user/login`. + /// + /// This is intentionally separate from the OAuth token issued by + /// `cloud.skychip.top/sdk/oauth2/token`; the two services use different + /// authentication domains. + String? lockServiceAccessToken, + String peerId = '', + String deviceName = '', + String mac = '', + Duration timeout = const Duration(seconds: 120), + }) async { + final List servers = await _fetchLockWifiServers( + context, + lockServiceAccessToken: lockServiceAccessToken, + ); + final int numberOfServers = servers.length ~/ 6; + if (numberOfServers == 0) { + throw StarCloudException( + 'No WiFi lock server is available from cloud configuration', + ); + } + final ConfiguringWifiReply reply = await configureLockWifi( + wifi_models.StarCloudWifiConfigRequest( + keyId: context.keyId, + userId: context.userId, + ssid: ssid, + password: password, + peerId: peerId, + numberOfServers: numberOfServers, + listOfServers: servers, + numberOfPhone: 1, + listOfPhone: [context.userId], + token: context.token, + needAuthor: context.needAuthor, + // 保留历史字段,实际直连配网摘要使用下方的 publicKey。 + signKey: context.signKey, + // 直连 WiFi 配网协议要求使用 publicKey 计算认证摘要。 + publicKey: context.publicKey, + privateKey: context.privateKey, + deviceName: deviceName, + mac: mac, + ), + timeout: timeout, + ); + final ApiResponse updateResponse = + await SDKCommon.instance.netApi.deviceApi.updateLockNetwork( + lockId: context.lockId, + network: ssid, + clientId: SDKCommon.instance.user.clientId, + ); + if (updateResponse.errcode != 0) { + throw StarCloudException( + 'WiFi was accepted by the lock, but cloud registration failed', + raw: updateResponse, + ); + } + return reply; + } + + Future> _fetchLockWifiServers( + StarCloudLockContext context, { + String? lockServiceAccessToken, + }) async { + print('[WifiConfig] fetching WiFi lock servers from cloud...'); + try { + if (lockServiceAccessToken == null || lockServiceAccessToken.isEmpty) { + throw StarCloudException( + 'A lock.skychip.top service access token is required for WiFi configuration. ' + 'It must come from /api/user/login and cannot be replaced by the cloud OAuth token.', + ); + } + final SdkWrapper cloudUser = + await SDKCommon.instance.fetchStarCloudUser(context.cloudUid); + SDKCommon.instance.netApi.switchCloudAccount(cloudUser.user); + final ApiResponse response = + await SDKCommon.instance.netApi.deviceApi.getWifiServiceIp( + lockServiceAccessToken: lockServiceAccessToken, + ); + print( + '[WifiConfig] getWifiServiceIp response: errcode=${response.errcode}'); + if (response.errcode != 0 || response.data is! Map) { + print( + '[WifiConfig] ERROR: getWifiServiceIp failed, errcode=${response.errcode}, errmsg=${response.errmsg}'); + final String message = + 'Unable to obtain WiFi lock server configuration: ' + '${response.errmsg.isNotEmpty ? response.errmsg : response.errcode}'; + throw StarCloudException(message, raw: response); + } + + final Map data = + Map.from(response.data as Map); + final dynamic rawList = data['serviceList']; + if (rawList is! List) { + print( + '[WifiConfig] ERROR: serviceList is not a List or is missing: $rawList'); + return []; + } + + final List result = []; + for (final dynamic rawItem in rawList) { + if (rawItem is! Map) continue; + final Map item = Map.from(rawItem); + final String host = item['serviceIp']?.toString().trim() ?? ''; + final int? port = int.tryParse(item['port']?.toString() ?? ''); + print('[WifiConfig] server item: host=$host, port=$port'); + if (host.isEmpty || port == null || port < 1 || port > 65535) continue; + + InternetAddress? address; + final InternetAddress? literal = InternetAddress.tryParse(host); + if (literal != null && literal.type == InternetAddressType.IPv4) { + address = literal; + } else { + try { + final List addresses = + await InternetAddress.lookup(host, + type: InternetAddressType.IPv4); + if (addresses.isNotEmpty) address = addresses.first; + } catch (e) { + print('[WifiConfig] DNS lookup failed for $host: $e'); + } + } + if (address == null) continue; + + result.addAll(address.rawAddress); + result.add(port ~/ 256); + result.add(port % 256); + print( + '[WifiConfig] added server: ${address.address}:$port -> ${address.rawAddress}, ${port ~/ 256}, ${port % 256}'); + } + print('[WifiConfig] total servers fetched: ${result.length ~/ 6}'); + return result; + } catch (e, s) { + print('[WifiConfig] ERROR in _fetchLockWifiServers: $e\n$s'); + rethrow; + } + } +} diff --git a/lib/src/auth/starcloud_auth_service.dart b/lib/src/auth/starcloud_auth_service.dart new file mode 100755 index 0000000..de2f121 --- /dev/null +++ b/lib/src/auth/starcloud_auth_service.dart @@ -0,0 +1,97 @@ +import 'package:starcloud/sdk/entity/cloud_user_info.dart'; +import 'package:starcloud/net/interceptor/token_interceptor.dart'; +import 'package:starcloud/sdk/business/sdk_common.dart'; +import 'package:starcloud/starcloud.dart'; + +import '../core/callback_bridge.dart'; +import '../core/starcloud_exception.dart'; + +class StarCloudAuthService { + StarCloudAuthService(); + + bool _initialized = false; + + void init({ + required String clientId, + required String clientSecret, + String? environmentUrl, + }) { + StarCloudSDK.init( + clientId: clientId, + clientSecret: clientSecret, + environmentUrl: environmentUrl, + ); + _initialized = true; + } + + void _ensureInitialized() { + if (!_initialized) { + throw const StarCloudException( + 'StarCloud SDK is not initialized. Call auth.init() first.', + ); + } + } + + void setDebugMode({required bool debug}) { + _ensureInitialized(); + StarCloudSDK.instance.setDebugMode(debug: debug); + } + + Future createCloudUser() { + _ensureInitialized(); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + StarCloudSDK.instance.createCloudUser( + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + void setCloudAccounts(List accounts) { + _ensureInitialized(); + if (accounts.isEmpty) { + throw const StarCloudException('Cloud accounts cannot be empty.'); + } + StarCloudSDK.instance.setCloudAccounts(accounts); + } + + /// 登录 lock.skychip.top 授权账号并返回动态 accessToken。 + Future loginLockService({ + required String username, + required String password, + String countryCode = '86', + String deviceBrand = 'android', + String deviceModel = 'unknown', + String deviceVersion = 'unknown', + }) async { + _ensureInitialized(); + final String deviceId = await TokenInterceptors.getDeviceId(); + final response = await SDKCommon.instance.netApi.userApi.loginLockService( + username: username, + password: password, + uniqueId: deviceId, + countryCode: countryCode, + deviceBrand: deviceBrand, + deviceModel: deviceModel, + deviceVersion: deviceVersion, + ); + if (response.errcode != 0 || response.data is! Map) { + throw StarCloudException( + 'Lock service login failed: ${response.errmsg}', + raw: response, + ); + } + final String token = + (response.data as Map)['accessToken']?.toString().trim() ?? ''; + if (token.isEmpty) { + throw StarCloudException( + 'Lock service login response does not contain data.accessToken', + raw: response, + ); + } + return token; + } +} diff --git a/lib/src/core/callback_bridge.dart b/lib/src/core/callback_bridge.dart new file mode 100755 index 0000000..e91f06f --- /dev/null +++ b/lib/src/core/callback_bridge.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'starcloud_exception.dart'; + +class CallbackBridge { + static Future fromCallback({ + required void Function( + void Function(T value) onSuccess, + void Function(Object? error) onError, + ) executor, + }) { + final completer = Completer(); + + executor( + (value) { + if (!completer.isCompleted) completer.complete(value); + }, + (error) { + if (!completer.isCompleted) { + completer.completeError( + StarCloudException( + error?.toString() ?? 'Unknown StarCloud error', + raw: error, + ), + ); + } + }, + ); + + return completer.future; + } + + static Future fromVoidCallback({ + required void Function( + void Function() onSuccess, + void Function(Object? error) onError, + ) executor, + }) { + final completer = Completer(); + + executor( + () { + if (!completer.isCompleted) completer.complete(); + }, + (error) { + if (!completer.isCompleted) { + completer.completeError( + StarCloudException( + error?.toString() ?? 'Unknown StarCloud error', + raw: error, + ), + ); + } + }, + ); + + return completer.future; + } +} \ No newline at end of file diff --git a/lib/src/core/starcloud_exception.dart b/lib/src/core/starcloud_exception.dart new file mode 100755 index 0000000..86e8d3c --- /dev/null +++ b/lib/src/core/starcloud_exception.dart @@ -0,0 +1,9 @@ +class StarCloudException implements Exception { + final String message; + final Object? raw; + + const StarCloudException(this.message, {this.raw}); + + @override + String toString() => 'StarCloudException(message: $message, raw: $raw)'; +} \ No newline at end of file diff --git a/lib/src/core/starcloud_lock_context.dart b/lib/src/core/starcloud_lock_context.dart new file mode 100755 index 0000000..ad07e60 --- /dev/null +++ b/lib/src/core/starcloud_lock_context.dart @@ -0,0 +1,80 @@ +import 'package:starcloud/entity/star_cloud_lock_list.dart'; +import 'package:starcloud/storage/storage_key.dart'; + +/// Encapsulates all credential and identity fields needed to execute +/// advanced BLE protocol commands for a specific lock. +/// +/// Build once from a [StarCloudLock] (obtained via [StarCloudDeviceService.getDeviceList] +/// or [StarCloudDeviceService.pairDevice]), then pass to any `ByContext` method +/// on the advanced services — no need to manually supply keys, tokens, or IDs +/// on every call. +/// +/// ```dart +/// // Token is fetched automatically from local storage (set during BLE auth). +/// final context = await StarCloudLockContext.fromLock(lock); +/// +/// // Advanced API – only business parameters required +/// await StarCloudClient.instance.wifi.searchWifiByContext(context); +/// await StarCloudClient.instance.lockSetting.setRemoteUnlockByContext( +/// context, +/// enabled: true, +/// ); +/// ``` +class StarCloudLockContext { + const StarCloudLockContext({ + required this.lockId, + required this.keyId, + required this.userId, + required this.token, + required this.publicKey, + required this.privateKey, + required this.signKey, + this.cloudUid, + this.needAuthor = 1, + }); + + /// Creates a [StarCloudLockContext] from a [StarCloudLock]. + /// + /// The BLE session token is read automatically from local storage — it is + /// written there by the underlying SDK after the device completes the + /// Bluetooth authentication handshake. + /// + /// [needAuthor] defaults to `1` (authorization required). Pass `0` only when + /// the device explicitly does not require authorization. + static Future fromLock( + StarCloudLock lock, { + int needAuthor = 1, + }) async { + var token = await StorageDeviceKey.getBlueToken( + lock.bluetooth.bluetoothDeviceName, + ); + if (token.length < 4) { + token = List.filled(4, 0); + } + return StarCloudLockContext( + lockId: lock.lockId, + keyId: lock.keyId.toString(), + userId: lock.uid.toString(), + token: token, + publicKey: lock.bluetooth.publicKey, + privateKey: lock.bluetooth.privateKey, + signKey: lock.bluetooth.signKey, + cloudUid: lock.cloudUid, + needAuthor: needAuthor, + ); + } + + final int lockId; + final String keyId; + final String userId; + final List token; + final List publicKey; + final List privateKey; + final List signKey; + + /// 该锁所属的云账户 UID;与发给锁端的 [userId] 是不同身份域。 + final int? cloudUid; + + /// `1` = device requires authorization (default), `0` = no authorization. + final int needAuthor; +} diff --git a/lib/src/credential/starcloud_biometric_service.dart b/lib/src/credential/starcloud_biometric_service.dart new file mode 100755 index 0000000..04b5582 --- /dev/null +++ b/lib/src/credential/starcloud_biometric_service.dart @@ -0,0 +1,91 @@ +import 'package:starcloud/sdk/business/sdk_common.dart'; +import 'package:starcloud/sdk/entity/entity_get/face_list.dart'; +import 'package:starcloud/sdk/entity/entity_get/fingerprint_list.dart'; +import 'package:starcloud/sdk/entity/entity_sender/face.dart'; +import 'package:starcloud/sdk/entity/entity_sender/fingerprint.dart'; + +import '../core/callback_bridge.dart'; + +class StarCloudBiometricService { + StarCloudBiometricService(); + + Future _fetchWrapper(int? cloudUid) { + return SDKCommon.instance.fetchStarCloudUser(cloudUid); + } + + Future getFingerprintList({ + required int lockId, + required int pageNo, + required int pageSize, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.getFingerprintList( + lockId: lockId, + pageNo: pageNo, + pageSize: pageSize, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future operateFingerprint({ + required Fingerprint fingerprint, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.operateFingerprint( + fingerprint: fingerprint, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future getFaceList({ + required int lockId, + required int pageNo, + required int pageSize, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.getFaceList( + lockId: lockId, + pageNo: pageNo, + pageSize: pageSize, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future operateFace({ + required Face face, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.operateFace( + face: face, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/src/credential/starcloud_card_service.dart b/lib/src/credential/starcloud_card_service.dart new file mode 100755 index 0000000..5ff519e --- /dev/null +++ b/lib/src/credential/starcloud_card_service.dart @@ -0,0 +1,51 @@ +import 'package:starcloud/sdk/business/sdk_common.dart'; +import 'package:starcloud/sdk/entity/entity_get/card_list.dart'; +import 'package:starcloud/sdk/entity/entity_sender/iccard.dart'; + +import '../core/callback_bridge.dart'; + +class StarCloudCardService { + StarCloudCardService(); + + Future _fetchWrapper(int? cloudUid) { + return SDKCommon.instance.fetchStarCloudUser(cloudUid); + } + + Future getCardList({ + required int lockId, + required int pageNo, + required int pageSize, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.getCardList( + lockId: lockId, + pageNo: pageNo, + pageSize: pageSize, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future operateICCard({ + required IcCard card, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.operateICCard( + card: card, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/src/credential/starcloud_electric_key_service.dart b/lib/src/credential/starcloud_electric_key_service.dart new file mode 100755 index 0000000..8261fdc --- /dev/null +++ b/lib/src/credential/starcloud_electric_key_service.dart @@ -0,0 +1,124 @@ +import 'package:starcloud/net/entity/api_response.dart'; +import 'package:starcloud/sdk/business/sdk_common.dart'; +import 'package:starcloud/sdk/entity/entity_get/electric_key_list.dart'; +import 'package:starcloud/sdk/entity/entity_sender/electric_key_model.dart'; + +import '../core/callback_bridge.dart'; + +class StarCloudElectricKeyService { + StarCloudElectricKeyService(); + + Future _fetchWrapper(int? cloudUid) { + return SDKCommon.instance.fetchStarCloudUser(cloudUid); + } + + Future getElectricKeysList({ + required int lockId, + required int pageNo, + required int pageSize, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.getElectricKeysList( + lockId: lockId, + pageNo: pageNo, + pageSize: pageSize, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future addElectricKeys({ + required ElectricKeyModel electricKeyModel, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.addElectricKeys( + electricKeyModel: electricKeyModel, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future editElectricKeys({ + required ElectricKeyItem electricKeyItem, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.editElectricKeys( + electricKeyItem: electricKeyItem, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future deleteElectricKeys({ + required int keyId, + required int includeUnderlings, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.deletElectricKeys( + keyId: keyId, + includeUnderlings: includeUnderlings, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future freezeElectricKeys({ + required int keyId, + required bool isFreeze, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.freezeElectricKeys( + keyId: keyId, + isFreeze: isFreeze, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future resetElectricKeys({ + required int lockId, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.resetElectricKeys( + lockId: lockId, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/src/credential/starcloud_password_service.dart b/lib/src/credential/starcloud_password_service.dart new file mode 100755 index 0000000..ee2b5b1 --- /dev/null +++ b/lib/src/credential/starcloud_password_service.dart @@ -0,0 +1,94 @@ +import 'package:starcloud/net/entity/api_response.dart'; +import 'package:starcloud/sdk/business/sdk_common.dart'; +import 'package:starcloud/sdk/entity/entity_get/password_list.dart'; +import 'package:starcloud/sdk/entity/entity_sender/password.dart'; + +import '../core/callback_bridge.dart'; + +class StarCloudPasswordService { + StarCloudPasswordService(); + + Future _fetchWrapper(int? cloudUid) { + return SDKCommon.instance.fetchStarCloudUser(cloudUid); + } + + Future getPasswordList({ + required int lockId, + required int pageNo, + required int pageSize, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.getPasswordList( + lockId: lockId, + pageNo: pageNo, + pageSize: pageSize, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future operateCustomPassword({ + required PasswordModel passwordModel, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.operateCustomPassword( + passwordModel: passwordModel, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future operateOfflinePassword({ + required PasswordModel passwordModel, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.operateOffLinePassword( + passwordModel: passwordModel, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future> batchGenerationOfflinePassword({ + required List passwordModels, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + return wrapper.batchGenerationOffLinePassword(passwordModels: passwordModels); + } + + Future resetPassword({ + required PasswordModel passwordModel, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.resetPassword( + passwordModel: passwordModel, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/src/device/starcloud_device_service.dart b/lib/src/device/starcloud_device_service.dart new file mode 100755 index 0000000..5b6809c --- /dev/null +++ b/lib/src/device/starcloud_device_service.dart @@ -0,0 +1,174 @@ +import 'package:starcloud/blue/blue_manage.dart'; +import 'package:starcloud/blue/blue_read_write.dart'; +import 'package:starcloud/entity/star_cloud_lock_list.dart'; +import 'package:starcloud/sdk/business/sdk_common.dart'; +import 'package:starcloud/starcloud.dart'; + +import '../core/callback_bridge.dart'; + +class StarCloudDeviceService { + StarCloudDeviceService(); + + Future _fetchWrapper(int? cloudUid) { + return SDKCommon.instance.fetchStarCloudUser(cloudUid); + } + + Future> scanDeviceList({ + int timeout = 3600, + bool notPaired = true, + void Function(List devices)? onScan, + }) { + return CallbackBridge.fromCallback>( + executor: (onSuccess, onError) { + StarCloudSDK.instance.scanDeviceList( + timeout: timeout, + notPaired: notPaired, + scanType: ScanType.lock, + onScan: (list) { + onScan?.call(list); + onSuccess(list); + }, + ); + }, + ); + } + + Future stopScan() { + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + StarCloudSDK.instance.stopScan(onError: onError).then((_) => onSuccess()); + }, + ); + } + + Future disconnect() { + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + StarCloudSDK.instance.disconnect().then((_) => onSuccess()); + }, + ); + } + + Future pairDevice({ + required StarCloudScanResult scanResult, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.pairDevice( + scanResult: scanResult, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future getDeviceList({ + required int pageNo, + required int pageSize, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.getDeviceList( + pageNo: pageNo, + pageSize: pageSize, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future getDevice({ + required int lockId, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.getDevice( + lockId: lockId, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future openLock({ + required int lockId, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.openLock( + lockId: lockId, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future closeLock({ + required int lockId, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.closeLock( + lockId: lockId, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future resetLock({ + required int lockId, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.resetLock( + lockId: lockId, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future updateAdminKeyboardPwd({ + required int lockId, + required String pwd, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.updateAdminKeyboardPwd( + lockId: lockId, + pwd: pwd, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/src/record/starcloud_record_service.dart b/lib/src/record/starcloud_record_service.dart new file mode 100755 index 0000000..8c33b64 --- /dev/null +++ b/lib/src/record/starcloud_record_service.dart @@ -0,0 +1,72 @@ +import 'package:starcloud/sdk/business/sdk_common.dart'; +import 'package:starcloud/sdk/entity/entity_get/record_list.dart'; + +import '../core/callback_bridge.dart'; + +class StarCloudRecordService { + StarCloudRecordService(); + + Future _fetchWrapper(int? cloudUid) { + return SDKCommon.instance.fetchStarCloudUser(cloudUid); + } + + Future getRecordList({ + required int lockId, + required int pageNo, + required int pageSize, + required int startDate, + required int endDate, + required List recordTypes, + int? cloudUid, + String? searchStr, + int? keyboardPwdId, + int? cardId, + int? fingerprintId, + int? keyId, + int? faceId, + int? remoteId, + int? palmVeinId, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromCallback( + executor: (onSuccess, onError) { + wrapper.getRecordList( + lockId: lockId, + pageNo: pageNo, + pageSize: pageSize, + startDate: startDate, + endDate: endDate, + recordTypes: recordTypes, + searchStr: searchStr, + keyboardPwdId: keyboardPwdId, + cardId: cardId, + fingerprintId: fingerprintId, + keyId: keyId, + faceId: faceId, + remoteId: remoteId, + palmVeinId: palmVeinId, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } + + Future uploadLockRecord({ + required int lockId, + int? cloudUid, + }) async { + final wrapper = await _fetchWrapper(cloudUid); + + return CallbackBridge.fromVoidCallback( + executor: (onSuccess, onError) { + wrapper.uploadLockRecord( + lockId: lockId, + onSuccess: onSuccess, + onError: onError, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/src/starcloud_client.dart b/lib/src/starcloud_client.dart new file mode 100755 index 0000000..0592812 --- /dev/null +++ b/lib/src/starcloud_client.dart @@ -0,0 +1,49 @@ +import 'advanced/protocol/starcloud_protocol_executor.dart'; +import 'advanced/protocol/starcloud_protocol_executor_impl.dart'; +import 'advanced/starcloud_lock_setting_service.dart'; +import 'advanced/starcloud_lock_state_service.dart'; +import 'advanced/starcloud_wifi_service.dart'; +import 'auth/starcloud_auth_service.dart'; +import 'credential/starcloud_biometric_service.dart'; +import 'credential/starcloud_card_service.dart'; +import 'credential/starcloud_electric_key_service.dart'; +import 'credential/starcloud_password_service.dart'; +import 'device/starcloud_device_service.dart'; +import 'record/starcloud_record_service.dart'; +import 'talk/starcloud_talk_service.dart'; + +class StarCloudClient { + StarCloudClient._() { + auth = StarCloudAuthService(); + device = StarCloudDeviceService(); + password = StarCloudPasswordService(); + card = StarCloudCardService(); + biometric = StarCloudBiometricService(); + electricKey = StarCloudElectricKeyService(); + record = StarCloudRecordService(); + talk = StarCloudTalkService(); + + lockState = StarCloudLockStateService(); + wifi = StarCloudWifiService(); + lockSetting = StarCloudLockSettingService(StarCloudProtocolExecutorImpl()); + } + + static late StarCloudClient instance; + + static void bootstrap() { + instance = StarCloudClient._(); + } + + late final StarCloudAuthService auth; + late final StarCloudDeviceService device; + late final StarCloudPasswordService password; + late final StarCloudCardService card; + late final StarCloudBiometricService biometric; + late final StarCloudElectricKeyService electricKey; + late final StarCloudRecordService record; + late final StarCloudTalkService talk; + + late final StarCloudLockStateService lockState; + late final StarCloudWifiService wifi; + late final StarCloudLockSettingService lockSetting; +} \ No newline at end of file diff --git a/lib/src/talk/starcloud_talk_service.dart b/lib/src/talk/starcloud_talk_service.dart new file mode 100755 index 0000000..fce2a99 --- /dev/null +++ b/lib/src/talk/starcloud_talk_service.dart @@ -0,0 +1,271 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:starchart/starchart_flutter_sdk.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 视频对讲服务 +/// +/// 提供视频门铃的可视对讲功能,包括: +/// - 接收来电 +/// - 接听/拒绝/挂断 +/// - 接收视频帧(H264/MJPEG)和音频(G711) +/// - 发送音频 +class StarCloudTalkService { + StarChartClient? _client; + bool _initialized = false; + Future? _initFuture; + + // 事件流控制器(用于暴露给外部) + final _incomingCallCtrl = StreamController.broadcast(); + final _talkDataCtrl = StreamController.broadcast(); + final _talkStatusCtrl = StreamController.broadcast(); + + /// 收到来电事件 + Stream get onIncomingCall => _incomingCallCtrl.stream; + + /// 收到音视频数据事件 + Stream get onTalkData => _talkDataCtrl.stream; + + /// 通话状态变化事件 + Stream get onTalkStatus => _talkStatusCtrl.stream; + final _talkExpectRespCtrl = StreamController.broadcast(); + + /// 收到锁端协商后的实际视频分辨率和编码格式 + Stream get onTalkExpectResp => + _talkExpectRespCtrl.stream; + + /// 当前通话状态 + TalkStatus get status => _client?.talk.status ?? TalkStatus.idle; + + /// 当前通话的锁 PeerID + String get currentLockPeerId => _client?.talk.currentLockPeerId ?? ''; + + /// 本机 PeerID(注册后生成,用于被锁板呼叫) + String get myPeerId => _client?.talk.fromPeerId ?? ''; + + void _ensureInitialized() { + if (!_initialized) { + throw const StarChartException( + 'Talk service is not initialized. Call talk.init() first.', + ); + } + } + + /// 初始化视频对讲服务 + /// + /// [productName] 产品名称,如 "MyApp" + /// [unique] 设备唯一标识,如 deviceId + /// [baseUrl] 星图服务器地址(可选) + /// [localUdpPort] 本地 UDP 端口(可选) + Future init({ + required String productName, + required String unique, + String baseUrl = 'http://scd.skychip.top:8710', + int localUdpPort = 62289, + }) async { + if (_initialized) return; + + final pending = _initFuture; + if (pending != null) { + await pending; + return; + } + + final future = _doInit( + productName: productName, + unique: unique, + baseUrl: baseUrl, + localUdpPort: localUdpPort, + ); + _initFuture = future; + try { + await future; + } finally { + if (identical(_initFuture, future)) { + _initFuture = null; + } + } + } + + Future _doInit({ + required String productName, + required String unique, + required String baseUrl, + required int localUdpPort, + }) async { + final prefs = await SharedPreferences.getInstance(); + final credKey = 'starchart_credential_$unique'; + + Future initializeClient( + PeerCredential? persistCred, + ) async { + final client = StarChartClient(); + _client = client; + + // 必须在 client.init() 之前订阅事件。Relay/Talk 初始化期间可能已经 + // 收到响应;若初始化完成后才监听,首个状态事件会被丢弃。 + client.onIncomingCall.listen(_incomingCallCtrl.add); + client.onTalkData.listen(_talkDataCtrl.add); + client.onTalkStatus.listen(_talkStatusCtrl.add); + client.onTalkExpectResp.listen(_talkExpectRespCtrl.add); + + try { + await client.init( + productName: productName, + unique: unique, + baseUrl: baseUrl, + localUdpPort: localUdpPort, + persistCredential: persistCred != null ? () => persistCred : null, + saveCredential: (cred) async { + final json = _encodeCredential(cred); + await prefs.setString(credKey, json); + }, + ); + return client; + } catch (_) { + client.dispose(); + if (identical(_client, client)) _client = null; + rethrow; + } + } + + PeerCredential? persistCred; + final credJson = prefs.getString(credKey); + if (credJson != null) { + try { + final map = _parseCredentialJson(credJson); + persistCred = PeerCredential( + id: map['id'] ?? '', + publicKey: map['publicKey'] ?? '', + privateKey: map['privateKey'] ?? '', + ); + } catch (_) {} + } + + try { + try { + await initializeClient(persistCred); + } catch (e) { + if (persistCred == null || !_isMissingPersistedPeer(e)) rethrow; + + // 服务端可能清理了旧 Peer,但本地仍持有凭证。清除后重新注册一次, + // 避免每次地址上报都因 "record not found" 初始化失败。 + await prefs.remove(credKey); + await initializeClient(null); + } + _initialized = true; + } catch (_) { + _client?.dispose(); + _client = null; + rethrow; + } + } + + bool _isMissingPersistedPeer(Object error) { + final message = error.toString().toLowerCase(); + return message.contains('get peer error') && + message.contains('record not found'); + } + + Map _parseCredentialJson(String json) { + // 简单解析 JSON + final result = {}; + final regex = RegExp(r'"(\w+)":\s*"([^"]*)"'); + for (final match in regex.allMatches(json)) { + result[match.group(1)!] = match.group(2)!; + } + return result; + } + + String _encodeCredential(PeerCredential cred) { + return '{"id":"${cred.id}","publicKey":"${cred.publicKey}","privateKey":"${cred.privateKey}"}'; + } + + /// App 主动发起对讲请求 + /// + /// [lockPeerId] 目标锁的 PeerID + /// [callerName] 呼叫方名称 + /// [options] 期望接收的音视频格式(可选,默认 H264 + G711) + void requestCall({ + required String lockPeerId, + String callerName = 'App', + TalkOptions options = TalkOptions.defaultOptions, + }) { + _ensureInitialized(); + _client!.talk.requestCall( + lockPeerId: lockPeerId, + callerName: callerName, + options: options, + ); + } + + /// 接听来电 + /// + /// [lockPeerId] 发起呼叫的锁 PeerID(从 IncomingCallEvent 获取) + /// [options] 期望接收的音视频格式(可选) + void accept({ + required String lockPeerId, + TalkOptions options = TalkOptions.defaultOptions, + }) { + _ensureInitialized(); + _client!.talk.accept(lockPeerId: lockPeerId, options: options); + } + + /// 拒绝来电 + void reject({required String lockPeerId}) { + _ensureInitialized(); + _client!.talk.reject(lockPeerId: lockPeerId); + } + + /// 挂断通话 + void hangup({required String lockPeerId}) { + _ensureInitialized(); + _client!.talk.hangup(lockPeerId: lockPeerId); + } + + /// 发送 G711 音频数据 + /// + /// [g711Data] 已编码为 G711 ALaw 的音频字节 + /// [durationMs] 音频时长(毫秒) + void sendAudio({required Uint8List g711Data, int durationMs = 0}) { + _ensureInitialized(); + _client!.talk.sendAudio(g711Data: g711Data, durationMs: durationMs); + } + + /// 发送 JPEG 视频帧 + void sendJpegFrame({required Uint8List jpegData, int durationMs = 0}) { + _ensureInitialized(); + _client!.talk.sendJpegFrame(jpegData: jpegData, durationMs: durationMs); + } + + /// 通话中重新协商视频格式,并请求锁端重新输出关键帧。 + void renegotiate({TalkOptions options = TalkOptions.defaultOptions}) { + _ensureInitialized(); + _client!.talk.renegotiate(options: options); + } + + /// 转移接听者到另一设备 + void transferReceiver({ + required String lockPeerId, + required String newPeerId, + }) { + _ensureInitialized(); + _client!.talk.transferReceiver( + lockPeerId: lockPeerId, + newPeerId: newPeerId, + ); + } + + /// 释放资源 + void dispose() { + _client?.dispose(); + _client = null; + _initialized = false; + _initFuture = null; + _incomingCallCtrl.close(); + _talkDataCtrl.close(); + _talkStatusCtrl.close(); + _talkExpectRespCtrl.close(); + } +} diff --git a/lib/starcloud_flutter_sdk.dart b/lib/starcloud_flutter_sdk.dart new file mode 100755 index 0000000..1c170f9 --- /dev/null +++ b/lib/starcloud_flutter_sdk.dart @@ -0,0 +1,31 @@ +library starcloud_flutter_sdk; + +export 'src/starcloud_client.dart'; +export 'src/core/starcloud_exception.dart'; +export 'src/core/starcloud_lock_context.dart'; + +export 'src/advanced/models/lock_state_models.dart'; +export 'src/advanced/models/wifi_models.dart'; +export 'src/advanced/models/lock_setting_models.dart'; + +// 导出常用的底层实体,方便开发者直接使用 +export 'package:starcloud/starcloud.dart' show StarCloudScanResult, StarCloudLock; +export 'package:starcloud/entity/star_cloud_lock_list.dart' show StarCloudLockList; + +// 视频对讲相关类型导出 +export 'package:starchart/starchart_flutter_sdk.dart' + show + IncomingCallEvent, + TalkDataEvent, + TalkDataH264Frame, + TalkStatusEvent, + TalkExpectRespEvent, + TalkStatus, + TalkOptions, + TalkPushEvent, + TalkExpectRespEvent, + VideoTypeE, + AudioTypeE, + PeerCredential, + RelayInfo, + StarChartException; diff --git a/pubspec.lock b/pubspec.lock new file mode 100755 index 0000000..049e098 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,551 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.5" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.3" + date_format: + dependency: transitive + description: + name: date_format + sha256: a48254e60bdb7f1d5a15cac7f86e37491808056c0a99dbdc850841def4754ddc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.9" + dio: + dependency: transitive + description: + name: dio + sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.10.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: + dependency: transitive + description: + name: flutter_blue_plus + sha256: "59fded880d769859e9e2fd6e5599555575174f642d758cc846988c86c4341131" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.33.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.2" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + permission_handler: + dependency: transitive + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "12.0.13" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.4.10" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.3" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.9.1" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + 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.flutter-io.cn" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.1" + starchart: + dependency: "direct main" + description: + path: "../starchart" + relative: true + source: path + version: "0.1.0" + starcloud: + dependency: "direct main" + description: + path: "../starcloud" + relative: true + source: path + version: "0.1.2" + starlock_ble: + dependency: "direct main" + description: + path: "../starlock_ble" + relative: true + source: path + version: "0.1.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.flutter-io.cn" + source: hosted + version: "13.0.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.3.4 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100755 index 0000000..a1ea6e6 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,28 @@ +name: flutter_sdk +description: A generic Flutter wrapper for SDK. +version: 0.1.0 +publish_to: none + +environment: + sdk: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" + +dependencies: + flutter: + sdk: flutter + + starlock_ble: + path: ../starlock_ble + + starcloud: + path: ../starcloud + + starchart: + path: ../starchart + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: \ No newline at end of file diff --git a/scripts/package_sdk.sh b/scripts/package_sdk.sh new file mode 100755 index 0000000..b419255 --- /dev/null +++ b/scripts/package_sdk.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Packages flutter_sdk + its two private dependencies into a deliverable ZIP. +# Usage: bash scripts/package_sdk.sh [version] +# e.g. bash scripts/package_sdk.sh 1.2.0 +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SDK_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +WORK_DIR="$(cd "$SDK_DIR/.." && pwd)" + +# ── Version ────────────────────────────────────────────────────────────────── +if [[ "${1:-}" != "" ]]; then + VERSION="$1" +else + VERSION="$(grep '^version:' "$SDK_DIR/pubspec.yaml" | awk '{print $2}')" +fi +DIST_NAME="starcloud_flutter_sdk_v${VERSION}" + +# Stage in /tmp so the SDK source tree is never inside the staging area +# (avoids rsync accidentally bundling dist/ into itself) +STAGE_DIR="$(mktemp -d)/$DIST_NAME" +mkdir -p "$STAGE_DIR" + +echo "Packaging $DIST_NAME ..." + +# ── Helpers ─────────────────────────────────────────────────────────────────── +copy_pkg() { + local src="$1" dst="$2" + rsync -a \ + --exclude='.git/' \ + --exclude='build/' \ + --exclude='dist/' \ + --exclude='.dart_tool/' \ + --exclude='.idea/' \ + --exclude='*.iml' \ + --exclude='.DS_Store' \ + --exclude='pubspec.lock' \ + "$src/" "$dst/" +} + +# ── 1. flutter_sdk ──────────────────────────────────────────────────────────── +echo " → flutter_sdk" +copy_pkg "$SDK_DIR" "$STAGE_DIR/flutter_sdk" + +# Rewrite starcloud path for the delivery layout (../starcloud instead of the +# internal monorepo path ../sikaigongyu/starcloud-sdk-flutter-master) +perl -i -pe \ + 's|path: \.\./sikaigongyu/starcloud-sdk-flutter-master|path: ../starcloud|g' \ + "$STAGE_DIR/flutter_sdk/pubspec.yaml" + +# Same rewrite for the example app (../../starcloud vs ../../sikaigongyu/...) +perl -i -pe \ + 's|path: \.\./\.\./sikaigongyu/starcloud-sdk-flutter-master|path: ../../starcloud|g' \ + "$STAGE_DIR/flutter_sdk/example/pubspec.yaml" + +# ── 2. starlock_ble ───────────────────────────────────────────────────────── +echo " → starlock_ble" +copy_pkg "$WORK_DIR/starlock_ble" "$STAGE_DIR/starlock_ble" + +# ── 3. starcloud ───────────────────────────────────────────────────────────── +echo " → starcloud" +copy_pkg "$WORK_DIR/sikaigongyu/starcloud-sdk-flutter-master" "$STAGE_DIR/starcloud" + +# ── 4. ZIP ─────────────────────────────────────────────────────────────────── +echo " → zipping" +mkdir -p "$SDK_DIR/dist" +(cd "$(dirname "$STAGE_DIR")" && zip -r "${DIST_NAME}.zip" "$DIST_NAME" --quiet) +mv "$(dirname "$STAGE_DIR")/${DIST_NAME}.zip" "$SDK_DIR/dist/" +rm -rf "$(dirname "$STAGE_DIR")" + +echo "" +echo "✅ dist/${DIST_NAME}.zip" +echo "" +echo "Partner setup (3 steps):" +echo " 1. Unzip and copy the three SDK folders into vendor/" +echo " 2. Add path dependencies to pubspec.yaml (see INTEGRATION.md)" +echo " 3. flutter pub get"