app/lib/app.dart
2025-11-13 20:24:09 +08:00

403 lines
14 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:hive_ce/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/conf/conf.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/guide/guide_ui.dart';
import 'package:starcitizen_doctor/ui/home/performance/performance_ui.dart';
import 'package:starcitizen_doctor/ui/splash_ui.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:uuid/uuid.dart';
import 'api/analytics.dart';
import 'api/api.dart';
import 'common/conf/url_conf.dart';
import 'common/helper/system_helper.dart';
import 'common/io/rs_http.dart';
// import 'common/rust/api/win32_api.dart' as win32; // Web 不支持
import 'data/app_version_data.dart';
import 'generated/no_l10n_strings.dart';
import 'ui/home/downloader/home_downloader_ui.dart';
import 'ui/home/game_doctor/game_doctor_ui.dart';
import 'ui/home/localization/advanced_localization_ui.dart';
import 'ui/index_ui.dart';
import 'ui/settings/upgrade_dialog.dart';
import 'ui/tools/unp4kc/unp4kc_ui.dart';
part 'app.g.dart';
part 'app.freezed.dart';
@freezed
abstract class AppGlobalState with _$AppGlobalState {
const factory AppGlobalState({
String? deviceUUID,
String? applicationSupportDir,
String? applicationBinaryModuleDir,
AppVersionData? networkVersionData,
@Default(ThemeConf()) ThemeConf themeConf,
Locale? appLocale,
Box? appConfBox,
@Default("assets/backgrounds/SC_01_Wallpaper_3840x2160.webp") String backgroundImageAssetsPath,
}) = _AppGlobalState;
}
@riverpod
GoRouter router(Ref ref) {
return GoRouter(
initialLocation: '/splash',
routes: [
GoRoute(path: '/splash', pageBuilder: (context, state) => myPageBuilder(context, state, const SplashUI())),
GoRoute(
path: '/',
pageBuilder: (context, state) => myPageBuilder(context, state, const IndexUI()),
routes: [
GoRoute(
path: "downloader",
pageBuilder: (context, state) => myPageBuilder(context, state, const HomeDownloaderUI()),
),
GoRoute(
path: 'game_doctor',
pageBuilder: (context, state) => myPageBuilder(context, state, const HomeGameDoctorUI()),
),
GoRoute(
path: 'performance',
pageBuilder: (context, state) => myPageBuilder(context, state, const HomePerformanceUI()),
),
GoRoute(
path: 'advanced_localization',
pageBuilder: (context, state) => myPageBuilder(context, state, const AdvancedLocalizationUI()),
),
],
),
GoRoute(
path: '/tools',
builder: (_, _) => const SizedBox(),
routes: [
GoRoute(path: 'unp4kc', pageBuilder: (context, state) => myPageBuilder(context, state, const UnP4kcUI())),
],
),
GoRoute(path: '/guide', pageBuilder: (context, state) => myPageBuilder(context, state, const GuideUI())),
],
);
}
@riverpod
class AppGlobalModel extends _$AppGlobalModel {
static Map<Locale, String> get appLocaleSupport => {
const Locale("auto"): S.current.settings_app_language_auto,
const Locale("zh", "CN"): NoL10n.langZHS,
const Locale("zh", "TW"): NoL10n.langZHT,
const Locale("en"): NoL10n.langEn,
const Locale("ja"): NoL10n.langJa,
const Locale("ru"): NoL10n.langRU,
};
@override
AppGlobalState build() {
return const AppGlobalState();
}
bool _initialized = false;
Future<void> initApp() async {
if (_initialized) return;
// init Data
final applicationSupportDir = await _initAppDir();
await RSHttp.init();
dPrint("---- rust bridge init -----");
// init Hive
try {
if (!kIsWeb) Hive.init("$applicationSupportDir/db");
final box = await Hive.openBox("app_conf");
state = state.copyWith(appConfBox: box);
// if (box.get("install_id", defaultValue: "") == "") {
// await box.put("install_id", const Uuid().v4());
// AnalyticsApi.touch("firstLaunch");
// }
final deviceUUID = box.get("install_id", defaultValue: "");
final localeCode = box.get("app_locale", defaultValue: null);
Locale? locale;
if (localeCode != null) {
final localeSplit = localeCode.toString().split("_");
if (localeSplit.length == 2 && localeSplit[1].isNotEmpty) {
locale = Locale(localeSplit[0], localeSplit[1]);
} else {
locale = Locale(localeSplit[0]);
}
}
state = state.copyWith(deviceUUID: deviceUUID, appLocale: locale);
} catch (e) {
// Web 平台不支持 win32 API
if (!kIsWeb) {
// await win32.setForegroundWindow(windowName: "SCToolBox");
}
dPrint("exit: db is locking ...");
if (!kIsWeb) exit(0);
}
// init powershell
if (!kIsWeb && Platform.isWindows) {
try {
await SystemHelper.initPowershellPath();
dPrint("---- Powershell init -----");
} catch (e) {
dPrint("powershell init failed : $e");
}
}
// get windows info
// WindowsDeviceInfo? windowsDeviceInfo;
// try {
// DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
// windowsDeviceInfo = await deviceInfo.windowsInfo;
// } catch (e) {
// dPrint("DeviceInfo.windowsInfo error: $e");
// }
// init windows
// if (!kIsWeb) {
// windowManager.waitUntilReadyToShow().then((_) async {
// await windowManager.setTitle("SCToolBox");
// await windowManager.setSkipTaskbar(false);
// await windowManager.show();
// if (Platform.isWindows) {
// await Window.initialize();
// await Window.hideWindowControls();
// if (windowsDeviceInfo?.productName.contains("Windows 11") ?? false) {
// await Window.setEffect(effect: WindowEffect.acrylic);
// }
// }
// });
// }
dPrint("---- Window init -----");
if (kIsWeb) {
_startBackgroundLoop();
}
_initialized = true;
ref.keepAlive();
}
Timer? _loopTimer;
void _startBackgroundLoop() async {
_loopTimer?.cancel();
_loopTimer = null;
final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle);
final imageAssetsList = assetManifest
.listAssets()
.where((string) => string.startsWith("assets/backgrounds"))
.toList();
void rollImage() {
final random = DateTime.now().millisecondsSinceEpoch % imageAssetsList.length;
final image = imageAssetsList[random];
state = state.copyWith(backgroundImageAssetsPath: image);
dPrint("rollImage: [$random] $image");
}
rollImage();
// 使用 timer 每 30 秒 更换一次随机图片
_loopTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
rollImage();
});
}
String getUpgradePath() {
return "${state.applicationSupportDir}/._upgrade";
}
bool isInOnlineMode() => state.networkVersionData != null;
// ignore: avoid_build_context_in_providers
Future<bool> checkUpdate(BuildContext context) async {
if (!kIsWeb && !ConstConf.isMSE) {
final dir = Directory(getUpgradePath());
if (await dir.exists()) {
dir.delete(recursive: true);
}
}
dynamic checkUpdateError;
try {
final networkVersionData = await Api.getAppVersion();
dPrint("networkVersionData == ${networkVersionData.toJson()}");
AppConf.setNetworkChannels(networkVersionData.gameChannels);
checkActivityThemeColor(networkVersionData);
if (ConstConf.isMSE) {
dPrint("lastVersion=${networkVersionData.mSELastVersion} ${networkVersionData.mSELastVersionCode}");
} else {
dPrint("lastVersion=${networkVersionData.lastVersion} ${networkVersionData.lastVersionCode}");
}
state = state.copyWith(networkVersionData: networkVersionData);
if (networkVersionData.nav42KitUrl != null) {
URLConf.nav42KitUrl = networkVersionData.nav42KitUrl!;
}
} catch (e) {
checkUpdateError = e;
dPrint("_checkUpdate Error:$e");
}
await Future.delayed(const Duration(milliseconds: 100));
if (state.networkVersionData == null) {
if (!context.mounted) return false;
await showToast(
context,
S.current.app_common_network_error(ConstConf.appVersionDate, checkUpdateError.toString()),
);
return false;
}
if (kIsWeb) return false; // Web 版本不支持自动更新
if (!Platform.isWindows) return false;
final lastVersion = ConstConf.isMSE
? state.networkVersionData?.mSELastVersionCode
: state.networkVersionData?.lastVersionCode;
if ((lastVersion ?? 0) > ConstConf.appVersionCode) {
// need update
if (!context.mounted) return false;
final r = await showDialog(
dismissWithEsc: false,
context: context,
builder: (context) => const UpgradeDialogUI(),
);
if (r != true) {
if (!context.mounted) return false;
await showToast(context, S.current.app_common_upgrade_info_error);
return false;
}
return true;
}
return false;
}
Timer? _activityThemeColorTimer;
void checkActivityThemeColor(AppVersionData networkVersionData) {
if (_activityThemeColorTimer != null) {
_activityThemeColorTimer?.cancel();
_activityThemeColorTimer = null;
}
final startTime = networkVersionData.activityColors?.startTime;
final endTime = networkVersionData.activityColors?.endTime;
if (startTime == null || endTime == null) return;
final now = DateTime.now().millisecondsSinceEpoch;
dPrint("now == $now start == $startTime end == $endTime");
if (now < startTime) {
_activityThemeColorTimer = Timer(
Duration(milliseconds: startTime - now),
() => checkActivityThemeColor(networkVersionData),
);
dPrint("start Timer ....");
} else if (now >= startTime && now <= endTime) {
dPrint("update Color ....");
// update Color
final colorCfg = networkVersionData.activityColors;
state = state.copyWith(
themeConf: ThemeConf(
backgroundColor: HexColor(colorCfg?.background ?? "#132431").withValues(alpha: .75),
menuColor: HexColor(colorCfg?.menu ?? "#132431").withValues(alpha: .95),
micaColor: HexColor(colorCfg?.mica ?? "#0A3142"),
),
);
// wait for end
_activityThemeColorTimer = Timer(
Duration(milliseconds: endTime - now),
() => checkActivityThemeColor(networkVersionData),
);
} else {
dPrint("reset Color ....");
state = state.copyWith(
themeConf: ThemeConf(
backgroundColor: HexColor("#132431").withValues(alpha: .75),
menuColor: HexColor("#132431").withValues(alpha: .95),
micaColor: HexColor("#0A3142"),
),
);
}
}
void changeLocale(dynamic value) async {
final appConfBox = await Hive.openBox("app_conf");
if (value is Locale) {
if (value.languageCode == "auto") {
state = state.copyWith(appLocale: null);
await appConfBox.put("app_locale", null);
return;
}
final localeCode = value.countryCode != null
? "${value.languageCode}_${value.countryCode ?? ""}"
: value.languageCode;
dPrint("changeLocale == $value localeCode=== $localeCode");
await appConfBox.put("app_locale", localeCode);
state = state.copyWith(appLocale: value);
}
}
Future<String> _initAppDir() async {
if (kIsWeb) {
await Future.delayed(const Duration(milliseconds: 10));
// Web 版本不需要本地目录
state = state.copyWith(applicationSupportDir: "", applicationBinaryModuleDir: "");
return Future.value("");
}
if (Platform.isWindows) {
final userProfileDir = Platform.environment["USERPROFILE"];
final applicationSupportDir = (await getApplicationSupportDirectory()).absolute.path;
String? applicationBinaryModuleDir;
try {
await initDPrintFile(applicationSupportDir);
} catch (e) {
dPrint("initDPrintFile Error: $e");
}
if (ConstConf.isMSE && userProfileDir != null) {
applicationBinaryModuleDir = "$userProfileDir\\AppData\\Local\\Temp\\SCToolbox\\modules";
} else {
applicationBinaryModuleDir = "$applicationSupportDir\\modules";
}
dPrint("applicationSupportDir == $applicationSupportDir");
dPrint("applicationBinaryModuleDir == $applicationBinaryModuleDir");
state = state.copyWith(
applicationSupportDir: applicationSupportDir,
applicationBinaryModuleDir: applicationBinaryModuleDir,
);
return applicationSupportDir;
} else {
final applicationSupportDir = (await getApplicationSupportDirectory()).absolute.path;
final applicationBinaryModuleDir = "$applicationSupportDir/modules";
dPrint("applicationSupportDir == $applicationSupportDir");
dPrint("applicationBinaryModuleDir == $applicationBinaryModuleDir");
state = state.copyWith(
applicationSupportDir: applicationSupportDir,
applicationBinaryModuleDir: applicationBinaryModuleDir,
);
return applicationSupportDir;
}
}
}
@freezed
abstract class ThemeConf with _$ThemeConf {
const factory ThemeConf({
@Default(Color(0xbf132431)) Color backgroundColor,
@Default(Color(0xf2132431)) Color menuColor,
@Default(Color(0xff0a3142)) Color micaColor,
}) = _ThemeConf;
}