541 lines
22 KiB
Dart
541 lines
22 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:get_it/get_it.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:maibu_satabot_v2/components/capsule_toast.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 '../../../auth/presentation/bloc/auth_cubit.dart'; // 🔥 导入 AuthCubit
|
||
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();
|
||
// 🔥 进入安全模式:防止异地登录推送导致控制中断
|
||
GetIt.I<AuthCubit>().enterSafeMode();
|
||
|
||
// 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;
|
||
}
|
||
|
||
// 🔥 只在控制循环未启动时才启动
|
||
remoteCubit.startControlLoop();
|
||
// debugPrint('✅ [RemoteControl] 控制循环已启动');
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
// 🔥 离开安全模式:启动 90 秒倒计时
|
||
GetIt.I<AuthCubit>().exitSafeMode();
|
||
|
||
// 释放远程控制权限(type=2:退出远程遥控页面释放)
|
||
final deviceId = _cubit.state.targetDevice?.deviceName;
|
||
if (deviceId != null && deviceId.isNotEmpty) {
|
||
_cubit.releasePermission(deviceId: deviceId, type: 2).then((success) {
|
||
CapsuleToast.show(
|
||
success ? '已释放远程控制权限' : '控制权限释放失败',
|
||
showCheck: success,
|
||
);
|
||
});
|
||
}
|
||
|
||
_permissionSubscription?.cancel(); // 🔥 取消订阅
|
||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||
_cubit.stopControlLoop();
|
||
// 🔥 重置权限状态,避免单例 hasPermission 残留导致下次进入不再查询接口
|
||
_cubit.resetPermissionState();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 🔥 只监听 RemoteControlCubit,避免其他状态变化导致频繁 rebuild
|
||
final remoteCubit = context.read<RemoteControlCubit>();
|
||
final targetDevice = remoteCubit.state.targetDevice;
|
||
|
||
// 🔥 调试日志:检查设备状态
|
||
debugPrint('🔍 [RemoteControlPage] build - targetDevice: ${targetDevice?.deviceName}');
|
||
|
||
if (targetDevice == null) {
|
||
debugPrint('❌ [RemoteControlPage] targetDevice 为空,显示离线页面');
|
||
return _buildOfflineScaffold();
|
||
}
|
||
|
||
// 🔥 进入页面只查询一次真实权限状态(不抢占),按接口 remoteControl 如实展示
|
||
if (_isLoadingPermission) {
|
||
// 直接置标志位,不在 build 期间调用 setState
|
||
_isLoadingPermission = false;
|
||
final deviceName = targetDevice.deviceName;
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted && deviceName.isNotEmpty) {
|
||
debugPrint('🔑 [RemoteControlPage] 进入页面,仅查询权限状态 - deviceName: $deviceName');
|
||
remoteCubit.requestControlPermissionS(
|
||
deviceName,
|
||
'app',
|
||
source: '进入页面查询',
|
||
grabControl: false,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
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;
|
||
|
||
// 🔥 从 RemoteControlCubit 获取 token(如果有的话)
|
||
// 注意:如果 token 不在 RemoteControlCubit 中,需要从其他地方获取
|
||
// 这里假设 token 是有效的,直接构建 URL
|
||
if (deviceId != null && deviceId.isNotEmpty) {
|
||
_videoStreamUrl =
|
||
"webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId";
|
||
debugPrint('🎬 [WebRTC] URL构建成功: $_videoStreamUrl');
|
||
} else {
|
||
_videoStreamUrl = '';
|
||
debugPrint('❌ [WebRTC] URL构建失败 - deviceId: $deviceId');
|
||
}
|
||
final int originY = context
|
||
.watch<RemoteControlCubit>()
|
||
.state
|
||
.controlEntity
|
||
.originY;
|
||
// debugPrint("${originY},originY");
|
||
return WebRTCLocalPlayer(
|
||
streamUrl: _videoStreamUrl,
|
||
showLeftPip: state.showLeftPip,
|
||
showRightPip: state.showRightPip,
|
||
isFrontMain: originY >= 0,
|
||
onDoubleTap: () {
|
||
debugPrint('👆 [RemoteControlPage] 双击屏幕,切换前后视角');
|
||
// 🔥 通过 TCP 发送切换视角指令
|
||
// 这里需要调用 RemoteControlCubit 的方法来切换视角
|
||
context.read<RemoteControlCubit>().toggleCameraView();
|
||
},
|
||
);
|
||
},
|
||
),
|
||
),
|
||
|
||
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'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 🔥 显示当前控制设备的弹窗 - 已移除:进入远程遥控页不再弹出设备确认弹窗
|
||
|
||
}
|