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: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/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/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/home/presentation/bloc/permission_request_bloc.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); // ⚠️ 注意:不要在启动时清除补丁版本记录! // 补丁版本记录只在整包更新成功后才清除 // 如果在启动时清除,会导致差量更新后划掉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(); return MultiBlocProvider( providers: [ BlocProvider(create: (_) => sl()), BlocProvider(create: (_) => sl()), BlocProvider(create: (_) => sl()), BlocProvider( create: (_) { final user = sl().state.user; final devicesCubit = sl(); if (user != null) devicesCubit.fetchAllDevices(user.username); return devicesCubit; }, ), BlocProvider.value(value: deviceStatusBloc), BlocProvider.value(value: localeCubit), BlocProvider.value(value: sl()), BlocProvider( create: (_) => sl(), ), BlocProvider( create: (_) => UpdateCubit(VersionCheckService()), ), ], child: BlocBuilder( bloc: localeCubit, builder: (context, locale) { 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, 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 { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { debugPrint('📱 [生命周期] 应用恢复到前台'); _handleResume(); } } Future _handleResume() async { try { final authCubit = context.read(); if (authCubit.state is! AuthAuthenticated) 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(); 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.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all( color: Colors.blue.shade200, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '💡 选择下载方式', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 8), const Text( '• 自动安装:App 内下载并调起系统安装\n' '• 手动下载:在浏览器中下载安装', 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() .downloadAndInstallApk( info.apkUrl!, ); } }, child: const Text('手动下载'), ), const SizedBox(width: 10), ElevatedButton( onPressed: () { final info = state.versionInfo; if (info.apkUrl != null) { context .read() .downloadAndInstallApk( info.apkUrl!, ); } }, child: const Text('自动安装'), ), ], ], ), ], ), ), ), ), ), ), // 🔥 整包更新步骤引导弹窗 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), ], ); } }