From 58a23ef21867b1d41ed22bbfb0d4a7d384186f0b Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Thu, 22 Oct 2026 12:53:24 +0800 Subject: [PATCH] =?UTF-8?q?USB=20=E6=91=84=E5=83=8F=E5=A4=B4=E9=80=82?= =?UTF-8?q?=E9=85=8DiPadOS17+?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/languages/en-US.json | 8 +- assets/languages/zh-CN.json | 8 +- lib/components/device_status_modal.dart | 22 ++ .../host_realtime_message_entity.dart | 8 + .../entities/running_status_entity.dart | 10 + .../presentation/bloc/device_status_bloc.dart | 5 +- .../presentation/pages/robot_list_page.dart | 16 +- .../widgets/device_item_widget.dart | 19 +- .../widgets/drone_station_item_card.dart | 9 +- .../presentation/widgets/robot_item_card.dart | 18 +- .../pages/robot_param_settings_page.dart | 237 ++++++++---------- .../presentation/cubit/usb_camera_cubit.dart | 11 + .../UsbCameraAndroidPlugin.java | 3 + .../lib/src/usb_camera_android.dart | 13 + .../lib/src/usb_camera_preview_view.dart | 10 +- packages/usb_camera_android/pubspec.yaml | 2 + packages/video_player_android/README.md | 20 ++ .../video_player_android/example/README.md | 9 + 18 files changed, 288 insertions(+), 140 deletions(-) create mode 100644 packages/video_player_android/README.md create mode 100644 packages/video_player_android/example/README.md diff --git a/assets/languages/en-US.json b/assets/languages/en-US.json index 7abd423c..4a96f477 100644 --- a/assets/languages/en-US.json +++ b/assets/languages/en-US.json @@ -96,6 +96,8 @@ "battery": "Battery", "chip_temp": "Chip Temp", "knife_speed": "Knife Speed", + "linear_speed": "Linear Speed", + "angular_speed": "Angular Speed", "control_mode": "Control Mode", "longitude": "Longitude", "latitude": "Latitude", @@ -552,7 +554,11 @@ "path_planning": "Path Planning", "path_planning_desc": "View and edit the robot patrol path", "release_permission_success": "Remote control permission released", - "release_permission_failed": "Failed to release control permission" + "release_permission_failed": "Failed to release control permission", + "stat_online_devices": "Online Devices", + "stat_working": "Working", + "stat_standby": "Standby", + "stat_offline": "Offline" }, "alarm_center": { diff --git a/assets/languages/zh-CN.json b/assets/languages/zh-CN.json index e1ba3914..d1dd456f 100644 --- a/assets/languages/zh-CN.json +++ b/assets/languages/zh-CN.json @@ -96,6 +96,8 @@ "battery": "电量", "chip_temp": "芯片温度", "knife_speed": "割刀速度", + "linear_speed": "线速度", + "angular_speed": "角速度", "control_mode": "控制模式", "longitude": "经度", "latitude": "纬度", @@ -565,7 +567,11 @@ "path_planning": "路径规划", "path_planning_desc": "查看和编辑机器人巡检路径", "release_permission_success": "已释放远程控制权限", - "release_permission_failed": "控制权限释放失败" + "release_permission_failed": "控制权限释放失败", + "stat_online_devices": "在线设备", + "stat_working": "作业中", + "stat_standby": "待命", + "stat_offline": "离线数" }, "alarm_center": { diff --git a/lib/components/device_status_modal.dart b/lib/components/device_status_modal.dart index f236dde7..4e0505cc 100644 --- a/lib/components/device_status_modal.dart +++ b/lib/components/device_status_modal.dart @@ -376,6 +376,28 @@ class _DeviceStatusModalState extends State { _buildTableCell('--'), ], ), + TableRow( + children: [ + _buildTableCell( + AppLocalizations.of( + context, + ).translate('running_status.linear_speed') + + "(m/s)", + ), + _buildTableCell(status.linearSpeed.toStringAsFixed(2)), + ], + ), + TableRow( + children: [ + _buildTableCell( + AppLocalizations.of( + context, + ).translate('running_status.angular_speed') + + "(rad/s)", + ), + _buildTableCell(status.angularSpeed.toStringAsFixed(2)), + ], + ), TableRow( children: [ _buildTableCell( diff --git a/lib/core/network/mqtt/domain/entities/host_realtime_message_entity.dart b/lib/core/network/mqtt/domain/entities/host_realtime_message_entity.dart index 5e46f4c0..afb1844c 100644 --- a/lib/core/network/mqtt/domain/entities/host_realtime_message_entity.dart +++ b/lib/core/network/mqtt/domain/entities/host_realtime_message_entity.dart @@ -28,6 +28,8 @@ class HostRealtimeMessageEntity extends Equatable { final double rightMotorRpm; // 实时右电机转速 final double roll; // 实时翻滚角度 final String controlMode; // 当前控制模式(遥控器拨杆对应) + final double linearSpeed; // 实时线速度 m/s(前进为正,后退为负) + final double angularSpeed; // 实时角速度 rad/s(左转为正,右转(偏航)为负) /// 原始 data 报文,便于后端补字段时兜底 final Map rawData; @@ -56,6 +58,8 @@ class HostRealtimeMessageEntity extends Equatable { this.rightMotorRpm = 0, this.roll = 0, this.controlMode = '', + this.linearSpeed = 0, + this.angularSpeed = 0, this.rawData = const {}, }); @@ -85,6 +89,8 @@ class HostRealtimeMessageEntity extends Equatable { rightMotorRpm: _toDouble(data['right_motor_rpm']), roll: _toDouble(data['roll']), controlMode: data['control_mode']?.toString() ?? '', + linearSpeed: _toDouble(data['linear_speed']), + angularSpeed: _toDouble(data['angular_speed']), rawData: data, ); } @@ -128,5 +134,7 @@ class HostRealtimeMessageEntity extends Equatable { rightMotorRpm, roll, controlMode, + linearSpeed, + angularSpeed, ]; } diff --git a/lib/features/devices/domain/entities/running_status_entity.dart b/lib/features/devices/domain/entities/running_status_entity.dart index 69cc1c5a..544d1c40 100644 --- a/lib/features/devices/domain/entities/running_status_entity.dart +++ b/lib/features/devices/domain/entities/running_status_entity.dart @@ -44,6 +44,8 @@ class RunningStatusEntity extends Equatable { // === 主机实时话题(device/{sn}/host/realTime/message)扩展字段(新增,纯附加) === final double totalWorkDistance; // host 累计作业里程(供"巡检里程"展示) + final double linearSpeed; // host 实时线速度 m/s(前进为正,后退为负) + final double angularSpeed; // host 实时角速度 rad/s(左转为正,右转为负) final bool hostDataReceived; // 是否已收到 host 实时数据(未收到时 UI 显示"未知") const RunningStatusEntity({ @@ -72,6 +74,8 @@ class RunningStatusEntity extends Equatable { this.workingArea = '0', this.obstacleFlag = '0', this.totalWorkDistance = 0.0, + this.linearSpeed = 0.0, + this.angularSpeed = 0.0, this.hostDataReceived = false, }); @@ -173,6 +177,8 @@ class RunningStatusEntity extends Equatable { String? workingArea, String? obstacleFlag, double? totalWorkDistance, + double? linearSpeed, + double? angularSpeed, bool? hostDataReceived, }) { return RunningStatusEntity( @@ -201,6 +207,8 @@ class RunningStatusEntity extends Equatable { workingArea: workingArea ?? this.workingArea, obstacleFlag: obstacleFlag ?? this.obstacleFlag, totalWorkDistance: totalWorkDistance ?? this.totalWorkDistance, + linearSpeed: linearSpeed ?? this.linearSpeed, + angularSpeed: angularSpeed ?? this.angularSpeed, hostDataReceived: hostDataReceived ?? this.hostDataReceived, ); } @@ -232,6 +240,8 @@ class RunningStatusEntity extends Equatable { workingArea, obstacleFlag, totalWorkDistance, + linearSpeed, + angularSpeed, hostDataReceived, ]; diff --git a/lib/features/devices/presentation/bloc/device_status_bloc.dart b/lib/features/devices/presentation/bloc/device_status_bloc.dart index 2a3ace3d..db80b364 100644 --- a/lib/features/devices/presentation/bloc/device_status_bloc.dart +++ b/lib/features/devices/presentation/bloc/device_status_bloc.dart @@ -327,6 +327,9 @@ class DeviceStatusBloc extends Bloc { workingArea: host.totalWorkArea.toString(), // 🔥 供全功能模式(pv_control)展示:累计作业里程 + host 已推数据标记 totalWorkDistance: host.totalWorkDistance, + // 🔥 车体实测线速度/角速度(后端新增 linear_speed / angular_speed) + linearSpeed: host.linearSpeed, + angularSpeed: host.angularSpeed, hostDataReceived: true, ); @@ -334,7 +337,7 @@ class DeviceStatusBloc extends Bloc { _cachedGps ??= GPSEntity(_cachedStatus!.latitude, _cachedStatus!.longitude); debugPrint( - '${LogTime.now()} 🛰️ [MQTT-主机实时] 已合并 → 电量=${host.batteryPercentage}% 电压=${host.batteryVoltage}V 控制模式=${host.controlMode}', + '${LogTime.now()} 🛰️ [MQTT-主机实时] 已合并 → 电量=${host.batteryPercentage}% 电压=${host.batteryVoltage}V 控制模式=${host.controlMode} 线速度=${host.linearSpeed}m/s 角速度=${host.angularSpeed}rad/s', ); // 500ms 节流:与 location/车辆实时 发射链路一致 diff --git a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart index bac5f8ff..7424c60d 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart @@ -426,25 +426,33 @@ class _RobotListViewState extends State { children: [ _buildStatusItem( value: '$onlineCount', - label: '在线设备', + label: AppLocalizations.of( + context, + ).translate('device_list_v2.stat_online_devices'), valueColor: context.appColors.primary, ), Container(width: 1, height: 40, color: context.appColors.divider), _buildStatusItem( value: '$workingCount', - label: '作业中', + label: AppLocalizations.of( + context, + ).translate('device_list_v2.stat_working'), valueColor: context.appColors.success, ), Container(width: 1, height: 40, color: context.appColors.divider), _buildStatusItem( value: '$standbyCount', - label: '待命', + label: AppLocalizations.of( + context, + ).translate('device_list_v2.stat_standby'), valueColor: context.appColors.warning, ), Container(width: 1, height: 40, color: context.appColors.divider), _buildStatusItem( value: '$faultCount', - label: '离线数', + label: AppLocalizations.of( + context, + ).translate('device_list_v2.stat_offline'), valueColor: context.appColors.danger, ), ], diff --git a/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart b/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart index aee2bce3..8d2f780d 100644 --- a/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart +++ b/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart @@ -157,10 +157,27 @@ class DeviceItemWidget extends StatelessWidget { color: bgColor, borderRadius: BorderRadius.circular(12.0), ), - child: Text(status, style: TextStyle(fontSize: 12, color: textColor)), + child: Text( + _displayStatus(context, status), + style: TextStyle(fontSize: 12, color: textColor), + ), ); } + /// 🔥 仅做显示层映射:把内部语义状态值转成当前语言文案,未命中则原样显示 + /// 数据层与颜色 switch 仍以 '在线'/'离线'/'异常' 为比较键,此处不改数据 + String _displayStatus(BuildContext context, String status) { + final loc = AppLocalizations.of(context); + switch (status) { + case '在线': + return loc.translate('device_list_v2.online'); + case '离线': + return loc.translate('device_list_v2.offline'); + default: + return status; + } + } + Widget _buildDeviceData(BuildContext context, DeviceEntity device) { final dataItems = []; diff --git a/lib/features/v2/device_list/presentation/widgets/drone_station_item_card.dart b/lib/features/v2/device_list/presentation/widgets/drone_station_item_card.dart index 824775ec..bbcd5378 100644 --- a/lib/features/v2/device_list/presentation/widgets/drone_station_item_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/drone_station_item_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import '../../domain/entities/drone_station_entity.dart'; @@ -99,7 +100,7 @@ class DroneStationItemCard extends StatelessWidget { ), ), // 机场状态徽章 - _buildStatusBadge(station.isOnline), + _buildStatusBadge(context, station.isOnline), ], ), ), @@ -230,7 +231,7 @@ class DroneStationItemCard extends StatelessWidget { } /// 状态徽章 - Widget _buildStatusBadge( bool isOnline) { + Widget _buildStatusBadge(BuildContext context, bool isOnline) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( @@ -263,7 +264,9 @@ class DroneStationItemCard extends StatelessWidget { ), const SizedBox(width: 5), Text( - '${isOnline ? '在线' : '离线'}', + AppLocalizations.of(context).translate( + isOnline ? 'device_list_v2.online' : 'device_list_v2.offline', + ), textAlign: TextAlign.center, style: const TextStyle( fontSize: 10, diff --git a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart index c0e0511d..2e0bc2a3 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart @@ -208,10 +208,26 @@ class RobotItemCard extends StatelessWidget { color: bgColor, borderRadius: BorderRadius.circular(12), ), - child: Text(status, style: TextStyle(fontSize: 12, color: textColor)), + child: Text( + _displayStatus(context, status), + style: TextStyle(fontSize: 12, color: textColor), + ), ); } + /// 🔥 仅做显示层映射:把内部语义状态值转成当前语言文案,未命中则原样显示 + String _displayStatus(BuildContext context, String status) { + final loc = AppLocalizations.of(context); + switch (status) { + case '在线': + return loc.translate('device_list_v2.online'); + case '离线': + return loc.translate('device_list_v2.offline'); + default: + return status; + } + } + /// 🔥 仅做显示层映射:把内部语义状态值转成当前语言文案,未命中则原样显示 /// 注意:model 默认值与列表过滤逻辑仍使用 '待机中'/'充电中',此处不改数据 String _displayTask(BuildContext context, String task) { diff --git a/lib/features/v2/device_run_param/presentation/pages/robot_param_settings_page.dart b/lib/features/v2/device_run_param/presentation/pages/robot_param_settings_page.dart index 2eb8b53b..5b747805 100644 --- a/lib/features/v2/device_run_param/presentation/pages/robot_param_settings_page.dart +++ b/lib/features/v2/device_run_param/presentation/pages/robot_param_settings_page.dart @@ -25,23 +25,12 @@ class _RobotParamSettingsPageState extends State String? _errorMessage; DeviceRunParamEntity? _param; - // 表单控制器(仅可编辑的5个字段) - late TextEditingController _runSpeedController; - late TextEditingController _leftForwardGainController; - late TextEditingController _leftBackwardGainController; - late TextEditingController _rightForwardGainController; - late TextEditingController _rightBackwardGainController; + // 表单控制器:字段名 -> 输入框控制器(所有展示字段均可编辑,数据驱动) + final Map _controllers = {}; String get _deviceId => widget.robot['name'] as String? ?? ''; // ============ 字段定义 ============ - static const _editableKeys = { - 'runSpeed', - 'leftForwardGain', - 'leftBackwardGain', - 'rightForwardGain', - 'rightBackwardGain', - }; static const _skipKeys = { 'createBy', 'createTime', 'updateBy', 'updateTime', 'delFlag', 'remark', @@ -49,10 +38,38 @@ class _RobotParamSettingsPageState extends State static const _fieldLabels = { 'id': 'ID', - 'deviceId': '设备ID', + 'deviceId': '设备序列号', 'siteId': '场站ID', 'orgId': '组织ID', 'runSpeed': '运行速度', + // ===== 设备参数实体字段(取自后端 @Excel 中文名)===== + 'productModel': '产品型号', + 'deviceType': '设备类型', + 'rtkMode': 'RTK模式', + 'cameraNum': '摄像头数量', + 'powerType': '动力类型', + 'wheelType': '车轮类型', + 'engineType': '发动机类型', + 'hasHost': '是否搭载上位机', + 'walkingDriveType': '行走驱动类型', + 'cutterDriveType': '割刀驱动类型', + 'rtkAntennasNum': 'RTK天线数', + 'handleType': '手柄类型', + 'pcbVersion': 'PCB硬件版本', + 'dimensions': '长宽高', + 'bladeWidth': '割刀宽度', + 'speedRatio': '转速比', + 'cellularInfo': '蜂窝网络通信信息', + 'bluetoothName': '蓝牙广播名', + 'hardwareIdentifiers': '硬件标识信息', + 'imuModel': 'IMU型号', + 'tempHumiditySensor': '温湿度传感器', + 'barometerPresent': '气压计在位', + 'ultrasonicPresent': '超声波在位', + 'dacPresent': 'DAC在位', + 'operator': '质检员', + 'inspectionNo': '检测报告号', + 'dealer': '经销商', 'header1': 'Header1', 'header2': 'Header2', 'cmd': 'CMD', @@ -106,56 +123,33 @@ class _RobotParamSettingsPageState extends State void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); - _initControllers(); _loadData(); } - void _initControllers() { - _runSpeedController = TextEditingController(); - _leftForwardGainController = TextEditingController(); - _leftBackwardGainController = TextEditingController(); - _rightForwardGainController = TextEditingController(); - _rightBackwardGainController = TextEditingController(); - } - + /// 根据接口返回的全部字段动态创建输入控制器(跳过 _skipKeys),全部可编辑 void _fillControllers(DeviceRunParamEntity param) { - _runSpeedController.text = param.runSpeed.toStringAsFixed(0); - _leftForwardGainController.text = param.leftForwardGain.toString(); - _leftBackwardGainController.text = param.leftBackwardGain.toString(); - _rightForwardGainController.text = param.rightForwardGain.toString(); - _rightBackwardGainController.text = param.rightBackwardGain.toString(); - } - - TextEditingController? _controllerForKey(String key) { - switch (key) { - case 'runSpeed': return _runSpeedController; - case 'leftForwardGain': return _leftForwardGainController; - case 'leftBackwardGain': return _leftBackwardGainController; - case 'rightForwardGain': return _rightForwardGainController; - case 'rightBackwardGain': return _rightBackwardGainController; - default: return null; + for (final c in _controllers.values) { + c.dispose(); + } + _controllers.clear(); + for (final entry in param.rawData.entries) { + if (_skipKeys.contains(entry.key)) continue; + _controllers[entry.key] = TextEditingController( + text: entry.value?.toString() ?? '', + ); } } - void _showReadonlyTip() { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('暂不支持修改'), - duration: Duration(seconds: 1), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only(bottom: 80, left: 80, right: 80), - ), - ); - } + /// 数值字段用数字键盘,其余用文本键盘(依据接口返回值的原始类型判断) + bool _isNumericField(String key) => _param?.rawData[key] is num; @override void dispose() { _tabController.dispose(); - _runSpeedController.dispose(); - _leftForwardGainController.dispose(); - _leftBackwardGainController.dispose(); - _rightForwardGainController.dispose(); - _rightBackwardGainController.dispose(); + for (final c in _controllers.values) { + c.dispose(); + } + _controllers.clear(); super.dispose(); } @@ -193,7 +187,9 @@ class _RobotParamSettingsPageState extends State setState(() => _isSaving = true); try { - // 🔐 前置权限校验:只有 code=200 && data=true 才允许保存 + // ⛔ 业务变更:设备参数保存不再校验权限,保留代码备查 + // (原逻辑:先调 GET /iot/device/hasPermission,通过后再弹"权限通过"确认框) + /* final permissionService = GetIt.I(); final hasPermission = await permissionService.checkPermission(_deviceId); @@ -204,7 +200,6 @@ class _RobotParamSettingsPageState extends State return; } - // 权限通过提示 await showDialog( context: context, barrierDismissible: false, @@ -226,30 +221,32 @@ class _RobotParamSettingsPageState extends State ), ); if (!mounted) return; + */ final siteId = GetIt.I().state.selectedSite?.id ?? 0; final orgId = GetIt.I().state.user?.orgId ?? 0; final useCase = GetIt.I(); - final params = { - 'id': _param!.id, - 'deviceId': _deviceId, - 'siteId': siteId, - 'orgId': orgId, - 'runSpeed': int.tryParse(_runSpeedController.text) ?? _param!.runSpeed, - 'leftForwardGain': - double.tryParse(_leftForwardGainController.text) ?? - _param!.leftForwardGain, - 'leftBackwardGain': - double.tryParse(_leftBackwardGainController.text) ?? - _param!.leftBackwardGain, - 'rightForwardGain': - double.tryParse(_rightForwardGainController.text) ?? - _param!.rightForwardGain, - 'rightBackwardGain': - double.tryParse(_rightBackwardGainController.text) ?? - _param!.rightBackwardGain, - }; + // 以接口原始数据为底,叠加各输入框编辑后的值(数值字段按原类型回转) + final params = {..._param!.rawData}; + for (final entry in _controllers.entries) { + final original = _param!.rawData[entry.key]; + final text = entry.value.text.trim(); + if (original is int) { + params[entry.key] = int.tryParse(text) ?? original; + } else if (original is double) { + params[entry.key] = double.tryParse(text) ?? original; + } else if (original is num) { + params[entry.key] = num.tryParse(text) ?? original; + } else { + params[entry.key] = text; + } + } + // 关键字段兜底:接口未返回时用当前上下文补齐 + params['id'] ??= _param!.id; + params['deviceId'] ??= _deviceId; + params['siteId'] ??= siteId; + params['orgId'] ??= orgId; final result = await useCase.execute(params); @@ -411,14 +408,11 @@ class _RobotParamSettingsPageState extends State child: Column( children: List.generate(keys.length, (i) { final key = keys[i]; - final label = _fieldLabels[key] ?? key; - final isEditable = _editableKeys.contains(key); final isLast = i == keys.length - 1; - final value = raw[key]; return Column( children: [ - _buildFieldRow(key, label, value, isEditable), + _buildFieldRow(key), if (!isLast) _buildDivider(), ], ); @@ -446,65 +440,54 @@ class _RobotParamSettingsPageState extends State } // ============ 单行字段 ============ - Widget _buildFieldRow( - String key, - String label, - dynamic value, - bool editable, - ) { - final controller = editable ? _controllerForKey(key) : null; - final displayValue = value?.toString() ?? '-'; + Widget _buildFieldRow(String key) { + final controller = _controllers[key]; + final isNumeric = _isNumericField(key); return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 90, - child: Text( - label, - style: const TextStyle( - fontSize: 14, - color: Color(0xFF4E5969), - fontWeight: FontWeight.w500, - ), + width: 130, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 主行:始终显示字段名(英文 key),避免中英混排 + Text( + key, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF4E5969), + fontWeight: FontWeight.w500, + ), + ), + // 副行:有中文映射才显示(小字灰色),没有则不显示 + if (_fieldLabels[key] != null) + Text( + _fieldLabels[key]!, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF86909C), + ), + ), + ], ), ), const SizedBox(width: 8), Expanded( - child: editable - ? TextField( - controller: controller, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[\d.]')), - ], - style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)), - decoration: _inputDecoration(), - ) - : GestureDetector( - onTap: _showReadonlyTip, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - decoration: BoxDecoration( - color: const Color(0xFFF2F3F5), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFFE5E6EB)), - ), - child: Text( - displayValue, - style: const TextStyle( - fontSize: 14, - color: Color(0xFF86909C), - ), - ), - ), - ), + child: TextField( + controller: controller, + keyboardType: isNumeric + ? const TextInputType.numberWithOptions(decimal: true) + : TextInputType.text, + inputFormatters: isNumeric + ? [FilteringTextInputFormatter.allow(RegExp(r'[\d.\-]'))] + : null, + style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)), + decoration: _inputDecoration(), + ), ), ], ), diff --git a/lib/features/v2/usb_camera/presentation/cubit/usb_camera_cubit.dart b/lib/features/v2/usb_camera/presentation/cubit/usb_camera_cubit.dart index 83c64dd1..c169fed0 100644 --- a/lib/features/v2/usb_camera/presentation/cubit/usb_camera_cubit.dart +++ b/lib/features/v2/usb_camera/presentation/cubit/usb_camera_cubit.dart @@ -39,6 +39,17 @@ class UsbCameraCubit extends Cubit { Future start() async { _safeEmit(state.copyWith(phase: UsbCameraPhase.scanning)); try { + // 平台/机型能力探测:iPhone 系统层面不开放外接摄像头输入,直接给专属提示, + // 避免枚举到空列表后显示含糊的「未检测到 USB 摄像头」。 + final supported = await plugin.UsbCameraAndroid.isSupported(); + if (!supported) { + _safeEmit(state.copyWith( + phase: UsbCameraPhase.openFailed, + message: 'iPhone 不支持 USB 摄像头输入\n请改用 iPad(USB-C 接口,iPadOS 17 及以上)连接采集卡', + )); + return; + } + final devices = await _service.enumerateDevices(); if (devices.isEmpty) { diff --git a/packages/usb_camera_android/android/src/main/java/com/maibu/usb_camera_android/UsbCameraAndroidPlugin.java b/packages/usb_camera_android/android/src/main/java/com/maibu/usb_camera_android/UsbCameraAndroidPlugin.java index 5a7c2894..bef8dd9e 100644 --- a/packages/usb_camera_android/android/src/main/java/com/maibu/usb_camera_android/UsbCameraAndroidPlugin.java +++ b/packages/usb_camera_android/android/src/main/java/com/maibu/usb_camera_android/UsbCameraAndroidPlugin.java @@ -39,6 +39,9 @@ public class UsbCameraAndroidPlugin implements FlutterPlugin, MethodChannel.Meth @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { + case "isSupported": + result.success(true); + break; case "getDeviceList": result.success(engine.getDeviceList()); break; diff --git a/packages/usb_camera_android/lib/src/usb_camera_android.dart b/packages/usb_camera_android/lib/src/usb_camera_android.dart index 02fefd9d..c2f0cdb7 100644 --- a/packages/usb_camera_android/lib/src/usb_camera_android.dart +++ b/packages/usb_camera_android/lib/src/usb_camera_android.dart @@ -46,6 +46,19 @@ class UsbCameraAndroid { return _eventStream!; } + /// 当前平台/设备是否支持 USB(UVC)摄像头。 + /// - Android:始终 true。 + /// - iOS:仅 iPadOS 17+ 的 iPad 为 true;iPhone 或 iOS<17 为 false。 + /// 通道未注册(异常)时保守返回 true,交由上层继续走原有枚举/错误流程。 + static Future isSupported() async { + try { + final result = await _methodChannel.invokeMethod('isSupported'); + return result as bool? ?? true; + } catch (_) { + return true; + } + } + /// 枚举当前连接的 UVC 设备。 static Future> getDeviceList() async { final List result = diff --git a/packages/usb_camera_android/lib/src/usb_camera_preview_view.dart b/packages/usb_camera_android/lib/src/usb_camera_preview_view.dart index 8d0aa730..9d8842a0 100644 --- a/packages/usb_camera_android/lib/src/usb_camera_preview_view.dart +++ b/packages/usb_camera_android/lib/src/usb_camera_preview_view.dart @@ -1,7 +1,8 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -/// Android 原生 UVC 预览画面。 +/// 原生 UVC 预览画面(Android: AndroidView / iOS: UiKitView)。 class UsbCameraPreviewView extends StatelessWidget { const UsbCameraPreviewView({super.key}); @@ -9,6 +10,13 @@ class UsbCameraPreviewView extends StatelessWidget { @override Widget build(BuildContext context) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return const UiKitView( + viewType: _viewType, + layoutDirection: TextDirection.ltr, + creationParamsCodec: StandardMessageCodec(), + ); + } return const AndroidView( viewType: _viewType, layoutDirection: TextDirection.ltr, diff --git a/packages/usb_camera_android/pubspec.yaml b/packages/usb_camera_android/pubspec.yaml index 3f69ec88..36e97ac6 100644 --- a/packages/usb_camera_android/pubspec.yaml +++ b/packages/usb_camera_android/pubspec.yaml @@ -13,6 +13,8 @@ flutter: android: package: com.maibu.usb_camera_android pluginClass: UsbCameraAndroidPlugin + ios: + pluginClass: UsbCameraAndroidPlugin dependencies: flutter: diff --git a/packages/video_player_android/README.md b/packages/video_player_android/README.md new file mode 100644 index 00000000..8bffc986 --- /dev/null +++ b/packages/video_player_android/README.md @@ -0,0 +1,20 @@ +# video\_player\_android + +The Android implementation of [`video_player`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `video_player` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +## Known issues + +Using `VideoViewType.platformView` is not currently recommended on Android due to a known [issue][3] affecting platform views on Android. + +[1]: https://pub.dev/packages/video_player +[2]: https://flutter.dev/to/endorsed-federated-plugin +[3]: https://github.com/flutter/flutter/issues/164899 diff --git a/packages/video_player_android/example/README.md b/packages/video_player_android/example/README.md new file mode 100644 index 00000000..96b8bb17 --- /dev/null +++ b/packages/video_player_android/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant.