From 2aa4fb6c0945bb0b2c6680814b4af3ff2250861e Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Sun, 10 Mar 2024 13:25:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:riverpod=20=E8=BF=81=E7=A7=BB=20HomeGameLo?= =?UTF-8?q?ginDialogUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dialogs/home_game_login_dialog_ui.dart | 107 +++++++ .../home_game_login_dialog_ui_model.dart | 296 +++++++++++++++++ ...me_game_login_dialog_ui_model.freezed.dart | 303 ++++++++++++++++++ .../home_game_login_dialog_ui_model.g.dart | 27 ++ lib/ui/home/home_ui_model.dart | 80 ++++- lib/ui/home/home_ui_model.freezed.dart | 92 +++--- lib/ui/home/home_ui_model.g.dart | 2 +- 7 files changed, 853 insertions(+), 54 deletions(-) create mode 100644 lib/ui/home/dialogs/home_game_login_dialog_ui.dart create mode 100644 lib/ui/home/dialogs/home_game_login_dialog_ui_model.dart create mode 100644 lib/ui/home/dialogs/home_game_login_dialog_ui_model.freezed.dart create mode 100644 lib/ui/home/dialogs/home_game_login_dialog_ui_model.g.dart diff --git a/lib/ui/home/dialogs/home_game_login_dialog_ui.dart b/lib/ui/home/dialogs/home_game_login_dialog_ui.dart new file mode 100644 index 0000000..c2dc880 --- /dev/null +++ b/lib/ui/home/dialogs/home_game_login_dialog_ui.dart @@ -0,0 +1,107 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; + +import 'home_game_login_dialog_ui_model.dart'; + +class HomeGameLoginDialogUI extends HookConsumerWidget { + const HomeGameLoginDialogUI({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final loginState = ref.watch(homeGameLoginUIModelProvider); + useEffect(() { + ref.read(homeGameLoginUIModelProvider.notifier).launchWebLogin(context); + return null; + }, const []); + return ContentDialog( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * .56, + ), + title: (loginState.loginStatus == 2) ? null : const Text("一键启动"), + content: AnimatedSize( + duration: const Duration(milliseconds: 230), + child: Padding( + padding: const EdgeInsets.only(top: 12, bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Row(), + if (loginState.loginStatus == 0) ...[ + Center( + child: Column( + children: [ + const Text("登录中..."), + const SizedBox(height: 12), + const ProgressRing(), + if (loginState.isDeviceSupportWinHello ?? false) + const SizedBox(height: 24), + Text( + "* 若开启了自动填充,请留意弹出的 Windows Hello 窗口", + style: TextStyle( + fontSize: 13, color: Colors.white.withOpacity(.6)), + ) + ], + ), + ), + ] else if (loginState.loginStatus == 1) ...[ + Text( + "请输入RSI账户 [${loginState.nickname}] 的邮箱,以保存登录状态(输入错误会导致无法进入游戏!)"), + const SizedBox(height: 12), + TextFormBox( + // controller: model.emailCtrl, + ), + const SizedBox(height: 6), + Text( + "*该操作同一账号只需执行一次,输入错误请在盒子设置中清理,切换账号请在汉化浏览器中操作。", + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(.6), + ), + ) + ] else if (loginState.loginStatus == 2 || + loginState.loginStatus == 3) ...[ + Center( + child: Column( + children: [ + const SizedBox(height: 12), + const Text( + "欢迎回来!", + style: TextStyle(fontSize: 20), + ), + const SizedBox(height: 24), + if (loginState.avatarUrl != null) + ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: CacheNetImage( + url: loginState.avatarUrl!, + width: 128, + height: 128, + fit: BoxFit.fill, + ), + ), + const SizedBox(height: 12), + Text( + loginState.nickname ?? "", + style: const TextStyle( + fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 32), + Text(loginState.loginStatus == 2 + ? "正在为您启动游戏..." + : "正在等待优化CPU参数..."), + const SizedBox(height: 12), + const ProgressRing(), + ], + ), + ) + ] + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/home/dialogs/home_game_login_dialog_ui_model.dart b/lib/ui/home/dialogs/home_game_login_dialog_ui_model.dart new file mode 100644 index 0000000..edfe390 --- /dev/null +++ b/lib/ui/home/dialogs/home_game_login_dialog_ui_model.dart @@ -0,0 +1,296 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hive/hive.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:jwt_decode/jwt_decode.dart'; +import 'package:starcitizen_doctor/common/helper/system_helper.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/common/win32/credentials.dart'; +import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; +import 'package:starcitizen_doctor/ui/webview/webview.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:uuid/uuid.dart'; + +part 'home_game_login_dialog_ui_model.freezed.dart'; + +part 'home_game_login_dialog_ui_model.g.dart'; + +@freezed +class HomeGameLoginState with _$HomeGameLoginState { + const factory HomeGameLoginState({ + required int loginStatus, + String? nickname, + String? avatarUrl, + String? authToken, + String? webToken, + Map? releaseInfo, + String? installPath, + bool? isDeviceSupportWinHello, + }) = _LoginStatus; +} + +@riverpod +class HomeGameLoginUIModel extends _$HomeGameLoginUIModel { + @override + HomeGameLoginState build() { + return const HomeGameLoginState(loginStatus: 0); + } + + final LocalAuthentication _localAuth = LocalAuthentication(); + + // ignore: avoid_build_context_in_providers + Future launchWebLogin(BuildContext context) async { + final homeState = ref.read(homeUIModelProvider); + final isDeviceSupportWinHello = await _localAuth.isDeviceSupported(); + state = state.copyWith(isDeviceSupportWinHello: isDeviceSupportWinHello); + + if (!context.mounted) return; + goWebView( + context, "登录 RSI 账户", "https://robertsspaceindustries.com/connect", + loginMode: true, rsiLoginCallback: (message, ok) async { + // dPrint( + // "======rsiLoginCallback=== $ok ===== data==\n${json.encode(message)}"); + if (message == null || !ok) { + Navigator.pop(context); + return; + } + // final emailBox = await Hive.openBox("quick_login_email"); + final data = message["data"]; + final authToken = data["authToken"]; + final webToken = data["webToken"]; + final releaseInfo = data["releaseInfo"]; + final avatarUrl = data["avatar"] + ?.toString() + .replaceAll("url(\"", "") + .replaceAll("\")", ""); + final Map payload = Jwt.parseJwt(authToken!); + final nickname = payload["nickname"] ?? ""; + + final inputEmail = data["inputEmail"]; + final inputPassword = data["inputPassword"]; + + final userBox = await Hive.openBox("rsi_account_data"); + if (inputEmail != null && inputEmail != "") { + await userBox.put("account_email", inputEmail); + } + state = state.copyWith( + nickname: nickname, + avatarUrl: avatarUrl, + authToken: authToken, + webToken: webToken, + releaseInfo: releaseInfo, + ); + + if (isDeviceSupportWinHello) { + if (await userBox.get("enable", defaultValue: true)) { + if (inputEmail != null && + inputEmail != "" && + inputPassword != null && + inputPassword != "") { + if (!context.mounted) return; + final ok = await showConfirmDialogs( + context, + "是否开启自动密码填充?", + const Text( + "盒子将使用 PIN 与 Windows 凭据加密保存您的密码,密码只存储在您的设备中。\n\n当下次登录需要输入密码时,您只需授权PIN即可自动填充登录。")); + if (ok == true) { + if (await _localAuth.authenticate( + localizedReason: "输入PIN以启用加密") == + true) { + await _savePwd(inputEmail, inputPassword); + } + } else { + await userBox.put("enable", false); + } + } + } + } + + final buildInfoFile = + File("${homeState.scInstalledPath}\\build_manifest.id"); + if (await buildInfoFile.exists()) { + final buildInfo = + json.decode(await buildInfoFile.readAsString())["Data"]; + dPrint("buildInfo ======= $buildInfo"); + + if (releaseInfo?["versionLabel"] != null && + buildInfo["RequestedP4ChangeNum"] != null) { + if (!(releaseInfo!["versionLabel"]! + .toString() + .endsWith(buildInfo["RequestedP4ChangeNum"]!.toString()))) { + if (!context.mounted) return; + final ok = await showConfirmDialogs( + context, + "游戏版本过期", + Text( + "RSI 服务器报告版本号:${releaseInfo?["versionLabel"]} \n\n本地版本号:${buildInfo["RequestedP4ChangeNum"]} \n\n建议使用 RSI Launcher 更新游戏!"), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * .4), + cancel: "忽略"); + if (ok == true) { + if (!context.mounted) return; + Navigator.pop(context); + return; + } + } + } + } + + _readyForLaunch(homeState, context); + }, useLocalization: true, homeState: homeState); + } + + // ignore: avoid_build_context_in_providers + goWebView(BuildContext context, String title, String url, + {bool useLocalization = false, + bool loginMode = false, + RsiLoginCallback? rsiLoginCallback, + required HomeUIModelState homeState}) async { + if (useLocalization) { + const tipVersion = 2; + final box = await Hive.openBox("app_conf"); + final skip = await box.get("skip_web_login_version", defaultValue: 0); + if (skip != tipVersion) { + if (!context.mounted) return; + final ok = await showConfirmDialogs( + context, + "盒子一键启动", + const Text( + "本功能可以帮您更加便利的启动游戏。\n\n为确保账户安全 ,本功能使用汉化浏览器保留登录状态,且不会保存您的密码信息(除非你启用了自动填充功能)。" + "\n\n使用此功能登录账号时请确保您的 SC汉化盒子 是从可信任的来源下载。", + style: 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_login_version", tipVersion); + } + } + if (!await WebviewWindow.isWebviewAvailable()) { + if (!context.mounted) return; + await showToast(context, "需要安装 WebView2 Runtime"); + if (!context.mounted) return; + await launchUrlString( + "https://developer.microsoft.com/en-us/microsoft-edge/webview2/"); + if (!context.mounted) return; + Navigator.pop(context); + return; + } + if (!context.mounted) return; + final webViewModel = WebViewModel(context, + loginMode: loginMode, + loginCallback: rsiLoginCallback, + loginChannel: getChannelID(homeState.scInstalledPath!)); + if (useLocalization) { + try { + await webViewModel + .initLocalization(homeState.webLocalizationVersionsData!); + } catch (_) {} + } + await Future.delayed(const Duration(milliseconds: 500)); + await webViewModel.initWebView( + title: title, + applicationSupportDir: appGlobalState.applicationSupportDir!, + appVersionData: appGlobalState.networkVersionData!, + ); + await webViewModel.launch(url, appGlobalState.networkVersionData!); + } + + Future _readyForLaunch( + HomeUIModelState homeState, + // ignore: avoid_build_context_in_providers + BuildContext context) async { + final userBox = await Hive.openBox("rsi_account_data"); + state = state.copyWith(loginStatus: 2); + final launchData = { + "username": userBox.get("account_email", defaultValue: ""), + "token": state.webToken, + "auth_token": state.authToken, + "star_network": { + "services_endpoint": state.releaseInfo?["servicesEndpoint"], + "hostname": state.releaseInfo?["universeHost"], + "port": state.releaseInfo?["universePort"], + }, + "TMid": const Uuid().v4(), + }; + final executable = state.releaseInfo?["executable"]; + final launchOptions = state.releaseInfo?["launchOptions"]; + dPrint("----------launch data ====== -----------\n$launchData"); + dPrint( + "----------executable data ====== -----------\n${homeState.scInstalledPath}\\$executable $launchOptions"); + final launchFile = File("${homeState.scInstalledPath}\\loginData.json"); + if (await launchFile.exists()) { + await launchFile.delete(); + } + await launchFile.create(); + await launchFile.writeAsString(json.encode(launchData)); + await Future.delayed(const Duration(seconds: 1)); + + await Future.delayed(const Duration(seconds: 3)); + final processorAffinity = await SystemHelper.getCpuAffinity(); + final homeUIModel = ref.read(homeUIModelProvider.notifier); + if (!context.mounted) return; + homeUIModel.doLaunchGame( + context, + '${homeState.scInstalledPath}\\$executable', + ["-no_login_dialog", ...launchOptions.toString().split(" ")], + homeState.scInstalledPath!, + processorAffinity); + await Future.delayed(const Duration(seconds: 1)); + if (!context.mounted) return; + Navigator.pop(context); + } + + String getChannelID(String installPath) { + if (installPath.endsWith("\\LIVE")) { + return "LIVE"; + } else if (installPath.endsWith("\\PTU")) { + return "PTU"; + } else if (installPath.endsWith("\\EPTU")) { + return "EPTU"; + } + return "LIVE"; + } + + _savePwd(String inputEmail, String inputPassword) async { + final algorithm = AesGcm.with256bits(); + final secretKey = await algorithm.newSecretKey(); + final nonce = algorithm.newNonce(); + + final secretBox = await algorithm.encrypt(utf8.encode(inputPassword), + secretKey: secretKey, nonce: nonce); + + await algorithm.decrypt( + SecretBox(secretBox.cipherText, + nonce: secretBox.nonce, mac: secretBox.mac), + secretKey: secretKey); + + final pwdEncrypted = base64.encode(secretBox.cipherText); + + final userBox = await Hive.openBox("rsi_account_data"); + await userBox.put("account_email", inputEmail); + await userBox.put("account_pwd_encrypted", pwdEncrypted); + await userBox.put("nonce", base64.encode(secretBox.nonce)); + await userBox.put("mac", base64.encode(secretBox.mac.bytes)); + + final secretKeyStr = base64.encode((await secretKey.extractBytes())); + + Win32Credentials.write( + credentialName: "SCToolbox_RSI_Account_secret", + userName: inputEmail, + password: secretKeyStr); + } +} diff --git a/lib/ui/home/dialogs/home_game_login_dialog_ui_model.freezed.dart b/lib/ui/home/dialogs/home_game_login_dialog_ui_model.freezed.dart new file mode 100644 index 0000000..5bd18ae --- /dev/null +++ b/lib/ui/home/dialogs/home_game_login_dialog_ui_model.freezed.dart @@ -0,0 +1,303 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'home_game_login_dialog_ui_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$HomeGameLoginState { + int get loginStatus => throw _privateConstructorUsedError; + String? get nickname => throw _privateConstructorUsedError; + String? get avatarUrl => throw _privateConstructorUsedError; + String? get authToken => throw _privateConstructorUsedError; + String? get webToken => throw _privateConstructorUsedError; + Map? get releaseInfo => throw _privateConstructorUsedError; + String? get installPath => throw _privateConstructorUsedError; + bool? get isDeviceSupportWinHello => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HomeGameLoginStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HomeGameLoginStateCopyWith<$Res> { + factory $HomeGameLoginStateCopyWith( + HomeGameLoginState value, $Res Function(HomeGameLoginState) then) = + _$HomeGameLoginStateCopyWithImpl<$Res, HomeGameLoginState>; + @useResult + $Res call( + {int loginStatus, + String? nickname, + String? avatarUrl, + String? authToken, + String? webToken, + Map? releaseInfo, + String? installPath, + bool? isDeviceSupportWinHello}); +} + +/// @nodoc +class _$HomeGameLoginStateCopyWithImpl<$Res, $Val extends HomeGameLoginState> + implements $HomeGameLoginStateCopyWith<$Res> { + _$HomeGameLoginStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loginStatus = null, + Object? nickname = freezed, + Object? avatarUrl = freezed, + Object? authToken = freezed, + Object? webToken = freezed, + Object? releaseInfo = freezed, + Object? installPath = freezed, + Object? isDeviceSupportWinHello = freezed, + }) { + return _then(_value.copyWith( + loginStatus: null == loginStatus + ? _value.loginStatus + : loginStatus // ignore: cast_nullable_to_non_nullable + as int, + nickname: freezed == nickname + ? _value.nickname + : nickname // ignore: cast_nullable_to_non_nullable + as String?, + avatarUrl: freezed == avatarUrl + ? _value.avatarUrl + : avatarUrl // ignore: cast_nullable_to_non_nullable + as String?, + authToken: freezed == authToken + ? _value.authToken + : authToken // ignore: cast_nullable_to_non_nullable + as String?, + webToken: freezed == webToken + ? _value.webToken + : webToken // ignore: cast_nullable_to_non_nullable + as String?, + releaseInfo: freezed == releaseInfo + ? _value.releaseInfo + : releaseInfo // ignore: cast_nullable_to_non_nullable + as Map?, + installPath: freezed == installPath + ? _value.installPath + : installPath // ignore: cast_nullable_to_non_nullable + as String?, + isDeviceSupportWinHello: freezed == isDeviceSupportWinHello + ? _value.isDeviceSupportWinHello + : isDeviceSupportWinHello // ignore: cast_nullable_to_non_nullable + as bool?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LoginStatusImplCopyWith<$Res> + implements $HomeGameLoginStateCopyWith<$Res> { + factory _$$LoginStatusImplCopyWith( + _$LoginStatusImpl value, $Res Function(_$LoginStatusImpl) then) = + __$$LoginStatusImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int loginStatus, + String? nickname, + String? avatarUrl, + String? authToken, + String? webToken, + Map? releaseInfo, + String? installPath, + bool? isDeviceSupportWinHello}); +} + +/// @nodoc +class __$$LoginStatusImplCopyWithImpl<$Res> + extends _$HomeGameLoginStateCopyWithImpl<$Res, _$LoginStatusImpl> + implements _$$LoginStatusImplCopyWith<$Res> { + __$$LoginStatusImplCopyWithImpl( + _$LoginStatusImpl _value, $Res Function(_$LoginStatusImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loginStatus = null, + Object? nickname = freezed, + Object? avatarUrl = freezed, + Object? authToken = freezed, + Object? webToken = freezed, + Object? releaseInfo = freezed, + Object? installPath = freezed, + Object? isDeviceSupportWinHello = freezed, + }) { + return _then(_$LoginStatusImpl( + loginStatus: null == loginStatus + ? _value.loginStatus + : loginStatus // ignore: cast_nullable_to_non_nullable + as int, + nickname: freezed == nickname + ? _value.nickname + : nickname // ignore: cast_nullable_to_non_nullable + as String?, + avatarUrl: freezed == avatarUrl + ? _value.avatarUrl + : avatarUrl // ignore: cast_nullable_to_non_nullable + as String?, + authToken: freezed == authToken + ? _value.authToken + : authToken // ignore: cast_nullable_to_non_nullable + as String?, + webToken: freezed == webToken + ? _value.webToken + : webToken // ignore: cast_nullable_to_non_nullable + as String?, + releaseInfo: freezed == releaseInfo + ? _value._releaseInfo + : releaseInfo // ignore: cast_nullable_to_non_nullable + as Map?, + installPath: freezed == installPath + ? _value.installPath + : installPath // ignore: cast_nullable_to_non_nullable + as String?, + isDeviceSupportWinHello: freezed == isDeviceSupportWinHello + ? _value.isDeviceSupportWinHello + : isDeviceSupportWinHello // ignore: cast_nullable_to_non_nullable + as bool?, + )); + } +} + +/// @nodoc + +class _$LoginStatusImpl implements _LoginStatus { + const _$LoginStatusImpl( + {required this.loginStatus, + this.nickname, + this.avatarUrl, + this.authToken, + this.webToken, + final Map? releaseInfo, + this.installPath, + this.isDeviceSupportWinHello}) + : _releaseInfo = releaseInfo; + + @override + final int loginStatus; + @override + final String? nickname; + @override + final String? avatarUrl; + @override + final String? authToken; + @override + final String? webToken; + final Map? _releaseInfo; + @override + Map? get releaseInfo { + final value = _releaseInfo; + if (value == null) return null; + if (_releaseInfo is EqualUnmodifiableMapView) return _releaseInfo; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + final String? installPath; + @override + final bool? isDeviceSupportWinHello; + + @override + String toString() { + return 'HomeGameLoginState(loginStatus: $loginStatus, nickname: $nickname, avatarUrl: $avatarUrl, authToken: $authToken, webToken: $webToken, releaseInfo: $releaseInfo, installPath: $installPath, isDeviceSupportWinHello: $isDeviceSupportWinHello)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginStatusImpl && + (identical(other.loginStatus, loginStatus) || + other.loginStatus == loginStatus) && + (identical(other.nickname, nickname) || + other.nickname == nickname) && + (identical(other.avatarUrl, avatarUrl) || + other.avatarUrl == avatarUrl) && + (identical(other.authToken, authToken) || + other.authToken == authToken) && + (identical(other.webToken, webToken) || + other.webToken == webToken) && + const DeepCollectionEquality() + .equals(other._releaseInfo, _releaseInfo) && + (identical(other.installPath, installPath) || + other.installPath == installPath) && + (identical( + other.isDeviceSupportWinHello, isDeviceSupportWinHello) || + other.isDeviceSupportWinHello == isDeviceSupportWinHello)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + loginStatus, + nickname, + avatarUrl, + authToken, + webToken, + const DeepCollectionEquality().hash(_releaseInfo), + installPath, + isDeviceSupportWinHello); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoginStatusImplCopyWith<_$LoginStatusImpl> get copyWith => + __$$LoginStatusImplCopyWithImpl<_$LoginStatusImpl>(this, _$identity); +} + +abstract class _LoginStatus implements HomeGameLoginState { + const factory _LoginStatus( + {required final int loginStatus, + final String? nickname, + final String? avatarUrl, + final String? authToken, + final String? webToken, + final Map? releaseInfo, + final String? installPath, + final bool? isDeviceSupportWinHello}) = _$LoginStatusImpl; + + @override + int get loginStatus; + @override + String? get nickname; + @override + String? get avatarUrl; + @override + String? get authToken; + @override + String? get webToken; + @override + Map? get releaseInfo; + @override + String? get installPath; + @override + bool? get isDeviceSupportWinHello; + @override + @JsonKey(ignore: true) + _$$LoginStatusImplCopyWith<_$LoginStatusImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/home/dialogs/home_game_login_dialog_ui_model.g.dart b/lib/ui/home/dialogs/home_game_login_dialog_ui_model.g.dart new file mode 100644 index 0000000..0529769 --- /dev/null +++ b/lib/ui/home/dialogs/home_game_login_dialog_ui_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'home_game_login_dialog_ui_model.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$homeGameLoginUIModelHash() => + r'3747a303c86553319c515ab29933abda935edb16'; + +/// See also [HomeGameLoginUIModel]. +@ProviderFor(HomeGameLoginUIModel) +final homeGameLoginUIModelProvider = AutoDisposeNotifierProvider< + HomeGameLoginUIModel, HomeGameLoginState>.internal( + HomeGameLoginUIModel.new, + name: r'homeGameLoginUIModelProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$homeGameLoginUIModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$HomeGameLoginUIModel = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/ui/home/home_ui_model.dart b/lib/ui/home/home_ui_model.dart index 9278cab..e8c6fdc 100644 --- a/lib/ui/home/home_ui_model.dart +++ b/lib/ui/home/home_ui_model.dart @@ -5,7 +5,6 @@ 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:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -23,6 +22,7 @@ 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; @@ -42,16 +42,20 @@ class HomeUIModelState with _$HomeUIModelState { String? scInstalledPath, @Default([]) List scInstallPaths, AppWebLocalizationVersionsData? webLocalizationVersionsData, - @Default(false) bool isCurGameRunning, @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 @@ -276,14 +280,10 @@ class HomeUIModel extends _$HomeUIModel { return; } AnalyticsApi.touch("gameLaunch"); - // showDialog( - // context: context, - // dismissWithEsc: false, - // builder: (context) { - // return BaseUIContainer( - // uiCreate: () => LoginDialog(), - // modelCreate: () => LoginDialogModel(scInstalledPath, this)); - // }); + showDialog( + context: context, + dismissWithEsc: false, + builder: (context) => const HomeGameLoginDialogUI()); } else { final ok = await showConfirmDialogs( context, @@ -305,4 +305,64 @@ class HomeUIModel extends _$HomeUIModel { if (value == null) return; state = state.copyWith(scInstalledPath: 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 ?? ""}"); + 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); + } } diff --git a/lib/ui/home/home_ui_model.freezed.dart b/lib/ui/home/home_ui_model.freezed.dart index ffebe40..ebf6120 100644 --- a/lib/ui/home/home_ui_model.freezed.dart +++ b/lib/ui/home/home_ui_model.freezed.dart @@ -23,7 +23,6 @@ mixin _$HomeUIModelState { List get scInstallPaths => throw _privateConstructorUsedError; AppWebLocalizationVersionsData? get webLocalizationVersionsData => throw _privateConstructorUsedError; - bool get isCurGameRunning => throw _privateConstructorUsedError; String get lastScreenInfo => throw _privateConstructorUsedError; List? get rssVideoItems => throw _privateConstructorUsedError; List? get rssTextItems => throw _privateConstructorUsedError; @@ -32,6 +31,7 @@ mixin _$HomeUIModelState { List? get scServerStatus => throw _privateConstructorUsedError; List? get countdownFestivalListData => throw _privateConstructorUsedError; + Map get isGameRunning => throw _privateConstructorUsedError; @JsonKey(ignore: true) $HomeUIModelStateCopyWith get copyWith => @@ -51,13 +51,13 @@ abstract class $HomeUIModelStateCopyWith<$Res> { String? scInstalledPath, List scInstallPaths, AppWebLocalizationVersionsData? webLocalizationVersionsData, - bool isCurGameRunning, String lastScreenInfo, List? rssVideoItems, List? rssTextItems, MapEntry? localizationUpdateInfo, List? scServerStatus, - List? countdownFestivalListData}); + List? countdownFestivalListData, + Map isGameRunning}); } /// @nodoc @@ -79,13 +79,13 @@ class _$HomeUIModelStateCopyWithImpl<$Res, $Val extends HomeUIModelState> Object? scInstalledPath = freezed, Object? scInstallPaths = null, Object? webLocalizationVersionsData = freezed, - Object? isCurGameRunning = null, Object? lastScreenInfo = null, Object? rssVideoItems = freezed, Object? rssTextItems = freezed, Object? localizationUpdateInfo = freezed, Object? scServerStatus = freezed, Object? countdownFestivalListData = freezed, + Object? isGameRunning = null, }) { return _then(_value.copyWith( appPlacardData: freezed == appPlacardData @@ -112,10 +112,6 @@ class _$HomeUIModelStateCopyWithImpl<$Res, $Val extends HomeUIModelState> ? _value.webLocalizationVersionsData : webLocalizationVersionsData // ignore: cast_nullable_to_non_nullable as AppWebLocalizationVersionsData?, - isCurGameRunning: null == isCurGameRunning - ? _value.isCurGameRunning - : isCurGameRunning // ignore: cast_nullable_to_non_nullable - as bool, lastScreenInfo: null == lastScreenInfo ? _value.lastScreenInfo : lastScreenInfo // ignore: cast_nullable_to_non_nullable @@ -140,6 +136,10 @@ class _$HomeUIModelStateCopyWithImpl<$Res, $Val extends HomeUIModelState> ? _value.countdownFestivalListData : countdownFestivalListData // ignore: cast_nullable_to_non_nullable as List?, + isGameRunning: null == isGameRunning + ? _value.isGameRunning + : isGameRunning // ignore: cast_nullable_to_non_nullable + as Map, ) as $Val); } } @@ -159,13 +159,13 @@ abstract class _$$HomeUIModelStateImplCopyWith<$Res> String? scInstalledPath, List scInstallPaths, AppWebLocalizationVersionsData? webLocalizationVersionsData, - bool isCurGameRunning, String lastScreenInfo, List? rssVideoItems, List? rssTextItems, MapEntry? localizationUpdateInfo, List? scServerStatus, - List? countdownFestivalListData}); + List? countdownFestivalListData, + Map isGameRunning}); } /// @nodoc @@ -185,13 +185,13 @@ class __$$HomeUIModelStateImplCopyWithImpl<$Res> Object? scInstalledPath = freezed, Object? scInstallPaths = null, Object? webLocalizationVersionsData = freezed, - Object? isCurGameRunning = null, Object? lastScreenInfo = null, Object? rssVideoItems = freezed, Object? rssTextItems = freezed, Object? localizationUpdateInfo = freezed, Object? scServerStatus = freezed, Object? countdownFestivalListData = freezed, + Object? isGameRunning = null, }) { return _then(_$HomeUIModelStateImpl( appPlacardData: freezed == appPlacardData @@ -218,10 +218,6 @@ class __$$HomeUIModelStateImplCopyWithImpl<$Res> ? _value.webLocalizationVersionsData : webLocalizationVersionsData // ignore: cast_nullable_to_non_nullable as AppWebLocalizationVersionsData?, - isCurGameRunning: null == isCurGameRunning - ? _value.isCurGameRunning - : isCurGameRunning // ignore: cast_nullable_to_non_nullable - as bool, lastScreenInfo: null == lastScreenInfo ? _value.lastScreenInfo : lastScreenInfo // ignore: cast_nullable_to_non_nullable @@ -246,6 +242,10 @@ class __$$HomeUIModelStateImplCopyWithImpl<$Res> ? _value._countdownFestivalListData : countdownFestivalListData // ignore: cast_nullable_to_non_nullable as List?, + isGameRunning: null == isGameRunning + ? _value._isGameRunning + : isGameRunning // ignore: cast_nullable_to_non_nullable + as Map, )); } } @@ -260,18 +260,19 @@ class _$HomeUIModelStateImpl implements _HomeUIModelState { this.scInstalledPath, final List scInstallPaths = const [], this.webLocalizationVersionsData, - this.isCurGameRunning = false, this.lastScreenInfo = "", final List? rssVideoItems, final List? rssTextItems, this.localizationUpdateInfo, final List? scServerStatus, - final List? countdownFestivalListData}) + final List? countdownFestivalListData, + final Map isGameRunning = const {}}) : _scInstallPaths = scInstallPaths, _rssVideoItems = rssVideoItems, _rssTextItems = rssTextItems, _scServerStatus = scServerStatus, - _countdownFestivalListData = countdownFestivalListData; + _countdownFestivalListData = countdownFestivalListData, + _isGameRunning = isGameRunning; @override final AppPlacardData? appPlacardData; @@ -296,9 +297,6 @@ class _$HomeUIModelStateImpl implements _HomeUIModelState { final AppWebLocalizationVersionsData? webLocalizationVersionsData; @override @JsonKey() - final bool isCurGameRunning; - @override - @JsonKey() final String lastScreenInfo; final List? _rssVideoItems; @override @@ -343,9 +341,18 @@ class _$HomeUIModelStateImpl implements _HomeUIModelState { return EqualUnmodifiableListView(value); } + final Map _isGameRunning; + @override + @JsonKey() + Map get isGameRunning { + if (_isGameRunning is EqualUnmodifiableMapView) return _isGameRunning; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_isGameRunning); + } + @override String toString() { - return 'HomeUIModelState(appPlacardData: $appPlacardData, isFixing: $isFixing, isFixingString: $isFixingString, scInstalledPath: $scInstalledPath, scInstallPaths: $scInstallPaths, webLocalizationVersionsData: $webLocalizationVersionsData, isCurGameRunning: $isCurGameRunning, lastScreenInfo: $lastScreenInfo, rssVideoItems: $rssVideoItems, rssTextItems: $rssTextItems, localizationUpdateInfo: $localizationUpdateInfo, scServerStatus: $scServerStatus, countdownFestivalListData: $countdownFestivalListData)'; + return 'HomeUIModelState(appPlacardData: $appPlacardData, isFixing: $isFixing, isFixingString: $isFixingString, scInstalledPath: $scInstalledPath, scInstallPaths: $scInstallPaths, webLocalizationVersionsData: $webLocalizationVersionsData, lastScreenInfo: $lastScreenInfo, rssVideoItems: $rssVideoItems, rssTextItems: $rssTextItems, localizationUpdateInfo: $localizationUpdateInfo, scServerStatus: $scServerStatus, countdownFestivalListData: $countdownFestivalListData, isGameRunning: $isGameRunning)'; } @override @@ -367,8 +374,6 @@ class _$HomeUIModelStateImpl implements _HomeUIModelState { webLocalizationVersionsData) || other.webLocalizationVersionsData == webLocalizationVersionsData) && - (identical(other.isCurGameRunning, isCurGameRunning) || - other.isCurGameRunning == isCurGameRunning) && (identical(other.lastScreenInfo, lastScreenInfo) || other.lastScreenInfo == lastScreenInfo) && const DeepCollectionEquality() @@ -380,7 +385,9 @@ class _$HomeUIModelStateImpl implements _HomeUIModelState { const DeepCollectionEquality() .equals(other._scServerStatus, _scServerStatus) && const DeepCollectionEquality().equals( - other._countdownFestivalListData, _countdownFestivalListData)); + other._countdownFestivalListData, _countdownFestivalListData) && + const DeepCollectionEquality() + .equals(other._isGameRunning, _isGameRunning)); } @override @@ -392,13 +399,13 @@ class _$HomeUIModelStateImpl implements _HomeUIModelState { scInstalledPath, const DeepCollectionEquality().hash(_scInstallPaths), webLocalizationVersionsData, - isCurGameRunning, lastScreenInfo, const DeepCollectionEquality().hash(_rssVideoItems), const DeepCollectionEquality().hash(_rssTextItems), localizationUpdateInfo, const DeepCollectionEquality().hash(_scServerStatus), - const DeepCollectionEquality().hash(_countdownFestivalListData)); + const DeepCollectionEquality().hash(_countdownFestivalListData), + const DeepCollectionEquality().hash(_isGameRunning)); @JsonKey(ignore: true) @override @@ -410,20 +417,19 @@ class _$HomeUIModelStateImpl implements _HomeUIModelState { abstract class _HomeUIModelState implements HomeUIModelState { factory _HomeUIModelState( - {final AppPlacardData? appPlacardData, - final bool isFixing, - final String isFixingString, - final String? scInstalledPath, - final List scInstallPaths, - final AppWebLocalizationVersionsData? webLocalizationVersionsData, - final bool isCurGameRunning, - final String lastScreenInfo, - final List? rssVideoItems, - final List? rssTextItems, - final MapEntry? localizationUpdateInfo, - final List? scServerStatus, - final List? countdownFestivalListData}) = - _$HomeUIModelStateImpl; + {final AppPlacardData? appPlacardData, + final bool isFixing, + final String isFixingString, + final String? scInstalledPath, + final List scInstallPaths, + final AppWebLocalizationVersionsData? webLocalizationVersionsData, + final String lastScreenInfo, + final List? rssVideoItems, + final List? rssTextItems, + final MapEntry? localizationUpdateInfo, + final List? scServerStatus, + final List? countdownFestivalListData, + final Map isGameRunning}) = _$HomeUIModelStateImpl; @override AppPlacardData? get appPlacardData; @@ -438,8 +444,6 @@ abstract class _HomeUIModelState implements HomeUIModelState { @override AppWebLocalizationVersionsData? get webLocalizationVersionsData; @override - bool get isCurGameRunning; - @override String get lastScreenInfo; @override List? get rssVideoItems; @@ -452,6 +456,8 @@ abstract class _HomeUIModelState implements HomeUIModelState { @override List? get countdownFestivalListData; @override + Map get isGameRunning; + @override @JsonKey(ignore: true) _$$HomeUIModelStateImplCopyWith<_$HomeUIModelStateImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/ui/home/home_ui_model.g.dart b/lib/ui/home/home_ui_model.g.dart index 4ac8371..64bcc71 100644 --- a/lib/ui/home/home_ui_model.g.dart +++ b/lib/ui/home/home_ui_model.g.dart @@ -6,7 +6,7 @@ part of 'home_ui_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$homeUIModelHash() => r'7ab7b3721ff81a18d67717c9bc91632226c516f6'; +String _$homeUIModelHash() => r'3094d9ab828a578670e11f3eaffa57bdb95a004b'; /// See also [HomeUIModel]. @ProviderFor(HomeUIModel)