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

526 lines
21 KiB
Dart
Raw Normal View History

import 'dart:async';
2026-01-18 20:21:14 +08:00
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';
2026-01-18 20:21:14 +08:00
import '../../../../core/app/app_user_cubit.dart';
2026-03-16 15:31:14 +08:00
import '../../../../core/router/route_paths.dart';
2026-06-05 19:06:21 +08:00
import '../../../devices/domain/entities/device_entity.dart';
2026-01-18 20:21:14 +08:00
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';
2026-01-19 18:24:24 +08:00
import '../widgets/top_status_bar.dart';
2026-01-21 13:27:55 +08:00
import '../widgets/webrtc/webrtc_local_player.dart';
2026-01-18 20:21:14 +08:00
class RemoteControlPage extends StatefulWidget {
const RemoteControlPage({super.key});
@override
State<RemoteControlPage> createState() => _RemoteControlPageState();
}
class _RemoteControlPageState extends State<RemoteControlPage> {
2026-03-12 16:25:57 +08:00
String _videoStreamUrl = "";
2026-03-25 16:42:35 +08:00
late RemoteControlCubit _cubit;
DevicesCubit? _devicesCubit;
2026-06-05 19:06:21 +08:00
StreamSubscription? _permissionSubscription; // 🔥 权限监听订阅
bool _isShowingPermissionDialog = false; // 🔥 防止弹窗重复显示
bool _isLoadingPermission = true; // 🔥 标记是否正在加载权限状态
2026-03-25 16:42:35 +08:00
@override
void didChangeDependencies() {
super.didChangeDependencies();
_cubit = context.read<RemoteControlCubit>();
_devicesCubit = context.read<DevicesCubit>();
2026-06-05 19:06:21 +08:00
// 🔥 监听权限弹窗状态变化 (只订阅一次)
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; // 弹窗关闭后重置标志
});
});
}
});
}
2026-03-25 16:42:35 +08:00
}
2026-06-05 19:06:21 +08:00
2026-01-18 20:21:14 +08:00
@override
void initState() {
super.initState();
2026-01-19 18:24:24 +08:00
// 1. 锁定横屏
2026-06-05 19:06:21 +08:00
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
2026-01-19 18:24:24 +08:00
// 2. 隐藏状态栏和虚拟按键
2026-01-18 20:21:14 +08:00
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
WidgetsBinding.instance.addPostFrameCallback((_) {
final remoteCubit = context.read<RemoteControlCubit>();
2026-06-05 19:06:21 +08:00
// debugPrint('🔍 [RemoteControl] initState - targetDevice: ${remoteCubit.state.targetDevice}');
// 🔥 检查 targetDevice 是否存在
if (remoteCubit.state.targetDevice == null) {
// debugPrint('❌ [RemoteControl] targetDevice 为空,无法进入远程控制');
return;
}
// 🔥 弹窗显示当前控制的设备信息
_showTargetDeviceDialog(context, remoteCubit.state.targetDevice!);
// 🔥 只在控制循环未启动时才启动
remoteCubit.startControlLoop();
2026-06-05 19:06:21 +08:00
// debugPrint('✅ [RemoteControl] 控制循环已启动');
});
2026-01-18 20:21:14 +08:00
}
@override
void dispose() {
// 释放远程控制权限
2026-06-05 19:06:21 +08:00
_cubit.releasePermission("app");
//print("远程控制要推出啦");
2026-06-05 19:06:21 +08:00
//final deviceState = _devicesCubit?.state;
//if (deviceState?.selectedDevice != null) {
// _cubit.releasePermission("app");
// }
2026-06-05 19:06:21 +08:00
_permissionSubscription?.cancel(); // 🔥 取消订阅
2026-01-18 20:21:14 +08:00
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
2026-03-25 16:42:35 +08:00
_cubit.stopControlLoop();
2026-01-18 20:21:14 +08:00
super.dispose();
}
@override
Widget build(BuildContext context) {
2026-01-19 18:24:24 +08:00
// 监听全局状态(这些通常不随摇杆频繁变化)
2026-01-18 20:21:14 +08:00
final userState = context.watch<AppUserCubit>().state;
final deviceState = context.watch<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
2026-06-05 19:06:21 +08:00
final remoteCubit = context.read<RemoteControlCubit>();
final hasPermission = remoteCubit.state.hasPermission;
2026-03-16 15:31:14 +08:00
///context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
2026-01-19 18:24:24 +08:00
if (currentDevice == null) {
2026-01-18 20:21:14 +08:00
return _buildOfflineScaffold();
}
2026-06-05 19:06:21 +08:00
// 🔥 只在没有权限时自动请求,避免重复调用
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] 已有权限或已请求过,跳过');
}
2026-01-18 20:21:14 +08:00
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
2026-01-21 13:27:55 +08:00
// 在 Stack 的最底层替换:
2026-01-18 20:21:14 +08:00
Positioned.fill(
2026-01-21 13:27:55 +08:00
child: BlocBuilder<RemoteControlCubit, RemoteControlState>(
// 只有当显示隐藏状态改变时才重构,内部的拖拽由组件自身 State 处理,不影响这里
2026-06-05 19:06:21 +08:00
buildWhen: (p, c) =>
p.showLeftPip != c.showLeftPip ||
p.showRightPip != c.showRightPip,
2026-03-12 16:25:57 +08:00
2026-01-21 13:27:55 +08:00
builder: (context, state) {
2026-06-05 19:06:21 +08:00
// 🔥 只从 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');
2026-03-12 16:25:57 +08:00
} else {
_videoStreamUrl = '';
2026-06-05 19:06:21 +08:00
// debugPrint('❌ [WebRTC] URL构建失败 - deviceId: $deviceId, hasUser: ${userState.user != null}, hasToken: ${userState.user?.token != null}');
2026-03-12 16:25:57 +08:00
}
2026-06-05 19:06:21 +08:00
final int originY = context
.watch<RemoteControlCubit>()
.state
.controlEntity
.originY;
// debugPrint("${originY},originY");
2026-01-21 13:27:55 +08:00
return WebRTCLocalPlayer(
// 这里的 URL 拼接根据你的后端规则
// streamUrl: "webrtc://${TCPConsts.TCP_IP}/live/livestream/${currentDevice.deviceName}?token=${userState.user!.token}",
2026-03-12 16:25:57 +08:00
streamUrl: _videoStreamUrl,
2026-01-21 13:27:55 +08:00
showLeftPip: state.showLeftPip, // 从 Cubit 状态中读取
showRightPip: state.showRightPip, // 从 Cubit 状态中读取
2026-06-05 19:06:21 +08:00
isFrontMain:
context
.watch<RemoteControlCubit>()
.state
.controlEntity
.originY >=
0,
2026-01-21 13:27:55 +08:00
);
},
2026-01-18 20:21:14 +08:00
),
),
2026-01-19 18:24:24 +08:00
BlocBuilder<RemoteControlCubit, RemoteControlState>(
buildWhen: (p, c) => p.isEmergency != c.isEmergency,
builder: (context, state) {
2026-06-05 19:06:21 +08:00
return state.isEmergency
? const Positioned.fill(child: EmergencyOverlay())
: const SizedBox.shrink();
2026-01-19 18:24:24 +08:00
},
),
2026-01-18 20:21:14 +08:00
SafeArea(
child: Column(
children: [
2026-06-05 19:06:21 +08:00
// 🔥 用 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);
},
),
2026-01-18 20:21:14 +08:00
Expanded(
2026-01-19 18:24:24 +08:00
child: LayoutBuilder(
builder: (context, constraints) {
final double sideAreaWidth = constraints.maxWidth * 0.25;
final double centerAreaWidth = constraints.maxWidth * 0.5;
// 关键:使用 BlocBuilder 局部刷新摇杆,不要 watch 整个 Page
2026-06-05 19:06:21 +08:00
return BlocBuilder<
RemoteControlCubit,
RemoteControlState
>(
2026-01-19 18:24:24 +08:00
// 只有 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,
2026-06-05 19:06:21 +08:00
child: LeftJoystickArea(
isLocked: remoteState.isLocked,
width: sideAreaWidth * 0.45,
),
2026-01-19 18:24:24 +08:00
),
),
// 中间区
SizedBox(
width: centerAreaWidth,
child: Align(
alignment: Alignment.bottomCenter,
2026-06-05 19:06:21 +08:00
child: CenterControlArea(
totalWidth: centerAreaWidth,
),
2026-01-19 18:24:24 +08:00
),
),
// 右摇杆
SizedBox(
width: sideAreaWidth,
child: Align(
alignment: Alignment.bottomCenter,
2026-06-05 19:06:21 +08:00
child: RightJoystickArea(
isLocked: remoteState.isLocked,
width: sideAreaWidth * 0.45,
),
2026-01-19 18:24:24 +08:00
),
),
],
),
);
},
);
},
2026-01-18 20:21:14 +08:00
),
),
],
),
),
2026-06-05 19:06:21 +08:00
],
),
);
}
// 🔥 显示权限请求弹窗 (使用 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',
);
2026-01-19 18:24:24 +08:00
}
2026-06-05 19:06:21 +08:00
// 🔥 关闭弹窗(在 TCP 发送完成后)
Navigator.pop(dialogContext);
2026-01-19 18:24:24 +08:00
},
2026-06-05 19:06:21 +08:00
child: Text(
AppLocalizations.of(
dialogContext,
).translate('remote_control.agree'),
),
2026-01-19 18:24:24 +08:00
),
2026-01-18 20:21:14 +08:00
],
),
);
}
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),
2026-06-05 19:06:21 +08:00
Text(
AppLocalizations.of(
context,
).translate('remote_control.device_disconnected'),
style: const TextStyle(color: Colors.white, fontSize: 18),
),
2026-01-18 20:21:14 +08:00
const SizedBox(height: 20),
2026-06-05 19:06:21 +08:00
ElevatedButton(
onPressed: () => context.pop(),
child: Text(
AppLocalizations.of(context).translate('remote_control.back'),
),
),
2026-01-18 20:21:14 +08:00
],
),
),
);
}
2026-06-05 19:06:21 +08:00
/// 🔥 显示当前控制设备的弹窗
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),
),
],
),
2026-01-18 20:21:14 +08:00
actions: [
2026-03-16 15:31:14 +08:00
TextButton(
2026-06-05 19:06:21 +08:00
onPressed: () => Navigator.pop(ctx),
child: const Text('确定'),
2026-03-16 15:31:14 +08:00
),
2026-01-18 20:21:14 +08:00
],
),
);
}
}