526 lines
21 KiB
Dart
526 lines
21 KiB
Dart
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 _isLoadingPermission = true; // 🔥 标记是否正在加载权限状态
|
||
|
||
@override
|
||
void didChangeDependencies() {
|
||
super.didChangeDependencies();
|
||
_cubit = context.read<RemoteControlCubit>();
|
||
_devicesCubit = context.read<DevicesCubit>();
|
||
|
||
// 🔥 监听权限弹窗状态变化 (只订阅一次)
|
||
if (_permissionSubscription == null) {
|
||
_permissionSubscription = _cubit.stream.listen((state) {
|
||
// 🔥 加强防重复逻辑:只在状态真正变化且不在显示弹窗时才显示
|
||
if (mounted &&
|
||
state.showPermissionRequestDialog &&
|
||
!_isShowingPermissionDialog) {
|
||
_isShowingPermissionDialog = true;
|
||
debugPrint('🔔 [权限弹窗] 检测到 showPermissionRequestDialog=true,准备显示弹窗');
|
||
// 使用微任务确保标志位已设置
|
||
Future.microtask(() {
|
||
_showPermissionDialog(state).then((_) {
|
||
debugPrint('🔔 [权限弹窗] 弹窗已关闭,重置标志位');
|
||
_isShowingPermissionDialog = false; // 弹窗关闭后重置标志
|
||
});
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
@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',
|
||
);
|
||
}
|
||
|
||
// 🔥 关闭弹窗(在 TCP 发送完成后)
|
||
Navigator.pop(dialogContext);
|
||
},
|
||
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',
|
||
);
|
||
}
|
||
|
||
// 🔥 关闭弹窗(在 TCP 发送完成后)
|
||
Navigator.pop(dialogContext);
|
||
},
|
||
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('确定'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|