Files
feature-tenant/lib/main.dart

562 lines
23 KiB
Dart
Raw Normal View History

2026-01-21 13:27:55 +08:00
import 'dart:async';
2026-01-19 18:24:24 +08:00
import 'dart:io';
2026-01-11 19:42:22 +08:00
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
2026-01-11 19:42:22 +08:00
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_patcher/flutter_patcher.dart';
2026-01-11 19:42:22 +08:00
import 'package:go_router/go_router.dart';
2026-01-18 20:21:14 +08:00
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
2026-01-21 13:27:55 +08:00
import 'package:maibu_satabot_v2/core/infrastructure/logging/app_bloc_observer.dart';
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
2026-01-18 20:21:14 +08:00
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';
2026-01-11 19:42:22 +08:00
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
2026-01-18 20:21:14 +08:00
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
2026-05-13 10:34:07 +08:00
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';
2026-01-11 19:42:22 +08:00
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';
2026-01-11 19:42:22 +08:00
import 'features/auth/presentation/bloc/login_cubit.dart';
import 'features/devices/presentation/bloc/device_status_bloc.dart';
2026-05-14 14:18:37 +08:00
import 'features/home/presentation/bloc/permission_request_bloc.dart';
2026-01-11 19:42:22 +08:00
// 🔥 定义全局 Navigator Key
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
2026-01-11 19:42:22 +08:00
void main() async {
2026-01-21 13:27:55 +08:00
runZonedGuarded(
() async {
WidgetsFlutterBinding.ensureInitialized();
2026-06-01 17:33:16 +08:00
await FlutterPatcher.init();
2026-01-21 13:27:55 +08:00
await init();
final logger = sl<ILoggerService>();
await logger.init();
Bloc.observer = AppBlocObserver(logger);
2026-06-01 17:33:16 +08:00
// ⚠️ 注意:不要在启动时清除补丁版本记录!
// 补丁版本记录只在整包更新成功后才清除
// 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新
2026-06-01 17:33:16 +08:00
2026-01-21 13:27:55 +08:00
runApp(const MyApp());
},
(error, stack) {
sl<ILoggerService>().captureException(error, stackTrace: stack);
},
);
2026-01-11 19:42:22 +08:00
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
sl<AuthCubit>().appStarted();
final deviceStatusBloc = sl<DeviceStatusBloc>();
final localeCubit = sl<LocaleCubit>();
2026-06-01 17:33:16 +08:00
2026-01-11 19:42:22 +08:00
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(create: (_) => sl<AuthCubit>()),
2026-01-18 20:21:14 +08:00
BlocProvider<AppUserCubit>(create: (_) => sl<AppUserCubit>()),
2026-01-11 19:42:22 +08:00
BlocProvider<LoginCubit>(create: (_) => sl<LoginCubit>()),
2026-01-18 20:21:14 +08:00
BlocProvider<DevicesCubit>(
create: (_) {
final user = sl<AppUserCubit>().state.user;
final devicesCubit = sl<DevicesCubit>();
if (user != null) devicesCubit.fetchAllDevices(user.username);
return devicesCubit;
},
2026-01-18 20:21:14 +08:00
),
BlocProvider<DeviceStatusBloc>.value(value: deviceStatusBloc),
BlocProvider<LocaleCubit>.value(value: localeCubit),
2026-05-13 10:34:07 +08:00
BlocProvider<TabConfigCubit>.value(value: sl<TabConfigCubit>()),
2026-06-01 17:33:16 +08:00
BlocProvider<PermissionRequestBloc>(
create: (_) => sl<PermissionRequestBloc>(),
),
BlocProvider<UpdateCubit>(
create: (_) => UpdateCubit(VersionCheckService()),
),
2026-01-11 19:42:22 +08:00
],
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!));
},
);
},
),
2026-01-11 19:42:22 +08:00
);
}
2026-01-21 13:27:55 +08:00
}
class _LifecycleListener extends StatefulWidget {
final Widget? child;
const _LifecycleListener({required this.child});
@override
State<_LifecycleListener> createState() => _LifecycleListenerState();
}
2026-06-01 17:33:16 +08:00
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(
2026-06-01 17:33:16 +08:00
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),
2026-06-01 17:33:16 +08:00
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
2026-06-01 17:33:16 +08:00
const Text(
'🎉 发现新版本',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
2026-06-01 17:33:16 +08:00
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),
2026-06-01 17:33:16 +08:00
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),
2026-06-01 17:33:16 +08:00
border: Border.all(
color: Colors.blue.shade200,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'💡 选择下载方式',
2026-06-01 17:33:16 +08:00
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
const Text(
'• 自动安装:App 内下载并调起系统安装\n'
'• 手动下载:在浏览器中下载安装',
2026-06-01 17:33:16 +08:00
style: TextStyle(
fontSize: 13,
height: 1.5,
),
),
],
),
),
const SizedBox(height: 16),
],
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (!state.versionInfo.forceUpdate)
TextButton(
2026-06-01 17:33:16 +08:00
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) {
2026-06-01 17:33:16 +08:00
context
.read<UpdateCubit>()
.downloadAndInstallApk(
info.apkUrl!,
);
}
},
child: const Text('手动下载'),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
final info = state.versionInfo;
if (info.apkUrl != null) {
2026-06-01 17:33:16 +08:00
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(
'📱 整包更新步骤',
2026-06-01 17:33:16 +08:00
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...',
2026-06-01 17:33:16 +08:00
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()}%',
2026-06-01 17:33:16 +08:00
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 16),
Text(
'请稍候...',
2026-06-01 17:33:16 +08:00
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: [
2026-06-01 17:33:16 +08:00
const Icon(
Icons.check_circle,
color: Colors.green,
size: 60,
),
const SizedBox(height: 16),
2026-06-01 17:33:16 +08:00
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: [
2026-06-01 17:33:16 +08:00
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(),
// ),
],
),
),
),
),
],
);
},
);
}
2026-06-01 17:33:16 +08:00
/// 构建步骤项
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,
),
),
),
2026-06-01 17:33:16 +08:00
if (completed) const Icon(Icons.check, color: Colors.green, size: 20),
],
);
}
}