243 lines
10 KiB
Dart
243 lines
10 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/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;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_cubit = context.read<RemoteControlCubit>();
|
|
_devicesCubit = context.read<DevicesCubit>();
|
|
}
|
|
@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>();
|
|
final devicesCubit = context.read<DevicesCubit>();
|
|
|
|
remoteCubit.startControlLoop();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// 释放远程控制权限
|
|
// _cubit.releasePermission("app");
|
|
//print("远程控制要推出啦");
|
|
final deviceState = _devicesCubit?.state;
|
|
if (deviceState?.selectedDevice != null) {
|
|
// _cubit.releasePermission("app");
|
|
}
|
|
|
|
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;
|
|
|
|
///context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
|
|
|
|
if (currentDevice == null) {
|
|
return _buildOfflineScaffold();
|
|
}
|
|
context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
|
|
|
|
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) {
|
|
final deviceId = context.watch<DevicesCubit>().state.selectedDevice?.deviceName;
|
|
if (deviceId != null && userState.user != null && userState.user!.token != null) {
|
|
_videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
|
|
} else {
|
|
_videoStreamUrl = '';
|
|
}
|
|
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: [
|
|
const TopStatusBar(),
|
|
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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 权限弹窗
|
|
BlocBuilder<RemoteControlCubit, RemoteControlState>(
|
|
buildWhen: (p, c) => p.showPermissionRequestDialog != c.showPermissionRequestDialog,
|
|
builder: (context, state) {
|
|
if (state.showPermissionRequestDialog) {
|
|
return _buildPermissionDialog(context, state);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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'))),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPermissionDialog(BuildContext context, RemoteControlState state) {
|
|
final deviceId = context.read<DevicesCubit>().state.selectedDevice?.deviceName ?? "";
|
|
return Container(
|
|
color: Colors.black54,
|
|
child: AlertDialog(
|
|
title: Text(AppLocalizations.of(context).translate('remote_control.permission_request_title')),
|
|
// content: Text("${state.permissionPlatform}端正请求控制权,同意释放吗?"),
|
|
content: Text(AppLocalizations.of(context).translate('remote_control.permission_request_content')),
|
|
actions: [
|
|
TextButton(onPressed: () => context.read<RemoteControlCubit>().respondPermission(false, deviceId), child: Text(AppLocalizations.of(context).translate('remote_control.refuse'))),
|
|
TextButton(
|
|
onPressed: () {
|
|
context.read<RemoteControlCubit>().respondPermission(true, deviceId);
|
|
// 🔥 用户同意,延迟跳转到首页
|
|
// Future.delayed(const Duration(milliseconds: 1000), () {
|
|
// if (context.mounted) {
|
|
// context.go(RoutePaths.home);
|
|
// }
|
|
// });
|
|
},
|
|
child: Text(AppLocalizations.of(context).translate('remote_control.agree')),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|