You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
11 KiB

import 'dart:async';
import 'dart:io';
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:http/http.dart' as http;
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart';
import '../../../data/services/network/api.dart';
import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/version_update_dialog.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart';
import '../../../core/utils/logger.dart';
class SplashController extends GetxController {
final GetStorage _storage = GetStorage();
static const String _tag = 'SplashCtrl';
final versionService = VersionUpdateService.to;
// 进度条动画控制
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
// 隐私政策同意状态 - 新增
final _hasAcceptedPrivacy = false.obs;
RxBool get hasAcceptedPrivacy => _hasAcceptedPrivacy;
// 使用一个反应式变量来决定是否可以导航,替代所有布尔标记
final _canNavigate = false.obs;
@override
void onInit() {
super.onInit();
// 初始化隐私政策状态
_hasAcceptedPrivacy.value =
_storage.read<bool>('hasAcceptedPrivacy') ?? false;
// 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航
once(_canNavigate, (_) => _navigateToTargetPage());
// 启动整个初始化流程
_initializeApp();
}
/// 启动并协调所有初始化任务
Future<void> _initializeApp() async {
// 步骤 1: 检查并等待隐私政策同意
final hasAccepted = await _checkAndRequestPrivacy();
if (!hasAccepted) {
// 如果用户拒绝隐私政策,应用会退出,后续代码不执行
return;
}
// 步骤 3: 并行执行所有启动任务,并等待它们全部完成
// Future.wait 会等待数组中所有的 Future 都完成后才继续
await Future.wait([
_startLoadingAnimation(), // 任务1: 最小动画时长
_performParallelTasks(), // 任务2: 其他所有后台任务
]);
// 步骤 4: 所有任务(动画和后台)都已完成,触发导航
_canNavigate.value = true;
}
/// 切换隐私政策同意状态 - 新增方法
void togglePrivacyAcceptance() {
_hasAcceptedPrivacy.value = !_hasAcceptedPrivacy.value;
_storage.write('hasAcceptedPrivacy', _hasAcceptedPrivacy.value);
}
/// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作
/// 返回用户是否同意
Future<bool> _checkAndRequestPrivacy() async {
if (_storage.read<bool>('hasAcceptedPrivacy') ?? false) {
_hasAcceptedPrivacy.value = true;
return true;
}
final completer = Completer<bool>();
WidgetsBinding.instance.addPostFrameCallback((_) {
PrivacyPolicyDialog.show(
onAccept: () {
_storage.write('hasAcceptedPrivacy', true);
_hasAcceptedPrivacy.value = true;
Get.back(); // 关闭弹窗
if (!completer.isCompleted) completer.complete(true);
},
onReject: () {
if (!completer.isCompleted) completer.complete(false);
exit(0); // 退出应用
},
canDismiss: false,
);
});
return completer.future;
}
/// 网络连接检查
/// 网络连接检查 - 阻塞版本
Future<void> _checkNetworkStatus() async {
while (true) {
// 无限循环直到网络检查成功
try {
final connectivityResult = await Connectivity().checkConnectivity();
// 检查是否有网络连接
if (connectivityResult.contains(ConnectivityResult.none)) {
// 显示弹窗并等待用户操作
await _showNoNetworkDialogAndWait();
continue; // 重新检查
}
// 进一步检查网络连通性
final hasInternet = await _checkInternetConnection();
if (!hasInternet) {
// 显示弹窗并等待用户操作
await _showNoNetworkDialogAndWait();
continue; // 重新检查
}
// 网络检查成功,跳出循环
Logger.d(_tag, '网络检查成功,继续APP初始化流程');
break;
} catch (e) {
Logger.e(_tag, '网络检查失败: $e');
// 显示弹窗并等待用户操作
await _showNoNetworkDialogAndWait();
continue; // 重新检查
}
}
}
/// 检查实际的网络连通性
Future<bool> _checkInternetConnection() async {
final testUrls = [
'https://api.voitrans.net/ping', // 国内服
'https://hw.api.voitrans.net/ping', // 海外服
'https://www.baidu.com', // 百度
'https://www.google.com', // Google
];
for (final url in testUrls) {
try {
final response = await http.get(
Uri.parse(url.trim()), // 去除空格
headers: {'User-Agent': 'Voitrans-App'}, // 有助于后端识别
).timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
Logger.d(_tag, '网络连接检查成功: $url');
return true;
}
} on TimeoutException {
Logger.w(_tag, '网络连接超时: $url');
continue;
} on Exception catch (e) {
Logger.w(_tag, '网络连接失败: $url, 错误: $e');
continue;
}
}
Logger.e(_tag, '所有网络连接检查均失败');
return false;
}
/// 动画任务:返回一个在动画结束时完成的 Future
Future<void> _startLoadingAnimation() {
const totalDuration = 1500; // 动画总时长1.5秒
const steps = totalDuration ~/ 30;
const stepValue = 1.0 / steps;
final completer = Completer<void>();
Timer.periodic(const Duration(milliseconds: 30), (timer) {
if (_loadingProgress.value < 1.0) {
_loadingProgress.value += stepValue;
} else {
_loadingProgress.value = 1.0;
timer.cancel();
if (!completer.isCompleted) completer.complete();
}
});
return completer.future;
}
/// 将所有并行的后台任务打包成一个 Future
Future<void> _performParallelTasks() async {
//步骤2:检查网络连接状态
await _checkNetworkStatus();
// 同样使用 Future.wait,确保此函数在所有子任务完成后才结束
await Future.wait([
_requestTrackingPermissionAsync(),
_validateTokenAsync(),
_performVersionCheckAsync(),
]);
Logger.d(_tag, '所有后台任务已完成');
}
/// 异步请求追踪权限
Future<void> _requestTrackingPermissionAsync() async {
try {
await AppTrackingTransparency.requestTrackingAuthorization();
Logger.d(_tag, '用户追踪权限请求完成');
} catch (e) {
Logger.e(_tag, '用户追踪权限请求失败: $e');
// 即使失败,也视为完成,不阻塞主流程
}
}
/// 异步验证Token和加载应用配置
Future<void> _validateTokenAsync() async {
try {
final token = _storage.read('logintoken');
if (token != null && token.isNotEmpty) {
final isLoggedIn = await getLoginToken();
if (isLoggedIn) {
// 只有登录成功,才获取App配置
await getAppConfig();
} else {
await User.clearUserSession(); //
}
}
} catch (e) {
Logger.e(_tag, 'Token验证或配置加载失败: $e');
await User.clearUserSession();
}
}
/// 异步执行版本检查
Future<void> _performVersionCheckAsync() async {
try {
final versionService = VersionUpdateService.to;
final updateInfo = await versionService.checkForUpdate();
if (updateInfo.hasUpdate) {
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}');
final completer = Completer<void>();
// 在主线程中显示更新对话框
WidgetsBinding.instance.addPostFrameCallback((_) {
// 使用 whenComplete 确保无论弹窗如何关闭,Future 都会完成
VersionUpdateDialog.show(updateInfo).whenComplete(() {
if (!completer.isCompleted) completer.complete();
});
});
// 等待弹窗交互结束
await completer.future;
} else {
Logger.i(_tag, '当前已是最新版本');
}
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也视为任务完成
}
}
/// 根据最终状态跳转到目标页面
void _navigateToTargetPage() {
// 导航时直接检查最终的数据状态,而不是依赖中间变量
final token = _storage.read('logintoken');
final isAppConfigInitialized = AppConfig.isInitialized();
// 检查登录状态和配置初始化状态
if (token != null && token.isNotEmpty && isAppConfigInitialized) {
Get.offAllNamed(Routes.pairing);
} else {
Get.offAllNamed(Routes.login);
}
}
/// 异步获取登录Token
Future<bool> getLoginToken() async {
try {
final appVersion = versionService.cachedAppVersion;
final response = await Api.tokenlogin({
'version': appVersion,
});
if (response?['user'] != null && response['user'] != '') {
final user = User.fromJson(response);
await _storage.write('user_info', user.toJson());
return true;
}
return false;
} catch (e) {
Logger.e(_tag, 'Token获取失败: $e');
return false;
}
}
Future<bool> getAppConfig() async {
try {
final response = await Api.getappconfig();
if (response == null) return false;
final resp = UserGetAppConfigResp.fromJson(response);
AppConfig.initialize(resp);
return true;
} catch (e) {
Logger.e(_tag, 'getAppConfig 获取失败: $e');
return false;
}
}
/// 处理QQ音乐登录回调
void handleQQMusicLogin() {
try {
Logger.i(_tag, '处理QQ音乐登录回调');
// 这里可以添加具体的QQ音乐登录处理逻辑
// 例如:验证登录状态、更新用户信息、导航到相应页面等
// 如果需要导航到特定页面,可以使用:
// Get.toNamed(Routes.qqmusic);
} catch (e) {
Logger.e(_tag, '处理QQ音乐登录回调失败: $e');
}
}
}
/// 显示无网络连接对话框并等待用户操作(阻塞版本)
Future<void> _showNoNetworkDialogAndWait() async {
final completer = Completer<void>();
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.dialog(
AlertDialog(
title: Row(
children: [
const Icon(Icons.wifi_off, color: Colors.orange),
const SizedBox(width: 8),
Text('networkError'.tr), // 无网络连接
],
),
content: Text('networkConnectionFailed'.tr), // 网络连接失败,请检查网络设置
actions: [
TextButton(
onPressed: () {
Get.back(); // 关闭弹窗
completer.complete(); // 完成Future,继续执行
},
child: Text('retry'.tr), // 重试
),
],
),
barrierDismissible: false, // 不允许点击外部关闭
);
});
// 等待用户点击重试按钮
return completer.future;
}