Files
feature-tenant/lib/features/remote_control/presentation/pages/remote_control_page.dart
Songzex 354f7d6290 优化了tcp实时更新对UI线程的压力和对用弹窗的重复弹窗的影响。
更换了路径规划的的经纬度的字段名和取操作的名字更换。
2026-06-08 13:35:36 +08:00

570 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../core/app/app_user_cubit.dart';
import '../../../../core/router/route_paths.dart';
import '../../../devices/domain/entities/device_entity.dart';
import '../../../devices/presentation/bloc/devices_cubit.dart';
import '../bloc/remote_control_cubit.dart';
import '../bloc/remote_control_state.dart';
import '../widgets/center_control_area.dart';
import '../widgets/emergency_overlay.dart';
import '../widgets/left_joystick_area.dart';
import '../widgets/right_joystick_area.dart';
import '../widgets/top_status_bar.dart';
import '../widgets/webrtc/webrtc_local_player.dart';
class RemoteControlPage extends StatefulWidget {
const RemoteControlPage({super.key});
@override
State<RemoteControlPage> createState() => _RemoteControlPageState();
}
class _RemoteControlPageState extends State<RemoteControlPage> {
String _videoStreamUrl = "";
late RemoteControlCubit _cubit;
DevicesCubit? _devicesCubit;
StreamSubscription? _permissionSubscription; // 🔥 权限监听订阅
bool _isShowingPermissionDialog = false; // 🔥 防止弹窗重复显示
bool? _lastPermissionDialogState; // 🔥 记录上次弹窗状态,检测状态变化
bool _isLoadingPermission = true; // 🔥 标记是否正在加载权限状态
@override
void didChangeDependencies() {
super.didChangeDependencies();
_cubit = context.read<RemoteControlCubit>();
_devicesCubit = context.read<DevicesCubit>();
// 🔥 监听权限弹窗状态变化 (只订阅一次)
if (_permissionSubscription == null) {
_permissionSubscription = _cubit.stream.listen((state) {
// 🔥 关键修复:只有当状态更新类型是弹窗更新时才处理弹窗逻辑
// 设备状态更新(500ms)不会触发弹窗显示
if (state.updateType != 'permission_dialog') {
// 如果是设备状态更新,只重置状态记录,不显示弹窗
if (!state.showPermissionRequestDialog) {
_lastPermissionDialogState = false;
}
return; // 跳过设备状态更新
}
// 🔥 只有当状态从 false 变为 true 时才显示弹窗
// 使用三个条件确保不会重复显示
final wasFalseBefore =
_lastPermissionDialogState == null ||
_lastPermissionDialogState == false;
final shouldShow =
state.showPermissionRequestDialog &&
!_isShowingPermissionDialog &&
wasFalseBefore;
if (mounted && shouldShow) {
_lastPermissionDialogState = true; // 记录当前状态
_isShowingPermissionDialog = true;
debugPrint('🔔 [权限弹窗] 检测到 showPermissionRequestDialog=true,准备显示弹窗');
// 使用微任务确保标志位已设置
Future.microtask(() {
_showPermissionDialog(state).then((_) {
debugPrint('🔔 [权限弹窗] 弹窗已关闭,重置标志位');
_isShowingPermissionDialog = false; // 弹窗关闭后重置标志
_lastPermissionDialogState = false; // 重置状态记录
});
});
} else if (!state.showPermissionRequestDialog) {
// 状态变为 false 时,重置记录
_lastPermissionDialogState = false;
}
});
} else {
// 🔥 如果已经订阅,重置状态记录,确保下次能正确检测状态变化
_lastPermissionDialogState = _cubit.state.showPermissionRequestDialog;
}
}
@override
void initState() {
super.initState();
// 1. 锁定横屏
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
// 2. 隐藏状态栏和虚拟按键
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
WidgetsBinding.instance.addPostFrameCallback((_) {
final remoteCubit = context.read<RemoteControlCubit>();
// debugPrint('🔍 [RemoteControl] initState - targetDevice: ${remoteCubit.state.targetDevice}');
// 🔥 检查 targetDevice 是否存在
if (remoteCubit.state.targetDevice == null) {
// debugPrint('❌ [RemoteControl] targetDevice 为空,无法进入远程控制');
return;
}
// 🔥 弹窗显示当前控制的设备信息
_showTargetDeviceDialog(context, remoteCubit.state.targetDevice!);
// 🔥 只在控制循环未启动时才启动
remoteCubit.startControlLoop();
// debugPrint('✅ [RemoteControl] 控制循环已启动');
});
}
@override
void dispose() {
// 释放远程控制权限
_cubit.releasePermission("app");
//print("远程控制要推出啦");
//final deviceState = _devicesCubit?.state;
//if (deviceState?.selectedDevice != null) {
// _cubit.releasePermission("app");
// }
_permissionSubscription?.cancel(); // 🔥 取消订阅
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
_cubit.stopControlLoop();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 监听全局状态(这些通常不随摇杆频繁变化)
final userState = context.watch<AppUserCubit>().state;
final deviceState = context.watch<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
final remoteCubit = context.read<RemoteControlCubit>();
final hasPermission = remoteCubit.state.hasPermission;
///context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
if (currentDevice == null) {
return _buildOfflineScaffold();
}
// 🔥 只在没有权限时自动请求,避免重复调用
if (!hasPermission && _isLoadingPermission) {
// 🔥 只使用 targetDevice
final deviceName = remoteCubit.state.targetDevice?.deviceName;
if (deviceName != null && deviceName.isNotEmpty) {
// debugPrint('🔑 [RemoteControl] 检测到无权限,自动发送权限请求 - deviceName: $deviceName');
setState(() => _isLoadingPermission = false); // 🔥 标记为已请求
remoteCubit.requestControlPermissionS(deviceName, "app").then((_) {
if (mounted) {
setState(() => _isLoadingPermission = false); // 🔥 请求完成后重置
}
});
} else {
// debugPrint('❌ [RemoteControl] 无法发送权限请求 - targetDevice 为空');
// debugPrint(' targetDevice: ${remoteCubit.state.targetDevice}');
setState(() => _isLoadingPermission = false);
}
} else {
// debugPrint('✅ [RemoteControl] 已有权限或已请求过,跳过');
}
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// 在 Stack 的最底层替换:
Positioned.fill(
child: BlocBuilder<RemoteControlCubit, RemoteControlState>(
// 只有当显示隐藏状态改变时才重构,内部的拖拽由组件自身 State 处理,不影响这里
buildWhen: (p, c) =>
p.showLeftPip != c.showLeftPip ||
p.showRightPip != c.showRightPip,
builder: (context, state) {
// 🔥 只从 targetDevice 获取 deviceId
final targetDevice = context
.watch<RemoteControlCubit>()
.state
.targetDevice;
final deviceId = targetDevice?.deviceName;
// debugPrint('🔍 [WebRTC检查] targetDevice: $targetDevice, deviceId: $deviceId');
// debugPrint('🔍 [WebRTC检查] user: ${userState.user != null}, token: ${userState.user?.token != null}');
if (deviceId != null &&
userState.user != null &&
userState.user!.token != null) {
_videoStreamUrl =
"webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
// debugPrint('🎬 [WebRTC] URL构建成功: $_videoStreamUrl');
} else {
_videoStreamUrl = '';
// debugPrint('❌ [WebRTC] URL构建失败 - deviceId: $deviceId, hasUser: ${userState.user != null}, hasToken: ${userState.user?.token != null}');
}
final int originY = context
.watch<RemoteControlCubit>()
.state
.controlEntity
.originY;
// debugPrint("${originY},originY");
return WebRTCLocalPlayer(
// 这里的 URL 拼接根据你的后端规则
// streamUrl: "webrtc://${TCPConsts.TCP_IP}/live/livestream/${currentDevice.deviceName}?token=${userState.user!.token}",
streamUrl: _videoStreamUrl,
showLeftPip: state.showLeftPip, // 从 Cubit 状态中读取
showRightPip: state.showRightPip, // 从 Cubit 状态中读取
isFrontMain:
context
.watch<RemoteControlCubit>()
.state
.controlEntity
.originY >=
0,
);
},
),
),
BlocBuilder<RemoteControlCubit, RemoteControlState>(
buildWhen: (p, c) => p.isEmergency != c.isEmergency,
builder: (context, state) {
return state.isEmergency
? const Positioned.fill(child: EmergencyOverlay())
: const SizedBox.shrink();
},
),
SafeArea(
child: Column(
children: [
// 🔥 用 BlocConsumer 包裹,确保 hasPermission 变化时重建
BlocConsumer<RemoteControlCubit, RemoteControlState>(
listenWhen: (p, c) => p.hasPermission != c.hasPermission,
listener: (context, state) {
debugPrint(
'🔍 [RemoteControlPage] BlocConsumer listener - hasPermission=${state.hasPermission}',
);
},
buildWhen: (p, c) =>
p.hasPermission != c.hasPermission ||
p.isLocked != c.isLocked ||
p.topRightIsExpanded != c.topRightIsExpanded ||
p.obstacleRecognitionFlag != c.obstacleRecognitionFlag ||
p.showLeftPip != c.showLeftPip ||
p.showRightPip != c.showRightPip ||
p.ping != c.ping ||
p.runningStatusModel.voltage !=
c.runningStatusModel.voltage ||
p.runningStatusModel.controlMode !=
c.runningStatusModel.controlMode ||
p.battery != c.battery,
builder: (context, state) {
// 🔥 调试日志:确认 BlocBuilder 接收到的状态值
/* debugPrint(
'🔍 [RemoteControlPage] BlocConsumer builder - hasPermission=${state.hasPermission}',
);*/
return TopStatusBar(remoteState: state);
},
),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final double sideAreaWidth = constraints.maxWidth * 0.25;
final double centerAreaWidth = constraints.maxWidth * 0.5;
// 关键:使用 BlocBuilder 局部刷新摇杆,不要 watch 整个 Page
return BlocBuilder<
RemoteControlCubit,
RemoteControlState
>(
// 只有 isLocked 改变时才重构摇杆区域,摇杆坐标改变由内部处理
buildWhen: (p, c) => p.isLocked != c.isLocked,
builder: (context, remoteState) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// 左摇杆
SizedBox(
width: sideAreaWidth,
child: Align(
alignment: Alignment.bottomCenter,
child: LeftJoystickArea(
isLocked: remoteState.isLocked,
width: sideAreaWidth * 0.45,
),
),
),
// 中间区
SizedBox(
width: centerAreaWidth,
child: Align(
alignment: Alignment.bottomCenter,
child: CenterControlArea(
totalWidth: centerAreaWidth,
),
),
),
// 右摇杆
SizedBox(
width: sideAreaWidth,
child: Align(
alignment: Alignment.bottomCenter,
child: RightJoystickArea(
isLocked: remoteState.isLocked,
width: sideAreaWidth * 0.45,
),
),
),
],
),
);
},
);
},
),
),
],
),
),
],
),
);
}
// 🔥 显示权限请求弹窗 (使用 showDialog 替代 Overlay)
Future<void> _showPermissionDialog(RemoteControlState state) async {
debugPrint('🔔 [权限弹窗] _showPermissionDialog 被调用');
final requestingDeviceId = state.requestingDeviceId ?? '未知设备';
final requestingPlatform = state.requestingPlatform ?? '未知平台';
final dialogContent =
'$requestingPlatform 端正在请求控制权\n\n'
'设备ID: $requestingDeviceId\n\n'
'是否同意释放控制权?';
debugPrint(
'🔔 [权限弹窗] 准备显示弹窗 - requestingPlatform=$requestingPlatform, requestingDeviceId=$requestingDeviceId',
);
showDialog(
context: context,
barrierDismissible: false, // 禁止点击外部关闭
builder: (dialogContext) => AlertDialog(
title: Text(
AppLocalizations.of(
dialogContext,
).translate('remote_control.permission_request_title'),
),
content: Text(dialogContent),
actions: [
TextButton(
onPressed: () async {
// debugPrint('👆 [权限弹窗] 用户点击了拒绝按钮');
final deviceId =
context
.read<DevicesCubit>()
.state
.selectedDevice
?.deviceName ??
"";
final remoteCubit = context.read<RemoteControlCubit>();
// 🔥 添加详细状态日志 - 检查为什么TCP指令发不出去
// debugPrint('👆 [权限弹窗] ====================状态检查====================');
// debugPrint('👆 [权限弹窗] showPermissionRequestDialog: ${remoteCubit.state.showPermissionRequestDialog}');
// debugPrint('👆 [权限弹窗] targetDevice: ${remoteCubit.state.targetDevice}');
// debugPrint('👆 [权限弹窗] requestingDeviceId: ${remoteCubit.state.requestingDeviceId}');
// debugPrint('👆 [权限弹窗] hasPermission: ${remoteCubit.state.hasPermission}');
// debugPrint('👆 [权限弹窗] ==============================================');
/* debugPrint(
'📋 [权限弹窗] targetDevice=${remoteCubit.state.targetDevice}',
);*/
// 🔥 先发送 TCP 响应,等完成后再关闭弹窗
// debugPrint('🔑 [权限弹窗] 用户拒绝,调用 confirmPermissionResponse');
final targetDevice = remoteCubit.state.targetDevice;
if (targetDevice != null) {
debugPrint(
'✅ [权限弹窗] targetDevice 不为空,开始调用 confirmPermissionResponse',
);
await remoteCubit.confirmPermissionResponse(
targetDevice.deviceName,
'app',
false,
);
// debugPrint('✅ [权限弹窗] TCP响应已发送,准备关闭弹窗');
} else {
debugPrint(
'❌ [权限弹窗] targetDevice 为空,无法调用 confirmPermissionResponse',
);
}
// 🔥 关键修复:先重置弹窗标志位,再关闭弹窗
// 避免 Navigator.pop() 后上下文失效导致后续代码不执行
context.read<RemoteControlCubit>().resetPermissionCoolDown();
context.read<RemoteControlCubit>().emit(
context.read<RemoteControlCubit>().state.copyWith(
showPermissionRequestDialog: false,
),
);
// 🔥 关闭弹窗(在状态重置之后)
Navigator.pop(dialogContext);
debugPrint('🔔 [权限弹窗] 弹窗已关闭,重置标志位');
},
child: Text(
AppLocalizations.of(
dialogContext,
).translate('remote_control.refuse'),
),
),
TextButton(
onPressed: () async {
debugPrint('👆 [权限弹窗] 用户点击了同意按钮');
final deviceId =
context
.read<DevicesCubit>()
.state
.selectedDevice
?.deviceName ??
"";
final remoteCubit = context.read<RemoteControlCubit>();
// 🔥 添加详细状态日志 - 检查为什么TCP指令发不出去
/* debugPrint(
'👆 [权限弹窗] ====================状态检查====================',
);
debugPrint(
'👆 [权限弹窗] showPermissionRequestDialog: ${remoteCubit.state.showPermissionRequestDialog}',
);
debugPrint(
'👆 [权限弹窗] targetDevice: ${remoteCubit.state.targetDevice}',
);
debugPrint(
'👆 [权限弹窗] requestingDeviceId: ${remoteCubit.state.requestingDeviceId}',
);
debugPrint(
'👆 [权限弹窗] hasPermission: ${remoteCubit.state.hasPermission}',
);
debugPrint(
'👆 [权限弹窗] ==============================================',
);
debugPrint(
'📋 [权限弹窗] targetDevice=${remoteCubit.state.targetDevice}',
);*/
// 🔥 先发送 TCP 响应,等完成后再关闭弹窗
debugPrint('🔑 [权限弹窗] 用户同意,调用 confirmPermissionResponse');
final targetDevice = remoteCubit.state.targetDevice;
if (targetDevice != null) {
debugPrint('✅ [权限弹窗] targetDevice 不为空');
await remoteCubit.confirmPermissionResponse(
targetDevice.deviceName,
'app',
true,
);
debugPrint('✅ [权限弹窗] TCP响应已发送,准备关闭弹窗');
} else {
debugPrint(
'❌ [权限弹窗] targetDevice 为空,无法调用 confirmPermissionResponse',
);
}
// 🔥 关键修复:先重置弹窗标志位,再关闭弹窗
// 避免 Navigator.pop() 后上下文失效导致后续代码不执行
context.read<RemoteControlCubit>().resetPermissionCoolDown();
context.read<RemoteControlCubit>().emit(
context.read<RemoteControlCubit>().state.copyWith(
showPermissionRequestDialog: false,
),
);
// 🔥 关闭弹窗(在状态重置之后)
Navigator.pop(dialogContext);
debugPrint('🔔 [权限弹窗] 弹窗已关闭,重置标志位');
},
child: Text(
AppLocalizations.of(
dialogContext,
).translate('remote_control.agree'),
),
),
],
),
);
}
Widget _buildOfflineScaffold() {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.signal_wifi_off, color: Colors.white, size: 60),
const SizedBox(height: 20),
Text(
AppLocalizations.of(
context,
).translate('remote_control.device_disconnected'),
style: const TextStyle(color: Colors.white, fontSize: 18),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.pop(),
child: Text(
AppLocalizations.of(context).translate('remote_control.back'),
),
),
],
),
),
);
}
/// 🔥 显示当前控制设备的弹窗
void _showTargetDeviceDialog(BuildContext context, DeviceEntity device) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => AlertDialog(
title: const Text('🎮 远程控制'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备名称: ${device.deviceName}',
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 8),
Text(
'设备ID: ${device.deviceName}',
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('确定'),
),
],
),
);
}
}