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:shared_preferences/shared_preferences.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'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; import 'package:maibu_satabot_v2/core/theme/theme_cubit.dart'; import 'package:maibu_satabot_v2/core/update/update_cubit.dart'; import 'package:maibu_satabot_v2/core/update/version_check_service.dart'; import 'package:maibu_satabot_v2/core/update/update_dialog.dart'; import 'package:maibu_satabot_v2/core/update/update_state.dart'; import 'package:maibu_satabot_v2/core/network/mqtt/mqtt_manager.dart'; 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 'core/di/injection.dart'; import 'core/localization/app_localizations.dart'; import 'core/localization/locale_cubit.dart'; import 'features/auth/presentation/bloc/auth_state.dart'; import 'features/auth/presentation/bloc/login_cubit.dart'; import 'features/devices/presentation/bloc/device_status_bloc.dart'; import 'features/devices/presentation/bloc/device_task_cubit.dart'; // 🔥 添加 DeviceTaskCubit import 'features/home/presentation/bloc/permission_request_bloc.dart'; import 'features/remote_control/presentation/bloc/remote_control_cubit.dart'; // 🔥 定义全局 Navigator Key final GlobalKey navigatorKey = GlobalKey(); void main() async { runZonedGuarded( () async { WidgetsFlutterBinding.ensureInitialized(); await FlutterPatcher.init(); await init(); final logger = sl(); await logger.init(); Bloc.observer = AppBlocObserver(logger); // 🔥 初始化 MQTT 连接(异步,不阻塞启动) MqttManager().initialize().catchError((e) { debugPrint('⚠️ [Main] MQTT 初始化失败,但不影响应用启动: $e'); }); // ⚠️ 注意:不要在启动时清除补丁版本记录! // 补丁版本记录只在整包更新成功后才清除 // 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新 runApp(const MyApp()); }, (error, stack) { sl().captureException(error, stackTrace: stack); }, ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { sl().appStarted(); final deviceStatusBloc = sl(); final localeCubit = sl(); final themeCubit = sl(); return MultiBlocProvider( providers: [ BlocProvider(create: (_) => sl()), BlocProvider(create: (_) => sl()), BlocProvider(create: (_) => sl()), BlocProvider( create: (_) { final user = sl().state.user; final devicesCubit = sl(); // 🔥 修复:检查是否已有 targetDevice,如果有则不需要加载设备列表 final remoteControlCubit = sl(); if (user != null && remoteControlCubit.state.targetDevice == null) { debugPrint('📱 [Main] 无 targetDevice,开始加载设备列表'); devicesCubit.fetchAllDevices(user.username); } else if (user != null) { debugPrint( '✅ [Main] 已有 targetDevice: ${remoteControlCubit.state.targetDevice!.deviceName},跳过设备列表加载', ); } return devicesCubit; }, ), BlocProvider.value(value: deviceStatusBloc), BlocProvider.value(value: localeCubit), BlocProvider.value(value: themeCubit), BlocProvider.value(value: sl()), BlocProvider( create: (_) => sl(), ), BlocProvider( create: (_) => UpdateCubit(VersionCheckService()), ), BlocProvider( create: (_) => sl(), ), BlocProvider(create: (_) => sl()), ], child: BlocBuilder( bloc: localeCubit, builder: (context, locale) { return BlocBuilder( bloc: themeCubit, builder: (context, themeMode) { return MaterialApp.router( title: 'Maibu Satabot', locale: locale, supportedLocales: const [Locale('zh', 'CN'), Locale('en', 'US')], localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: themeMode, routerConfig: sl(), builder: (context, child) { return _LifecycleListener(child: _UpdateChecker(child: child!)); }, ); }, ); }, ), ); } } class _LifecycleListener extends StatefulWidget { final Widget? child; const _LifecycleListener({required this.child}); @override State<_LifecycleListener> createState() => _LifecycleListenerState(); } class _LifecycleListenerState extends State<_LifecycleListener> with WidgetsBindingObserver { String? _currentSessionId; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _initSession(); } Future _initSession() async { final prefs = sl(); final savedCurrentId = prefs.getString('current_session_id'); if (savedCurrentId != null) { _currentSessionId = savedCurrentId; debugPrint('📱 [生命周期] 恢复会话ID: $savedCurrentId'); } else { debugPrint('📱 [生命周期] 首次启动,等待 appStarted 初始化会话'); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { _handleAppPaused(); } else if (state == AppLifecycleState.resumed) { _handleAppResumed(); } else if (state == AppLifecycleState.detached) { _handleAppDetached(); } } Future _handleAppPaused() async { final prefs = sl(); await prefs.setBool('pending_kill_logout', true); if (_currentSessionId != null) { await prefs.setString('saved_session_id', _currentSessionId!); } debugPrint('📱 [生命周期] 应用进入后台,已保存会话ID和杀后台标记'); } Future _handleAppResumed() async { final prefs = sl(); final savedSessionId = prefs.getString('saved_session_id'); final currentSessionId = prefs.getString('current_session_id') ?? _currentSessionId; if (currentSessionId != null && savedSessionId == currentSessionId) { debugPrint('📱 [生命周期] 应用正常恢复(会话ID匹配),清除杀后台标记'); await prefs.setBool('pending_kill_logout', false); await prefs.remove('saved_session_id'); } else if (savedSessionId != null && savedSessionId != currentSessionId) { debugPrint('📱 [生命周期] 应用从后台恢复但会话ID不匹配,设置杀后台标记'); await prefs.setBool('pending_kill_logout', true); } else { debugPrint('📱 [生命周期] 应用恢复(未进入过后台),清除杀后台标记'); await prefs.setBool('pending_kill_logout', false); } _handleResume(); } Future _handleAppDetached() async { final prefs = sl(); final savedSessionId = prefs.getString('saved_session_id'); final currentSessionId = prefs.getString('current_session_id') ?? _currentSessionId; if (savedSessionId != null && currentSessionId != null && savedSessionId == currentSessionId) { debugPrint('📱 [生命周期] 应用在后台被杀(detached),保留杀后台标记'); } else { debugPrint('📱 [生命周期] 应用在前台被杀(detached),清除杀后台标记'); await prefs.setBool('pending_kill_logout', false); await prefs.remove('saved_session_id'); } } Future _handleResume() async { try { final authCubit = context.read(); if (authCubit.state is! AuthAuthenticated) return; final remoteControlCubit = sl(); if (remoteControlCubit.state.targetDevice == null) { debugPrint('📱 [生命周期] 未选择设备,跳过 TCP 重连(避免创建无用连接)'); return; } await Future.delayed(const Duration(milliseconds: 500)); await authCubit.reconnectAfterResume(); } catch (e) { debugPrint('❌ [生命周期] 重连错误:$e'); } } @override Widget build(BuildContext context) => widget.child!; } class _UpdateChecker extends StatefulWidget { final Widget child; const _UpdateChecker({required this.child}); @override State<_UpdateChecker> createState() => _UpdateCheckerState(); } class _UpdateCheckerState extends State<_UpdateChecker> { @override void initState() { super.initState(); // TODO: 暂时注释掉应用启动时的自动更新检查 // Future.delayed(const Duration(seconds: 2), () { // if (mounted) context.read().checkUpdate(); // }); } @override Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { if (state is UpdateFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('更新失败: ${state.error}'), backgroundColor: Colors.red, ), ); } }, builder: (context, state) { return Stack( children: [ widget.child, // 🔥 核心修正:直接在 UI 树中渲染更新提示,完全不依赖 Navigator if (state is UpdateAvailable) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.6), // 半透明遮罩 child: Center( child: Card( margin: const EdgeInsets.symmetric(horizontal: 30), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '🎉 发现新版本', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), Text( '版本号: ${state.versionInfo.version}', style: const TextStyle(fontSize: 16), ), Text( '更新类型: ${state.versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}', style: const TextStyle( fontSize: 16, color: Colors.grey, ), ), if (state.versionInfo.updateDesc.isNotEmpty) ...[ const SizedBox(height: 8), const Text( '更新内容:', style: TextStyle(fontWeight: FontWeight.bold), ), Text(state.versionInfo.updateDesc), ], const SizedBox(height: 20), if (state.versionInfo.updateType != 'patch') ...[ // 🔥 整包更新:显示两个选项 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), border: Border.all( color: Colors.orange.shade200, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '💡 选择下载方式', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 8), const Text( '• 浏览器下载(推荐):打开浏览器下载,手动安装并卸载旧版本\n' '• App 内下载:App 内下载 APK,调起系统安装界面', style: TextStyle( fontSize: 13, height: 1.5, ), ), ], ), ), const SizedBox(height: 16), ], Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (!state.versionInfo.forceUpdate) TextButton( onPressed: () => context .read() .cancelUpdate(), child: const Text('稍后'), ), const SizedBox(width: 10), if (state.versionInfo.updateType == 'patch') // 🔥 差量更新:单个按钮 ElevatedButton( onPressed: () { final info = state.versionInfo; if (info.patchUrl != null) { context.read().applyPatch( info.patchUrl!, info.version, info.versionCode, md5: info.patchMd5, ); } }, child: const Text('立即更新'), ) else ...[ // 🔥 整包更新:两个按钮 OutlinedButton( onPressed: () { final info = state.versionInfo; if (info.apkUrl != null) { // 🔥 浏览器下载 + 引导弹窗 context .read() .fullApkUpdateWithBrowser( info.apkUrl!, ); // 显示引导弹窗 showBrowserDownloadGuide(context); } }, child: const Text('浏览器下载'), ), const SizedBox(width: 10), ElevatedButton( onPressed: () { final info = state.versionInfo; if (info.apkUrl != null) { // App 内下载(保留备用) context .read() .downloadAndInstallApk( info.apkUrl!, ); } }, child: const Text('App 内下载'), ), ], ], ), ], ), ), ), ), ), ), // 🔥 整包更新步骤引导弹窗 if (state is UpdateInstalling && !(state.isPatch ?? false)) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.7), child: Center( child: Card( margin: const EdgeInsets.symmetric(horizontal: 30), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '📱 整包更新步骤', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), _buildStepItem(1, '在浏览器中下载 APK', true), const SizedBox(height: 8), _buildStepItem(2, '手动安装下载的 APK', false), const SizedBox(height: 8), _buildStepItem(3, '清除旧版本记录', false), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { context.read().cancelUpdate(); }, child: const Text('我知道了'), ), ), ], ), ), ), ), ), ), // 🔥 下载进度显示(差量补丁和整包APK) if (state is UpdateDownloading) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.7), child: Center( child: Card( margin: const EdgeInsets.symmetric(horizontal: 40), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( state.isPatch ? '⬇️ 正在下载补丁...' : '⬇️ 正在下载 APK...', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), SizedBox( width: 80, height: 80, child: Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( value: state.progress, strokeWidth: 5, ), Text( '${(state.progress * 100).toInt()}%', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), ), const SizedBox(height: 16), Text( '请稍候...', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ), ), ), ), ), ), // 🔥 更新成功提示 if (state is UpdateSuccess) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.7), child: Center( child: Card( margin: const EdgeInsets.symmetric(horizontal: 40), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.check_circle, color: Colors.green, size: 60, ), const SizedBox(height: 16), const Text( '✅ 更新已完成', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), const Text( '补丁已成功应用!\n\n请完全关闭 App 后重新打开,\n即可体验新版本功能。', style: TextStyle(fontSize: 15), textAlign: TextAlign.center, ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // 提示用户手动重启 ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('请从后台划掉 App,然后重新打开'), duration: Duration(seconds: 3), ), ); context.read().cancelUpdate(); }, child: const Text('我知道了'), ), ], ), ), ), ), ), ), // 调试面板 - 暂时关闭 // Positioned( // top: 40, // right: 10, // child: Material( // color: Colors.black54, // borderRadius: BorderRadius.circular(8), // child: Padding( // padding: const EdgeInsets.all(8.0), // child: Column( // mainAxisSize: MainAxisSize.min, // children: [ // Text( // '状态: ${state.runtimeType.toString().replaceAll('Update', '')}', // style: const TextStyle( // color: Colors.white, // fontSize: 12, // ), // ), // // 🔥 暂时关闭刷新按钮和点击阴影 // // IconButton( // // icon: const Icon(Icons.refresh, color: Colors.white), // // onPressed: () => context.read().checkUpdate(), // // ), // ], // ), // ), // ), // ), ], ); }, ); } /// 构建步骤项 Widget _buildStepItem(int step, String text, bool completed) { return Row( children: [ Container( width: 28, height: 28, decoration: BoxDecoration( color: completed ? Colors.green : Colors.grey.shade300, shape: BoxShape.circle, ), child: Center( child: Text( '$step', style: TextStyle( color: completed ? Colors.white : Colors.black54, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(width: 12), Expanded( child: Text( text, style: TextStyle( fontSize: 15, color: completed ? Colors.green : Colors.black87, decoration: completed ? TextDecoration.lineThrough : null, ), ), ), if (completed) const Icon(Icons.check, color: Colors.green, size: 20), ], ); } } /// 显示浏览器下载引导弹窗(全局方法) void showBrowserDownloadGuide(BuildContext context) { Future.delayed(const Duration(milliseconds: 500), () { showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( title: const Text('🔔 重要提示'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '请在浏览器中完成以下操作:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), const Text('✅ 等待 APK 下载完成'), const SizedBox(height: 8), const Text('✅ 点击 APK 文件进行安装'), const SizedBox(height: 8), const Text('✅ 安装完成后,卸载当前旧版本'), const SizedBox(height: 8), const Text('✅ 重新打开新版本 APP'), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.red.shade200), ), child: const Text( '⚠️ 注意:必须先卸载旧版本,否则无法正常使用新功能!', style: TextStyle(color: Colors.red, fontSize: 12), ), ), ], ), actions: [ ElevatedButton( onPressed: () { Navigator.of(ctx).pop(); }, child: const Text('我知道了'), ), ], ), ); }); }