Author SHA1 Message Date
linfeng 43373bec54 更新主裁判 APP 的上传描述,添加构建时间戳 2026-07-29 18:47:23 +08:00
linfeng ac304b10f8 更新主裁判 APP 的启动页 2026-07-29 18:39:59 +08:00
linfeng df38ecee18 登录后清空输入框 2026-07-29 18:06:53 +08:00
linfeng 7faa8e5c47 更新获取赛事列表功能,增加 NAS IP 地址参数以支持动态请求 2026-07-29 18:00:45 +08:00
linfeng 7d65be0514 选手赛项列表增加 选手与该赛项的选手编号属性 2026-07-29 11:51:11 +08:00
linfeng 40799f2d15 扫码页支持返回上一页 2026-07-29 10:13:47 +08:00
linfeng f7f7413b55 优化扫码页 UI 2026-07-28 18:50:14 +08:00
linfeng 8e599f5a86 完成视频播放UI 调整 2026-07-28 18:02:40 +08:00
linfeng 9fca76f679 1.完成查看录像模块功能
2.完成录像页面播放视频功能
2026-07-28 16:36:32 +08:00
linfeng a324825e8a update 2026-07-28 15:46:52 +08:00
linfeng 6793ca53af 还原参赛队伍列表页、详情页 2026-07-28 15:46:23 +08:00
linfeng 9e72bc522e 解决查看录像页 UI 2026-07-28 14:59:34 +08:00
linfeng 1096645e79 update git 湖绿 2026-07-28 12:04:28 +08:00
linfeng 13ecb2fccc 优化赛事信息页 UI 2026-07-28 10:12:13 +08:00
linfeng e3f9c8ebc0 优化团队赛页面 UI 2026-07-28 10:00:02 +08:00
linfeng 6790ea41b2 优化人工处理弹窗 UI 2026-07-28 09:30:24 +08:00
linfeng b2dd593e93 完成团队赛、 UI 开发 2026-07-27 18:41:20 +08:00
linfeng 5ffe524e20 完成选手信息页 2026-07-27 17:57:09 +08:00
linfeng 356636c4b4 解决解析 TOKEN 类型报错问题 2026-07-27 16:57:05 +08:00
linfeng 686db244fd 1.还原授权页 UI
2.还原扫码页 UI
2026-07-27 16:52:58 +08:00
linfeng 20d501f673 修改启动页 2026-07-27 15:45:06 +08:00
linfeng ddc1c9bf98 更换 APP 图标 2026-07-27 15:43:26 +08:00
linfeng 30d317c0b9 修改名字 2026-07-27 15:30:17 +08:00
linfeng 10d9eee60e 新增人工设置晋级/淘汰功能,更新相关数据模型和API接口;重构比赛页面以支持新逻辑,优化用户交互体验。 2026-07-23 15:35:31 +08:00
linfeng 00937bf4b6 更新比赛页面以传递新的事件注册项,简化数据处理逻辑并优化构建脚本。 2026-07-23 11:55:26 +08:00
linfeng 3c3baede85 移除比赛页面中的备注文本,优化UI布局以提升用户体验。 2026-07-23 11:48:07 +08:00
linfeng 8c127dd0e7 1.重构参赛队伍详情页面,更新数据模型以支持新结构;
2.优化比赛信息展示逻辑,增强用户交互体验;
3.修复API请求以获取队伍详情并处理异常情况。
2026-07-23 11:44:47 +08:00
linfeng 1264ebdd7c 重构参赛队伍相关模型,更新API接口以支持队伍详情获取;
调整UI逻辑以使用新的数据结构,优化比赛信息展示;
更新构建脚本以支持新功能。
2026-07-22 13:35:53 +08:00
linfeng d6e80df5bf 新增网络可视化请求工具 2026-07-22 09:24:48 +08:00
linfeng 4ff93edaab 授权页增加获取设备码 2026-07-21 22:45:41 +08:00
linfeng 1f0e0407ab 修复口令过期处理并增加参赛队伍接口 2026-07-20 10:52:19 +08:00
linfeng da52dc9797 Handle zero opponent IDs and hide video button 2026-07-20 10:23:44 +08:00
linfeng e5b6246458 更新应用配置以添加主裁判计分 H5 链接;
重构事件注册模型以支持团队成员信息,并简化事件信息页面的导航逻辑。
2026-07-20 10:10:51 +08:00
linfeng 4e0a675198 1.新增参赛队伍页面交互
2.新增人工处理功能交互
2026-07-20 09:29:53 +08:00
linfeng a4333d3bd1 解决华为 webview Chrome 78 内核版本渲染白屏问题 2026-07-17 15:22:57 +08:00
linfeng e08a8ed26b 重构事件信息页面,移除不必要的加载状态和错误消息字段,简化状态管理逻辑;在扫码页面中增强选手信息查询功能,添加加载提示和错误处理。 2026-07-17 15:17:08 +08:00
linfeng 62d717c3ee 更新build-apk-split.sh脚本以添加错误处理和构建完成时间输出;在ScanQrCodePage中增强玩家ID验证,添加无效凭证提示。 2026-07-17 12:19:57 +08:00
linfeng 1f9c5af51b 重构事件注册模型并更新应用配置。更改不同环境的基本url,增强事件注册项结构,并改进ScanQrCodePage中的导航。在AuthViewModel中添加clearAuth方法,以便更好地进行状态管理。 2026-07-17 11:55:13 +08:00
linfeng 831ca1059b Merge branch 'linfeng/drone_video/dev_1.1.0/20260707' into linfeng/drone_scoring/dev_1.1.0/2026630 2026-07-16 11:59:22 +08:00
linfeng 613bd90bad update 2026-07-16 11:58:58 +08:00
linfeng 9818a56fe1 删除单例测试文件 2026-07-16 11:58:43 +08:00
linfeng 16ad7b253c 添加 jwt_decoder 依赖;更新 AuthApi 枚举以支持获取录像列表;新增录像响应模型和相关状态管理;重构 AuthViewModel 以解析和存储 JWT 数据;更新 ScanQrCodePage 以处理用户鉴权和获取录像列表。 2026-07-14 10:16:31 +08:00
linfeng 45000b4188 update 2026-07-14 09:10:27 +08:00
linfeng fab49a9750 通过在扫描仪对话框中启用自动缩放和改进错误处理来重构QR码扫描功能。更新ScanQrCodePage以利用AppQrScannerDialog获取玩家ID。 2026-07-09 17:56:17 +08:00
linfeng 1c909277ff 更新 devtools_options.yaml 以启用 riverpod 和 shared_preferences 扩展;更新 AuthApi 枚举以添加 playerRegistrationList API;重构 Auth 页面以在用户已登录时直接导航到扫码页面;重构 EventInfoPage 以使用新的状态管理和加载赛事报名信息;更新扫码页面以导航到 EventInfoPage。 2026-07-09 17:38:56 +08:00
linfeng 6138eddc16 删除未使用的相机和记录类来重构记录功能;更新pubspec中的依赖项。Yaml为本地插件路径;简化MainActivity和相关类,以提高性能和可维护性。 2026-07-09 16:17:34 +08:00
linfeng 75c42ea168 新增赛事信息页 2026-07-09 14:57:09 +08:00
linfeng c38f6d69c8 更新 AuthApi 枚举以添加获取推流地址的 API 路径;重构 Auth 页面以优化输入控件和按钮布局;更新扫码页面以导航到录制页面。 2026-07-09 14:32:39 +08:00
linfeng 9766c43722 更新 .gitignore 文件以排除 AGENTS.md 和 test/ 目录;在 AGENTS.md 中添加页面级 Feature 目录规范;删除不再使用的 dialog-record.dart 文件,并更新相关引用路径。 2026-07-09 14:01:37 +08:00
linfeng a1f6ea14cf 添加设备代码信息到 Android 和 iOS 平台信息;更新 Auth 页面以使用新的 TextEditingController 管理输入;重构推流测试页面以支持动态 RTMP URL 解析;删除不再需要的测试文件。 2026-07-09 12:26:41 +08:00
linfeng 98a3381716 更新 AndroidManifest.xml 以启用明文流量;在 index.html 中替换 token 值
增强 WebviewPage 的清理和导航功能。
2026-07-09 10:10:53 +08:00
linfeng bd5937f910 添加 HTML 资源路径到 pubspec.yaml;更新 Auth 页面以使用新的 Webview 组件加载本地 HTML 文件;重构 WebviewPage 以支持加载本地资产和注入 JavaScript 通信桥接;清理不必要的导入和注释。 2026-07-08 12:03:25 +08:00
linfeng b21857f364 添加 apivideo_live_stream 依赖;
修改 Auth 页面文本为“输入执裁口令”;
添加公用 AppBar 组件
模拟核心流程交互
2026-07-08 09:37:38 +08:00
linfeng f6c477c9be 添加webview_flutter及相关依赖
添加公共的 webview 组件
2026-07-07 17:40:24 +08:00
linfeng 1246e41e4b 添加mobile_scanner包,
添加AuthPageWidget
添加全局扫码组件
2026-07-07 17:16:51 +08:00
linfeng 26574808df Merge branch 'linfeng/run/dev/20260630' into linfeng/drone/dev/2026630 2026-07-01 12:18:58 +08:00
linfeng d4c7d7180e 删除AndroidManifest.xml中的READ_MEDIA_VIDEO权限,并更新view_model_recording中的权限逻辑。dart确保文件保存权限仅适用于Android SDK版本28及以下。添加测试以验证Android 13和未知SDK版本的权限行为。 2026-07-01 12:18:12 +08:00
linfeng 4a85b9f83e Merge run recording updates into drone branch 2026-07-01 11:54:06 +08:00
linfeng e4057c3d6a 添加 CLAUDE.md 到 .gitignore 文件 2026-07-01 11:32:22 +08:00
linfeng 90e8966528 重构通道名称,在Android和iOS实现中使用统一的命名空间‘app.record_tool’。相应地更新相关的常量和方法通道。增强在录制HUD中的缩放比例显示格式。 2026-07-01 11:19:30 +08:00
linfeng c8a7494344 1.广角/主摄切换按钮已上移:
2.新增 isSwitchingLens 状态、避免切换镜头    stop 按钮禁用,不会触发 stopRecording
倍距按钮禁用,避免连续切换
ViewModel 层也兜底:isSwitchingLens=true 时直接拒绝 stop
3.合并失败兜底提示
2026-07-01 10:59:28 +08:00
linfeng 02614c2817 更新 .gitignore 文件以排除 CLAUDE.md,并注释掉录制头部小部件中的 mock 按钮 2026-07-01 10:38:50 +08:00
linfeng 9f49f73a31 删除 agent 说明书 2026-07-01 10:37:09 +08:00
linfeng da2e69b014 支持录制中切换广角/主摄 2026-07-01 09:59:21 +08:00
linfeng f6347e99cd 兼容 IOS 2026-06-30 17:28:03 +08:00
linfeng d40c13d802 兼容 IOS 2026-06-30 17:27:54 +08:00
linfeng 23319cfddb 修复部分机型无法切换广角和主摄功能 2026-06-30 17:06:35 +08:00
linfeng 6d93c1c8dd 解决安卓端广角无法读取问题 2026-06-30 16:30:24 +08:00
linfeng c9323f26a7 启动图修改 2026-06-30 14:19:02 +08:00
linfeng 7dafd61314 修改图标 2026-06-30 13:57:18 +08:00
linfeng d056d28d83 1.更换包名
2.更换 APP 名字
2026-06-30 13:38:16 +08:00
linfeng 8570486798 兼容 IOS 2026-06-13 19:00:37 +08:00
linfeng 208920dfea Merge branch 'linfeng/dev/20260603' into linfeng/dev/2026612 2026-06-13 16:09:20 +08:00
linfeng 88d8dfda04 更新超广角相机的变焦比例,确保在相机能力允许的情况下使用0.6x的缩放比例,优化相关UI和测试用例。 2026-06-12 19:04:00 +08:00
linfeng d39d85cd99 增强变焦功能 2026-06-12 18:35:18 +08:00
linfeng c01ce1dca0 Merge branch 'linfeng/dev/compatibility/20260609' into linfeng/dev/2026612 2026-06-12 17:10:24 +08:00
linfeng 25ac9c4c35 关闭测试按钮 2026-06-12 16:39:41 +08:00
linfeng a3a02e623f 实现缩放功能:增加缩放功能检索和设置方法,更新UI以支持缩放调整,增强缩放比例的状态管理。 2026-06-12 16:38:31 +08:00
linfeng 7a654d54f0 新增 一键清除项目缓存 sh 工具 2026-06-09 15:28:39 +08:00
linfeng de2aacca90 兼容 IOS 2026-06-09 12:29:27 +08:00
linfeng cf1c2d7d0e 完成 IOS 端启动页 2026-06-09 10:46:01 +08:00
linfeng 13cb3bfd7b 更换包名:com.dronex.rec 2026-06-09 10:25:34 +08:00
linfeng bcd2162cd7 兼容 IOS 端 ,IOS 端包名修改为 com.dronex.dronex 2026-06-09 09:18:25 +08:00
linfeng 41fcd730f0 粘贴删除按钮不参与过渡 2026-06-08 15:51:05 +08:00
linfeng 7ab03dd912 重构录制页面,优化 UI 组件,简化状态管理,移除不必要的参数,提升代码可读性和维护性。 2026-06-08 11:23:45 +08:00
linfeng 29cfbdf8c4 录制按钮增加动画 2026-06-08 11:21:13 +08:00
linfeng 7031765b4d 优化录制页面功能,修正剪贴板信息提示,新增停止录制后的结果提示,改进触摸锁定解锁逻辑,提升用户交互体验。 2026-06-08 11:10:22 +08:00
linfeng 942d15e54c 优化交互体验增加动画效果 2026-06-08 10:58:10 +08:00
linfeng 6b168ccd62 1.UI 优化
2.新增打包构建脚本
2026-06-08 10:19:19 +08:00
linfeng 551d10dec4 兼容 IOS 端 2026-06-08 08:53:49 +08:00
linfeng e1446337e9 优化了 启动录制效果 2026-06-05 18:52:47 +08:00
linfeng 26098114d2 更换 录制按钮 UI 2026-06-05 18:29:49 +08:00
linfeng 1e08b70c39 更换 APP 图标 2026-06-05 18:08:25 +08:00
linfeng e821bd68a7 更换 APP 图标 2026-06-05 18:08:19 +08:00
linfeng 9c21915bf7 取消 mock 按钮 2026-06-05 17:59:24 +08:00
linfeng 1221b16c7f 优化录制页面的相机预览逻辑,增加预览未就绪时的错误提示,确保用户体验更流畅。 2026-06-05 16:26:37 +08:00
linfeng 54738d53f9 用户开始录制,拒绝录音录像权限,增加弹窗引导用户前往系统页设置 2026-06-05 16:11:54 +08:00
linfeng 4d83f38960 状态管理 函数增加注释 2026-06-05 16:02:08 +08:00
linfeng 36da37c6c0 调整对话框背景图适应方式,简化赛事信息复制按钮标签 2026-06-05 16:00:53 +08:00
linfeng 0183bd9a6d 重构录制页面,更新对话框逻辑,优化赛事信息粘贴功能,调整相关文本标签。 2026-06-05 15:46:16 +08:00
linfeng 016aad49b7 解决录制结束后,无法重新预览相机问题 2026-06-05 15:10:03 +08:00
linfeng a39fcdb929 更新录制会话的时间格式,添加小时显示;在录制页面中引入计时器组件,并调整触摸锁定覆盖层的样式。 2026-06-05 15:03:53 +08:00
linfeng 0a2cfe27ac 打开防误触模式逻辑 2026-06-05 14:41:12 +08:00
linfeng d598b36449 优化录制、停止录制逻辑 2026-06-05 14:30:56 +08:00
linfeng c0aa2db6db 1.还原 UI 稿,地址和时间与录制按钮排版优化 2026-06-05 14:09:25 +08:00
linfeng 0d06975313 重构录制页面,优化HUD布局,添加头部和底部组件,移除触摸锁定功能,简化事件信息处理。 2026-06-05 14:02:01 +08:00
linfeng f6440ea8b7 规范化代码结构 2026-06-05 12:07:29 +08:00
linfeng 1e936bfc12 更新pubspec。修改启动屏幕的启动后台XML,并通过删除过时的文件和增强会话管理来重构记录功能。 2026-06-05 11:44:51 +08:00
linfeng 4c5bf22638 更新 gitignore 2026-06-05 11:07:48 +08:00
linfeng e387dfad0a 删除无用文件 2026-06-05 10:13:24 +08:00
linfeng 846c6a8edb Stop tracking pubspec.lock 2026-06-05 10:07:50 +08:00
linfeng f49d208042 1.开始录制、结束录制增加
2. 增加电量检测、内存检查,是否低于 10%
2026-06-04 18:25:58 +08:00
linfeng 124b4c1882 新增删除剪切板内容功能 2026-06-04 17:55:18 +08:00
linfeng 7c342c4477 重构ClipboardRecordingModel以支持可选的时间戳,并更新相关测试以改进JSON解析和验证。 2026-06-04 17:32:54 +08:00
linfeng dfbdbbdb66 开始录制增加 剪切板参数校验 2026-06-04 17:20:59 +08:00
linfeng 1b404525d2 1.更换包名
2.调整录制页地址下方时间为当前时间读秒
2026-06-04 16:58:34 +08:00
linfeng 77d9c35592 1.确定 APP 包名
2.录制结束增加弹窗提示
3.完成读取剪切板内容、新增粘贴剪切板内容
2026-06-04 16:25:26 +08:00
linfeng 5ddcb95358 屏幕适配 2026-06-04 14:34:46 +08:00
linfeng 02c1c87b46 1. 升级依赖版本
2. 解决运行项目警告日志问题
3. 优化代码
2026-06-04 13:55:33 +08:00
linfeng 66435302b3 升级 Gradle → 8.14、AGP → 8.11、Kotlin → 2.2.20 JVM 堆降到 -Xmx4G 2026-06-04 13:35:10 +08:00
linfeng 250f21a2b8 优化 2026-06-04 10:50:24 +08:00
linfeng 8f9f3a9779 兼容 IOS 端 2026-06-03 23:37:02 +08:00
linfeng fb61e28e2f 完成录制功能 2026-06-03 16:04:52 +08:00
447 changed files with 107568 additions and 1586 deletions
+10 -1
View File
@@ -11,12 +11,15 @@
.svn/
.swiftpm/
migrate_working_dir/
.vscode
pubspec.lock
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
.cursor
Podfile.lock
# 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
@@ -43,3 +46,9 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/android/.kotlin
CLAUDE.md
AGENTS.md
test/
script
+17 -17
View File
@@ -13,22 +13,22 @@ A production-ready Flutter quick-start template extracted from real-world projec
## Tech Stack
| Category | Package | Purpose |
|---|---|---|
| State Management | flutter_riverpod | Compile-safe, testable state management |
| Networking | dio | HTTP client with interceptor chain |
| Local Cache | shared_preferences | Key-value persistence |
| Network Monitor | connectivity_plus | Real-time connectivity tracking |
| Permissions | permission_handler | Runtime permission requests |
| Screen Adaptation | flutter_screenutil | Design-dimension-based layout |
| Image Loading | cached_network_image | Network image caching |
| SVG | flutter_svg | SVG rendering |
| Pull to Refresh | pull_to_refresh | Refresh and load-more |
| Loading HUD | flutter_easyloading | Toast and loading indicator |
| Device Info | device_info_plus | Device metadata |
| Package Info | package_info_plus | App version info |
| URL Launcher | url_launcher | Open external URLs |
| Linting | flutter_lints | Recommended Dart lint rules |
| Category | Package | Purpose |
| ----------------- | -------------------- | --------------------------------------- |
| State Management | flutter_riverpod | Compile-safe, testable state management |
| Networking | dio | HTTP client with interceptor chain |
| Local Cache | shared_preferences | Key-value persistence |
| Network Monitor | connectivity_plus | Real-time connectivity tracking |
| Permissions | permission_handler | Runtime permission requests |
| Screen Adaptation | flutter_screenutil | Design-dimension-based layout |
| Image Loading | cached_network_image | Network image caching |
| SVG | flutter_svg | SVG rendering |
| Pull to Refresh | pull_to_refresh | Refresh and load-more |
| Loading HUD | flutter_easyloading | Toast and loading indicator |
| Device Info | device_info_plus | Device metadata |
| Package Info | package_info_plus | App version info |
| URL Launcher | url_launcher | Open external URLs |
| Linting | flutter_lints | Recommended Dart lint rules |
## Directory Structure
@@ -72,7 +72,7 @@ lib/
## Getting Started
```bash
cd flutter-template
cd record-tool
flutter pub get
flutter analyze
flutter test
+17 -17
View File
@@ -13,22 +13,22 @@
## 技术栈
| 类别 | 依赖 | 用途 |
|---|---|---|
| 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 |
| 网络请求 | dio | HTTP 客户端,支持拦截器链 |
| 本地缓存 | shared_preferences | KV 持久化存储 |
| 网络监听 | connectivity_plus | 实时网络状态监测 |
| 权限申请 | permission_handler | 运行时权限请求 |
| 屏幕适配 | flutter_screenutil | 设计稿尺寸适配 |
| 图片加载 | cached_network_image | 网络图片缓存 |
| SVG | flutter_svg | SVG 渲染 |
| 下拉刷新 | pull_to_refresh | 下拉刷新 / 上拉加载 |
| 加载提示 | flutter_easyloading | Toast 和 loading |
| 设备信息 | device_info_plus | 设备元数据 |
| 应用信息 | package_info_plus | 版本号等应用信息 |
| 链接跳转 | url_launcher | 外部 URL 打开 |
| 代码规范 | flutter_lints | Dart 推荐 lint 规则 |
| 类别 | 依赖 | 用途 |
| -------- | -------------------- | -------------------------- |
| 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 |
| 网络请求 | dio | HTTP 客户端,支持拦截器链 |
| 本地缓存 | shared_preferences | KV 持久化存储 |
| 网络监听 | connectivity_plus | 实时网络状态监测 |
| 权限申请 | permission_handler | 运行时权限请求 |
| 屏幕适配 | flutter_screenutil | 设计稿尺寸适配 |
| 图片加载 | cached_network_image | 网络图片缓存 |
| SVG | flutter_svg | SVG 渲染 |
| 下拉刷新 | pull_to_refresh | 下拉刷新 / 上拉加载 |
| 加载提示 | flutter_easyloading | Toast 和 loading |
| 设备信息 | device_info_plus | 设备元数据 |
| 应用信息 | package_info_plus | 版本号等应用信息 |
| 链接跳转 | url_launcher | 外部 URL 打开 |
| 代码规范 | flutter_lints | Dart 推荐 lint 规则 |
## 目录结构
@@ -71,7 +71,7 @@ lib/
## 快速开始
```bash
cd flutter-template
cd record-tool
flutter pub get
flutter analyze
flutter test
+16 -9
View File
@@ -1,12 +1,13 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
// The Flutter Gradle Plugin must be applied after the Android Gradle plugin.
id("dev.flutter.flutter-gradle-plugin")
}
val appPackageName = "com.dronex.rec"
android {
namespace = "com.example.flutter_template"
namespace = appPackageName
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -15,13 +16,8 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.flutter_template"
applicationId = appPackageName
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
@@ -39,6 +35,17 @@ android {
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
}
}
dependencies {
implementation("androidx.lifecycle:lifecycle-service:2.8.7")
implementation("androidx.core:core-ktx:1.15.0")
}
flutter {
source = "../.."
}
@@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+36 -21
View File
@@ -1,8 +1,28 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dronex.rec">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<application
android:label="flutter_template"
android:label="SportsX裁判工作台"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -12,34 +32,29 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<service
android:name=".recording.RecordingForegroundService"
android:exported="false"
android:foregroundServiceType="camera|microphone" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
@@ -0,0 +1,10 @@
package com.dronex.rec
object AppConstants {
private const val CHANNEL_NAMESPACE = "app.record_tool"
const val PLATFORM_INFO_CHANNEL = "$CHANNEL_NAMESPACE/platform_info"
const val RECORDING_METHOD_CHANNEL = "$CHANNEL_NAMESPACE/recording"
const val RECORDING_EVENT_CHANNEL = "$CHANNEL_NAMESPACE/recording_events"
const val RECORDING_ACTION_START = "$CHANNEL_NAMESPACE.recording.START"
const val RECORDING_ACTION_STOP = "$CHANNEL_NAMESPACE.recording.STOP"
}
@@ -0,0 +1,129 @@
package com.dronex.rec
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.BatteryManager
import android.os.Build
import android.os.Environment
import android.os.StatFs
import android.provider.Settings
import com.dronex.rec.recording.RecordingPlatformHandler
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private var platformHandler: RecordingPlatformHandler? = null
private var platformInfoChannel: MethodChannel? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
platformInfoChannel =
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
AppConstants.PLATFORM_INFO_CHANNEL,
)
.also { channel ->
channel.setMethodCallHandler { call, result ->
when (call.method) {
"packageInfo" -> result.success(packageInfoMap())
"deviceInfo" -> result.success(deviceInfoMap())
"deviceHealth" -> result.success(deviceHealthMap())
else -> result.notImplemented()
}
}
}
platformHandler =
RecordingPlatformHandler(
this,
flutterEngine.dartExecutor.binaryMessenger,
)
}
override fun onDestroy() {
platformInfoChannel?.setMethodCallHandler(null)
platformInfoChannel = null
platformHandler?.dispose()
platformHandler = null
super.onDestroy()
}
private fun packageInfoMap(): Map<String, String> {
val packageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(
packageName,
android.content.pm.PackageManager.PackageInfoFlags.of(0),
)
} else {
@Suppress("DEPRECATION") packageManager.getPackageInfo(packageName, 0)
}
val appName = applicationInfo.loadLabel(packageManager)?.toString().orEmpty()
val versionCode =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode.toString()
} else {
@Suppress("DEPRECATION") packageInfo.versionCode.toString()
}
return mapOf(
"appName" to appName,
"packageName" to packageName,
"version" to packageInfo.versionName.orEmpty(),
"buildNumber" to versionCode,
)
}
private fun deviceInfoMap(): Map<String, Any> {
val flags = applicationInfo.flags
val isEmulator =
Build.FINGERPRINT.startsWith("generic") ||
Build.FINGERPRINT.startsWith("unknown") ||
Build.MODEL.contains("google_sdk") ||
Build.MODEL.contains("Emulator") ||
Build.MODEL.contains("Android SDK built for x86") ||
Build.MANUFACTURER.contains("Genymotion") ||
Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") ||
Build.PRODUCT == "google_sdk" ||
flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 &&
Build.HARDWARE.contains("ranchu")
return mapOf(
"platform" to "android",
"deviceCode" to Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID).orEmpty(),
"brand" to Build.BRAND,
"model" to Build.MODEL,
"systemVersion" to Build.VERSION.RELEASE,
"sdkInt" to Build.VERSION.SDK_INT,
"isPhysicalDevice" to !isEmulator,
)
}
private fun deviceHealthMap(): Map<String, Any?> {
val batteryLevelPercent = readBatteryLevelPercent()
val storageAvailablePercent = readStorageAvailablePercent()
return mapOf(
"batteryLevelPercent" to batteryLevelPercent,
"storageAvailablePercent" to storageAvailablePercent,
)
}
private fun readBatteryLevelPercent(): Int? {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as? BatteryManager
?: return null
val level =
batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
return if (level in 0..100) level else null
}
private fun readStorageAvailablePercent(): Double {
val stat = StatFs(Environment.getDataDirectory().path)
val totalBytes = stat.totalBytes
if (totalBytes <= 0L) return 100.0
val availableBytes = stat.availableBytes
return availableBytes.toDouble() / totalBytes.toDouble() * 100.0
}
}
@@ -0,0 +1,37 @@
package com.dronex.rec.recording
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
object BatteryOptimizationHelper {
fun isIgnoringOptimizations(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true
val manager = context.getSystemService(PowerManager::class.java) ?: return true
return manager.isIgnoringBatteryOptimizations(context.packageName)
}
fun openSettings(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
val intent =
Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
return
}
val fallback =
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(fallback)
}
}
@@ -0,0 +1,48 @@
package com.dronex.rec.recording
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.provider.Settings
import androidx.core.app.NotificationManagerCompat
object DoNotDisturbHelper {
private var savedInterruptionFilter: Int? = null
fun hasAccess(context: Context): Boolean {
val manager = context.getSystemService(NotificationManager::class.java)
return manager?.isNotificationPolicyAccessGranted == true
}
fun openAccessSettings(context: Context) {
val intent =
Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
fun enable(context: Context): Boolean {
val manager = context.getSystemService(NotificationManager::class.java) ?: return false
if (!manager.isNotificationPolicyAccessGranted) return false
if (savedInterruptionFilter == null) {
savedInterruptionFilter = manager.currentInterruptionFilter
}
manager.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_NONE)
return true
}
fun disable(context: Context) {
val manager = context.getSystemService(NotificationManager::class.java) ?: return
if (!manager.isNotificationPolicyAccessGranted) return
val previous = savedInterruptionFilter ?: NotificationManager.INTERRUPTION_FILTER_ALL
manager.setInterruptionFilter(previous)
savedInterruptionFilter = null
}
fun areNotificationsEnabled(context: Context): Boolean {
return NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
@@ -0,0 +1,179 @@
package com.dronex.rec.recording
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleService
import com.dronex.rec.AppConstants
import com.dronex.rec.MainActivity
class RecordingForegroundService : LifecycleService() {
private var wakeLock: PowerManager.WakeLock? = null
override fun onCreate() {
super.onCreate()
instance = this
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
when (intent?.action) {
AppConstants.RECORDING_ACTION_START -> {
acquireWakeLock()
val notification = buildNotification("正在录制")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
notification,
foregroundServiceTypes(),
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
isRunning = true
}
AppConstants.RECORDING_ACTION_STOP -> {
releaseWakeLock()
stopForeground(STOP_FOREGROUND_REMOVE)
isRunning = false
stopSelf()
}
}
return START_STICKY
}
override fun onDestroy() {
releaseWakeLock()
isRunning = false
if (instance === this) {
instance = null
}
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
private fun acquireWakeLock() {
if (wakeLock?.isHeld == true) return
val manager = getSystemService(PowerManager::class.java) ?: return
wakeLock =
manager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
setReferenceCounted(false)
acquire(4 * 60 * 60 * 1000L)
}
}
private fun releaseWakeLock() {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
wakeLock = null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val channel =
NotificationChannel(
CHANNEL_ID,
"录制服务",
NotificationManager.IMPORTANCE_LOW,
)
.apply {
description = "保持相机录制在后台与息屏时继续运行"
setShowBadge(false)
}
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel)
}
private fun foregroundServiceTypes(): Int {
var types = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
if (hasRecordAudioPermission()) {
types = types or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
}
return types
}
private fun hasRecordAudioPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.RECORD_AUDIO,
) == PackageManager.PERMISSION_GRANTED
}
private fun buildNotification(content: String): Notification {
val launchIntent =
Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent =
PendingIntent.getActivity(
this,
0,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("录制进行中")
.setContentText(content)
.setSmallIcon(android.R.drawable.presence_video_online)
.setOngoing(true)
.setContentIntent(pendingIntent)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
companion object {
const val CHANNEL_ID = "recording_foreground"
const val NOTIFICATION_ID = 1001
private const val WAKE_LOCK_TAG = "record_tool:recording_wake_lock"
@Volatile var isRunning: Boolean = false
@Volatile var instance: RecordingForegroundService? = null
fun start(context: Context) {
val intent =
Intent(context, RecordingForegroundService::class.java).apply {
action = AppConstants.RECORDING_ACTION_START
}
ContextCompatStart.startForegroundService(context, intent)
}
fun stop(context: Context) {
val intent =
Intent(context, RecordingForegroundService::class.java).apply {
action = AppConstants.RECORDING_ACTION_STOP
}
context.startService(intent)
}
}
}
private object ContextCompatStart {
fun startForegroundService(context: Context, intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
@@ -0,0 +1,66 @@
package com.dronex.rec.recording
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.dronex.rec.AppConstants
import com.dronex.rec.MainActivity
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class RecordingPlatformHandler(
private val activity: MainActivity,
messenger: BinaryMessenger,
) : MethodChannel.MethodCallHandler {
private val methodChannel = MethodChannel(messenger, AppConstants.RECORDING_METHOD_CHANNEL)
init {
methodChannel.setMethodCallHandler(this)
}
fun dispose() {
methodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"hasNotificationPolicyAccess" -> result.success(DoNotDisturbHelper.hasAccess(activity))
"openNotificationPolicySettings" -> {
DoNotDisturbHelper.openAccessSettings(activity)
result.success(null)
}
"enableDoNotDisturb" -> result.success(DoNotDisturbHelper.enable(activity))
"disableDoNotDisturb" -> {
DoNotDisturbHelper.disable(activity)
result.success(null)
}
"isIgnoringBatteryOptimizations" ->
result.success(BatteryOptimizationHelper.isIgnoringOptimizations(activity))
"openBatteryOptimizationSettings" -> {
BatteryOptimizationHelper.openSettings(activity)
result.success(null)
}
"setImmersiveMode" -> {
val enabled = call.argument<Boolean>("enabled") ?: false
setImmersiveMode(enabled)
result.success(null)
}
"isForegroundServiceRunning" -> result.success(RecordingForegroundService.isRunning)
else -> result.notImplemented()
}
}
private fun setImmersiveMode(enabled: Boolean) {
val window = activity.window
WindowCompat.setDecorFitsSystemWindows(window, !enabled)
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
if (enabled) {
insetsController.hide(WindowInsetsCompat.Type.systemBars())
insetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} else {
insetsController.show(WindowInsetsCompat.Type.systemBars())
}
}
}
@@ -1,5 +0,0 @@
package com.example.flutter_template
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

@@ -1,12 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
android:gravity="fill"
android:src="@drawable/startup_background" />
</item>
</layer-list>
@@ -1,12 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
android:gravity="fill"
android:src="@drawable/startup_background" />
</item>
</layer-list>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=1G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+1 -1
View File
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+2 -2
View File
@@ -19,8 +19,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.aa {
margin: 100px;
width: 200px;
height: 100px;
border-radius: 20px;
}
</style>
</head>
<body>
<button class="aa" onclick="postMsgToAPP()">向 APP 发送数据</button>
</body>
<script>
(function () {
if (window.__appInstallData) {
return;
}
window.__appInstallData = true;
window.AppBridge.token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjgxNDI4NTczNTY2NjY4ODAwLCJkYXRhIjp7fSwic3ViIjoiODE0Mjg1NzM1NjY2Njg4MDAiLCJleHAiOjE3ODM5MzM1ODQsIm5iZiI6MTc4MzMyODc4NCwiaWF0IjoxNzgzMzI4Nzg0fQ.r1wYfvpV-5HIgwUuvq1Or3jPgjc3AhfJmoV1PNP23-Y';
///TODO 后续业务
})();
function postMsgToAPP() {
window.AppBridge.postMessage(JSON.stringify({ type: 'navigator_pop', value: null }))
console.log(window.AppBridge.token)
console.log(window.AppBridge.playId)
}
</script>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
set -e
flutter build apk --release --split-per-abi
# echo "构建完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
pgyer upload build/app/outputs/flutter-apk/app-arm64-v8a-release.apk --build-update-description "主裁判 APP $(date '+%Y-%m-%d %H:%M:%S')"
+1
View File
@@ -0,0 +1 @@
flutter build apk --release
+19
View File
@@ -0,0 +1,19 @@
{
"name": "xcode build server",
"version": "1.3.0",
"bspVersion": "2.2.0",
"languages": [
"c",
"cpp",
"objective-c",
"objective-cpp",
"swift"
],
"argv": [
"/opt/homebrew/bin/xcode-build-server"
],
"workspace": "/Users/ZhuanZ/Documents/gdfw/record-tool/ios/Runner.xcworkspace",
"build_root": "/Users/ZhuanZ/Library/Developer/Xcode/DerivedData/Runner-ckjfuyjdkgumnpbnnftroxddsppq",
"scheme": "Runner",
"kind": "xcode"
}
+7
View File
@@ -0,0 +1,7 @@
flutter clean
flutter pub get
rm -rf ios/Pods
rm -rf ios/Podfile.lock
cd ios
pod install
cd ..
+5
View File
@@ -0,0 +1,5 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- riverpod: true
- shared_preferences: true
-2
View File
@@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
+1
View File
@@ -1,2 +1,3 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}"
+1
View File
@@ -1,2 +1,3 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}"
+35
View File
@@ -39,5 +39,40 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
'PERMISSION_MICROPHONE=1',
]
end
end
pods_runner_dir = File.join(
installer.sandbox.root,
'Target Support Files',
'Pods-Runner'
)
Dir.glob(File.join(pods_runner_dir, 'Pods-Runner.*.xcconfig')).each do |config_path|
config = File.read(config_path)
config.gsub!(
'FRAMEWORK_SEARCH_PATHS = $(inherited)',
'FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}"'
)
File.write(config_path, config)
end
Dir.glob(File.join(pods_runner_dir, 'Pods-Runner-frameworks-*input-files.xcfilelist')).each do |file_list_path|
file_list = File.read(file_list_path)
file_list.gsub!('${BUILT_PRODUCTS_DIR}/', '${PODS_CONFIGURATION_BUILD_DIR}/')
File.write(file_list_path, file_list)
end
frameworks_script = File.join(pods_runner_dir, 'Pods-Runner-frameworks.sh')
if File.exist?(frameworks_script)
script = File.read(frameworks_script)
script.gsub!('${BUILT_PRODUCTS_DIR}/', '${PODS_CONFIGURATION_BUILD_DIR}/')
File.write(frameworks_script, script)
end
end
+48 -14
View File
@@ -1,15 +1,25 @@
PODS:
- apivideo_live_stream (0.0.1):
- ApiVideoLiveStream (= 1.4.1)
- Flutter
- ApiVideoLiveStream (1.4.1):
- HaishinKit (= 1.7.3)
- connectivity_plus (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- HaishinKit (1.7.3):
- Logboard (~> 2.4.1)
- Logboard (2.4.2)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- native_device_orientation (0.0.1):
- Flutter
- permission_handler_apple (9.4.8):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
@@ -19,49 +29,73 @@ PODS:
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- apivideo_live_stream (from `.symlinks/plugins/apivideo_live_stream/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- native_device_orientation (from `.symlinks/plugins/native_device_orientation/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- ApiVideoLiveStream
- HaishinKit
- Logboard
EXTERNAL SOURCES:
apivideo_live_stream:
:path: ".symlinks/plugins/apivideo_live_stream/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
native_device_orientation:
:path: ".symlinks/plugins/native_device_orientation/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
apivideo_live_stream: caab45dd35fb3b140d423c099d0f6672378e7abe
ApiVideoLiveStream: 8f9dce7f6d15d5e4bb3c7a25e406bf2a36337a5a
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
HaishinKit: 326e27c4d06427ba53bffc68e516a92033293051
Logboard: 759d82599c439945d430d5a0958455b5a1974a0c
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
native_device_orientation: d6a4dc6887cd8a5ce1049962367aec60139ea0f1
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
PODFILE CHECKSUM: 858401fbd980bedce6ecd2f9b429bf271f11f74b
COCOAPODS: 1.16.2
+47 -22
View File
@@ -13,6 +13,8 @@
64BC3B3C75FABAA128C4FE7A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D84231833EC084C45F76D9A0 /* Pods_RunnerTests.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
87DA76CA5231F693C9392B80 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6AA1C86CE710449CC3E8FCD /* Pods_Runner.framework */; };
8D5D9D9E2C69000100F10001 /* RecordingPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5D9D9D2C69000100F10001 /* RecordingPlugin.swift */; };
8E5D9D9E2C69000100F10002 /* PlatformInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5D9D9D2C69000100F10002 /* PlatformInfoPlugin.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 */; };
@@ -52,6 +54,8 @@
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8D5D9D9D2C69000100F10001 /* RecordingPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingPlugin.swift; sourceTree = "<group>"; };
8E5D9D9D2C69000100F10002 /* PlatformInfoPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformInfoPlugin.swift; sourceTree = "<group>"; };
95C6AC1536EA3DBA2D369C55 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@@ -97,7 +101,6 @@
DFA822DC4965C5AFAFF6BB41 /* Pods-RunnerTests.release.xcconfig */,
99DBE6263F70650B13C33EAC /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -160,6 +163,8 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
8D5D9D9D2C69000100F10001 /* RecordingPlugin.swift */,
8E5D9D9D2C69000100F10002 /* PlatformInfoPlugin.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
@@ -198,8 +203,8 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
CD126DA202245CE8BB749787 /* [CP] Embed Pods Frameworks */,
62D3774184B6C76B8E34C8C2 /* [CP] Copy Pods Resources */,
7858230D5ADC7A99F778CB03 /* [CP] Embed Pods Frameworks */,
99E9790F23C2D0C0B51A6C19 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -331,21 +336,21 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
62D3774184B6C76B8E34C8C2 /* [CP] Copy Pods Resources */ = {
7858230D5ADC7A99F778CB03 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
@@ -363,21 +368,21 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
CD126DA202245CE8BB749787 /* [CP] Embed Pods Frameworks */ = {
99E9790F23C2D0C0B51A6C19 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@@ -396,6 +401,8 @@
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
8D5D9D9E2C69000100F10001 /* RecordingPlugin.swift in Sources */,
8E5D9D9E2C69000100F10002 /* PlatformInfoPlugin.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -488,16 +495,22 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 35634V629S;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MT26BPCKF6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate;
PRODUCT_BUNDLE_IDENTIFIER = com.dronex.rec;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "dev-profile-dronex";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -513,7 +526,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.qxy.dronex.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -531,7 +544,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.qxy.dronex.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -547,7 +560,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.qxy.dronex.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -671,16 +684,22 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 35634V629S;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MT26BPCKF6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate;
PRODUCT_BUNDLE_IDENTIFIER = com.dronex.rec;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "dev-profile-dronex";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -694,16 +713,22 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 35634V629S;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MT26BPCKF6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate;
PRODUCT_BUNDLE_IDENTIFIER = com.dronex.rec;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "dev-profile-dronex";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -1,10 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -52,7 +70,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
+11 -2
View File
@@ -2,12 +2,21 @@ import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "RecordingPlugin") {
RecordingPlugin.register(with: registrar)
}
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "PlatformInfoPlugin") {
PlatformInfoPlugin.register(with: registrar)
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,13 @@
{
"images" : [
{
"filename" : "startup_background.png",
"idiom" : "universal",
"scale" : "1x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

@@ -16,13 +16,15 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" image="StartupBackground" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="4X2-HB-R7a"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="E8f-e3-JEx"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="trailing" secondItem="Ze5-6b-2t3" secondAttribute="trailing" id="rwG-Bh-0uU"/>
</constraints>
</view>
</viewController>
@@ -32,6 +34,6 @@
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
<image name="StartupBackground" width="750" height="1624"/>
</resources>
</document>
+22 -5
View File
@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24412" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24405"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
@@ -14,13 +16,28 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" image="StartupBackground" translatesAutoresizingMaskIntoConstraints="NO" id="YQm-Ov-qw7">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YQm-Ov-qw7" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="top" id="7pp-OK-Dgk"/>
<constraint firstItem="YQm-Ov-qw7" firstAttribute="bottom" secondItem="8bC-Xf-vdC" secondAttribute="bottom" id="Hbf-gY-rbf"/>
<constraint firstItem="YQm-Ov-qw7" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="WPc-7k-20p"/>
<constraint firstItem="YQm-Ov-qw7" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailing" id="w1G-WA-3QR"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="139" y="122"/>
</scene>
</scenes>
<resources>
<image name="StartupBackground" width="750" height="1624"/>
</resources>
</document>
+35 -6
View File
@@ -2,10 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Flutter Template</string>
<string>飞行极控录像工作台</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -13,7 +15,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>flutter_template</string>
<string>飞行极控录像工作台</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -24,6 +26,37 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>需要访问相机以显示预览并录制视频。</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风以录制视频声音;未授权时将静音录制。</string>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -41,9 +74,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
+98
View File
@@ -0,0 +1,98 @@
import Flutter
import UIKit
final class PlatformInfoPlugin: NSObject, FlutterPlugin {
static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "app.record_tool/platform_info",
binaryMessenger: registrar.messenger()
)
let plugin = PlatformInfoPlugin()
registrar.addMethodCallDelegate(plugin, channel: channel)
}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "packageInfo":
result(packageInfoMap())
case "deviceInfo":
result(deviceInfoMap())
case "deviceHealth":
result(deviceHealthMap())
default:
result(FlutterMethodNotImplemented)
}
}
private func packageInfoMap() -> [String: String] {
let bundle = Bundle.main
return [
"appName": displayName(bundle: bundle),
"packageName": bundle.bundleIdentifier ?? "",
"version": bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "",
"buildNumber": bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "",
]
}
private func deviceHealthMap() -> [String: Any?] {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
var batteryLevelPercent: Int?
let batteryLevel = device.batteryLevel
if batteryLevel >= 0 {
batteryLevelPercent = Int((batteryLevel * 100).rounded())
}
var storageAvailablePercent = 100.0
if let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()),
let free = attrs[.systemFreeSize] as? NSNumber,
let total = attrs[.systemSize] as? NSNumber,
total.doubleValue > 0 {
storageAvailablePercent = free.doubleValue / total.doubleValue * 100.0
}
return [
"batteryLevelPercent": batteryLevelPercent,
"storageAvailablePercent": storageAvailablePercent,
]
}
private func deviceInfoMap() -> [String: Any] {
let device = UIDevice.current
return [
"platform": "ios",
"deviceCode": "",
"brand": device.systemName,
"model": machineIdentifier(),
"systemVersion": device.systemVersion,
"isPhysicalDevice": !isSimulator(),
]
}
private func displayName(bundle: Bundle) -> String {
if let displayName = bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String {
return displayName
}
return bundle.object(forInfoDictionaryKey: "CFBundleName") as? String ?? ""
}
private func isSimulator() -> Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
private func machineIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let mirror = Mirror(reflecting: systemInfo.machine)
let identifier = mirror.children.reduce(into: "") { value, element in
guard let byte = element.value as? Int8, byte != 0 else { return }
value.append(String(UnicodeScalar(UInt8(byte))))
}
return identifier
}
}
+722
View File
@@ -0,0 +1,722 @@
import AVFoundation
import Flutter
import UIKit
private enum RecordingState: String {
case idle
case previewing
case recording
case stopping
case error
}
private struct RecordingStatus {
let state: RecordingState
let outputPath: String?
let elapsedMillis: Int
let message: String?
init(
state: RecordingState,
outputPath: String? = nil,
elapsedMillis: Int = 0,
message: String? = nil
) {
self.state = state
self.outputPath = outputPath
self.elapsedMillis = elapsedMillis
self.message = message
}
func toMap() -> [String: Any] {
var map: [String: Any] = [
"state": state.rawValue,
"elapsedMillis": elapsedMillis,
]
if let outputPath {
map["outputPath"] = outputPath
}
if let message {
map["message"] = message
}
return map
}
}
private final class RecordingPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var previewLayer: AVCaptureVideoPreviewLayer {
layer as! AVCaptureVideoPreviewLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
previewLayer.videoGravity = .resizeAspectFill
backgroundColor = .black
}
required init?(coder: NSCoder) {
super.init(coder: coder)
previewLayer.videoGravity = .resizeAspectFill
backgroundColor = .black
}
}
private final class RecordingPreviewPlatformView: NSObject, FlutterPlatformView {
private let previewView: RecordingPreviewView
init(frame: CGRect) {
previewView = RecordingPreviewView(frame: frame)
super.init()
RecordingCameraController.shared.attach(previewView: previewView)
}
func view() -> UIView {
previewView
}
deinit {
RecordingCameraController.shared.detach(previewView: previewView)
}
}
private final class RecordingPreviewFactory: NSObject, FlutterPlatformViewFactory {
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
RecordingPreviewPlatformView(frame: frame)
}
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
FlutterStandardMessageCodec.sharedInstance()
}
}
private final class RecordingCameraController: NSObject, AVCaptureFileOutputRecordingDelegate {
static let shared = RecordingCameraController()
private let session = AVCaptureSession()
private let sessionQueue = DispatchQueue(label: "recording.camera.session")
private let movieOutput = AVCaptureMovieFileOutput()
private weak var previewView: RecordingPreviewView?
private var videoInput: AVCaptureDeviceInput?
private var audioInput: AVCaptureDeviceInput?
private var configured = false
private var latestOutputPath: String?
private var latestFileSaved = true
private var latestFileErrorMessage: String?
private var pendingDisplayName: String?
private var recordingStartedAt: Date?
private var elapsedTimer: Timer?
private var pendingStopResult: FlutterResult?
private var currentZoomRatio: CGFloat = 1.0
private(set) var status = RecordingStatus(state: .idle) {
didSet {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.statusListener?(self.currentStatusMap())
}
}
}
var statusListener: (([String: Any]) -> Void)?
func attach(previewView: RecordingPreviewView) {
let bindPreview = { [weak self, weak previewView] in
guard let self, let previewView else { return }
self.previewView = previewView
previewView.previewLayer.session = self.session
}
if Thread.isMainThread {
bindPreview()
} else {
DispatchQueue.main.async(execute: bindPreview)
}
}
func detach(previewView: RecordingPreviewView) {
let unbindPreview = { [weak self, weak previewView] in
guard let self, let previewView else { return }
if self.previewView === previewView {
previewView.previewLayer.session = nil
self.previewView = nil
}
}
if Thread.isMainThread {
unbindPreview()
} else {
DispatchQueue.main.async(execute: unbindPreview)
}
}
func initializePreview(result: @escaping FlutterResult) {
guard let previewView else {
result(
FlutterError(code: "NO_PREVIEW", message: "Camera preview is not attached", details: nil))
return
}
previewView.previewLayer.session = session
sessionQueue.async { [weak self] in
guard let self else { return }
do {
try self.configureSession(withAudio: self.isMicrophoneAuthorized())
if !self.session.isRunning {
self.session.startRunning()
}
self.updateStatus(RecordingStatus(state: .previewing))
DispatchQueue.main.async {
result(self.currentStatusMap())
}
} catch {
self.updateStatus(RecordingStatus(state: .error, message: error.localizedDescription))
DispatchQueue.main.async {
result(
FlutterError(
code: "PREVIEW_FAILED",
message: error.localizedDescription,
details: nil
)
)
}
}
}
}
func startRecording(withAudio: Bool, displayName: String?, result: @escaping FlutterResult) {
guard previewView != nil else {
result(
FlutterError(code: "NO_PREVIEW", message: "Camera preview is not attached", details: nil))
return
}
sessionQueue.async { [weak self] in
guard let self else { return }
do {
try self.configureSession(withAudio: withAudio && self.isMicrophoneAuthorized())
if !self.session.isRunning {
self.session.startRunning()
}
guard !self.movieOutput.isRecording else {
DispatchQueue.main.async {
result(FlutterError(code: "START_FAILED", message: "Already recording", details: nil))
}
return
}
self.pendingDisplayName = displayName
self.latestFileSaved = true
self.latestFileErrorMessage = nil
let outputURL = try self.createOutputURL(displayName: displayName)
self.latestOutputPath = outputURL.path
self.recordingStartedAt = Date()
self.updateStatus(RecordingStatus(state: .recording, outputPath: outputURL.path))
self.movieOutput.startRecording(to: outputURL, recordingDelegate: self)
DispatchQueue.main.async {
self.startElapsedTimer()
result([
"outputPath": outputURL.path,
"status": self.currentStatusMap(),
])
}
} catch {
self.updateStatus(RecordingStatus(state: .error, message: error.localizedDescription))
DispatchQueue.main.async {
result(
FlutterError(
code: "START_FAILED",
message: error.localizedDescription,
details: nil
)
)
}
}
}
}
func stopRecording(result: @escaping FlutterResult) {
sessionQueue.async { [weak self] in
guard let self else { return }
guard self.movieOutput.isRecording else {
DispatchQueue.main.async {
var payload: [String: Any] = [
"outputPath": self.latestOutputPath as Any,
"status": self.currentStatusMap(),
"fileSaved": self.latestFileSaved,
]
if !self.latestFileSaved {
payload["fileErrorMessage"] =
self.latestFileErrorMessage ?? "保存到文件夹失败"
}
result(payload)
}
return
}
self.pendingStopResult = result
self.updateStatus(RecordingStatus(state: .stopping, outputPath: self.latestOutputPath))
self.movieOutput.stopRecording()
}
}
func disposePreview(result: @escaping FlutterResult) {
sessionQueue.async { [weak self] in
guard let self else { return }
if self.movieOutput.isRecording {
self.movieOutput.stopRecording()
}
if self.session.isRunning {
self.session.stopRunning()
}
self.session.beginConfiguration()
for input in self.session.inputs {
self.session.removeInput(input)
}
for output in self.session.outputs {
self.session.removeOutput(output)
}
self.session.commitConfiguration()
self.videoInput = nil
self.audioInput = nil
self.currentZoomRatio = 1.0
self.configured = false
self.updateStatus(RecordingStatus(state: .idle))
DispatchQueue.main.async {
self.stopElapsedTimer()
result(nil)
}
}
}
func currentStatusMap() -> [String: Any] {
if status.state == .recording {
return RecordingStatus(
state: .recording,
outputPath: latestOutputPath,
elapsedMillis: elapsedMillis()
).toMap()
}
return status.toMap()
}
func zoomCapabilities(result: @escaping FlutterResult) {
sessionQueue.async { [weak self] in
guard let self else { return }
let capabilities = self.currentZoomCapabilitiesMap()
DispatchQueue.main.async {
result(capabilities)
}
}
}
func setZoomRatio(_ ratio: CGFloat, result: @escaping FlutterResult) {
sessionQueue.async { [weak self] in
guard let self else { return }
guard let device = self.videoInput?.device else {
self.currentZoomRatio = max(1.0, ratio)
let capabilities = self.currentZoomCapabilitiesMap()
DispatchQueue.main.async {
result(capabilities)
}
return
}
do {
// (1.0x = ) S zoomFactor
let baseline = self.mainBaselineFactor(for: device)
let nextZoom = self.clampedZoomRatio(ratio * baseline, for: device)
try device.lockForConfiguration()
device.videoZoomFactor = nextZoom
device.unlockForConfiguration()
self.currentZoomRatio = nextZoom
let capabilities = self.currentZoomCapabilitiesMap()
DispatchQueue.main.async {
result(capabilities)
}
} catch {
DispatchQueue.main.async {
result(
FlutterError(
code: "ZOOM_FAILED",
message: error.localizedDescription,
details: nil
)
)
}
}
}
}
func fileOutput(
_ output: AVCaptureFileOutput,
didFinishRecordingTo outputFileURL: URL,
from connections: [AVCaptureConnection],
error: Error?
) {
let stopResult = pendingStopResult
pendingStopResult = nil
if let error {
latestFileSaved = false
latestFileErrorMessage = error.localizedDescription
updateStatus(
RecordingStatus(
state: .error, outputPath: latestOutputPath, message: error.localizedDescription))
finishStopRecording(stopResult: stopResult)
return
}
latestFileSaved = true
latestFileErrorMessage = nil
latestOutputPath = outputFileURL.path
guard FileManager.default.fileExists(atPath: outputFileURL.path) else {
latestFileSaved = false
latestFileErrorMessage = "录制文件未生成"
updateStatus(
RecordingStatus(
state: .error,
outputPath: latestOutputPath,
message: latestFileErrorMessage
)
)
finishStopRecording(stopResult: stopResult)
return
}
updateStatus(
RecordingStatus(
state: .previewing,
outputPath: latestOutputPath,
elapsedMillis: elapsedMillis()
)
)
finishStopRecording(stopResult: stopResult)
}
private func finishStopRecording(stopResult: FlutterResult?) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.stopElapsedTimer()
var payload: [String: Any] = [
"outputPath": self.latestOutputPath as Any,
"status": self.currentStatusMap(),
"fileSaved": self.latestFileSaved,
]
if !self.latestFileSaved {
payload["fileErrorMessage"] =
self.latestFileErrorMessage ?? "保存到文件夹失败,请检查文件保存权限"
}
stopResult?(payload)
}
}
private func configureSession(withAudio: Bool) throws {
if configured {
try configureAudioInput(enabled: withAudio)
return
}
guard let videoDevice = Self.preferredVideoDevice() else {
throw NSError(
domain: "RecordingCamera", code: 1,
userInfo: [NSLocalizedDescriptionKey: "No camera device available"])
}
let nextVideoInput = try AVCaptureDeviceInput(device: videoDevice)
session.beginConfiguration()
session.sessionPreset = .high
guard session.canAddInput(nextVideoInput) else {
session.commitConfiguration()
throw NSError(
domain: "RecordingCamera", code: 2,
userInfo: [NSLocalizedDescriptionKey: "Cannot add camera input"])
}
session.addInput(nextVideoInput)
videoInput = nextVideoInput
guard session.canAddOutput(movieOutput) else {
session.commitConfiguration()
throw NSError(
domain: "RecordingCamera", code: 3,
userInfo: [NSLocalizedDescriptionKey: "Cannot add movie output"])
}
session.addOutput(movieOutput)
session.commitConfiguration()
configured = true
// ( 1.0x) zoomFactor S
currentZoomRatio = mainBaselineFactor(for: videoDevice)
try applyCurrentZoom()
try configureAudioInput(enabled: withAudio)
}
/// 广使 minAvailableVideoZoomFactor ( 0.6x)
private static func preferredVideoDevice() -> AVCaptureDevice? {
let preferredTypes: [AVCaptureDevice.DeviceType] = [
.builtInTripleCamera,
.builtInDualWideCamera,
.builtInWideAngleCamera,
]
for type in preferredTypes {
if let device = AVCaptureDevice.default(type, for: .video, position: .back) {
return device
}
}
return AVCaptureDevice.default(for: .video)
}
/// ( 1.0x) zoomFactor S
/// ultra-wide wide 1.0()
private func mainBaselineFactor(for device: AVCaptureDevice) -> CGFloat {
if let first = device.virtualDeviceSwitchOverVideoZoomFactors.first {
let value = CGFloat(truncating: first)
if value > 0 {
return value
}
}
return 1.0
}
private func currentZoomCapabilitiesMap() -> [String: Any] {
guard let device = videoInput?.device else {
return [
"zoomRatio": Double(currentZoomRatio),
"minZoomRatio": 1.0,
"maxZoomRatio": 3.0,
]
}
// zoomFactor S App 使(1.0x = )
let baseline = mainBaselineFactor(for: device)
let minZoom = device.minAvailableVideoZoomFactor
let maxZoom = device.maxAvailableVideoZoomFactor
let zoom = clampedZoomRatio(device.videoZoomFactor, for: device)
currentZoomRatio = zoom
return [
"zoomRatio": Double(zoom / baseline),
"minZoomRatio": Double(minZoom / baseline),
"maxZoomRatio": Double(maxZoom / baseline),
]
}
private func applyCurrentZoom() throws {
guard let device = videoInput?.device else { return }
let nextZoom = clampedZoomRatio(currentZoomRatio, for: device)
try device.lockForConfiguration()
device.videoZoomFactor = nextZoom
device.unlockForConfiguration()
currentZoomRatio = nextZoom
}
private func clampedZoomRatio(_ ratio: CGFloat, for device: AVCaptureDevice) -> CGFloat {
min(max(ratio, device.minAvailableVideoZoomFactor), device.maxAvailableVideoZoomFactor)
}
private func configureAudioInput(enabled: Bool) throws {
session.beginConfiguration()
defer { session.commitConfiguration() }
if let audioInput {
session.removeInput(audioInput)
self.audioInput = nil
}
guard enabled else { return }
guard let audioDevice = AVCaptureDevice.default(for: .audio) else { return }
let nextAudioInput = try AVCaptureDeviceInput(device: audioDevice)
if session.canAddInput(nextAudioInput) {
session.addInput(nextAudioInput)
audioInput = nextAudioInput
}
}
private func isMicrophoneAuthorized() -> Bool {
AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
}
private func createOutputURL(displayName: String?) throws -> URL {
let baseURL = try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let recordingsURL = baseURL.appendingPathComponent("recordings", isDirectory: true)
try FileManager.default.createDirectory(at: recordingsURL, withIntermediateDirectories: true)
let fileName = Self.resolveFileName(displayName: displayName)
return uniqueOutputURL(in: recordingsURL, preferredFileName: fileName)
}
private func uniqueOutputURL(in directoryURL: URL, preferredFileName: String) -> URL {
let preferredURL = directoryURL.appendingPathComponent(preferredFileName)
guard !FileManager.default.fileExists(atPath: preferredURL.path) else {
let fileExtension = preferredURL.pathExtension
let baseName = preferredURL.deletingPathExtension().lastPathComponent
let timestamp = Self.fileNameDateFormatter.string(from: Date())
var index = 0
while true {
let suffix = index == 0 ? timestamp : "\(timestamp)_\(index)"
let nextName = fileExtension.isEmpty
? "\(baseName)_\(suffix)"
: "\(baseName)_\(suffix).\(fileExtension)"
let nextURL = directoryURL.appendingPathComponent(nextName)
if !FileManager.default.fileExists(atPath: nextURL.path) {
return nextURL
}
index += 1
}
}
return preferredURL
}
private static func resolveFileName(displayName: String?) -> String {
let trimmed = displayName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmed.isEmpty {
let lower = trimmed.lowercased()
if lower.hasSuffix(".mov") || lower.hasSuffix(".mp4") {
return trimmed
}
return "\(trimmed).mov"
}
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyyMMdd_HHmmss"
return "REC_\(formatter.string(from: Date())).mov"
}
private static let fileNameDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyyMMdd_HHmmss"
return formatter
}()
private func updateStatus(_ next: RecordingStatus) {
status = next
}
private func elapsedMillis() -> Int {
guard let recordingStartedAt else { return 0 }
return max(0, Int(Date().timeIntervalSince(recordingStartedAt) * 1000))
}
private func startElapsedTimer() {
stopElapsedTimer()
elapsedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self, self.status.state == .recording else { return }
self.statusListener?(self.currentStatusMap())
}
}
private func stopElapsedTimer() {
elapsedTimer?.invalidate()
elapsedTimer = nil
}
}
private enum RecordingChannelNames {
static let namespace = "app.record_tool"
static let method = "\(namespace)/recording"
static let events = "\(namespace)/recording_events"
}
final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private let controller = RecordingCameraController.shared
private var eventSink: FlutterEventSink?
static func register(with registrar: FlutterPluginRegistrar) {
let plugin = RecordingPlugin()
let messenger = registrar.messenger()
registrar.register(RecordingPreviewFactory(), withId: "recording-camera-preview")
let methodChannel = FlutterMethodChannel(
name: RecordingChannelNames.method,
binaryMessenger: messenger
)
registrar.addMethodCallDelegate(plugin, channel: methodChannel)
let eventChannel = FlutterEventChannel(
name: RecordingChannelNames.events,
binaryMessenger: messenger
)
eventChannel.setStreamHandler(plugin)
plugin.controller.statusListener = { [weak plugin] status in
plugin?.eventSink?(status)
}
}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "initializePreview":
controller.initializePreview(result: result)
case "startRecording":
let args = call.arguments as? [String: Any]
let withAudio = args?["withAudio"] as? Bool ?? true
let displayName = args?["displayName"] as? String
controller.startRecording(withAudio: withAudio, displayName: displayName, result: result)
case "stopRecording":
controller.stopRecording(result: result)
case "getZoomCapabilities":
controller.zoomCapabilities(result: result)
case "setZoomRatio":
let args = call.arguments as? [String: Any]
let ratio = args?["zoomRatio"] as? Double ?? 1.0
controller.setZoomRatio(CGFloat(ratio), result: result)
case "disposePreview":
controller.disposePreview(result: result)
case "getStatus":
result(controller.currentStatusMap())
case "hasNotificationPolicyAccess":
result(true)
case "openNotificationPolicySettings":
result(nil)
case "enableDoNotDisturb":
result(false)
case "disableDoNotDisturb":
result(nil)
case "isIgnoringBatteryOptimizations":
result(true)
case "openBatteryOptimizationSettings":
result(nil)
case "setImmersiveMode":
result(nil)
case "isForegroundServiceRunning":
result(false)
default:
result(FlutterMethodNotImplemented)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
-> FlutterError?
{
eventSink = events
events(controller.currentStatusMap())
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
}
+7 -7
View File
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/app/router/app_navigator.dart';
import 'package:flutter_template/app/theme/app_theme.dart';
import 'package:flutter_template/features/demo/demo_page.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:recording_tool/app/config/app_config.dart';
import 'package:recording_tool/app/router/app_navigator.dart';
import 'package:recording_tool/app/theme/app_theme.dart';
import 'package:recording_tool/features/auth/pages/page_auth.dart';
class FlutterTemplateApp extends StatelessWidget {
const FlutterTemplateApp({super.key});
@@ -14,7 +14,7 @@ class FlutterTemplateApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
designSize: AppConfig.designSize,
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
@@ -43,8 +43,8 @@ class FlutterTemplateApp extends StatelessWidget {
),
home: RefreshConfiguration(
enableLoadingWhenNoData: false,
headerTriggerDistance: 80,
child: const DemoPage(),
headerTriggerDistance: 80.h,
child: const AuthPageWidget(),
),
);
},
+47 -8
View File
@@ -1,11 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/app/app.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/core/cache/app_storage.dart';
import 'package:flutter_template/core/logging/app_logger.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter_ume/flutter_ume.dart';
import 'package:flutter_ume_kit_device/components/device_info/device_info_panel.dart';
import 'package:flutter_ume_kit_dio/flutter_ume_kit_dio.dart';
import 'package:recording_tool/app/app.dart';
import 'package:recording_tool/app/config/app_config.dart';
import 'package:recording_tool/core/cache/app_storage.dart';
import 'package:recording_tool/core/logging/app_logger.dart';
import 'package:recording_tool/core/network/app_dio.dart';
import 'package:recording_tool/core/platform/app_platform_info.dart';
class AppBootstrapper {
AppBootstrapper._();
@@ -18,12 +24,45 @@ class AppBootstrapper {
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await AppStorage.init();
final packageInfo = await PackageInfo.fromPlatform();
AppConfig.configure(environment: environment, packageInfo: packageInfo);
AppConfig.configure(environment: environment);
AppLogger.debug('App started in ${AppConfig.current.environment.name}');
runApp(const ProviderScope(child: FlutterTemplateApp()));
// 注册 UME 调试插件(DioInspector 需与业务共用同一 Dio 实例)
PluginManager.instance
..register(DioInspector(dio: AppDio.instance))
..register(DeviceInfoPanel());
runApp(
UMEWidget(
enable: true,
child: Stack(
alignment: Alignment.center,
children: [const ProviderScope(child: FlutterTemplateApp())],
),
),
);
// runApp(const ProviderScope(child: FlutterTemplateApp()));
// Load native package metadata after the first frame can render.
// Awaiting MethodChannel calls before runApp() can stall the Android
// splash screen on some devices.
unawaited(_loadPackageInfo(environment));
}
static Future<void> _loadPackageInfo(AppEnvironment environment) async {
try {
final packageInfo = await AppPlatformInfo.packageInfo().timeout(
const Duration(seconds: 8),
);
AppConfig.configure(environment: environment, packageInfo: packageInfo);
} catch (error, stackTrace) {
AppLogger.debug(
'Native packageInfo unavailable',
error: error,
stackTrace: stackTrace,
);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
enum AuthApi {
/// 获取 token
getToken('/api/events/device/token'),
/// 获取推流地址
getStreamKey('/api/events/device/stream/key'),
/// 根据赛事目录获取视频列表
getRecordList('/api/files'),
/// 获取参赛队伍列表
getTeamList('/api/events/device/item/group'),
/// 参赛队伍详情
getTeamDetail('/api/events/device/schedule/pending'),
/// 人工设置晋级/淘汰
setTeamStatus('/api/events/device/schedule/team/score'),
/// 获取登录设备信息
getLoginDeviceInfo('/api/events/device/nas/info'),
/// 获取选手的赛事信息
playerRegistrationList('/api/events/device/player/registration/list');
final String path;
const AuthApi(this.path);
}
+20 -7
View File
@@ -1,4 +1,5 @@
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:recording_tool/core/platform/app_platform_info.dart';
enum AppEnvironment { dev, staging, prod }
@@ -7,40 +8,52 @@ class EnvironmentValues {
required this.environment,
required this.baseUrl,
required this.enableNetworkLog,
this.mainRefereeScoreH5Url = '',
});
final AppEnvironment environment;
final String baseUrl;
final bool enableNetworkLog;
final String mainRefereeScoreH5Url;
}
class AppConfig {
AppConfig._();
static late EnvironmentValues current;
static PackageInfo? packageInfo;
static AppPackageInfo? packageInfo;
static const appName = 'Flutter Template';
static const appName = 'SportsX裁判工作台';
static const designSize = Size(640, 1024);
/// 主裁判计分 H5 链接
static void configure({
required AppEnvironment environment,
PackageInfo? packageInfo,
AppPackageInfo? packageInfo,
}) {
AppConfig.packageInfo = packageInfo;
current = switch (environment) {
AppEnvironment.dev => const EnvironmentValues(
environment: AppEnvironment.dev,
baseUrl: 'https://example.com/api',
// baseUrl: 'http://192.168.1.104:8000',
baseUrl: 'https://apitest.dronex.cc',
mainRefereeScoreH5Url:
'https://drone.apptest.sportsx.cc/#/pages/h5/score-entry',
enableNetworkLog: true,
),
AppEnvironment.staging => const EnvironmentValues(
environment: AppEnvironment.staging,
baseUrl: 'https://staging.example.com/api',
baseUrl: 'https://apitest.dronex.cc',
mainRefereeScoreH5Url:
'https://drone.apptest.sportsx.cc/#/pages/h5/score-entry',
enableNetworkLog: true,
),
AppEnvironment.prod => const EnvironmentValues(
environment: AppEnvironment.prod,
baseUrl: 'https://api.example.com',
baseUrl: 'https://api.dronex.cc',
mainRefereeScoreH5Url:
'https://drone.sportsx.cc/#/pages/h5/score-entry',
enableNetworkLog: false,
),
};
+3 -6
View File
@@ -94,8 +94,8 @@ class AppNavigator {
barrierDismissible: dismissible,
transitionDuration: duration,
reverseTransitionDuration: duration,
pageBuilder: (_, __, ___) => page,
transitionsBuilder: (_, animation, __, child) {
pageBuilder: (_, _, _) => page,
transitionsBuilder: (_, animation, _, child) {
return FadeTransition(opacity: animation, child: child);
},
),
@@ -103,10 +103,7 @@ class AppNavigator {
}
static void pop<T extends Object?>({BuildContext? context, T? result}) {
Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
).pop<T>(result);
Navigator.maybePop(context ?? AppNavigator.context!);
}
static void popTimes({BuildContext? context, int count = 1}) {
+12 -11
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class AppTheme {
AppTheme._();
@@ -32,20 +33,20 @@ class AppTheme {
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(88, 44),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
minimumSize: Size(88.w, 44.h),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(88, 44),
minimumSize: Size(88.w, 44.h),
side: const BorderSide(color: border),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)),
),
),
);
@@ -71,10 +72,10 @@ class AppTheme {
class AppSpacing {
AppSpacing._();
static const double xs = 4;
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 24;
static const double xxl = 32;
static double get xs => 4.r;
static double get sm => 8.r;
static double get md => 12.r;
static double get lg => 16.r;
static double get xl => 24.r;
static double get xxl => 32.r;
}
+21 -6
View File
@@ -1,9 +1,9 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_template/core/network/api_exception.dart';
import 'package:flutter_template/core/network/api_response.dart';
import 'package:flutter_template/core/network/http_method.dart';
import 'package:recording_tool/core/network/api_exception.dart';
import 'package:recording_tool/core/network/api_response.dart';
import 'package:recording_tool/core/network/http_method.dart';
typedef JsonParser<T> = T Function(dynamic json);
@@ -38,11 +38,15 @@ class ApiClient {
return _parseData<T>(raw, parser);
}
if (raw is! Map<String, dynamic>) {
final payload = raw is Map ? Map<String, dynamic>.from(raw) : null;
if (payload == null ||
!(payload.containsKey('code') || payload.containsKey('message'))) {
return _parseData<T>(raw, parser);
}
final wrapped = ApiResponse<T>.fromJson(raw, fromJsonT: parser);
// 先按 {code,message,data} 解包,再用业务 parser 解析 data
// 避免把 Map 直接强转成业务模型。
final wrapped = ApiResponse<dynamic>.fromJson(payload);
if (!wrapped.isSuccess) {
throw ApiException(
code: wrapped.code,
@@ -52,7 +56,17 @@ class ApiClient {
);
}
return wrapped.data as T;
if (wrapped.data == null) {
if (null is T) return null as T;
throw ApiException(
code: wrapped.code,
statusCode: response.statusCode,
message: '响应 data 为空',
details: raw,
);
}
return _parseData<T>(wrapped.data, parser);
} on DioException catch (error) {
throw _mapDioException(error);
}
@@ -108,6 +122,7 @@ class ApiClient {
final statusCode = error.response?.statusCode;
final message = switch (error.type) {
DioExceptionType.connectionTimeout => '网络连接超时',
DioExceptionType.transformTimeout => '网络请求处理超时',
DioExceptionType.sendTimeout => '请求发送超时',
DioExceptionType.receiveTimeout => '响应接收超时',
DioExceptionType.badCertificate => '证书校验失败',
+18 -2
View File
@@ -5,16 +5,32 @@ class ApiResponse<T> {
final String message;
final T? data;
bool get isSuccess => code >= 200 && code < 300;
bool get isSuccess => code == 0 || code >= 200 && code < 300;
factory ApiResponse.fromJson(
Map<String, dynamic> json, {
T Function(dynamic json)? fromJsonT,
}) {
final rawData = json['data'];
T? data;
if (rawData != null && fromJsonT != null) {
data = fromJsonT(rawData);
} else if (rawData != null && fromJsonT == null) {
// 无 parser 时仅在类型已匹配时接收,避免 Map 被强转为业务模型。
if (rawData is T) {
data = rawData;
} else {
throw FormatException(
'ApiResponse data 类型不匹配,且未提供 fromJsonT;'
'期望 $T,实际 ${rawData.runtimeType}',
);
}
}
return ApiResponse<T>(
code: (json['code'] as num?)?.toInt() ?? 200,
message: (json['message'] ?? json['msg'] ?? '').toString(),
data: fromJsonT == null ? json['data'] as T? : fromJsonT(json['data']),
data: data,
);
}
}
+21
View File
@@ -0,0 +1,21 @@
import 'package:dio/dio.dart';
import 'package:recording_tool/app/config/app_config.dart';
/// 应用全局 Dio 单例,供业务请求与调试插件共用。
class AppDio {
AppDio._();
static Dio? _instance;
static Dio get instance {
return _instance ??= Dio(
BaseOptions(
baseUrl: AppConfig.current.baseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
responseType: ResponseType.json,
),
);
}
}
+20 -4
View File
@@ -1,10 +1,13 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/core/cache/app_storage.dart';
import 'package:flutter_template/core/cache/storage_keys.dart';
import 'package:flutter_template/core/utils/device_utils.dart';
import 'package:recording_tool/app/config/app_config.dart';
import 'package:recording_tool/app/router/app_navigator.dart';
import 'package:recording_tool/core/cache/app_storage.dart';
import 'package:recording_tool/core/cache/storage_keys.dart';
import 'package:recording_tool/core/utils/device_utils.dart';
import 'package:recording_tool/features/auth/pages/page_auth.dart';
import 'package:recording_tool/shared/widgets/app_toast.dart';
class HeaderInterceptor extends Interceptor {
@override
@@ -28,4 +31,17 @@ class HeaderInterceptor extends Interceptor {
handler.next(options);
}
@override
Future<void> onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
if (err.response?.statusCode == 401) {
await AppStorage.remove(StorageKeys.authToken);
AppToast.show('口令已过期,请重新输入执裁口令');
AppNavigator.pushAndRemoveUntil(AuthPageWidget());
}
handler.next(err);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_template/core/network/network_state.dart';
import 'package:recording_tool/core/network/network_state.dart';
class NetworkMonitor {
final _controller = StreamController<NetworkState>.broadcast();
@@ -1,7 +1,7 @@
import 'package:dio/dio.dart';
import 'package:flutter_template/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
import 'package:recording_tool/core/network/network_monitor.dart';
import 'package:recording_tool/core/network/offline_queue/offline_queue_manager.dart';
import 'package:recording_tool/core/network/offline_queue/offline_request.dart';
import 'package:uuid/uuid.dart';
class OfflineQueueInterceptor extends Interceptor {
@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_template/core/logging/app_logger.dart';
import 'package:flutter_template/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
import 'package:recording_tool/core/logging/app_logger.dart';
import 'package:recording_tool/core/network/network_monitor.dart';
import 'package:recording_tool/core/network/offline_queue/offline_queue_state.dart';
import 'package:recording_tool/core/network/offline_queue/offline_queue_storage.dart';
import 'package:recording_tool/core/network/offline_queue/offline_request.dart';
class OfflineQueueManager {
OfflineQueueManager({
@@ -1,6 +1,6 @@
import 'package:flutter_template/core/cache/app_storage.dart';
import 'package:flutter_template/core/cache/storage_keys.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
import 'package:recording_tool/core/cache/app_storage.dart';
import 'package:recording_tool/core/cache/storage_keys.dart';
import 'package:recording_tool/core/network/offline_queue/offline_request.dart';
class OfflineQueueStorage {
Future<List<OfflineRequest>> loadQueue() async {
+27 -27
View File
@@ -1,37 +1,37 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/core/network/api_client.dart';
import 'package:flutter_template/core/network/header_interceptor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_interceptor.dart';
import 'package:flutter_template/core/network/providers/network_providers.dart';
import 'package:flutter_template/core/network/providers/offline_queue_providers.dart';
import 'package:recording_tool/app/config/app_config.dart';
import 'package:recording_tool/core/network/api_client.dart';
import 'package:recording_tool/core/network/app_dio.dart';
import 'package:recording_tool/core/network/header_interceptor.dart';
import 'package:recording_tool/core/network/offline_queue/offline_queue_interceptor.dart';
import 'package:recording_tool/core/network/providers/network_providers.dart';
import 'package:recording_tool/core/network/providers/offline_queue_providers.dart';
bool _dioConfigured = false;
final dioProvider = Provider<Dio>((ref) {
final dio = Dio(
BaseOptions(
baseUrl: AppConfig.current.baseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
responseType: ResponseType.json,
),
);
final dio = AppDio.instance;
dio.interceptors.add(HeaderInterceptor());
if (!_dioConfigured) {
_dioConfigured = true;
dio.interceptors.add(HeaderInterceptor());
final monitor = ref.watch(networkMonitorProvider);
final queueManager = ref.watch(offlineQueueManagerProvider);
dio.interceptors.add(
OfflineQueueInterceptor(
monitor: monitor,
manager: queueManager,
enabled: false,
),
);
final monitor = ref.read(networkMonitorProvider);
final queueManager = ref.read(offlineQueueManagerProvider);
dio.interceptors.add(
OfflineQueueInterceptor(
monitor: monitor,
manager: queueManager,
enabled: false,
),
);
if (AppConfig.current.enableNetworkLog) {
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
if (AppConfig.current.enableNetworkLog) {
dio.interceptors.add(
LogInterceptor(requestBody: true, responseBody: true),
);
}
}
return dio;
@@ -1,6 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/network_state.dart';
import 'package:recording_tool/core/network/network_monitor.dart';
import 'package:recording_tool/core/network/network_state.dart';
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
final monitor = NetworkMonitor()..start();
@@ -1,8 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart';
import 'package:flutter_template/core/network/providers/network_providers.dart';
import 'package:recording_tool/core/network/offline_queue/offline_queue_manager.dart';
import 'package:recording_tool/core/network/offline_queue/offline_queue_state.dart';
import 'package:recording_tool/core/network/offline_queue/offline_queue_storage.dart';
import 'package:recording_tool/core/network/providers/network_providers.dart';
import 'package:dio/dio.dart';
final offlineQueueStorageProvider = Provider<OfflineQueueStorage>((ref) {
@@ -17,6 +17,24 @@ class PermissionService {
return permissions.toList().request();
}
/// 仅对尚未授予的权限发起系统授权弹窗,已授予则直接返回当前状态。
static Future<Map<Permission, PermissionStatus>> requestMissing(
Iterable<Permission> permissions,
) async {
final result = <Permission, PermissionStatus>{};
for (final permission in permissions) {
final current = await permission.status;
if (current.isGranted ||
current.isLimited ||
current.isPermanentlyDenied) {
result[permission] = current;
continue;
}
result[permission] = await permission.request();
}
return result;
}
static Future<bool> ensure(
Permission permission, {
bool openSettingsWhenPermanentlyDenied = true,
+89
View File
@@ -0,0 +1,89 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:recording_tool/core/platform/device_health_snapshot.dart';
class AppPackageInfo {
const AppPackageInfo({
required this.appName,
required this.packageName,
required this.version,
required this.buildNumber,
});
factory AppPackageInfo.fromMap(Map<Object?, Object?> map) {
return AppPackageInfo(
appName: map['appName'] as String? ?? '',
packageName: map['packageName'] as String? ?? '',
version: map['version'] as String? ?? '',
buildNumber: map['buildNumber'] as String? ?? '',
);
}
final String appName;
final String packageName;
final String version;
final String buildNumber;
}
class AppDeviceInfo {
const AppDeviceInfo({
required this.platform,
required this.isPhysicalDevice,
required this.values,
required this.deviceCode,
});
factory AppDeviceInfo.fromMap(Map<Object?, Object?> map) {
final values = <String, String>{};
for (final entry in map.entries) {
final key = entry.key;
final value = entry.value;
if (key is String && value != null) {
values[key] = value.toString();
}
}
final isPhysicalDevice = map['isPhysicalDevice'];
return AppDeviceInfo(
platform: map['platform'] as String? ?? Platform.operatingSystem,
isPhysicalDevice: isPhysicalDevice is bool ? isPhysicalDevice : true,
values: values,
deviceCode: values['deviceCode'] ?? '',
);
}
final String platform;
final bool isPhysicalDevice;
final Map<String, String> values;
final String deviceCode;
}
class AppPlatformInfo {
AppPlatformInfo._();
static const MethodChannel _channel = MethodChannel(
'app.record_tool/platform_info',
);
static Future<AppPackageInfo> packageInfo() async {
final result = await _channel.invokeMapMethod<Object?, Object?>(
'packageInfo',
);
return AppPackageInfo.fromMap(result ?? const <Object?, Object?>{});
}
static Future<AppDeviceInfo> deviceInfo() async {
final result = await _channel.invokeMapMethod<Object?, Object?>(
'deviceInfo',
);
return AppDeviceInfo.fromMap(result ?? const <Object?, Object?>{});
}
static Future<DeviceHealthSnapshot> deviceHealth() async {
final result = await _channel.invokeMapMethod<Object?, Object?>(
'deviceHealth',
);
return DeviceHealthSnapshot.fromMap(result ?? const <Object?, Object?>{});
}
}
@@ -0,0 +1,25 @@
import 'package:recording_tool/core/platform/device_health_snapshot.dart';
class DeviceHealthChecker {
DeviceHealthChecker._();
static const int thresholdPercent = 10;
static const String lowBatteryMessage = '电量低于10%,请充电';
static const String lowStorageMessage = '内存低于10%,请清理内存';
static List<String> warningLines(DeviceHealthSnapshot snapshot) {
final lines = <String>[];
final battery = snapshot.batteryLevelPercent;
if (battery != null && battery < thresholdPercent) {
lines.add(lowBatteryMessage);
}
if (snapshot.storageAvailablePercent < thresholdPercent) {
lines.add(lowStorageMessage);
}
return lines;
}
}
@@ -0,0 +1,30 @@
class DeviceHealthSnapshot {
const DeviceHealthSnapshot({
this.batteryLevelPercent,
required this.storageAvailablePercent,
});
factory DeviceHealthSnapshot.fromMap(Map<Object?, Object?> map) {
final batteryRaw = map['batteryLevelPercent'];
int? batteryLevelPercent;
if (batteryRaw is int) {
batteryLevelPercent = batteryRaw;
} else if (batteryRaw is num) {
batteryLevelPercent = batteryRaw.round();
}
final storageRaw = map['storageAvailablePercent'];
final storageAvailablePercent = switch (storageRaw) {
final num value => value.toDouble(),
_ => 100.0,
};
return DeviceHealthSnapshot(
batteryLevelPercent: batteryLevelPercent,
storageAvailablePercent: storageAvailablePercent,
);
}
final int? batteryLevelPercent;
final double storageAvailablePercent;
}
+12 -27
View File
@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:recording_tool/core/platform/app_platform_info.dart';
class DeviceUtils {
DeviceUtils._();
@@ -19,36 +19,21 @@ class DeviceUtils {
MediaQuery.paddingOf(context).bottom;
static Future<bool> isPhysicalDevice() async {
final plugin = DeviceInfoPlugin();
if (Platform.isAndroid) {
return (await plugin.androidInfo).isPhysicalDevice;
}
if (Platform.isIOS) {
return (await plugin.iosInfo).isPhysicalDevice;
}
return true;
return (await AppPlatformInfo.deviceInfo()).isPhysicalDevice;
}
static Future<Map<String, String>> deviceInfo() async {
final plugin = DeviceInfoPlugin();
if (Platform.isAndroid) {
final info = await plugin.androidInfo;
return {
'platform': 'android',
'brand': info.brand,
'model': info.model,
'systemVersion': info.version.release,
};
if (!Platform.isAndroid && !Platform.isIOS) {
return {'platform': Platform.operatingSystem};
}
if (Platform.isIOS) {
final info = await plugin.iosInfo;
return {
'platform': 'ios',
'brand': info.systemName,
'model': info.utsname.machine,
'systemVersion': info.systemVersion,
};
return (await AppPlatformInfo.deviceInfo()).values;
}
static Future<String> deviceCode() async {
try {
return (await AppPlatformInfo.deviceInfo()).deviceCode.trim();
} catch (_) {
return '';
}
return {'platform': Platform.operatingSystem};
}
}

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