import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dart_rss/domain/rss_item.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:starcitizen_doctor/api/analytics.dart'; import 'package:starcitizen_doctor/api/api.dart'; import 'package:starcitizen_doctor/api/rss.dart'; import 'package:starcitizen_doctor/common/conf/const_conf.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/common/helper/log_helper.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; import 'package:starcitizen_doctor/common/io/rs_http.dart'; import 'package:starcitizen_doctor/common/rust/api/win32_api.dart' as win32; import 'package:starcitizen_doctor/common/utils/async.dart'; import 'package:starcitizen_doctor/common/utils/base_utils.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/provider.dart'; import 'package:starcitizen_doctor/data/app_placard_data.dart'; import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart'; import 'package:starcitizen_doctor/data/countdown_festival_item_data.dart'; import 'package:starcitizen_doctor/ui/home/dialogs/home_game_login_dialog_ui.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:html/parser.dart' as html; import 'package:html/dom.dart' as html_dom; import '../webview/webview.dart'; import 'localization/localization_ui_model.dart'; part 'home_ui_model.freezed.dart'; part 'home_ui_model.g.dart'; @freezed class HomeUIModelState with _$HomeUIModelState { factory HomeUIModelState({ AppPlacardData? appPlacardData, @Default(false) bool isFixing, @Default("") String isFixingString, String? scInstalledPath, @Default([]) List scInstallPaths, AppWebLocalizationVersionsData? webLocalizationVersionsData, @Default("") String lastScreenInfo, List? rssVideoItems, List? rssTextItems, MapEntry? localizationUpdateInfo, List? scServerStatus, List? countdownFestivalListData, @Default({}) Map isGameRunning, }) = _HomeUIModelState; } extension HomeUIModelStateEx on HomeUIModelState { bool get isCurGameRunning => isGameRunning[scInstalledPath] ?? false; } @riverpod class HomeUIModel extends _$HomeUIModel { @override HomeUIModelState build() { state = HomeUIModelState(); _init(); _loadData(); return state; } closePlacard() async { final box = await Hive.openBox("app_conf"); await box.put("close_placard", state.appPlacardData?.version); state = state.copyWith(appPlacardData: null); } Future reScanPath() async { state = state.copyWith( scInstalledPath: "not_install", lastScreenInfo: S.current.home_action_info_scanning); try { final listData = await SCLoggerHelper.getLauncherLogList(); if (listData == null) { state = state.copyWith(scInstalledPath: "not_install"); return; } final scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData, withVersion: ["LIVE", "PTU", "EPTU"], checkExists: true); String scInstalledPath = "not_install"; if (scInstallPaths.isNotEmpty) { if (scInstallPaths.first.isNotEmpty) { scInstalledPath = scInstallPaths.first; } } final lastScreenInfo = S.current .home_action_info_scan_complete_valid_directories_found( scInstallPaths.length.toString()); state = state.copyWith( scInstalledPath: scInstalledPath, scInstallPaths: scInstallPaths, lastScreenInfo: lastScreenInfo); } catch (e) { state = state.copyWith( scInstalledPath: "not_install", lastScreenInfo: S.current.home_action_info_log_file_parse_fail); AnalyticsApi.touch("error_launchLogs"); // showToast(context!, // "${S.current.home_action_info_log_file_parse_fail} \n请关闭游戏,退出RSI启动器后重试,若仍有问题,请使用工具箱中的 RSI Launcher log 修复。"); } } String getRssImage(RssItem item) { final h = html.parse(item.description ?? ""); if (h.body == null) return ""; for (var node in h.body!.nodes) { if (node is html_dom.Element) { if (node.localName == "img") { return node.attributes["src"]?.trim() ?? ""; } } } return ""; } String handleTitle(String? title) { if (title == null) return ""; title = title.replaceAll("【", "[ "); title = title.replaceAll("】", " ] "); return title; } // ignore: avoid_build_context_in_providers Future goWebView(BuildContext context, String title, String url, {bool useLocalization = false, bool loginMode = false, RsiLoginCallback? rsiLoginCallback}) async { if (useLocalization) { const tipVersion = 2; final box = await Hive.openBox("app_conf"); final skip = await box.get("skip_web_localization_tip_version", defaultValue: 0); if (skip != tipVersion) { if (!context.mounted) return; final ok = await showConfirmDialogs( context, S.current.home_action_title_star_citizen_website_localization, Text( S.current.home_action_info_web_localization_plugin_disclaimer, style: const TextStyle(fontSize: 16), ), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * .6)); if (!ok) { if (loginMode) { rsiLoginCallback?.call(null, false); } return; } await box.put("skip_web_localization_tip_version", tipVersion); } } if (!await WebviewWindow.isWebviewAvailable()) { if (!context.mounted) return; showToast( context, S.current.home_login_action_title_need_webview2_runtime); launchUrlString( "https://developer.microsoft.com/en-us/microsoft-edge/webview2/"); return; } if (!context.mounted) return; final webViewModel = WebViewModel(context, loginMode: loginMode, loginCallback: rsiLoginCallback); if (useLocalization) { state = state.copyWith( isFixing: true, isFixingString: S.current.home_action_info_initializing_resources); try { await webViewModel.initLocalization(state.webLocalizationVersionsData!); } catch (e) { if (!context.mounted) return; showToast(context, S.current.home_action_info_initialization_failed(e)); } state = state.copyWith(isFixingString: "", isFixing: false); } await webViewModel.initWebView( title: title, applicationSupportDir: appGlobalState.applicationSupportDir!, appVersionData: appGlobalState.networkVersionData!, ); await Future.delayed(const Duration(milliseconds: 500)); await webViewModel.launch(url, appGlobalState.networkVersionData!); } bool isRSIServerStatusOK(Map map) { return (map["status"] == "ok" || map["status"] == "operational"); } Timer? _serverUpdateTimer; Timer? _appUpdateTimer; void _init() { reScanPath(); _serverUpdateTimer = Timer.periodic( const Duration(minutes: 10), (timer) { _updateSCServerStatus(); }, ); _appUpdateTimer = Timer.periodic(const Duration(minutes: 30), (timer) { checkLocalizationUpdate(); }); ref.onDispose(() { _serverUpdateTimer?.cancel(); _serverUpdateTimer = null; _appUpdateTimer?.cancel(); _appUpdateTimer = null; }); } void _loadData() async { if (appGlobalState.networkVersionData == null) return; try { final r = await Api.getAppPlacard(); final box = await Hive.openBox("app_conf"); final version = box.get("close_placard", defaultValue: ""); if (r.enable == true) { if (r.alwaysShow != true && version == r.version) { } else { state = state.copyWith(appPlacardData: r); } } final appWebLocalizationVersionsData = AppWebLocalizationVersionsData.fromJson(json.decode( (await RSHttp.getText( "${URLConf.webTranslateHomeUrl}/versions.json")))); final countdownFestivalListData = await Api.getFestivalCountdownList(); state = state.copyWith( webLocalizationVersionsData: appWebLocalizationVersionsData, countdownFestivalListData: countdownFestivalListData); _updateSCServerStatus(); _loadRRS(); } catch (e) { dPrint(e); } // check Localization update checkLocalizationUpdate(); } Future _updateSCServerStatus() async { try { final s = await Api.getScServerStatus(); dPrint("updateSCServerStatus===$s"); state = state.copyWith(scServerStatus: s); } catch (e) { dPrint(e); } } Future _loadRRS() async { try { final rssVideoItems = await RSSApi.getRssVideo(); state = state.copyWith(rssVideoItems: rssVideoItems); final rssTextItems = await RSSApi.getRssText(); state = state.copyWith(rssTextItems: rssTextItems); dPrint("RSS update Success !"); } catch (e) { dPrint("_loadRRS Error:$e"); } } Future checkLocalizationUpdate({bool skipReload = false}) async { dPrint("_checkLocalizationUpdate"); final updates = await (ref.read(localizationUIModelProvider.notifier)) .checkLangUpdate(skipReload: skipReload) .unwrap>(); if (updates == null || updates.isEmpty) { state = state.copyWith(localizationUpdateInfo: null); return; } state = state.copyWith(localizationUpdateInfo: MapEntry(updates.first, true)); if (_appUpdateTimer != null) { _appUpdateTimer?.cancel(); _appUpdateTimer = null; // 发送通知 await win32.sendNotify( summary: S.current.home_localization_new_version_available, body: S.current.home_localization_new_version_installed(updates.first), appName: S.current.home_title_app_name, appId: ConstConf.isMSE ? "56575xkeyC.MSE_bsn1nexg8e4qe!starcitizendoctor" : "{6D809377-6AF0-444B-8957-A3773F02200E}\\Starcitizen_Doctor\\starcitizen_doctor.exe"); } } // ignore: avoid_build_context_in_providers launchRSI(BuildContext context) async { if (state.scInstalledPath == "not_install") { showToast(context, S.current.home_info_valid_installation_required); return; } if (ConstConf.isMSE) { if (state.isCurGameRunning) { await Process.run( SystemHelper.powershellPath, ["ps \"StarCitizen\" | kill"]); return; } AnalyticsApi.touch("gameLaunch"); showDialog( context: context, dismissWithEsc: false, builder: (context) => HomeGameLoginDialogUI(context)); } else { final ok = await showConfirmDialogs( context, S.current.home_info_one_click_launch_warning, Text(S.current.home_info_account_security_warning), confirm: S.current.home_action_install_microsoft_store_version, cancel: S.current.home_action_cancel); if (ok == true) { await launchUrlString( "https://apps.microsoft.com/detail/9NF3SWFWNKL1?launch=true"); await Future.delayed(const Duration(seconds: 2)); exit(0); } } } void onChangeInstallPath(String? value) { if (value == null) return; state = state.copyWith(scInstalledPath: value); ref .read(localizationUIModelProvider.notifier) .onChangeGameInstallPath(value); } doLaunchGame( // ignore: avoid_build_context_in_providers BuildContext context, String launchExe, List args, String installPath, String? processorAffinity) async { var runningMap = Map.from(state.isGameRunning); runningMap[installPath] = true; state = state.copyWith(isGameRunning: runningMap); try { late ProcessResult result; if (processorAffinity == null) { result = await Process.run(launchExe, args); } else { dPrint("set Affinity === $processorAffinity launchExe === $launchExe"); result = await Process.run("cmd.exe", [ '/C', 'Start', '"StarCitizen"', '/High', '/Affinity', processorAffinity, launchExe, ...args ]); } dPrint('Exit code: ${result.exitCode}'); dPrint('stdout: ${result.stdout}'); dPrint('stderr: ${result.stderr}'); if (result.exitCode != 0) { final logs = await SCLoggerHelper.getGameRunningLogs(installPath); MapEntry? exitInfo; bool hasUrl = false; if (logs != null) { exitInfo = SCLoggerHelper.getGameRunningLogInfo(logs); if (exitInfo!.value.startsWith("https://")) { hasUrl = true; } } if (!context.mounted) return; // showToast(context, // "游戏非正常退出\nexitCode=${result.exitCode}\nstdout=${result.stdout ?? ""}\nstderr=${result.stderr ?? ""}\n\n诊断信息:${exitInfo == null ? "未知错误,请通过一键诊断加群反馈。" : exitInfo.key} \n${hasUrl ? "请查看弹出的网页链接获得详细信息。" : exitInfo?.value ?? ""}"); // S.current.home_action_info_abnormal_game_exit showToast( context, S.current.home_action_info_abnormal_game_exit( result.exitCode.toString(), result.stdout ?? "", result.stderr ?? "", exitInfo == null ? S.current.home_action_info_unknown_error : exitInfo.key, hasUrl ? S.current.home_action_info_check_web_link : exitInfo?.value ?? "")); if (hasUrl) { await Future.delayed(const Duration(seconds: 3)); launchUrlString(exitInfo!.value); } } final launchFile = File("$installPath\\loginData.json"); if (await launchFile.exists()) { await launchFile.delete(); } } catch (_) {} runningMap = Map.from(state.isGameRunning); runningMap[installPath] = false; state = state.copyWith(isGameRunning: runningMap); } }