Files
flutterApp/lib/features/remote_control/presentation/pages/remote_control_page.dart

541 lines
22 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: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'),
),
),
],
),
),
);
}
/// 🔥 显示当前控制设备的弹窗 - 已移除:进入远程遥控页不再弹出设备确认弹窗
}