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/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 createState() => _RemoteControlPageState(); } class _RemoteControlPageState extends State { 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(); _devicesCubit = context.read(); // 🔥 监听权限弹窗状态变化 (只订阅一次) 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().enterSafeMode(); // 1. 锁定横屏 SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); // 2. 隐藏状态栏和虚拟按键 SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); WidgetsBinding.instance.addPostFrameCallback((_) { final remoteCubit = context.read(); // 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() { // 🔥 离开安全模式:启动 90 秒倒计时 GetIt.I().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(); 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( // 只有当显示隐藏状态改变时才重构,内部的拖拽由组件自身 State 处理,不影响这里 buildWhen: (p, c) => p.showLeftPip != c.showLeftPip || p.showRightPip != c.showRightPip, builder: (context, state) { // 🔥 从 targetDevice 获取 deviceId final targetDevice = context .watch() .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() .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().toggleCameraView(); }, ); }, ), ), BlocBuilder( 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( 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 _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() .state .selectedDevice ?.deviceName ?? ""; final remoteCubit = context.read(); // 🔥 添加详细状态日志 - 检查为什么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().resetPermissionCoolDown(); context.read().emit( context.read().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() .state .selectedDevice ?.deviceName ?? ""; final remoteCubit = context.read(); // 🔥 添加详细状态日志 - 检查为什么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().resetPermissionCoolDown(); context.read().emit( context.read().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('确定'), ), ], ), ); } }