2026-01-21 13:27:55 +08:00
|
|
|
|
import 'dart:async';
|
2026-06-16 17:34:00 +08:00
|
|
|
|
import 'dart:io';
|
2026-01-19 18:24:24 +08:00
|
|
|
|
|
2026-01-11 19:42:22 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
2026-06-16 17:34:00 +08:00
|
|
|
|
import 'package:flutter/scheduler.dart';
|
2026-04-24 15:24:43 +08:00
|
|
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
2026-01-11 19:42:22 +08:00
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
2026-05-27 16:05:21 +08:00
|
|
|
|
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';
|
2026-05-27 16:05:21 +08:00
|
|
|
|
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-06-18 16:41:02 +08:00
|
|
|
|
import 'package:maibu_satabot_v2/core/network/mqtt/mqtt_manager.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';
|
2026-04-24 15:24:43 +08:00
|
|
|
|
import 'core/localization/app_localizations.dart';
|
|
|
|
|
|
import 'core/localization/locale_cubit.dart';
|
2026-03-16 16:55:03 +08:00
|
|
|
|
import 'features/auth/presentation/bloc/auth_state.dart';
|
2026-01-11 19:42:22 +08:00
|
|
|
|
import 'features/auth/presentation/bloc/login_cubit.dart';
|
2026-02-27 15:33:59 +08:00
|
|
|
|
import 'features/devices/presentation/bloc/device_status_bloc.dart';
|
2026-06-18 16:41:02 +08:00
|
|
|
|
import 'features/devices/presentation/bloc/device_task_cubit.dart'; // 🔥 添加 DeviceTaskCubit
|
2026-05-14 14:18:37 +08:00
|
|
|
|
import 'features/home/presentation/bloc/permission_request_bloc.dart';
|
2026-06-16 17:34:00 +08:00
|
|
|
|
import 'features/remote_control/presentation/bloc/remote_control_cubit.dart';
|
2026-06-12 15:00:44 +08:00
|
|
|
|
|
2026-05-27 16:05:21 +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
|
|
|
|
|
2026-05-27 16:05:21 +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
|
|
|
|
|
2026-06-18 16:41:02 +08:00
|
|
|
|
// 🔥 初始化 MQTT 连接(异步,不阻塞启动)
|
|
|
|
|
|
MqttManager().initialize().catchError((e) {
|
|
|
|
|
|
debugPrint('⚠️ [Main] MQTT 初始化失败,但不影响应用启动: $e');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-29 14:13:34 +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();
|
2026-03-05 21:07:33 +08:00
|
|
|
|
final deviceStatusBloc = sl<DeviceStatusBloc>();
|
2026-04-24 15:24:43 +08:00
|
|
|
|
final localeCubit = sl<LocaleCubit>();
|
2026-06-01 17:33:16 +08:00
|
|
|
|
|
2026-01-11 19:42:22 +08:00
|
|
|
|
return MultiBlocProvider(
|
|
|
|
|
|
providers: [
|
2026-02-25 13:41:23 +08:00
|
|
|
|
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>(
|
2026-02-25 13:41:23 +08:00
|
|
|
|
create: (_) {
|
|
|
|
|
|
final user = sl<AppUserCubit>().state.user;
|
|
|
|
|
|
final devicesCubit = sl<DevicesCubit>();
|
2026-06-18 16:41:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 🔥 修复:检查是否已有 targetDevice,如果有则不需要加载设备列表
|
|
|
|
|
|
final remoteControlCubit = sl<RemoteControlCubit>();
|
|
|
|
|
|
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},跳过设备列表加载');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 13:41:23 +08:00
|
|
|
|
return devicesCubit;
|
|
|
|
|
|
},
|
2026-01-18 20:21:14 +08:00
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
BlocProvider<DeviceStatusBloc>.value(value: deviceStatusBloc),
|
2026-04-24 15:24:43 +08:00
|
|
|
|
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-06-16 17:34:00 +08:00
|
|
|
|
BlocProvider<RemoteControlCubit>(
|
|
|
|
|
|
create: (_) => sl<RemoteControlCubit>(),
|
|
|
|
|
|
),
|
2026-06-18 16:41:02 +08:00
|
|
|
|
BlocProvider<DeviceTaskCubit>(
|
|
|
|
|
|
create: (_) => sl<DeviceTaskCubit>(),
|
|
|
|
|
|
),
|
2026-01-11 19:42:22 +08:00
|
|
|
|
],
|
2026-04-24 15:24:43 +08:00
|
|
|
|
child: BlocBuilder<LocaleCubit, Locale>(
|
|
|
|
|
|
bloc: localeCubit,
|
|
|
|
|
|
builder: (context, locale) {
|
|
|
|
|
|
return MaterialApp.router(
|
|
|
|
|
|
title: 'Maibu Satabot',
|
|
|
|
|
|
locale: locale,
|
2026-05-27 16:05:21 +08:00
|
|
|
|
supportedLocales: const [Locale('zh', 'CN'), Locale('en', 'US')],
|
2026-04-24 15:24:43 +08:00
|
|
|
|
localizationsDelegates: const [
|
|
|
|
|
|
AppLocalizations.delegate,
|
|
|
|
|
|
GlobalMaterialLocalizations.delegate,
|
|
|
|
|
|
GlobalWidgetsLocalizations.delegate,
|
|
|
|
|
|
GlobalCupertinoLocalizations.delegate,
|
|
|
|
|
|
],
|
|
|
|
|
|
theme: AppTheme.lightTheme,
|
|
|
|
|
|
routerConfig: sl<GoRouter>(),
|
|
|
|
|
|
builder: (context, child) {
|
2026-06-16 17:34:00 +08:00
|
|
|
|
return _LifecycleListener(child: _UpdateChecker(child: child!));
|
2026-04-24 15:24:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-01-11 19:42:22 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-21 13:27:55 +08:00
|
|
|
|
}
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
2026-03-16 16:55:03 +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 {
|
2026-03-16 16:55:03 +08:00
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
2026-05-27 16:05:21 +08:00
|
|
|
|
if (state == AppLifecycleState.resumed) {
|
|
|
|
|
|
debugPrint('📱 [生命周期] 应用恢复到前台');
|
|
|
|
|
|
_handleResume();
|
2026-03-16 16:55:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _handleResume() async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final authCubit = context.read<AuthCubit>();
|
2026-05-27 16:05:21 +08:00
|
|
|
|
if (authCubit.state is! AuthAuthenticated) return;
|
2026-07-02 16:04:30 +08:00
|
|
|
|
|
|
|
|
|
|
// 🔥 关键修复:只有在用户已选择设备时才重连 TCP
|
|
|
|
|
|
// 否则创建一个无用的 TCP 连接,服务端会残留 session,
|
|
|
|
|
|
// 导致后续 connectBySwitch() 复用该连接时触发 have_logged_in
|
|
|
|
|
|
final remoteControlCubit = sl<RemoteControlCubit>();
|
|
|
|
|
|
if (remoteControlCubit.state.targetDevice == null) {
|
|
|
|
|
|
debugPrint('📱 [生命周期] 未选择设备,跳过 TCP 重连(避免创建无用连接)');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 16:55:03 +08:00
|
|
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
|
|
await authCubit.reconnectAfterResume();
|
2026-05-27 16:05:21 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
debugPrint('❌ [生命周期] 重连错误:$e');
|
2026-03-16 16:55:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 16:05:21 +08:00
|
|
|
|
@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();
|
2026-07-11 16:46:53 +08:00
|
|
|
|
// TODO: 暂时注释掉应用启动时的自动更新检查
|
|
|
|
|
|
// Future.delayed(const Duration(seconds: 2), () {
|
|
|
|
|
|
// if (mounted) context.read<UpdateCubit>().checkUpdate();
|
|
|
|
|
|
// });
|
2026-05-27 16:05:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 16:55:03 +08:00
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-05-27 16:05:21 +08:00
|
|
|
|
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,
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
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),
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
if (state.versionInfo.updateDesc.isNotEmpty) ...[
|
|
|
|
|
|
const SizedBox(height: 8),
|
2026-06-01 17:33:16 +08:00
|
|
|
|
const Text(
|
|
|
|
|
|
'更新内容:',
|
|
|
|
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
Text(state.versionInfo.updateDesc),
|
|
|
|
|
|
],
|
|
|
|
|
|
const SizedBox(height: 20),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
if (state.versionInfo.updateType != 'patch') ...[
|
|
|
|
|
|
// 🔥 整包更新:显示两个选项
|
|
|
|
|
|
Container(
|
|
|
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
|
|
|
decoration: BoxDecoration(
|
2026-06-16 17:34:00 +08:00
|
|
|
|
color: Colors.orange.shade50,
|
2026-05-29 14:13:34 +08:00
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
2026-06-01 17:33:16 +08:00
|
|
|
|
border: Border.all(
|
2026-06-16 17:34:00 +08:00
|
|
|
|
color: Colors.orange.shade200,
|
2026-06-01 17:33:16 +08:00
|
|
|
|
),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
const Text(
|
|
|
|
|
|
'💡 选择下载方式',
|
2026-06-01 17:33:16 +08:00
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
const Text(
|
2026-06-16 17:34:00 +08:00
|
|
|
|
'• 浏览器下载(推荐):打开浏览器下载,手动安装并卸载旧版本\n'
|
|
|
|
|
|
'• App 内下载:App 内下载 APK,调起系统安装界面',
|
2026-06-01 17:33:16 +08:00
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
height: 1.5,
|
|
|
|
|
|
),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
],
|
2026-05-27 16:05:21 +08:00
|
|
|
|
Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
if (!state.versionInfo.forceUpdate)
|
|
|
|
|
|
TextButton(
|
2026-06-01 17:33:16 +08:00
|
|
|
|
onPressed: () => context
|
|
|
|
|
|
.read<UpdateCubit>()
|
|
|
|
|
|
.cancelUpdate(),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
child: const Text('稍后'),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 10),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
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-16 17:34:00 +08:00
|
|
|
|
// 🔥 浏览器下载 + 引导弹窗
|
2026-06-01 17:33:16 +08:00
|
|
|
|
context
|
|
|
|
|
|
.read<UpdateCubit>()
|
2026-06-16 17:34:00 +08:00
|
|
|
|
.fullApkUpdateWithBrowser(
|
2026-06-01 17:33:16 +08:00
|
|
|
|
info.apkUrl!,
|
|
|
|
|
|
);
|
2026-06-16 17:34:00 +08:00
|
|
|
|
// 显示引导弹窗
|
|
|
|
|
|
showBrowserDownloadGuide(context);
|
2026-05-29 14:13:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-06-16 17:34:00 +08:00
|
|
|
|
child: const Text('浏览器下载'),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 10),
|
|
|
|
|
|
ElevatedButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
final info = state.versionInfo;
|
|
|
|
|
|
if (info.apkUrl != null) {
|
2026-06-16 17:34:00 +08:00
|
|
|
|
// App 内下载(保留备用)
|
2026-06-01 17:33:16 +08:00
|
|
|
|
context
|
|
|
|
|
|
.read<UpdateCubit>()
|
|
|
|
|
|
.downloadAndInstallApk(
|
|
|
|
|
|
info.apkUrl!,
|
|
|
|
|
|
);
|
2026-05-29 14:13:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-06-16 17:34:00 +08:00
|
|
|
|
child: const Text('App 内下载'),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-05-27 16:05:21 +08:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
// 🔥 整包更新步骤引导弹窗
|
|
|
|
|
|
if (state is UpdateInstalling && !(state.isPatch ?? false))
|
2026-05-27 16:05:21 +08:00
|
|
|
|
Positioned.fill(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.7),
|
|
|
|
|
|
child: Center(
|
|
|
|
|
|
child: Card(
|
2026-05-29 14:13:34 +08:00
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 30),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(24.0),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
2026-05-29 14:13:34 +08:00
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
2026-05-27 16:05:21 +08:00
|
|
|
|
children: [
|
2026-05-29 14:13:34 +08:00
|
|
|
|
const Text(
|
|
|
|
|
|
'📱 整包更新步骤',
|
2026-06-01 17:33:16 +08:00
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
const SizedBox(height: 16),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
_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('我知道了'),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// 🔥 下载进度显示(差量补丁和整包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,
|
|
|
|
|
|
),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
Text(
|
2026-05-29 14:13:34 +08:00
|
|
|
|
'请稍候...',
|
2026-06-01 17:33:16 +08:00
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
color: Colors.grey.shade600,
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// 🔥 更新成功提示
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
const SizedBox(height: 16),
|
2026-06-01 17:33:16 +08:00
|
|
|
|
const Text(
|
|
|
|
|
|
'✅ 更新已完成',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
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('我知道了'),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-06-05 19:06:21 +08:00
|
|
|
|
// 调试面板 - 暂时关闭
|
|
|
|
|
|
// 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(),
|
|
|
|
|
|
// // ),
|
|
|
|
|
|
// ],
|
|
|
|
|
|
// ),
|
|
|
|
|
|
// ),
|
|
|
|
|
|
// ),
|
|
|
|
|
|
// ),
|
2026-05-27 16:05:21 +08:00
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-03-16 16:55:03 +08:00
|
|
|
|
}
|
2026-06-01 17:33:16 +08:00
|
|
|
|
|
2026-05-29 14:13:34 +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),
|
2026-05-29 14:13:34 +08:00
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-05-27 16:05:21 +08:00
|
|
|
|
}
|
2026-06-16 17:34:00 +08:00
|
|
|
|
|
|
|
|
|
|
/// 显示浏览器下载引导弹窗(全局方法)
|
|
|
|
|
|
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('我知道了'),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|