362 lines
15 KiB
Dart
362 lines
15 KiB
Dart
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<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);
|
||
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) {
|
||
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<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),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
if (!state.versionInfo.forceUpdate)
|
||
TextButton(
|
||
onPressed: () => context.read<UpdateCubit>().cancelUpdate(),
|
||
child: const Text('稍后'),
|
||
),
|
||
const SizedBox(width: 10),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
final info = state.versionInfo;
|
||
if (info.patchUrl != null) {
|
||
// 🔥 执行差量更新
|
||
context.read<UpdateCubit>().applyPatch(
|
||
info.patchUrl!,
|
||
info.version,
|
||
info.versionCode,
|
||
md5: info.patchMd5,
|
||
);
|
||
} else if (info.apkUrl != null) {
|
||
// 🔥 执行整包更新
|
||
context.read<UpdateCubit>().downloadAndInstallApk(info.apkUrl!);
|
||
}
|
||
},
|
||
child: const Text('立即更新'),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
// 🔥 下载/安装进度弹窗
|
||
if (state is UpdateDownloading || state is UpdateInstalling)
|
||
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: [
|
||
if (state is UpdateDownloading)
|
||
const Text('⬇️ 正在下载更新...',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||
if (state is UpdateInstalling)
|
||
const Text('🔧 正在应用更新...',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||
const SizedBox(height: 16),
|
||
if (state is UpdateDownloading)
|
||
LinearProgressIndicator(
|
||
value: state.progress,
|
||
minHeight: 8,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
if (state is UpdateDownloading)
|
||
const SizedBox(height: 8),
|
||
if (state is UpdateDownloading)
|
||
Text('${(state.progress * 100).toInt()}%',
|
||
style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||
if (state is UpdateInstalling)
|
||
const CircularProgressIndicator(),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
state is UpdateDownloading ? '请稍候,正在下载补丁文件' : '正在应用补丁,请勿关闭应用',
|
||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
// 🔥 更新成功提示
|
||
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(),
|
||
// ),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|