安卓权限添加
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||
<application
|
||||
android:label="maibu_satabot_v2"
|
||||
android:name="${applicationName}"
|
||||
@@ -35,6 +36,17 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<!--
|
||||
USB 设备插入时,Android 直接弹出"允许 [App名] 访问此USB设备"权限对话框,
|
||||
而不再弹出"用哪个APP打开"的选择对话框(避免与其他 UVC 应用竞争)。
|
||||
用户勾选"总是允许"后,以后插入同类型设备将不再弹窗。
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/device_filter" />
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
|
||||
@@ -231,8 +231,8 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.target_speed') +
|
||||
"\n(rpm)",
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(status.leftTargetSpeed.toStringAsFixed(0)),
|
||||
_buildTableCell(status.rightTargetSpeed.toStringAsFixed(0)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -243,8 +243,8 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.measure_speed') +
|
||||
"\n(rpm)",
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(status.leftMeasureSpeed.toStringAsFixed(0)),
|
||||
_buildTableCell(status.rightMeasureSpeed.toStringAsFixed(0)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -255,8 +255,8 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.current') +
|
||||
"(A)",
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(status.leftCurrent.toStringAsFixed(2)),
|
||||
_buildTableCell(status.rightCurrent.toStringAsFixed(2)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -267,8 +267,8 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.motor_temp') +
|
||||
"(°C)",
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(status.leftMotorTemp.toStringAsFixed(1)),
|
||||
_buildTableCell(status.rightMotorTemp.toStringAsFixed(1)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -318,7 +318,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.pitch_angle') +
|
||||
"(°)",
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(status.pitch.toStringAsFixed(2)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -329,7 +329,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.roll_angle') +
|
||||
"(°)",
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(status.roll.toStringAsFixed(2)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -351,7 +351,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.battery') +
|
||||
"(%)",
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(status.battery),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -362,7 +362,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.chip_temp') +
|
||||
"(°C)",
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(status.chipTemp.toStringAsFixed(1)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -383,7 +383,11 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
context,
|
||||
).translate('running_status.control_mode'),
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell(
|
||||
AppLocalizations.of(context).translate(
|
||||
_getControlModeKey(int.tryParse(status.controlMode) ?? 0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
|
||||
@@ -39,6 +39,8 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
// 🔥 MQTT机器状态订阅(车辆实时+定位,替代TCP 0x02)
|
||||
StreamSubscription? _mqttVehicleSubscription;
|
||||
StreamSubscription? _mqttLocationSubscription;
|
||||
// 🔥 主机实时消息订阅(device/{sn}/host/realTime/message,新增,纯附加)
|
||||
StreamSubscription? _mqttHostSubscription;
|
||||
|
||||
// 🔥 节流相关:500ms节流控制0x02数据推送频率
|
||||
Timer? _throttleTimer;
|
||||
@@ -301,6 +303,48 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
}
|
||||
});
|
||||
|
||||
// 🔥 主机实时消息(device/{sn}/host/realTime/message):新值到达即 copyWith 覆盖,复用 500ms 节流 emit
|
||||
_mqttHostSubscription ??= _mowerRealtimeRepo.hostRealtimeStream.listen((
|
||||
host,
|
||||
) {
|
||||
final base = _cachedStatus ?? RunningStatusEntity();
|
||||
_cachedStatus = base.copyWith(
|
||||
voltage: host.batteryVoltage,
|
||||
battery: host.batteryPercentage.toString(),
|
||||
leftCurrent: host.leftMotorCurrent,
|
||||
rightCurrent: host.rightMotorCurrent,
|
||||
leftMotorTemp: host.leftMotorTemperature,
|
||||
rightMotorTemp: host.rightMotorTemperature,
|
||||
chipTemp: host.mainTemperature,
|
||||
pitch: host.pitch,
|
||||
roll: host.roll,
|
||||
leftTargetSpeed: host.leftTargetRpm,
|
||||
rightTargetSpeed: host.rightTargetRpm,
|
||||
leftMeasureSpeed: host.leftMeasuredRpm,
|
||||
rightMeasureSpeed: host.rightMeasuredRpm,
|
||||
controlMode:
|
||||
host.controlMode.isNotEmpty ? host.controlMode : base.controlMode,
|
||||
workingArea: host.totalWorkArea.toString(),
|
||||
// 🔥 供全功能模式(pv_control)展示:累计作业里程 + host 已推数据标记
|
||||
totalWorkDistance: host.totalWorkDistance,
|
||||
hostDataReceived: true,
|
||||
);
|
||||
|
||||
// 🔥 host 无坐标:GPS 缺省时用当前经纬度占位,保证能即时刷新(不覆盖 location 已写入的真坐标)
|
||||
_cachedGps ??= GPSEntity(_cachedStatus!.latitude, _cachedStatus!.longitude);
|
||||
|
||||
debugPrint(
|
||||
'${LogTime.now()} 🛰️ [MQTT-主机实时] 已合并 → 电量=${host.batteryPercentage}% 电压=${host.batteryVoltage}V 控制模式=${host.controlMode}',
|
||||
);
|
||||
|
||||
// 500ms 节流:与 location/车辆实时 发射链路一致
|
||||
if (_throttleTimer == null || !_throttleTimer!.isActive) {
|
||||
_throttleTimer = Timer(_throttleDuration, () {
|
||||
_emitCachedStatus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint('${LogTime.now()} ✅ [DeviceStatusBloc] MQTT机器状态监听已启动: $deviceSn');
|
||||
_logger.log('✅ [DeviceStatusBloc] MQTT机器状态监听已启动: $deviceSn');
|
||||
}
|
||||
@@ -434,10 +478,13 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
}
|
||||
|
||||
// 🔥 节流发射:500ms到期后发射缓存的最新数据
|
||||
// 🔥 放宽:允许仅有机器状态(host 先到、GPS 未到)时也能刷新 UI
|
||||
void _emitCachedStatus() {
|
||||
if (_cachedStatus != null && _cachedGps != null && !isClosed) {
|
||||
debugPrint('📤 [节流发射] 电压=${_cachedStatus!.voltage}V 电量=${_cachedStatus!.battery}% 经纬度=(${_cachedGps!.latitude}, ${_cachedGps!.longitude}) yaw=${_cachedStatus!.yaw} | ${DateTime.now().toString().substring(11, 19)}');
|
||||
emit(DeviceStatusUpdated(_cachedStatus!, _cachedGps!));
|
||||
if (_cachedStatus != null && !isClosed) {
|
||||
final gps = _cachedGps ??
|
||||
GPSEntity(_cachedStatus!.latitude, _cachedStatus!.longitude);
|
||||
debugPrint('📤 [节流发射] 电压=${_cachedStatus!.voltage}V 电量=${_cachedStatus!.battery}% 经纬度=(${gps.latitude}, ${gps.longitude}) yaw=${_cachedStatus!.yaw} 控制模式=${_cachedStatus!.controlMode} | ${DateTime.now().toString().substring(11, 19)}');
|
||||
emit(DeviceStatusUpdated(_cachedStatus!, gps));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,6 +616,8 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
_mqttVehicleSubscription = null;
|
||||
_mqttLocationSubscription?.cancel();
|
||||
_mqttLocationSubscription = null;
|
||||
_mqttHostSubscription?.cancel();
|
||||
_mqttHostSubscription = null;
|
||||
|
||||
// 🔥 清理节流timer和缓存
|
||||
_throttleTimer?.cancel();
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
/// 差速转向算法:摇杆坐标 -> 线速度/角速度(物理量)
|
||||
///
|
||||
/// 协议变更(2026-10):0x00 指令不再下发左右轮速(int16),
|
||||
/// 改为下发线速度(m/s)、角速度(rad/s) 的 double 值,
|
||||
/// 速度到轮速的运动学换算改由机器端完成。
|
||||
class DiffSteerUseCase {
|
||||
static const int polarityHigh = 0;
|
||||
static const int polarityLow = 1;
|
||||
/// 线速度满量程(m/s),以后按车型配置
|
||||
static const double maxLinearSpeed = 1.0;
|
||||
|
||||
double _turnSpeedScale = 1500 / 100;
|
||||
double _forwardSpeedScale = 3000 / 100;
|
||||
int _speedAmplLimit = 3000;
|
||||
/// 角速度满量程(rad/s),以后按车型配置
|
||||
static const double maxAngularSpeed = 2.0;
|
||||
|
||||
int _leftWheelDir = polarityHigh;
|
||||
int _rightWheelDir = polarityHigh;
|
||||
int _xDir = polarityHigh;
|
||||
int _yDir = polarityHigh;
|
||||
/// 摇杆满格值(CCJoystick 输出范围 ±100)
|
||||
static const int stickFullScale = 100;
|
||||
|
||||
Map<String, int> calculate(int x, int y) {
|
||||
int turnSpeed = _signOperator((x * _turnSpeedScale).toInt(), _xDir);
|
||||
int forwardSpeed = _signOperator((y * _forwardSpeedScale).toInt(), _yDir);
|
||||
/// 摇杆坐标 (x, y) -> {'linear': 线速度 m/s, 'angular': 角速度 rad/s}
|
||||
Map<String, double> calculate(int x, int y) {
|
||||
// 摇杆原始值先归一化为 0~1 比例,再乘满量程物理量
|
||||
double linear = (y / stickFullScale) * maxLinearSpeed;
|
||||
double angular = (x / stickFullScale) * maxAngularSpeed;
|
||||
|
||||
// 倒车补偿
|
||||
int adjustedTurn = (forwardSpeed >= 0 ? turnSpeed : -turnSpeed);
|
||||
// 倒车补偿:倒车时转向反向,保持与旧轮速版一致的操作手感
|
||||
// (若联调确认机器端已做该补偿,删除本行)
|
||||
if (linear < 0) angular = -angular;
|
||||
|
||||
int left = _signOperator(forwardSpeed + adjustedTurn, _leftWheelDir);
|
||||
int right = _signOperator(forwardSpeed - adjustedTurn, _rightWheelDir);
|
||||
|
||||
return {
|
||||
'left': left.clamp(-_speedAmplLimit, _speedAmplLimit),
|
||||
'right': right.clamp(-_speedAmplLimit, _speedAmplLimit),
|
||||
};
|
||||
return {'linear': linear, 'angular': angular};
|
||||
}
|
||||
|
||||
int _signOperator(int num, int polarity) =>
|
||||
polarity != polarityHigh ? -num : num;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -118,10 +118,11 @@ class CenterControlArea extends StatelessWidget {
|
||||
|
||||
// 🔥 门槛检查:与摇杆一致,未收到 TCP 0x02 推送时禁止操作并提示。
|
||||
// 胶囊 Toast 全局去重、自动消失,适配 StatelessWidget 无实例状态。
|
||||
if (!context.read<RemoteControlCubit>().isStatusPushActive()) {
|
||||
CapsuleToast.show('暂未收到该机器的推送,不能控制', showCheck: false);
|
||||
return;
|
||||
}
|
||||
// 🔥 临时关闭,便于机器未开时用 Mock 假数据联调倒车影像;联调后取消注释恢复,勿删。
|
||||
// if (!context.read<RemoteControlCubit>().isStatusPushActive()) {
|
||||
// CapsuleToast.show('暂未收到该机器的推送,不能控制', showCheck: false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// 🔥 使用deviceKey进行判断,不依赖显示文本
|
||||
switch (deviceKey) {
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 主屏“行驶辅助线”覆盖层:中央绿色行进箭头 + 两侧汇聚车道线(外黄内绿)。
|
||||
///
|
||||
/// 从 pv 全功能 HUD 的假 AR 导向层抽离而来,可独立复用:
|
||||
/// - 自带流动动画(绿色箭头由底部向远处“递进流动”,无需外部驱动);
|
||||
/// - 脊柱以画面底部正中为固定顶点,随 [steering](度,右正左负,-45~45)
|
||||
/// 做等曲率圆弧弯曲;[steering]=0 时为笔直隧道;
|
||||
/// - 采样用连续插值,静止即严格直线(杜绝整型下标量化导致的折线抖动)。
|
||||
///
|
||||
/// 纯装饰层,调用方用 `IgnorePointer` 包一层即可,不影响手势。
|
||||
class DrivingGuideOverlay extends StatefulWidget {
|
||||
const DrivingGuideOverlay({super.key, this.steering = 0});
|
||||
|
||||
/// 转向角(度):驱动整条脊柱左右弯曲。
|
||||
final double steering;
|
||||
|
||||
@override
|
||||
State<DrivingGuideOverlay> createState() => _DrivingGuideOverlayState();
|
||||
}
|
||||
|
||||
class _DrivingGuideOverlayState extends State<DrivingGuideOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _flow;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_flow = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_flow.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RepaintBoundary(
|
||||
child: AnimatedBuilder(
|
||||
animation: _flow,
|
||||
builder: (context, _) => CustomPaint(
|
||||
size: Size.infinite,
|
||||
painter: _DrivingGuidePainter(
|
||||
progress: _flow.value,
|
||||
steering: widget.steering,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DrivingGuidePainter extends CustomPainter {
|
||||
_DrivingGuidePainter({required this.progress, required this.steering});
|
||||
|
||||
/// 0..1 循环进度,驱动绿色箭头的流动。
|
||||
final double progress;
|
||||
|
||||
/// 转向角(度,右正左负,-45~45):驱动辅助线左右弯曲。
|
||||
final double steering;
|
||||
|
||||
/// 近端(底部顶点)纵向比例
|
||||
static const double _bottomT = 0.98;
|
||||
|
||||
/// 远端(收束)纵向比例
|
||||
static const double _topT = 0.52;
|
||||
|
||||
/// 满舵时脊柱总转角(弧度):越大弯得越急;嫌弯不够调大、嫌夸张调小。
|
||||
static const double _maxHeading = 0.5;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final w = size.width;
|
||||
final h = size.height;
|
||||
final cx = w / 2;
|
||||
final bottomY = h * _bottomT;
|
||||
final topY = h * _topT;
|
||||
|
||||
// 近零死区:摇杆回中或极小漂移时强制视为直行,杜绝静态抖动
|
||||
final rawTurn = (steering / 45.0).clamp(-1.0, 1.0);
|
||||
final turn = rawTurn.abs() < 0.02 ? 0.0 : rawTurn;
|
||||
final totalHeading = turn * _maxHeading;
|
||||
|
||||
// 航向积分采样脊柱中心线:底部正中出发、初始竖直向上
|
||||
const steps = 36;
|
||||
final ds = (bottomY - topY) / steps;
|
||||
final pts = <Offset>[];
|
||||
final heads = <double>[];
|
||||
double x = cx, y = bottomY, th = 0;
|
||||
for (var i = 0; i <= steps; i++) {
|
||||
pts.add(Offset(x, y));
|
||||
heads.add(th);
|
||||
x += math.sin(th) * ds;
|
||||
y -= math.cos(th) * ds;
|
||||
th += totalHeading / steps;
|
||||
}
|
||||
|
||||
// 连续插值取脊柱点/切线角:避免整型下标量化导致的“折线抖动”
|
||||
Offset spineAt(double f) {
|
||||
final pos = (f * steps).clamp(0.0, steps.toDouble());
|
||||
final i = pos.floor().clamp(0, steps - 1);
|
||||
return Offset.lerp(pts[i], pts[i + 1], pos - i)!;
|
||||
}
|
||||
|
||||
double headAt(double f) {
|
||||
final pos = (f * steps).clamp(0.0, steps.toDouble());
|
||||
final i = pos.floor().clamp(0, steps - 1);
|
||||
return heads[i] + (heads[i + 1] - heads[i]) * (pos - i);
|
||||
}
|
||||
|
||||
// 沿脊柱法线偏移半宽得到一条车道线(root/far 为相对 w 的半宽比例,近宽远窄汇聚)
|
||||
void lane(
|
||||
double rootHalf,
|
||||
double farHalf,
|
||||
Color color,
|
||||
double widthPx,
|
||||
double sign,
|
||||
) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..strokeWidth = widthPx
|
||||
..strokeCap = StrokeCap.round
|
||||
..style = PaintingStyle.stroke;
|
||||
final path = Path();
|
||||
const seg = 40;
|
||||
for (var s = 0; s <= seg; s++) {
|
||||
final f = s / seg;
|
||||
final half = (rootHalf + (farHalf - rootHalf) * f) * w;
|
||||
final n = Offset(math.cos(headAt(f)), math.sin(headAt(f)));
|
||||
final pt = spineAt(f) + n * (half * sign);
|
||||
if (s == 0) {
|
||||
path.moveTo(pt.dx, pt.dy);
|
||||
} else {
|
||||
path.lineTo(pt.dx, pt.dy);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
|
||||
// 外黄内绿两侧车道线
|
||||
lane(0.14, 0.05, const Color(0xCCFFC24D), 4, -1);
|
||||
lane(0.14, 0.05, const Color(0xCCFFC24D), 4, 1);
|
||||
lane(0.08, 0.02, const Color(0xAA22E0A0), 3, -1);
|
||||
lane(0.08, 0.02, const Color(0xAA22E0A0), 3, 1);
|
||||
|
||||
// 中央绿色导向箭头:沿弯曲脊柱放置并随切线旋转,虚实渐隐
|
||||
const n = 4;
|
||||
for (var i = 0; i < n; i++) {
|
||||
final phase = (progress + i / n) % 1.0; // 0=近/底 → 1=远/顶
|
||||
final ay = h * (0.90 - phase * 0.34);
|
||||
final f = ((bottomY - ay) / (bottomY - topY)).clamp(0.0, 1.0).toDouble();
|
||||
final half = w * (0.105 - phase * 0.05);
|
||||
final drop = half * 0.72;
|
||||
final env = math.sin(phase * math.pi);
|
||||
final arrow = Paint()
|
||||
..color = const Color(
|
||||
0xFF22E0A0,
|
||||
).withValues(alpha: (0.12 + 0.8 * env).clamp(0.0, 0.95).toDouble())
|
||||
..strokeWidth = 2.0 + 6.0 * env
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..style = PaintingStyle.stroke;
|
||||
final c = spineAt(f);
|
||||
canvas.save();
|
||||
canvas.translate(c.dx, c.dy);
|
||||
canvas.rotate(headAt(f));
|
||||
final path = Path()
|
||||
..moveTo(-half, drop)
|
||||
..lineTo(0, 0)
|
||||
..lineTo(half, drop);
|
||||
canvas.drawPath(path, arrow);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _DrivingGuidePainter oldDelegate) =>
|
||||
oldDelegate.progress != progress || oldDelegate.steering != steering;
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import '../../../../../core/network/mqtt/data/datasources/drone_osd_datasource.d
|
||||
import 'qr_scanner_page.dart';
|
||||
import 'bind_device_page.dart';
|
||||
import '../../../local_camera/presentation/pages/local_camera_page.dart';
|
||||
import '../../../usb_camera/presentation/pages/usb_camera_page.dart';
|
||||
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
import '../../../../devices/domain/entities/device_entity.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
@@ -1153,6 +1154,20 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
);
|
||||
},
|
||||
),
|
||||
Divider(height: 1, color: ctx.appColors.divider),
|
||||
_buildMenuOption(
|
||||
ctx,
|
||||
icon: Icons.usb,
|
||||
label: 'USB 摄像头',
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const UsbCameraPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -14,6 +14,11 @@ class CCExpandSlider extends StatefulWidget {
|
||||
final String svgCenter;
|
||||
final String svgEnd;
|
||||
final double iconSize;
|
||||
|
||||
/// 图标额外旋转的 90° 段数。有些素材是按「横向滑块」预先旋过 90° 的
|
||||
/// (如 remote_flame*,外层配合 RotatedBox 使用),竖向摆放时需要传 1 转回正。
|
||||
/// 默认 0,不影响既有横 / 竖两种用法。
|
||||
final int iconQuarterTurns;
|
||||
final double borderRadius;
|
||||
final VoidCallback? onStart;
|
||||
final VoidCallback? onCenter;
|
||||
@@ -28,6 +33,7 @@ class CCExpandSlider extends StatefulWidget {
|
||||
required this.svgEnd,
|
||||
required this.label,
|
||||
this.iconSize = 22.0,
|
||||
this.iconQuarterTurns = 0,
|
||||
this.borderRadius = 12.0,
|
||||
this.onStart,
|
||||
this.onCenter,
|
||||
@@ -292,12 +298,15 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
||||
} else {
|
||||
color = Colors.white;
|
||||
}
|
||||
return SvgPicture.asset(
|
||||
assetPath,
|
||||
width: widget.iconSize,
|
||||
height: widget.iconSize,
|
||||
fit: BoxFit.contain,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
return RotatedBox(
|
||||
quarterTurns: widget.iconQuarterTurns,
|
||||
child: SvgPicture.asset(
|
||||
assetPath,
|
||||
width: widget.iconSize,
|
||||
height: widget.iconSize,
|
||||
fit: BoxFit.contain,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.maibu.usb_camera_android">
|
||||
<application>
|
||||
<!--
|
||||
USB 权限结果广播接收器(静态注册)。
|
||||
Android 14+ 要求 FLAG_MUTABLE 的 PendingIntent 必须搭配显式 Intent
|
||||
(指定目标 class),因此必须在 manifest 里声明 Receiver。
|
||||
exported=false:只响应本 App 显式广播,禁止其他 App 触发。
|
||||
-->
|
||||
<receiver
|
||||
android:name="com.maibu.usb_camera_android.UsbPermissionReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.maibu.usb_camera_android.USB_PERMISSION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
Reference in New Issue
Block a user