Files
feature-next-arch/lib/features/remote_control/presentation/pages/remote_control_page.dart

574 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:get_it/get_it.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 '../../../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<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;
}
// 🔥 弹窗显示当前控制的设备信息
_showTargetDeviceDialog(context, remoteCubit.state.targetDevice!);
// 🔥 只在控制循环未启动时才启动
remoteCubit.startControlLoop();
// debugPrint('✅ [RemoteControl] 控制循环已启动');
});
}
@override
void dispose() {
// 🔥 离开安全模式:启动 90 秒倒计时
GetIt.I<AuthCubit>().exitSafeMode();
// 释放远程控制权限
_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) {
// 🔥 只监听 RemoteControlCubit,避免其他状态变化导致频繁 rebuild
final remoteCubit = context.read<RemoteControlCubit>();
final targetDevice = remoteCubit.state.targetDevice;
final hasPermission = remoteCubit.state.hasPermission;
// 🔥 调试日志:检查设备状态
debugPrint('🔍 [RemoteControlPage] build - targetDevice: ${targetDevice?.deviceName}');
if (targetDevice == null) {
debugPrint('❌ [RemoteControlPage] targetDevice 为空,显示离线页面');
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;
// 🔥 从 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'),
),
),
],
),
),
);
}
/// 🔥 显示当前控制设备的弹窗
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('确定'),
),
],
),
);
}
}