diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index 301d34e9..f470a468 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -17,6 +17,7 @@ import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart'; import 'package:maibu_satabot_v2/features/my/repository/my_repository_impl.dart'; import 'package:maibu_satabot_v2/features/my/usecases/updatename_usecase.dart'; import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart'; +import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart'; import 'package:maibu_satabot_v2/features/remote_control/data/repositories/remote_control_repository_impl.dart'; import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart'; import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_steer_usecase.dart'; @@ -368,6 +369,11 @@ Future init() async { // Tab 配置 Cubit (单例) sl.registerLazySingleton(() => TabConfigCubit(sl())); + // 🔥 悬浮条设置服务 (单例) + sl.registerLazySingleton( + () => FloatBarSettingService(sl()), + ); + sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl())); sl.registerLazySingleton( () => DevicesCubit( @@ -399,7 +405,16 @@ Future init() async { ); // 🔥 RemoteControlCubit 注入 DeviceStatusBloc(工厂模式,每次新建) - sl.registerFactory(() => RemoteControlCubit(sl(), sl(), sl(), sl(), sl())); + sl.registerFactory( + () => RemoteControlCubit( + sl(), // RemoteControlRepository + sl(), // RequestControlPermissionUseCase + sl(), // DeviceRepository + sl(), // TcpClient + sl(), // NetMessageDispatcher + sl(), // DeviceStatusBloc + ), + ); // 🔥 PermissionRequestBloc 用于首页权限弹窗(单例,通过 NetMessageDispatcher 监听) sl.registerLazySingleton( diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index 4325edeb..a14803a6 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -61,17 +61,17 @@ class AuthCubit extends Cubit { ); if (user != null) { - // 🔥 冷启动时重新初始化 TCP 连接 - await tcp.initializeTcp( - host: TCPConsts.TCP_IP, - port: TCPConsts.TCP_PORT, - ); + // 🔥 冷启动时不再自动连接TCP,改为选择设备时再连接 + // await tcp.initializeTcp( + // host: TCPConsts.TCP_IP, + // port: TCPConsts.TCP_PORT, + // ); // 2. 同步全局 App 状态 appCubit.setAuth(user); // 3. 进入已登录状态 emit(AuthAuthenticated(user)); - logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态并建立TCP连接', level: 'INFO'); + logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态(TCP将在选择设备时连接)', level: 'INFO'); } else { logger.logWithLevel('⚠️ [AUTH] 应用启动 - 无本地缓存,进入未登录状态', level: 'INFO'); emit(AuthUnauthenticated()); @@ -86,8 +86,8 @@ class AuthCubit extends Cubit { Future loginSuccess(UserEntity user) async { await storage.saveUser(user); - // 🔥 使用封装的TCP初始化方法:连接 + 认证 + 心跳 - await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); + // 🔥 登录后不再立即连接TCP,改为选择设备时再连接 + // await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); appCubit.setAuth(user); emit(AuthAuthenticated(user)); diff --git a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart index 4c40f43e..092fde72 100644 --- a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart +++ b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart @@ -8,6 +8,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; +import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart'; +import 'package:maibu_satabot_v2/core/network/tcp/tcp_client.dart'; import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart'; import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart'; @@ -27,6 +29,7 @@ class RemoteControlCubit extends Cubit { final RemoteControlRepository _repository; final RequestControlPermissionUseCase _requestControlPermissionUseCase; final DeviceRepository _deviceRepository; // 🔥 注入设备仓库 + final TcpClient tcpClient; // 🔥 注入TCP客户端 Timer? _timer; StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期 final NetMessageDispatcher dispatcher; @@ -56,6 +59,7 @@ class RemoteControlCubit extends Cubit { this._repository, this._requestControlPermissionUseCase, this._deviceRepository, // 🔥 注入 + this.tcpClient, // 🔥 注入TCP客户端 this.dispatcher, this.deviceStatusBloc, // 🔥 注入 ) : super( @@ -937,25 +941,49 @@ class RemoteControlCubit extends Cubit { } /// 🔥 设置待控制的设备(从机器人列表点击进入时调用) - void setTargetDevice(DeviceEntity device) { - // debugPrint('🎯 [RemoteControl] 设置待控制设备 ${device.deviceName}'); - // _logger.logWithLevel('🎯 [RemoteControl] 设置待控制设备 ${device.deviceName}'); + void setTargetDevice(DeviceEntity device) async { + debugPrint('🎯 [RemoteControl] ========== 开始设置目标设备 =========='); + debugPrint('🎯 [RemoteControl] 设备名称: ${device.deviceName}'); + debugPrint('🎯 [RemoteControl] 当前TCP状态: ${tcpClient.isConnected ? "已连接" : "未连接"}'); + + // 🔥 关键修改:选择设备时才连接TCP + if (!tcpClient.isConnected) { + debugPrint('🔌 [RemoteControl] TCP未连接,开始建立连接...'); + try { + debugPrint('🔌 [RemoteControl] 调用 connectBySwitch,目标: ${device.deviceName}'); + await tcpClient.connectBySwitch( + host: TCPConsts.TCP_IP, + port: TCPConsts.TCP_PORT, + deviceName: device.deviceName, + ); + debugPrint('✅ [RemoteControl] TCP连接成功!'); + debugPrint('✅ [RemoteControl] 认证流程已完成(包含发送0x03认证包和设备订阅)'); + } catch (e) { + debugPrint('❌ [RemoteControl] TCP连接失败: $e'); + debugPrint('⚠️ [RemoteControl] 将继续执行,允许用户进入页面(但无实时数据)'); + // 即使TCP连接失败,也继续设置设备,允许用户进入页面 + } + } else { + debugPrint('✅ [RemoteControl] TCP已连接,跳过连接步骤'); + debugPrint('🔄 [RemoteControl] 将直接切换设备订阅'); + } // 🔥 通知后端订阅该设备 + debugPrint('📡 [RemoteControl] 开始HTTP切换设备订阅...'); _deviceRepository.switchDevice("app", device.deviceName).then((result) { result.fold( (failure) { - // debugPrint('>>> [RemoteControl] 切换设备失败: ${failure.message}'); - // _logger.logWithLevel('>>> [RemoteControl] 切换设备失败: ${failure.message}'); + debugPrint('❌ [RemoteControl] HTTP切换设备失败: ${failure.message}'); }, (success) { - // debugPrint('>>> [RemoteControl] 切换设备成功, code: $success'); - // _logger.logWithLevel('>>> [RemoteControl] 切换设备成功, code: $success'); + debugPrint('✅ [RemoteControl] HTTP切换设备成功, code: $success'); }, ); }); + debugPrint('📦 [RemoteControl] 更新targetDevice状态'); emit(state.copyWith(targetDevice: device)); + debugPrint('🎯 [RemoteControl] ========== 目标设备设置完成 =========='); } /// 🔥 清除待控制设备(退出远程控制页时调用) diff --git a/lib/features/v2/device_list/presentation/float_bar/bloc/float_bar_bloc.dart b/lib/features/v2/device_list/presentation/float_bar/bloc/float_bar_bloc.dart new file mode 100644 index 00000000..31c94dd4 --- /dev/null +++ b/lib/features/v2/device_list/presentation/float_bar/bloc/float_bar_bloc.dart @@ -0,0 +1,107 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import '../model/robot_status_model.dart'; +import '../service/robot_status_service.dart'; +import '../manager/float_bar_manager.dart'; + +/// 悬浮条事件 +abstract class FloatBarEvent {} + +/// 切换折叠/展开状态事件 +class ToggleExpandEvent extends FloatBarEvent {} + +/// 更新状态数据事件 +class UpdateStatusEvent extends FloatBarEvent { + final RobotStatusModel status; + + UpdateStatusEvent(this.status); +} + +/// 悬浮条状态 +abstract class FloatBarState {} + +/// 折叠状态 +class FloatBarCollapsedState extends FloatBarState { + final RobotStatusModel status; + + FloatBarCollapsedState(this.status); +} + +/// 展开状态 +class FloatBarExpandedState extends FloatBarState { + final RobotStatusModel status; + + FloatBarExpandedState(this.status); +} + +/// 悬浮条Bloc +class FloatBarBloc extends Bloc { + final RobotStatusService _statusService; + final FloatBarManager? _floatBarManager; + StreamSubscription? _statusSubscription; + + FloatBarBloc(this._statusService, [this._floatBarManager]) + : super(FloatBarCollapsedState(_statusService.currentStatus)) { + // 监听服务层数据流 + _startListening(); + + on(_handleToggleExpand); + on(_handleUpdateStatus); + } + + /// 开始监听服务层数据 + void _startListening() { + _statusSubscription?.cancel(); + _statusSubscription = _statusService.statusStream.listen((status) { + add(UpdateStatusEvent(status)); + }); + } + + /// 处理切换折叠/展开 + void _handleToggleExpand( + ToggleExpandEvent event, + Emitter emit, + ) { + final currentState = state; + if (currentState is FloatBarCollapsedState) { + emit(FloatBarExpandedState(currentState.status)); + } else if (currentState is FloatBarExpandedState) { + emit(FloatBarCollapsedState(currentState.status)); + } + // 触发UI刷新(使用 ?. 处理空安全) + _floatBarManager?.refresh(); + } + + /// 处理状态数据更新 + void _handleUpdateStatus( + UpdateStatusEvent event, + Emitter emit, + ) { + final currentState = state; + if (currentState is FloatBarCollapsedState) { + emit(FloatBarCollapsedState(event.status)); + } else if (currentState is FloatBarExpandedState) { + emit(FloatBarExpandedState(event.status)); + } + // 触发UI刷新(使用 ?. 处理空安全) + _floatBarManager?.refresh(); + } + + /// 启动状态服务 + void startService() { + _statusService.start(); + } + + /// 停止状态服务 + void stopService() { + _statusService.stop(); + } + + @override + Future close() { + _statusSubscription?.cancel(); + _statusService.stop(); + return super.close(); + } +} diff --git a/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart b/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart new file mode 100644 index 00000000..c9f91841 --- /dev/null +++ b/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 悬浮条设置服务 - 简化版 +/// 使用静态变量存储状态,确保全局同步 +class FloatBarSettingService { + final SharedPreferences _prefs; + static const String _key = 'float_bar_enabled'; + + /// 🔥 静态实例引用 + static FloatBarSettingService? _instance; + + /// 🔥 静态状态变量 - 所有组件共享 + static bool _isEnabled = true; + + /// 🔥 静态 ValueNotifier - 用于通知UI变化 + static final ValueNotifier _settingNotifier = ValueNotifier(true); + + FloatBarSettingService(this._prefs) { + _instance = this; + // 从持久化读取初始状态 + _isEnabled = _prefs.getBool(_key) ?? true; + _settingNotifier.value = _isEnabled; + print('✅ [FloatBarSettingService] 初始化完成,初始状态: $_isEnabled'); + } + + /// 获取静态实例 + static FloatBarSettingService? get instance => _instance; + + /// 获取 ValueNotifier + static ValueNotifier get settingNotifier => _settingNotifier; + + /// 获取当前是否启用(直接从静态变量读取) + static bool get isEnabled => _isEnabled; + + /// 设置是否启用 + static Future setEnabled(bool enabled) async { + print('🔍 [FloatBarSettingService] setEnabled 被调用,新值: $enabled'); + + // 1. 更新静态变量 + _isEnabled = enabled; + print('🔍 [FloatBarSettingService] 静态变量已更新: $_isEnabled'); + + // 2. 更新 ValueNotifier(通知所有监听者) + _settingNotifier.value = enabled; + print( + '🔍 [FloatBarSettingService] ValueNotifier 已更新: ${_settingNotifier.value}', + ); + + // 3. 持久化到 SharedPreferences + final instance = _instance; + if (instance != null) { + await instance._prefs.setBool(_key, enabled); + print('🔍 [FloatBarSettingService] 已保存到 SharedPreferences'); + } + } + + /// 切换开关 + static Future toggle() async { + await setEnabled(!_isEnabled); + } +} diff --git a/lib/features/v2/device_list/presentation/float_bar/float_bar_controller.dart b/lib/features/v2/device_list/presentation/float_bar/float_bar_controller.dart new file mode 100644 index 00000000..c6e748b1 --- /dev/null +++ b/lib/features/v2/device_list/presentation/float_bar/float_bar_controller.dart @@ -0,0 +1,24 @@ +import 'package:flutter/foundation.dart'; + +/// 悬浮条控制器 - 极简版 +/// 使用静态变量管理全局状态 +class FloatBarController { + /// 🔥 是否显示悬浮条 + static bool isVisible = true; + + /// 🔥 状态变化通知器 + static final ValueNotifier visibilityNotifier = ValueNotifier(true); + + /// 设置显示/隐藏 + static void setVisible(bool visible) { + if (isVisible != visible) { + isVisible = visible; + visibilityNotifier.value = visible; + } + } + + /// 切换显示状态 + static void toggle() { + setVisible(!isVisible); + } +} diff --git a/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart b/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart new file mode 100644 index 00000000..ddb1e579 --- /dev/null +++ b/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import '../view/float_bar_widget.dart'; + +/// 全局悬浮条管理器 +/// 单例模式,负责管理OverlayEntry的创建、显示、隐藏和刷新 +class FloatBarManager { + static final FloatBarManager _instance = FloatBarManager._internal(); + + factory FloatBarManager() => _instance; + + FloatBarManager._internal(); + + /// OverlayEntry实例 + OverlayEntry? _overlayEntry; + + /// 是否已初始化 + bool _isInitialized = false; + + /// 全局上下文 + BuildContext? _globalContext; + + /// 初始化管理器,保存全局上下文 + void initialize(BuildContext context) { + if (_isInitialized) return; + _globalContext = context; + _isInitialized = true; + } + + /// 显示悬浮条 + void show() { + if (!_isInitialized || _globalContext == null) { + throw Exception('FloatBarManager has not been initialized!'); + } + + if (_overlayEntry != null) { + // 已有浮层,先移除再重新创建 + hide(); + } + + _overlayEntry = OverlayEntry( + builder: (context) => const FloatBarWidget(), + ); + + Overlay.of(_globalContext!)?.insert(_overlayEntry!); + } + + /// 隐藏悬浮条 + void hide() { + if (_overlayEntry != null) { + _overlayEntry!.remove(); + _overlayEntry = null; + } + } + + /// 强制刷新UI + void refresh() { + _overlayEntry?.markNeedsBuild(); + } + + /// 检查浮层是否显示中 + bool get isVisible => _overlayEntry != null; + + /// 释放资源 + void dispose() { + hide(); + _globalContext = null; + _isInitialized = false; + } +} \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/float_bar/model/robot_status_model.dart b/lib/features/v2/device_list/presentation/float_bar/model/robot_status_model.dart new file mode 100644 index 00000000..27b06229 --- /dev/null +++ b/lib/features/v2/device_list/presentation/float_bar/model/robot_status_model.dart @@ -0,0 +1,104 @@ +import 'package:equatable/equatable.dart'; + +/// 机器人状态数据模型 +class RobotStatusModel extends Equatable { + /// 任务名称 + final String taskName; + + /// 电量百分比 + final int battery; + + /// 设备状态:idle/running/charging/error + final String status; + + /// 信号强度 + final int signal; + + /// 当前位置 + final String location; + + /// 速度 + final double speed; + + /// 温度 + final int temperature; + + /// 运行时间 + final String runTime; + + const RobotStatusModel({ + this.taskName = '未知任务', + this.battery = 100, + this.status = 'idle', + this.signal = 100, + this.location = '未知位置', + this.speed = 0.0, + this.temperature = 25, + this.runTime = '00:00:00', + }); + + /// 创建副本 + RobotStatusModel copyWith({ + String? taskName, + int? battery, + String? status, + int? signal, + String? location, + double? speed, + int? temperature, + String? runTime, + }) { + return RobotStatusModel( + taskName: taskName ?? this.taskName, + battery: battery ?? this.battery, + status: status ?? this.status, + signal: signal ?? this.signal, + location: location ?? this.location, + speed: speed ?? this.speed, + temperature: temperature ?? this.temperature, + runTime: runTime ?? this.runTime, + ); + } + + /// 状态描述文本 + String get statusText { + switch (status) { + case 'running': + return '运行中'; + case 'charging': + return '充电中'; + case 'error': + return '故障'; + case 'idle': + default: + return '待机'; + } + } + + /// 状态颜色 + String get statusColor { + switch (status) { + case 'running': + return '#00C853'; + case 'charging': + return '#03DAC6'; + case 'error': + return '#FF5252'; + case 'idle': + default: + return '#9E9E9E'; + } + } + + @override + List get props => [ + taskName, + battery, + status, + signal, + location, + speed, + temperature, + runTime, + ]; +} \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/float_bar/service/robot_status_service.dart b/lib/features/v2/device_list/presentation/float_bar/service/robot_status_service.dart new file mode 100644 index 00000000..ee2b576d --- /dev/null +++ b/lib/features/v2/device_list/presentation/float_bar/service/robot_status_service.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:math'; +import '../model/robot_status_model.dart'; + +/// 机器人状态服务 +/// 负责模拟设备状态推送,实际项目中应替换为真实的TCP/接口对接 +class RobotStatusService { + static final RobotStatusService _instance = RobotStatusService._internal(); + + factory RobotStatusService() => _instance; + + RobotStatusService._internal(); + + /// 状态数据流控制器 + final StreamController _statusController = + StreamController.broadcast(); + + /// 当前状态 + RobotStatusModel _currentStatus = const RobotStatusModel(); + + /// 模拟定时器 + Timer? _timer; + + /// 状态数据流 + Stream get statusStream => _statusController.stream; + + /// 获取当前状态 + RobotStatusModel get currentStatus => _currentStatus; + + /// 启动状态推送 + void start() { + if (_timer != null) return; + + // 立即发送初始状态 + _statusController.add(_currentStatus); + + // 模拟每3秒更新一次状态 + _timer = Timer.periodic(const Duration(seconds: 3), (timer) { + _simulateStatusUpdate(); + }); + } + + /// 停止状态推送 + void stop() { + _timer?.cancel(); + _timer = null; + } + + /// 手动更新状态(用于外部触发更新) + void updateStatus(RobotStatusModel status) { + _currentStatus = status; + _statusController.add(status); + } + + /// 模拟状态更新 + void _simulateStatusUpdate() { + final random = Random(); + final statuses = ['idle', 'running', 'charging', 'error']; + + _currentStatus = _currentStatus.copyWith( + battery: max(0, _currentStatus.battery + random.nextInt(3) - 1), + status: random.nextDouble() > 0.95 ? statuses[random.nextInt(statuses.length)] : _currentStatus.status, + signal: min(100, max(0, _currentStatus.signal + random.nextInt(5) - 2)), + speed: _currentStatus.status == 'running' ? random.nextDouble() * 5 : 0, + temperature: min(50, max(20, _currentStatus.temperature + random.nextInt(3) - 1)), + runTime: _updateRunTime(), + ); + + _statusController.add(_currentStatus); + } + + /// 更新运行时间 + String _updateRunTime() { + if (_currentStatus.status != 'running') { + return _currentStatus.runTime; + } + + final parts = _currentStatus.runTime.split(':'); + int hours = int.parse(parts[0]); + int minutes = int.parse(parts[1]); + int seconds = int.parse(parts[2]); + + seconds++; + if (seconds >= 60) { + seconds = 0; + minutes++; + } + if (minutes >= 60) { + minutes = 0; + hours++; + } + + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + /// 释放资源 + void dispose() { + stop(); + _statusController.close(); + } +} \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/float_bar/simple_float_bar.dart b/lib/features/v2/device_list/presentation/float_bar/simple_float_bar.dart new file mode 100644 index 00000000..fb388e66 --- /dev/null +++ b/lib/features/v2/device_list/presentation/float_bar/simple_float_bar.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'float_bar_controller.dart'; + +/// 简单悬浮条组件 +class SimpleFloatBar extends StatefulWidget { + const SimpleFloatBar({super.key}); + + @override + State createState() => _SimpleFloatBarState(); +} + +class _SimpleFloatBarState extends State { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), + child: GestureDetector( + onTap: () => setState(() => _isExpanded = !_isExpanded), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: _isExpanded ? 200 : 56, + width: double.infinity, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFE8F5E9), Color(0xFFFFFFFF)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.green.withOpacity(0.2), width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 12, + offset: const Offset(0, 4), + spreadRadius: 2, + ), + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 1, + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: _isExpanded + ? SingleChildScrollView(child: _buildExpanded()) + : _buildCollapsed(), + ), + ), + ), + ); + } + + Widget _buildCollapsed() { + return Row(children: [ + Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(5))), + const SizedBox(width: 12), + const Expanded(child: Text('设备运行中', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black87))), + const SizedBox(width: 12), + Row(children: const [Icon(Icons.battery_full, size: 18, color: Colors.grey), SizedBox(width: 4), Text('85%', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500))]), + const SizedBox(width: 8), + const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey), + const SizedBox(width: 8), + _buildCloseButton(), + ]); + } + + Widget _buildExpanded() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(5))), + const SizedBox(width: 8), + Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(4)), child: const Text('运行中', style: TextStyle(fontSize: 12, color: Colors.green, fontWeight: FontWeight.w500))), + const SizedBox(width: 12), + const Expanded(child: Text('设备运行中', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.black87))), + const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey), + const SizedBox(width: 8), + _buildCloseButton(), + ]), + const SizedBox(height: 16), + Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ + _infoItem(Icons.battery_full, '电量', '85%'), + _infoItem(Icons.signal_cellular_alt, '信号', '100%'), + _infoItem(Icons.speed, '速度', '0.0m/s'), + _infoItem(Icons.thermostat, '温度', '25°C'), + ]), + const SizedBox(height: 12), + Row(children: const [ + Icon(Icons.location_on, size: 14, color: Colors.grey), + SizedBox(width: 4), + Expanded(child: Text('北京市朝阳区', style: TextStyle(fontSize: 12, color: Colors.grey))), + SizedBox(width: 12), + Icon(Icons.timer, size: 14, color: Colors.grey), + SizedBox(width: 4), + Text('02:35:18', style: TextStyle(fontSize: 12, color: Colors.grey)), + ]), + ]); + } + + Widget _buildCloseButton() { + return GestureDetector( + onTap: () => FloatBarController.setVisible(false), + child: Container(padding: const EdgeInsets.all(4), decoration: BoxDecoration(color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.close, size: 16, color: Colors.grey)), + ); + } + + Widget _infoItem(IconData icon, String label, String value) { + return Column(mainAxisSize: MainAxisSize.min, children: [ + Icon(icon, size: 20, color: Colors.grey), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)), + const SizedBox(height: 2), + Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), + ]); + } +} diff --git a/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart b/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart new file mode 100644 index 00000000..110fee43 --- /dev/null +++ b/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart @@ -0,0 +1,350 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../bloc/float_bar_bloc.dart'; +import '../model/robot_status_model.dart'; +import '../service/robot_status_service.dart'; +import '../cubit/float_bar_setting_cubit.dart'; + +/// 悬浮条UI组件 +/// 内部独立管理Bloc,不依赖外部注入 +class FloatBarWidget extends StatefulWidget { + const FloatBarWidget({super.key}); + + @override + State createState() => _FloatBarWidgetState(); +} + +class _FloatBarWidgetState extends State { + late final FloatBarBloc _bloc; + + @override + void initState() { + super.initState(); + debugPrint('🔥 [FloatBarWidget] initState - 开始创建 Bloc'); + // 内部创建Bloc并启动服务(不需要FloatBarManager,因为我们使用Stack方式) + _bloc = FloatBarBloc(RobotStatusService()); + _bloc.startService(); + debugPrint('✅ [FloatBarWidget] Bloc 已创建并启动服务'); + } + + @override + void dispose() { + debugPrint('🔥 [FloatBarWidget] dispose - 关闭 Bloc'); + _bloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint('🔥 [FloatBarWidget] build - 渲染悬浮条'); + return BlocProvider.value(value: _bloc, child: const _FloatBarContent()); + } +} + +/// 悬浮条内容组件 +class _FloatBarContent extends StatelessWidget { + const _FloatBarContent(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final isExpanded = state is FloatBarExpandedState; + final status = state is FloatBarCollapsedState + ? state.status + : (state as FloatBarExpandedState).status; + + debugPrint( + '🔥 [FloatBarContent] build - isExpanded: $isExpanded, status: ${status.taskName}', + ); + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), // 底部留出 Tab 栏空间 + child: GestureDetector( + onTap: () { + debugPrint('🔥 [FloatBarContent] 点击悬浮条'); + context.read().add(ToggleExpandEvent()); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: isExpanded ? 180 : 56, + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFFE8F5E9), // 浅绿色 + Color(0xFFFFFFFF), // 白色 + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.green.withOpacity(0.2), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 12, + offset: const Offset(0, 4), + spreadRadius: 2, + ), + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 1, + ), + BoxShadow( + color: Colors.white.withOpacity(0.6), + blurRadius: 8, + offset: const Offset(0, -2), + spreadRadius: -2, + ), + ], + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: isExpanded + ? _buildExpandedContent(status) + : _buildCollapsedContent(status), + ), + ), + ), + ); + }, + ); + } + + /// 折叠态内容 + Widget _buildCollapsedContent(RobotStatusModel status) { + return Row( + children: [ + // 状态指示灯 + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: _parseColor(status.statusColor), + borderRadius: BorderRadius.circular(5), + ), + ), + const SizedBox(width: 12), + // 任务名称 + Expanded( + child: Text( + status.taskName, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 12), + // 电量 + Row( + children: [ + Icon( + status.battery > 20 ? Icons.battery_full : Icons.battery_alert, + size: 18, + color: status.battery > 20 ? Colors.grey : Colors.red, + ), + const SizedBox(width: 4), + Text( + '${status.battery}%', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: status.battery > 20 ? Colors.black87 : Colors.red, + ), + ), + ], + ), + const SizedBox(width: 8), + // 展开箭头 + const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey), + const SizedBox(width: 8), + // 关闭按钮 + GestureDetector( + onTap: () { + debugPrint('🔥 [FloatBarContent] 点击关闭按钮'); + // 🔥 使用静态方法关闭悬浮条 + FloatBarSettingService.setEnabled(false); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.close, size: 16, color: Colors.grey), + ), + ), + ], + ); + } + + /// 展开态内容 + Widget _buildExpandedContent(RobotStatusModel status) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 顶部:状态+任务名+折叠按钮 + Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: _parseColor(status.statusColor), + borderRadius: BorderRadius.circular(5), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _parseColor(status.statusColor).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + status.statusText, + style: TextStyle( + fontSize: 12, + color: _parseColor(status.statusColor), + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + status.taskName, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey), + const SizedBox(width: 8), + // 关闭按钮 + GestureDetector( + onTap: () { + debugPrint('🔥 [FloatBarContent] 点击关闭按钮(展开态)'); + // 🔥 使用静态方法关闭悬浮条 + FloatBarSettingService.setEnabled(false); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.close, size: 16, color: Colors.grey), + ), + ), + ], + ), + const SizedBox(height: 16), + // 中间:详细信息网格 + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildInfoItem( + status.battery > 20 ? Icons.battery_full : Icons.battery_alert, + '电量', + '${status.battery}%', + status.battery > 20 ? Colors.black87 : Colors.red, + ), + _buildInfoItem( + Icons.signal_cellular_alt, + '信号', + '${status.signal}%', + Colors.black87, + ), + _buildInfoItem( + Icons.speed, + '速度', + '${status.speed.toStringAsFixed(1)}m/s', + Colors.black87, + ), + _buildInfoItem( + Icons.thermostat, + '温度', + '${status.temperature}°C', + Colors.black87, + ), + ], + ), + const SizedBox(height: 12), + // 底部:位置+时间 + Row( + children: [ + const Icon(Icons.location_on, size: 14, color: Colors.grey), + const SizedBox(width: 4), + Expanded( + child: Text( + status.location, + style: const TextStyle(fontSize: 12, color: Colors.grey), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 12), + const Icon(Icons.timer, size: 14, color: Colors.grey), + const SizedBox(width: 4), + Text( + status.runTime, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ], + ); + } + + /// 信息项组件 + Widget _buildInfoItem( + IconData icon, + String label, + String value, + Color valueColor, + ) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 20, color: Colors.grey), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: valueColor, + ), + ), + ], + ); + } + + /// 解析颜色字符串 + Color _parseColor(String colorStr) { + try { + return Color(int.parse(colorStr.replaceFirst('#', '0xFF'))); + } catch (_) { + return Colors.grey; + } + } +} diff --git a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart index 73dd14fe..b33aa5a9 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart @@ -35,6 +35,7 @@ class _DroneStationDetailPageState extends State { bool showFloatingMonitor = false; bool isFloatingIndoor = true; Offset floatingPosition = const Offset(20, 200); + bool _isFloatingMonitorEnabled = true; // 悬浮窗默认开启 // 视频流状态 VideoStreamEntity? _floatingVideoStream; @@ -73,6 +74,30 @@ class _DroneStationDetailPageState extends State { _startDroneStatusPolling(); } + /// 检查并自动打开悬浮窗 + void _checkAndShowFloatingMonitor(UAVDetailEntity detail) { + debugPrint('🔍 检查悬浮窗条件:'); + debugPrint(' - 开关状态: $_isFloatingMonitorEnabled'); + debugPrint(' - 是否已显示: $showFloatingMonitor'); + debugPrint(' - 无人机在线: ${detail.droneOnlineStatus}'); + debugPrint(' - 机场摄像头: ${detail.gatewayCameraList?.length ?? 0}'); + + if (!_isFloatingMonitorEnabled || showFloatingMonitor) { + debugPrint('❌ 不满足条件,退出检查'); + return; // 开关关闭或已显示,不执行 + } + + // 无人机在线且有机场摄像头,自动打开悬浮窗 + if (detail.droneOnlineStatus == 1 && + detail.gatewayCameraList != null && + detail.gatewayCameraList!.isNotEmpty) { + debugPrint('✅ 检测到无人机在线,自动打开悬浮窗'); + _loadFloatingVideoStream(); + } else { + debugPrint('❌ 无人机离线或无摄像头'); + } + } + @override void dispose() { _bloc.close(); @@ -98,6 +123,17 @@ class _DroneStationDetailPageState extends State { deviceSn: widget.station.deviceSn, ), ); + + // 如果悬浮窗开启且无人机上线,自动显示悬浮窗 + if (_isFloatingMonitorEnabled) { + final currentState = _bloc.state; + if (currentState is UAVDetailLoaded && + currentState.detail.droneOnlineStatus == 1 && + !showFloatingMonitor) { + debugPrint('✅ 无人机已上线,自动打开悬浮窗'); + _loadFloatingVideoStream(); + } + } }); } @@ -219,6 +255,8 @@ class _DroneStationDetailPageState extends State { } if (state is UAVDetailLoaded) { + // 检查是否自动打开悬浮窗 + _checkAndShowFloatingMonitor(state.detail); return _buildContent(state.detail); } @@ -836,7 +874,10 @@ class _DroneStationDetailPageState extends State { GestureDetector( onTap: () { _destroyFloatingRtcEngine(); - setState(() => showFloatingMonitor = false); + setState(() { + showFloatingMonitor = false; + _isFloatingMonitorEnabled = false; // 关闭开关 + }); }, child: const Padding( padding: EdgeInsets.all(4), @@ -965,6 +1006,7 @@ class _DroneStationDetailPageState extends State { _isFloatingLoading = true; _floatingErrorMessage = null; showFloatingMonitor = true; + _isFloatingMonitorEnabled = true; // 开启开关 }); _destroyFloatingRtcEngine(); diff --git a/lib/features/v2/my/presentation/pages/system_settings_page.dart b/lib/features/v2/my/presentation/pages/system_settings_page.dart index 95c7faeb..54f0efdc 100644 --- a/lib/features/v2/my/presentation/pages/system_settings_page.dart +++ b/lib/features/v2/my/presentation/pages/system_settings_page.dart @@ -8,6 +8,9 @@ import 'package:maibu_satabot_v2/core/router/route_paths.dart'; import 'package:maibu_satabot_v2/core/storage/user_storage.dart'; import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart'; import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart'; +import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/float_bar_controller.dart'; + +final sl = GetIt.instance; /// 系统设置综合页面 class SystemSettingsPage extends StatefulWidget { @@ -18,6 +21,17 @@ class SystemSettingsPage extends StatefulWidget { } class _SystemSettingsPageState extends State { + /// 悬浮条开关状态 + bool _floatBarEnabled = true; + + @override + void initState() { + super.initState(); + // 初始化时获取当前状态(使用静态属性) + _floatBarEnabled = FloatBarController.isVisible; + print('🔍 [设置页面] initState,初始状态: $_floatBarEnabled'); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -45,6 +59,9 @@ class _SystemSettingsPageState extends State { // Tab 设置 _buildTabSettingsSection(), const SizedBox(height: 12), + // 悬浮条设置 + _buildFloatBarSection(), + const SizedBox(height: 12), // 语言设置 _buildLanguageSection(), const SizedBox(height: 12), @@ -88,9 +105,9 @@ class _SystemSettingsPageState extends State { if (state.runtimeType.toString() != 'TabConfigLoaded') { return const Center(child: CircularProgressIndicator()); } - + final tabs = state.config.items; - + return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -164,14 +181,18 @@ class _SystemSettingsPageState extends State { AppLocalizations.of(context).translate('my.chinese'), 'zh', locale.languageCode == 'zh', - () => context.read().setLocale(const Locale('zh', 'CN')), + () => context.read().setLocale( + const Locale('zh', 'CN'), + ), ), const SizedBox(height: 12), _buildLanguageOption( AppLocalizations.of(context).translate('my.english'), 'en', locale.languageCode == 'en', - () => context.read().setLocale(const Locale('en', 'US')), + () => context.read().setLocale( + const Locale('en', 'US'), + ), ), ], ); @@ -182,16 +203,25 @@ class _SystemSettingsPageState extends State { ); } - Widget _buildLanguageOption(String label, String code, bool isSelected, VoidCallback onTap) { + Widget _buildLanguageOption( + String label, + String code, + bool isSelected, + VoidCallback onTap, + ) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: isSelected ? const Color(0xFF165DFF).withOpacity(0.1) : Colors.transparent, + color: isSelected + ? const Color(0xFF165DFF).withOpacity(0.1) + : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( - color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFE5E6EB), + color: isSelected + ? const Color(0xFF165DFF) + : const Color(0xFFE5E6EB), width: 1, ), ), @@ -202,7 +232,9 @@ class _SystemSettingsPageState extends State { label, style: TextStyle( fontSize: 14, - color: isSelected ? const Color(0xFF165DFF) : const Color(0xFF1D2129), + color: isSelected + ? const Color(0xFF165DFF) + : const Color(0xFF1D2129), fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), @@ -252,6 +284,94 @@ class _SystemSettingsPageState extends State { ); } + /// 悬浮条设置 + Widget _buildFloatBarSection() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '悬浮条设置', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + _buildFloatBarSwitch(), + ], + ), + ); + } + + /// 悬浮条开关 + Widget _buildFloatBarSwitch() { + print( + '🔍 [设置页面] _buildFloatBarSwitch 被调用,_floatBarEnabled: $_floatBarEnabled', + ); + + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + '显示悬浮条', + style: TextStyle(fontSize: 14, color: Color(0xFF1D2129)), + ), + SizedBox(height: 4), + Text( + '在Tab页面显示设备状态悬浮条', + style: TextStyle(fontSize: 12, color: Color(0xFF8F959E)), + ), + ], + ), + ), + Switch( + value: _floatBarEnabled, + onChanged: (value) { + print('🔍 [设置页面] 开关被点击,新值: $value,当前状态: $_floatBarEnabled'); + + // 🔥 立即更新本地状态 + setState(() { + _floatBarEnabled = value; + }); + print('🔍 [设置页面] 本地状态已更新为: $_floatBarEnabled'); + + // 🔥 使用静态方法设置新值 + FloatBarController.setVisible(value); + print('🔍 [设置页面] setVisible 已调用完成'); + + // 显示提示 + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(value ? '悬浮条已开启' : '悬浮条已关闭'), + duration: const Duration(seconds: 2), + ), + ); + } + }, + activeColor: const Color(0xFF165DFF), + ), + ], + ); + } + /// 显示退出登录确认对话框 void _showLogoutDialog(BuildContext context) { showDialog( diff --git a/lib/main.dart b/lib/main.dart index 22f48dcc..9f297b1f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,16 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_patcher/flutter_patcher.dart'; import 'package:go_router/go_router.dart'; +import 'package:get_it/get_it.dart'; + +// 悬浮条相关 +import 'features/v2/device_list/presentation/float_bar/float_bar_controller.dart'; +import 'features/v2/device_list/presentation/float_bar/simple_float_bar.dart'; + import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/core/infrastructure/logging/app_bloc_observer.dart'; import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart'; @@ -19,8 +23,13 @@ import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart'; import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart'; import 'package:maibu_satabot_v2/features/main_container/presentation/main_wrapper.dart'; +// 🔥 导入悬浮条管理器和设置服务 +import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart'; +import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart'; +import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart'; import 'core/di/injection.dart'; +import 'core/router/route_paths.dart'; import 'core/localization/app_localizations.dart'; import 'core/localization/locale_cubit.dart'; import 'features/auth/presentation/bloc/auth_state.dart'; @@ -28,6 +37,9 @@ import 'features/auth/presentation/bloc/login_cubit.dart'; import 'features/devices/presentation/bloc/device_status_bloc.dart'; import 'features/home/presentation/bloc/permission_request_bloc.dart'; +// 🔥 全局 GetIt 实例 +final sl = GetIt.instance; + // 🔥 定义全局 Navigator Key final GlobalKey navigatorKey = GlobalKey(); @@ -38,10 +50,17 @@ void main() async { await FlutterPatcher.init(); await init(); + final logger = sl(); await logger.init(); Bloc.observer = AppBlocObserver(logger); + // 🔥 确保 FloatBarSettingService 被初始化,这样 settingNotifier 才不会是 null + sl(); + debugPrint( + '🔍 [FloatBar] FloatBarSettingService 已初始化,settingNotifier: ${FloatBarSettingService.settingNotifier}', + ); + // ⚠️ 注意:不要在启动时清除补丁版本记录! // 补丁版本记录只在整包更新成功后才清除 // 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新 @@ -102,7 +121,23 @@ class MyApp extends StatelessWidget { theme: AppTheme.lightTheme, routerConfig: sl(), builder: (context, child) { - return _LifecycleListener(child: _UpdateChecker(child: child!)); + // 🔥 初始化悬浮条管理器并显示悬浮条 + debugPrint( + '======== [FloatBar] MaterialApp.builder START ========', + ); + debugPrint('🔍 [FloatBar] child: $child'); + debugPrint( + '🔍 [FloatBar] settingNotifier: ${FloatBarSettingService.settingNotifier}', + ); + debugPrint( + '🔍 [FloatBar] isEnabled: ${FloatBarSettingService.isEnabled}', + ); + debugPrint( + '======== [FloatBar] MaterialApp.builder END ========', + ); + return _FloatBarInitializer( + child: _LifecycleListener(child: _UpdateChecker(child: child!)), + ); }, ); }, @@ -111,6 +146,35 @@ class MyApp extends StatelessWidget { } } +/// 悬浮条初始化组件 - 极简版 +class _FloatBarInitializer extends StatelessWidget { + final Widget child; + + const _FloatBarInitializer({required this.child}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: FloatBarController.visibilityNotifier, + builder: (context, isVisible, child) { + return Stack( + children: [ + child!, + if (isVisible) + const Positioned( + left: 0, + right: 0, + bottom: 0, + child: SimpleFloatBar(), + ), + ], + ); + }, + child: child, + ); + } +} + class _LifecycleListener extends StatefulWidget { final Widget? child; const _LifecycleListener({required this.child}); @@ -247,6 +311,7 @@ class _UpdateCheckerState extends State<_UpdateChecker> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 💡 选择下载方式 const Text( '💡 选择下载方式', style: TextStyle(