Files
feature-next-arch/lib/main.dart
2026-06-12 15:00:44 +08:00

627 lines
25 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.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';
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 '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';
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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() async {
runZonedGuarded(
() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterPatcher.init();
await init();
final logger = sl<ILoggerService>();
await logger.init();
Bloc.observer = AppBlocObserver(logger);
// 🔥 确保 FloatBarSettingService 被初始化,这样 settingNotifier 才不会是 null
sl<FloatBarSettingService>();
debugPrint(
'🔍 [FloatBar] FloatBarSettingService 已初始化,settingNotifier: ${FloatBarSettingService.settingNotifier}',
);
// ⚠️ 注意:不要在启动时清除补丁版本记录!
// 补丁版本记录只在整包更新成功后才清除
// 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新
runApp(const MyApp());
},
(error, stack) {
sl<ILoggerService>().captureException(error, stackTrace: stack);
},
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
sl<AuthCubit>().appStarted();
final deviceStatusBloc = sl<DeviceStatusBloc>();
final localeCubit = sl<LocaleCubit>();
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(create: (_) => sl<AuthCubit>()),
BlocProvider<AppUserCubit>(create: (_) => sl<AppUserCubit>()),
BlocProvider<LoginCubit>(create: (_) => sl<LoginCubit>()),
BlocProvider<DevicesCubit>(
create: (_) {
final user = sl<AppUserCubit>().state.user;
final devicesCubit = sl<DevicesCubit>();
if (user != null) devicesCubit.fetchAllDevices(user.username);
return devicesCubit;
},
),
BlocProvider<DeviceStatusBloc>.value(value: deviceStatusBloc),
BlocProvider<LocaleCubit>.value(value: localeCubit),
BlocProvider<TabConfigCubit>.value(value: sl<TabConfigCubit>()),
BlocProvider<PermissionRequestBloc>(
create: (_) => sl<PermissionRequestBloc>(),
),
BlocProvider<UpdateCubit>(
create: (_) => UpdateCubit(VersionCheckService()),
),
],
child: BlocBuilder<LocaleCubit, Locale>(
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<GoRouter>(),
builder: (context, 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!)),
);
},
);
},
),
);
}
}
/// 悬浮条初始化组件 - 极简版
class _FloatBarInitializer extends StatelessWidget {
final Widget child;
const _FloatBarInitializer({required this.child});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
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});
@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<void> _handleResume() async {
try {
final authCubit = context.read<AuthCubit>();
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<UpdateCubit>().checkUpdate();
});
}
@override
Widget build(BuildContext context) {
return BlocConsumer<UpdateCubit, UpdateState>(
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<UpdateCubit>()
.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<UpdateCubit>().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<UpdateCubit>()
.downloadAndInstallApk(
info.apkUrl!,
);
}
},
child: const Text('手动下载'),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
final info = state.versionInfo;
if (info.apkUrl != null) {
context
.read<UpdateCubit>()
.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<UpdateCubit>().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<UpdateCubit>().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<UpdateCubit>().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),
],
);
}
}