app/lib/ui/home/home_ui_model.dart

416 lines
14 KiB
Dart
Raw Normal View History

2024-03-09 21:53:37 +08:00
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dart_rss/domain/rss_item.dart';
2024-03-10 12:07:30 +08:00
import 'package:desktop_webview_window/desktop_webview_window.dart';
2024-03-09 21:53:37 +08:00
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;
2024-03-10 21:25:03 +08:00
import 'package:starcitizen_doctor/common/utils/async.dart';
2024-03-09 21:53:37 +08:00
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';
2024-03-09 21:53:37 +08:00
import 'package:url_launcher/url_launcher_string.dart';
import 'package:html/parser.dart' as html;
import 'package:html/dom.dart' as html_dom;
2024-03-10 12:07:30 +08:00
import '../webview/webview.dart';
2024-03-10 21:25:03 +08:00
import 'localization/localization_ui_model.dart';
2024-03-10 12:07:30 +08:00
2024-03-09 21:53:37 +08:00
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<String> scInstallPaths,
AppWebLocalizationVersionsData? webLocalizationVersionsData,
@Default("") String lastScreenInfo,
List<RssItem>? rssVideoItems,
List<RssItem>? rssTextItems,
MapEntry<String, bool>? localizationUpdateInfo,
List? scServerStatus,
List<CountdownFestivalItemData>? countdownFestivalListData,
@Default({}) Map<String, bool> isGameRunning,
2024-03-09 21:53:37 +08:00
}) = _HomeUIModelState;
}
extension HomeUIModelStateEx on HomeUIModelState {
bool get isCurGameRunning => isGameRunning[scInstalledPath] ?? false;
}
2024-03-09 21:53:37 +08:00
@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<void> reScanPath() async {
state = state.copyWith(
2024-03-15 00:01:06 +08:00
scInstalledPath: "not_install",
lastScreenInfo: S.current.home_action_info_scanning);
2024-03-09 21:53:37 +08:00
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);
2024-03-17 20:47:06 +08:00
String scInstalledPath = "not_install";
2024-03-09 21:53:37 +08:00
if (scInstallPaths.isNotEmpty) {
2024-03-17 20:47:06 +08:00
if (scInstallPaths.first.isNotEmpty) {
scInstalledPath = scInstallPaths.first;
}
2024-03-09 21:53:37 +08:00
}
2024-03-16 19:13:49 +08:00
final lastScreenInfo = S.current
.home_action_info_scan_complete_valid_directories_found(
scInstallPaths.length.toString());
2024-03-09 21:53:37 +08:00
state = state.copyWith(
scInstalledPath: scInstalledPath,
scInstallPaths: scInstallPaths,
lastScreenInfo: lastScreenInfo);
} catch (e) {
state = state.copyWith(
2024-03-15 00:01:06 +08:00
scInstalledPath: "not_install",
lastScreenInfo: S.current.home_action_info_log_file_parse_fail);
2024-03-09 21:53:37 +08:00
AnalyticsApi.touch("error_launchLogs");
// showToast(context!,
2024-03-15 00:01:06 +08:00
// "${S.current.home_action_info_log_file_parse_fail} \n请关闭游戏退出RSI启动器后重试若仍有问题请使用工具箱中的 RSI Launcher log 修复。");
2024-03-09 21:53:37 +08:00
}
}
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;
}
2024-03-10 12:07:30 +08:00
// ignore: avoid_build_context_in_providers
Future<void> 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,
2024-03-15 00:01:06 +08:00
S.current.home_action_title_star_citizen_website_localization,
2024-03-16 19:13:49 +08:00
Text(
S.current.home_action_info_web_localization_plugin_disclaimer,
style: const TextStyle(fontSize: 16),
2024-03-10 12:07:30 +08:00
),
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;
2024-03-16 19:13:49 +08:00
showToast(
context, S.current.home_login_action_title_need_webview2_runtime);
2024-03-10 12:07:30 +08:00
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) {
2024-03-15 00:01:06 +08:00
state = state.copyWith(
isFixing: true,
2024-03-16 19:13:49 +08:00
isFixingString: S.current.home_action_info_initializing_resources);
2024-03-10 12:07:30 +08:00
try {
await webViewModel.initLocalization(state.webLocalizationVersionsData!);
} catch (e) {
if (!context.mounted) return;
2024-03-16 19:13:49 +08:00
showToast(context, S.current.home_action_info_initialization_failed(e));
2024-03-10 12:07:30 +08:00
}
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!);
}
2024-03-09 21:53:37 +08:00
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) {
2024-03-10 21:25:03 +08:00
checkLocalizationUpdate();
2024-03-09 21:53:37 +08:00
});
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
2024-03-10 21:25:03 +08:00
checkLocalizationUpdate();
2024-03-09 21:53:37 +08:00
}
Future<void> _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();
2024-03-10 14:21:36 +08:00
state = state.copyWith(rssVideoItems: rssVideoItems);
final rssTextItems = await RSSApi.getRssText();
2024-03-10 14:21:36 +08:00
state = state.copyWith(rssTextItems: rssTextItems);
2024-03-09 21:53:37 +08:00
dPrint("RSS update Success !");
} catch (e) {
dPrint("_loadRRS Error:$e");
}
}
2024-03-10 21:25:03 +08:00
Future<void> checkLocalizationUpdate({bool skipReload = false}) async {
dPrint("_checkLocalizationUpdate");
final updates = await (ref.read(localizationUIModelProvider.notifier))
.checkLangUpdate(skipReload: skipReload)
.unwrap<List<String>>();
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");
2024-03-10 21:25:03 +08:00
}
}
2024-03-09 21:53:37 +08:00
// ignore: avoid_build_context_in_providers
launchRSI(BuildContext context) async {
if (state.scInstalledPath == "not_install") {
2024-03-15 00:01:06 +08:00
showToast(context, S.current.home_info_valid_installation_required);
2024-03-09 21:53:37 +08:00
return;
}
if (ConstConf.isMSE) {
if (state.isCurGameRunning) {
await Process.run(
SystemHelper.powershellPath, ["ps \"StarCitizen\" | kill"]);
return;
}
AnalyticsApi.touch("gameLaunch");
showDialog(
context: context,
dismissWithEsc: false,
2024-03-31 15:43:59 +08:00
builder: (context) => HomeGameLoginDialogUI(context));
2024-03-09 21:53:37 +08:00
} else {
final ok = await showConfirmDialogs(
context,
2024-03-15 00:01:06 +08:00
S.current.home_info_one_click_launch_warning,
2024-03-16 19:13:49 +08:00
Text(S.current.home_info_account_security_warning),
2024-03-15 00:01:06 +08:00
confirm: S.current.home_action_install_microsoft_store_version,
cancel: S.current.home_action_cancel);
2024-03-09 21:53:37 +08:00
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);
}
doLaunchGame(
// ignore: avoid_build_context_in_providers
BuildContext context,
String launchExe,
List<String> args,
String installPath,
String? processorAffinity) async {
var runningMap = Map<String, bool>.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<String, String>? exitInfo;
bool hasUrl = false;
if (logs != null) {
exitInfo = SCLoggerHelper.getGameRunningLogInfo(logs);
if (exitInfo!.value.startsWith("https://")) {
hasUrl = true;
}
}
if (!context.mounted) return;
2024-03-16 19:13:49 +08:00
// 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<String, bool>.from(state.isGameRunning);
runningMap[installPath] = false;
state = state.copyWith(isGameRunning: runningMap);
}
2024-03-09 21:53:37 +08:00
}