From e465bc913d58645fee666c7da52546201ebbb2f6 Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Sat, 28 Oct 2023 18:19:18 +0800 Subject: [PATCH] launch game --- ...calization_web_script.js => web_script.js} | 14 +- lib/ui/home/home_ui.dart | 24 +-- lib/ui/home/home_ui_model.dart | 60 +++--- lib/ui/home/login/login_dialog_ui.dart | 106 +++++++++++ lib/ui/home/login/login_dialog_ui_model.dart | 174 ++++++++++++++++++ lib/ui/home/webview/webview.dart | 17 +- lib/ui/tools/tools_ui_model.dart | 2 +- lib/widgets/cache_image.dart | 41 +++++ pubspec.yaml | 3 + 9 files changed, 395 insertions(+), 46 deletions(-) rename assets/{localization_web_script.js => web_script.js} (96%) create mode 100644 lib/ui/home/login/login_dialog_ui.dart create mode 100644 lib/ui/home/login/login_dialog_ui_model.dart create mode 100644 lib/widgets/cache_image.dart diff --git a/assets/localization_web_script.js b/assets/web_script.js similarity index 96% rename from assets/localization_web_script.js rename to assets/web_script.js index 5a6425e..c355e1f 100644 --- a/assets/localization_web_script.js +++ b/assets/web_script.js @@ -1,5 +1,3 @@ -/// https://github.com/CxJuice/Uex_Chinese_Translate - /// ------- WebLocalization Script -------------- let SCLocalizationReplaceLocalesMap = {}; let enable_webview_localization_capture = false; @@ -220,7 +218,7 @@ InitWebLocalization(); /// ----- Login Script ---- -async function getRSILauncherToken() { +async function getRSILauncherToken(channelId) { // check login let r = await fetch("api/launcher/v3/account/check", { method: 'POST', headers: { @@ -253,12 +251,12 @@ async function getRSILauncherToken() { }); if (tokenR.status !== 200) return; - let TokenData = (await tokenR.json())["data"]; + let TokenData = (await tokenR.json())["data"]["token"]; console.log(TokenData); // get release Data let releaseFormData = new FormData(); - releaseFormData.append("channelId", "LIVE"); + releaseFormData.append("channelId", channelId); releaseFormData.append("claims", claimsData); releaseFormData.append("gameId", "SC"); releaseFormData.append("platformId", "prod"); @@ -271,6 +269,9 @@ async function getRSILauncherToken() { if (releaseR.status !== 200) return; let releaseDataJson = (await releaseR.json())['data']; console.log(releaseDataJson); + // get user avatar + let $avatarElement = $(".c-account-sidebar__profile-metas-avatar"); + let avatarUrl = $avatarElement.css("background-image"); // post message window.chrome.webview.postMessage({ @@ -278,7 +279,8 @@ async function getRSILauncherToken() { 'webToken': $.cookie('Rsi-Token'), 'claims': claimsData, 'authToken': TokenData, - 'releaseInfo': releaseDataJson + 'releaseInfo': releaseDataJson, + "avatar": avatarUrl } }); } diff --git a/lib/ui/home/home_ui.dart b/lib/ui/home/home_ui.dart index 8a713e1..a728cf0 100644 --- a/lib/ui/home/home_ui.dart +++ b/lib/ui/home/home_ui.dart @@ -237,10 +237,10 @@ class HomeUI extends BaseUI { child: Center( child: Icon( FontAwesomeIcons.solidCircle, - color: - model.isRSIServerStatusOK(item) - ? Colors.green - : Colors.red, + color: model + .isRSIServerStatusOK(item) + ? Colors.green + : Colors.red, size: 12, ), ), @@ -296,14 +296,14 @@ class HomeUI extends BaseUI { const SizedBox(width: 12), AnimatedSize( duration: const Duration(milliseconds: 130), - child: model.isRsiLauncherStarting - ? makeLoading(context, width: 28) - : Button( - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(FluentIcons.play), - ), - onPressed: () => model.launchRSI()), + child: Button( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon(model.isCurGameRunning + ? FluentIcons.stop_solid + : FluentIcons.play), + ), + onPressed: () => model.launchRSI()), ), const SizedBox(width: 12), Button( diff --git a/lib/ui/home/home_ui_model.dart b/lib/ui/home/home_ui_model.dart index 5365230..81684e8 100644 --- a/lib/ui/home/home_ui_model.dart +++ b/lib/ui/home/home_ui_model.dart @@ -13,6 +13,8 @@ import 'package:starcitizen_doctor/data/app_placard_data.dart'; import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui.dart'; import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/localization/localization_ui_model.dart'; +import 'package:starcitizen_doctor/ui/home/login/login_dialog_ui.dart'; +import 'package:starcitizen_doctor/ui/home/login/login_dialog_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/performance/performance_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/webview/webview.dart'; import 'package:starcitizen_doctor/ui/home/webview/webview_localization_capture_ui_model.dart'; @@ -36,6 +38,10 @@ class HomeUIModel extends BaseUIModel { bool isFixing = false; String isFixingString = ""; + final Map _isGameRunning = {}; + + bool get isCurGameRunning => _isGameRunning[scInstalledPath] ?? false; + set lastScreenInfo(String info) { _lastScreenInfo = info; notifyListeners(); @@ -58,8 +64,6 @@ class HomeUIModel extends BaseUIModel { "Arena Commander": "竞技场指挥官" }; - bool isRsiLauncherStarting = false; - @override Future loadData() async { if (AppConf.networkVersionData == null) return; @@ -110,7 +114,7 @@ class HomeUIModel extends BaseUIModel { return; } scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData, - withVersion: ["LIVE", "PTU", "EPTU"], checkExists: true); + withVersion: ["LIVE", "PTU", "EVO"], checkExists: true); if (scInstallPaths.isNotEmpty) { scInstalledPath = scInstallPaths.first; } @@ -402,8 +406,8 @@ class HomeUIModel extends BaseUIModel { context!, "星际公民官网汉化", const Text( - "\n\n\n本插功能件仅供大致浏览使用,不对任何有关本功能产生的问题负责!在涉及账号操作前请注意确认网站的原本内容!" - "\n\n\n使用此功能登录账号时请确保您的 StarCitizenDoctor 是从可信任的来源下载。", + "本插功能件仅供大致浏览使用,不对任何有关本功能产生的问题负责!在涉及账号操作前请注意确认网站的原本内容!" + "\n\n\n使用此功能登录账号时请确保您的 星际公民盒子 是从可信任的来源下载。", style: TextStyle(fontSize: 16), ), constraints: BoxConstraints( @@ -460,25 +464,39 @@ class HomeUIModel extends BaseUIModel { } launchRSI() async { - isRsiLauncherStarting = true; - notifyListeners(); - // final rsiLauncherInstalledPath = await SystemHelper.getRSILauncherPath(); - // if (rsiLauncherInstalledPath.isEmpty) { - // isRsiLauncherStarting = false; - // notifyListeners(); - // showToast(context!, "未找到 RSI 启动器目录"); - // return; - // } - // SystemHelper.checkAndLaunchRSILauncher(rsiLauncherInstalledPath); - goWebView("登录 RSI 账户", "https://robertsspaceindustries.com/connect", - loginMode: true, rsiLoginCallback: (data, ok) { - dPrint("======rsiLoginCallback=== $ok =====\n$data}"); - isRsiLauncherStarting = false; - notifyListeners(); - }, useLocalization: true); + if (scInstalledPath == "not_install") { + showToast(context!, "该功能需要一个有效的安装位置"); + return; + } + if (isCurGameRunning) { + await Process.run("powershell.exe", ["ps \"StarCitizen\" | kill"]); + return; + } + showDialog( + context: context!, + dismissWithEsc: false, + builder: (context) { + return BaseUIContainer( + uiCreate: () => LoginDialog(), + modelCreate: () => LoginDialogModel(scInstalledPath, this)); + }); } bool isRSIServerStatusOK(Map map) { return (map["status"] == "ok" || map["status"] == "operational"); } + + doLaunchGame(String launchExe, List args, String installPath) async { + _isGameRunning[installPath] = true; + notifyListeners(); + try { + await Process.run(launchExe, args); + final launchFile = File("$installPath\\loginData.json"); + if (await launchFile.exists()) { + await launchFile.delete(); + } + } catch (_) {} + _isGameRunning[installPath] = false; + notifyListeners(); + } } diff --git a/lib/ui/home/login/login_dialog_ui.dart b/lib/ui/home/login/login_dialog_ui.dart new file mode 100644 index 0000000..7e6363c --- /dev/null +++ b/lib/ui/home/login/login_dialog_ui.dart @@ -0,0 +1,106 @@ +import 'package:starcitizen_doctor/base/ui.dart'; +import 'package:starcitizen_doctor/widgets/cache_image.dart'; + +import 'login_dialog_ui_model.dart'; + +class LoginDialog extends BaseUI { + @override + Widget? buildBody(BuildContext context, LoginDialogModel model) { + return ContentDialog( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * .56, + ), + title: (model.loginStatus == 2) ? null : const Text("一键启动"), + content: AnimatedSize( + duration: const Duration(milliseconds: 230), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Row(), + if (model.loginStatus == 0) ...[ + const Center( + child: Column( + children: [ + Text("登录中..."), + SizedBox(height: 12), + ProgressRing() + ], + ), + ), + ] else if (model.loginStatus == 1) ...[ + Text("请输入RSI账户 [${model.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 (model.loginStatus == 2) ...[ + Center( + child: Column( + children: [ + const SizedBox(height: 12), + const Text( + "欢迎回来!", + style: TextStyle(fontSize: 20), + ), + const SizedBox(height: 24), + if (model.avatarUrl != null) + ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: CacheNetImage( + url: model.avatarUrl!, + width: 128, + height: 128, + fit: BoxFit.fill, + ), + ), + const SizedBox(height: 12), + Text( + model.nickname, + style: const TextStyle( + fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 32), + const Text("正在为您启动游戏..."), + const SizedBox(height: 12), + const ProgressRing(), + ], + ), + ) + ] + ], + ), + ), + actions: [ + if (model.loginStatus == 1) ...[ + Button( + child: const Padding( + padding: EdgeInsets.all(4), + child: Text("取消"), + ), + onPressed: () { + Navigator.pop(context); + }), + const SizedBox(width: 80), + FilledButton( + child: const Padding( + padding: EdgeInsets.all(4), + child: Text("保存"), + ), + onPressed: () => model.onSaveEmail()), + ], + ], + ); + } + + @override + String getUITitle(BuildContext context, LoginDialogModel model) => ""; +} diff --git a/lib/ui/home/login/login_dialog_ui_model.dart b/lib/ui/home/login/login_dialog_ui_model.dart new file mode 100644 index 0000000..cdb5b43 --- /dev/null +++ b/lib/ui/home/login/login_dialog_ui_model.dart @@ -0,0 +1,174 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:hive/hive.dart'; +import 'package:jwt_decode/jwt_decode.dart'; +import 'package:starcitizen_doctor/base/ui_model.dart'; +import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; +import 'package:starcitizen_doctor/ui/home/webview/webview.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:uuid/uuid.dart'; + +class LoginDialogModel extends BaseUIModel { + int loginStatus = 0; + + String nickname = ""; + String? avatarUrl; + String? authToken; + String? webToken; + Map? releaseInfo; + + final String installPath; + + final HomeUIModel homeUIModel; + + TextEditingController emailCtrl = TextEditingController(); + + LoginDialogModel(this.installPath, this.homeUIModel); + + @override + void initModel() { + _launchWebLogin(); + super.initModel(); + } + + void _launchWebLogin() { + goWebView("登录 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"]; + authToken = data["authToken"]; + webToken = data["webToken"]; + releaseInfo = data["releaseInfo"]; + avatarUrl = data["avatar"] + ?.toString() + .replaceAll("url(\"", "") + .replaceAll("\")", ""); + Map payload = Jwt.parseJwt(authToken!); + nickname = payload["nickname"] ?? ""; + if (emailBox.get(nickname, defaultValue: "") == "") { + loginStatus = 1; + notifyListeners(); + } else { + emailCtrl.text = emailBox.get(nickname, defaultValue: ""); + _readyForLaunch(); + } + }, useLocalization: true); + } + + goWebView(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_login_version", defaultValue: 0); + if (skip != tipVersion) { + final ok = await showConfirmDialogs( + context!, + "星际公民盒子一键启动", + const Text( + "本功能可以帮您更加便利的启动游戏。\n\n为确保账户安全 ,本功能使用汉化浏览器保留登录状态,且不会保存您的密码信息,与 RSI 启动器行为一致。" + "\n\n使用此功能登录账号时请确保您的 星际公民盒子 是从可信任的来源下载。", + 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()) { + await showToast(context!, "需要安装 WebView2 Runtime"); + await launchUrlString( + "https://developer.microsoft.com/en-us/microsoft-edge/webview2/"); + Navigator.pop(context!); + return; + } + final webViewModel = WebViewModel(context!, + loginMode: loginMode, + loginCallback: rsiLoginCallback, + loginChannel: getChannelID()); + if (useLocalization) { + try { + await webViewModel.initLocalization(); + } catch (_) {} + } + await webViewModel.initWebView( + title: title, + ); + await webViewModel.launch(url); + notifyListeners(); + } + + onSaveEmail() async { + final RegExp emailRegex = RegExp(r'^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$'); + if (!emailRegex.hasMatch(emailCtrl.text.trim())) { + showToast(context!, "邮箱输入有误!"); + return; + } + final emailBox = await Hive.openBox("quick_login_email"); + await emailBox.put(nickname, emailCtrl.text.trim()); + _readyForLaunch(); + notifyListeners(); + } + + Future _readyForLaunch() async { + loginStatus = 2; + notifyListeners(); + final launchData = { + "username": emailCtrl.text.trim(), + "token": webToken, + "auth_token": authToken, + "star_network": { + "services_endpoint": releaseInfo?["servicesEndpoint"], + "hostname": releaseInfo?["universeHost"], + "port": releaseInfo?["universePort"], + }, + "TMid": const Uuid().v4(), + }; + final executable = releaseInfo?["executable"]; + final launchOptions = releaseInfo?["launchOptions"]; + dPrint("----------launch data ====== -----------\n$launchData"); + dPrint( + "----------executable data ====== -----------\n$installPath\\$executable $launchOptions"); + final launchFile = File("$installPath\\loginData.json"); + if (await launchFile.exists()) { + await launchFile.delete(); + } + await launchFile.create(); + await launchFile.writeAsString(json.encode(launchData)); + notifyListeners(); + await Future.delayed(const Duration(seconds: 1)); + homeUIModel.doLaunchGame( + '$installPath\\$executable', + ["-no_login_dialog", ...launchOptions.toString().split(" ")], + installPath); + await Future.delayed(const Duration(seconds: 3)); + Navigator.pop(context!); + } + + String getChannelID() { + if (installPath.endsWith("\\LIVE")) { + return "LIVE"; + } else if (installPath.endsWith("\\PTU")) { + return "PTU"; + } else if (installPath.endsWith("\\EVO")) { + return "EVO"; + } + return "LIVE"; + } +} diff --git a/lib/ui/home/webview/webview.dart b/lib/ui/home/webview/webview.dart index 06e1768..7fb58b3 100644 --- a/lib/ui/home/webview/webview.dart +++ b/lib/ui/home/webview/webview.dart @@ -23,7 +23,8 @@ class WebViewModel { bool get isClosed => _isClosed; - WebViewModel(this.context, {this.loginMode = false, this.loginCallback}); + WebViewModel(this.context, + {this.loginMode = false, this.loginCallback, this.loginChannel = "LIVE"}); String url = ""; bool canGoBack = false; @@ -39,6 +40,7 @@ class WebViewModel { Map? get curReplaceWords => _curReplaceWords; final bool loginMode; + final String loginChannel; bool _loginModeSuccess = false; @@ -56,7 +58,6 @@ class WebViewModel { if (loginMode) { await webview.setWebviewWindowVisibility(false); } - // webview.openDevToolsWindow(); webview.isNavigating.addListener(() async { if (!webview.isNavigating.value && localizationResource.isNotEmpty) { @@ -117,10 +118,14 @@ class WebViewModel { await Future.delayed(const Duration(milliseconds: 100)); await webview.evaluateJavaScript( "WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)"); + + /// loginMode if (loginMode) { - dPrint("--- do rsi login ---"); + dPrint( + "--- do rsi login ---\n run === getRSILauncherToken(\"$loginChannel\");"); await Future.delayed(const Duration(milliseconds: 200)); - webview.evaluateJavaScript("getRSILauncherToken();"); + webview.evaluateJavaScript( + "getRSILauncherToken(\"$loginChannel\");"); } } else if (uri.host.contains("www.erkul.games") || uri.host.contains("uexcorp.space") || @@ -163,8 +168,7 @@ class WebViewModel { } initLocalization() async { - localizationScript = - await rootBundle.loadString('assets/localization_web_script.js'); + localizationScript = await rootBundle.loadString('assets/web_script.js'); /// https://github.com/CxJuice/Uex_Chinese_Translate // get versions @@ -172,6 +176,7 @@ class WebViewModel { final v = AppWebLocalizationVersionsData.fromJson( await _getJson("$hostUrl/versions.json")); + dPrint("AppWebLocalizationVersionsData === ${v.toJson()}"); localizationResource["zh-CN"] = await _getJson("$hostUrl/zh-CN-rsi.json", diff --git a/lib/ui/tools/tools_ui_model.dart b/lib/ui/tools/tools_ui_model.dart index 43ff85e..eb6d21a 100644 --- a/lib/ui/tools/tools_ui_model.dart +++ b/lib/ui/tools/tools_ui_model.dart @@ -183,7 +183,7 @@ class ToolsUIModel extends BaseUIModel { return; } scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData, - checkExists: false, withVersion: ["LIVE", "PTU", "EPTU"]); + checkExists: false, withVersion: ["LIVE", "PTU", "EVO"]); if (scInstallPaths.isNotEmpty) { scInstalledPath = scInstallPaths.first; } diff --git a/lib/widgets/cache_image.dart b/lib/widgets/cache_image.dart new file mode 100644 index 0000000..5cc350a --- /dev/null +++ b/lib/widgets/cache_image.dart @@ -0,0 +1,41 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class CacheNetImage extends StatelessWidget { + final String url; + final double? width; + final double? height; + final BoxFit? fit; + + const CacheNetImage( + {super.key, required this.url, this.width, this.height, this.fit}); + + @override + Widget build(BuildContext context) { + return ExtendedImage.network( + url, + width: width, + height: height, + fit: fit, + loadStateChanged: (ExtendedImageState state) { + switch (state.extendedImageLoadState) { + case LoadState.loading: + return const Center( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + ProgressRing(), + ], + ), + ), + ); + case LoadState.failed: + return const Text("Loading Image error"); + case LoadState.completed: + return null; + } + }, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 8a5a6d8..1a0db38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,9 @@ dependencies: desktop_webview_window: ^0.2.3 flutter_svg: ^2.0.7 archive: ^3.4.4 + jwt_decode: ^0.3.1 + html: ^0.15.4 + uuid: ^4.1.0 dev_dependencies: flutter_test: