feat: Replace desktop_webview_window with tao&wry , from tauri

This commit is contained in:
xkeyC
2025-12-05 01:29:48 +08:00
parent 125fedbc84
commit 6f0c760ab4
31 changed files with 6894 additions and 539 deletions

View File

@@ -5,7 +5,6 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_ce/hive.dart';
import 'package:riverpod_annotation/riverpod_annotation.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';
@@ -14,7 +13,6 @@ import 'package:starcitizen_doctor/common/utils/provider.dart';
import 'package:starcitizen_doctor/data/rsi_game_library_data.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';
@@ -47,82 +45,87 @@ class HomeGameLoginUIModel extends _$HomeGameLoginUIModel {
Future<void> launchWebLogin(BuildContext context) async {
final homeState = ref.read(homeUIModelProvider);
if (!context.mounted) return;
goWebView(context, S.current.home_action_login_rsi_account,
"https://robertsspaceindustries.com/en/connect?jumpto=/account/dashboard",
loginMode: true, rsiLoginCallback: (message, ok) async {
// dPrint(
// "======rsiLoginCallback=== $ok ===== data==\n${json.encode(message)}");
if (message == null || !ok) {
Navigator.pop(context);
return;
}
goWebView(
context,
S.current.home_action_login_rsi_account,
"https://robertsspaceindustries.com/en/connect?jumpto=/account/dashboard",
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 libraryData = RsiGameLibraryData.fromJson(data["libraryData"]);
var avatarUrl = data["avatar"]
?.toString()
.replaceAll("url(\"", "")
.replaceAll("\")", "");
if (avatarUrl?.startsWith("/") ?? false) {
avatarUrl = "https://robertsspaceindustries.com$avatarUrl";
}
final Map<String, dynamic> payload = Jwt.parseJwt(authToken!);
final nickname = payload["nickname"] ?? "";
// 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 libraryData = RsiGameLibraryData.fromJson(data["libraryData"]);
var avatarUrl = data["avatar"]?.toString().replaceAll("url(\"", "").replaceAll("\")", "");
if (avatarUrl?.startsWith("/") ?? false) {
avatarUrl = "https://robertsspaceindustries.com$avatarUrl";
}
final Map<String, dynamic> payload = Jwt.parseJwt(authToken!);
final nickname = payload["nickname"] ?? "";
state = state.copyWith(
nickname: nickname,
avatarUrl: avatarUrl,
authToken: authToken,
webToken: webToken,
releaseInfo: releaseInfo,
libraryData: libraryData,
);
state = state.copyWith(
nickname: nickname,
avatarUrl: avatarUrl,
authToken: authToken,
webToken: webToken,
releaseInfo: releaseInfo,
libraryData: libraryData,
);
final buildInfoFile =
File("${homeState.scInstalledPath}\\build_manifest.id");
if (await buildInfoFile.exists()) {
final buildInfo =
json.decode(await buildInfoFile.readAsString())["Data"];
final buildInfoFile = File("${homeState.scInstalledPath}\\build_manifest.id");
if (await buildInfoFile.exists()) {
final buildInfo = json.decode(await buildInfoFile.readAsString())["Data"];
if (releaseInfo?["versionLabel"] != null &&
buildInfo["RequestedP4ChangeNum"] != null) {
if (!(releaseInfo!["versionLabel"]!
.toString()
.endsWith(buildInfo["RequestedP4ChangeNum"]!.toString()))) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
if (releaseInfo?["versionLabel"] != null && buildInfo["RequestedP4ChangeNum"] != null) {
if (!(releaseInfo!["versionLabel"]!.toString().endsWith(buildInfo["RequestedP4ChangeNum"]!.toString()))) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context,
S.current.home_login_info_game_version_outdated,
Text(S.current.home_login_info_rsi_server_report(
Text(
S.current.home_login_info_rsi_server_report(
releaseInfo?["versionLabel"],
buildInfo["RequestedP4ChangeNum"])),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .4),
cancel: S.current.home_login_info_action_ignore);
if (ok == true) {
if (!context.mounted) return;
Navigator.pop(context);
return;
buildInfo["RequestedP4ChangeNum"],
),
),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .4),
cancel: S.current.home_login_info_action_ignore,
);
if (ok == true) {
if (!context.mounted) return;
Navigator.pop(context);
return;
}
}
}
}
}
if (!context.mounted) return;
_readyForLaunch(homeState, context);
}, useLocalization: true, homeState: homeState);
if (!context.mounted) return;
_readyForLaunch(homeState, context);
},
useLocalization: true,
homeState: homeState,
);
}
// ignore: avoid_build_context_in_providers
Future<void> goWebView(BuildContext context, String title, String url,
{bool useLocalization = false,
bool loginMode = false,
RsiLoginCallback? rsiLoginCallback,
required HomeUIModelState homeState}) async {
Future<void> 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");
@@ -130,14 +133,11 @@ class HomeGameLoginUIModel extends _$HomeGameLoginUIModel {
if (skip != tipVersion) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context,
S.current.home_login_action_title_box_one_click_launch,
Text(
S.current.home_login_info_one_click_launch_description,
style: const TextStyle(fontSize: 16),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .6));
context,
S.current.home_login_action_title_box_one_click_launch,
Text(S.current.home_login_info_one_click_launch_description, style: const TextStyle(fontSize: 16)),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .6),
);
if (!ok) {
if (loginMode) {
rsiLoginCallback?.call(null, false);
@@ -147,26 +147,17 @@ class HomeGameLoginUIModel extends _$HomeGameLoginUIModel {
await box.put("skip_web_login_version", tipVersion);
}
}
if (!await WebviewWindow.isWebviewAvailable()) {
if (!context.mounted) return;
await showToast(
context, S.current.home_login_action_title_need_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;
}
// Rust WebView using wry + tao - no WebView2 runtime check needed as wry handles it internally
if (!context.mounted) return;
final webViewModel = WebViewModel(context,
loginMode: loginMode,
loginCallback: rsiLoginCallback,
loginChannel: getChannelID(homeState.scInstalledPath!));
final webViewModel = WebViewModel(
context,
loginMode: loginMode,
loginCallback: rsiLoginCallback,
loginChannel: getChannelID(homeState.scInstalledPath!),
);
if (useLocalization) {
try {
await webViewModel
.initLocalization(homeState.webLocalizationVersionsData!);
await webViewModel.initLocalization(homeState.webLocalizationVersionsData!);
} catch (_) {}
}
await Future.delayed(const Duration(milliseconds: 500));
@@ -179,9 +170,10 @@ class HomeGameLoginUIModel extends _$HomeGameLoginUIModel {
}
Future<void> _readyForLaunch(
HomeUIModelState homeState,
// ignore: avoid_build_context_in_providers
BuildContext context) async {
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 = {
@@ -211,11 +203,12 @@ class HomeGameLoginUIModel extends _$HomeGameLoginUIModel {
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);
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);

View File

@@ -42,7 +42,7 @@ final class HomeGameLoginUIModelProvider
}
String _$homeGameLoginUIModelHash() =>
r'c9e9ec2e85f2459b6bfc1518406b091ff4675a85';
r'217a57f797b37f3467be2e7711f220610e9e67d8';
abstract class _$HomeGameLoginUIModel extends $Notifier<HomeGameLoginState> {
HomeGameLoginState build();

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
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_ce/hive.dart';
@@ -164,12 +163,7 @@ class HomeUIModel extends _$HomeUIModel {
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;
}
// Rust WebView using wry + tao - no WebView2 runtime check needed as wry handles it internally
if (!context.mounted) return;
final webViewModel = WebViewModel(context, loginMode: loginMode, loginCallback: rsiLoginCallback);
if (useLocalization) {

View File

@@ -41,7 +41,7 @@ final class HomeUIModelProvider
}
}
String _$homeUIModelHash() => r'7dfe73383f7be2e520a42d176e199a8db208f008';
String _$homeUIModelHash() => r'cc795e27213d02993459dd711de4a897c8491575';
abstract class _$HomeUIModel extends $Notifier<HomeUIModelState> {
HomeUIModelState build();

View File

@@ -41,7 +41,7 @@ final class ToolsUIModelProvider
}
}
String _$toolsUIModelHash() => r'78732ff16e87cc9f92174bda43d0fafadba51146';
String _$toolsUIModelHash() => r'ee1de3d555443f72b4fbb395a5728b2de1e8aaf4';
abstract class _$ToolsUIModel extends $Notifier<ToolsUIState> {
ToolsUIState build();

View File

@@ -4,22 +4,24 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_ce/hive.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/common/rust/rust_webview_controller.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/data/app_version_data.dart';
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
typedef RsiLoginCallback = void Function(Map? data, bool success);
typedef OnWebMessageReceivedCallback = void Function(String message);
class WebViewModel {
late Webview webview;
late RustWebViewController webview;
final BuildContext context;
bool _isClosed = false;
@@ -51,6 +53,8 @@ class WebViewModel {
final RsiLoginCallback? loginCallback;
late AppVersionData _appVersionData;
Future<void> initWebView({
String title = "",
required String applicationSupportDir,
@@ -59,114 +63,35 @@ class WebViewModel {
try {
final userBox = await Hive.openBox("app_conf");
isEnableToolSiteMirrors = userBox.get("isEnableToolSiteMirrors", defaultValue: false);
webview = await WebviewWindow.create(
configuration: CreateConfiguration(
windowWidth: loginMode ? 960 : 1920,
windowHeight: loginMode ? 720 : 1080,
userDataFolderWindows: "$applicationSupportDir/webview_data",
title: Platform.isMacOS ? "" : title,
),
_appVersionData = appVersionData;
webview = await RustWebViewController.create(
title: Platform.isMacOS ? "" : title,
width: loginMode ? 960 : 1920,
height: loginMode ? 720 : 1080,
userDataFolder: "$applicationSupportDir/webview_data",
enableDevtools: kDebugMode,
);
// webview.openDevToolsWindow();
webview.isNavigating.addListener(() async {
if (!webview.isNavigating.value && localizationResource.isNotEmpty) {
dPrint("webview Navigating url === $url");
if (url.contains("robertsspaceindustries.com")) {
// SC 官网
final replaceWords = _getLocalizationResource("zh-CN");
const org = "https://robertsspaceindustries.com/orgs";
const citizens = "https://robertsspaceindustries.com/citizens";
const organization = "https://robertsspaceindustries.com/account/organization";
const concierge = "https://robertsspaceindustries.com/account/concierge";
const referral = "https://robertsspaceindustries.com/account/referral-program";
const address = "https://robertsspaceindustries.com/account/addresses";
const hangar = "https://robertsspaceindustries.com/account/pledges";
// 添加导航完成回调(用于注入脚本)
webview.addOnNavigationCompletedCallback(_onNavigationCompleted);
const spectrum = "https://robertsspaceindustries.com/spectrum";
// 跳过光谱论坛 https://github.com/StarCitizenToolBox/StarCitizenBoxBrowserEx/issues/1
if (url.startsWith(spectrum)) {
return;
}
// 添加关闭回调
webview.addOnCloseCallback(dispose);
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
await webview.evaluateJavaScript(localizationScript);
if (url.startsWith(org) || url.startsWith(citizens) || url.startsWith(organization)) {
replaceWords.add({"word": 'members', "replacement": S.current.webview_localization_name_member});
replaceWords.addAll(_getLocalizationResource("orgs"));
}
if (address.startsWith(address)) {
replaceWords.addAll(_getLocalizationResource("address"));
}
if (url.startsWith(referral)) {
replaceWords.addAll([
{"word": 'Total recruits: ', "replacement": S.current.webview_localization_total_invitations},
{"word": 'Prospects ', "replacement": S.current.webview_localization_unfinished_invitations},
{"word": 'Recruits', "replacement": S.current.webview_localization_finished_invitations},
]);
}
if (url.startsWith(concierge)) {
replaceWords.clear();
replaceWords.addAll(_getLocalizationResource("concierge"));
}
if (url.startsWith(hangar)) {
replaceWords.addAll(_getLocalizationResource("hangar"));
}
_curReplaceWords = {};
for (var element in replaceWords) {
_curReplaceWords?[element["word"] ?? ""] = element["replacement"] ?? "";
}
await webview.evaluateJavaScript("InitWebLocalization()");
await Future.delayed(const Duration(milliseconds: 100));
dPrint("update replaceWords");
await webview.evaluateJavaScript(
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)",
);
/// loginMode
if (loginMode) {
dPrint("--- do rsi login ---\n run === getRSILauncherToken(\"$loginChannel\");");
await Future.delayed(const Duration(milliseconds: 200));
webview.evaluateJavaScript("getRSILauncherToken(\"$loginChannel\");");
}
} else if (url.startsWith(await _handleMirrorsUrl("https://www.erkul.games", appVersionData))) {
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
await webview.evaluateJavaScript(localizationScript);
dPrint("update replaceWords");
final replaceWords = _getLocalizationResource("DPS");
await webview.evaluateJavaScript(
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)",
);
} else if (url.startsWith(await _handleMirrorsUrl("https://uexcorp.space", appVersionData))) {
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
await webview.evaluateJavaScript(localizationScript);
dPrint("update replaceWords");
final replaceWords = _getLocalizationResource("UEX");
await webview.evaluateJavaScript(
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)",
);
}
}
});
webview.addOnUrlRequestCallback(_onUrlRequest);
webview.onClose.whenComplete(dispose);
if (loginMode) {
webview.addOnWebMessageReceivedCallback((messageString) {
final message = json.decode(messageString);
if (message["action"] == "webview_rsi_login_show_window") {
webview.setWebviewWindowVisibility(true);
} else if (message["action"] == "webview_rsi_login_success") {
_loginModeSuccess = true;
loginCallback?.call(message, true);
webview.close();
webview.addOnWebMessageCallback((messageString) {
try {
final message = json.decode(messageString);
if (message["action"] == "webview_rsi_login_show_window") {
webview.setVisible(true);
} else if (message["action"] == "webview_rsi_login_success") {
_loginModeSuccess = true;
loginCallback?.call(message, true);
webview.close();
}
} catch (e) {
dPrint("Error parsing login message: $e");
}
});
}
@@ -175,6 +100,98 @@ class WebViewModel {
}
}
Future<void> _onNavigationCompleted(String newUrl) async {
dPrint("Navigation completed: $newUrl");
url = newUrl;
// 在页面加载时注入拦截器
if (requestInterceptorScript.isNotEmpty) {
dPrint("Injecting request interceptor for: $url");
webview.executeScript(requestInterceptorScript);
}
if (localizationResource.isEmpty) return;
dPrint("webview Navigating url === $url");
if (url.contains("robertsspaceindustries.com")) {
// SC 官网
final replaceWords = _getLocalizationResource("zh-CN");
const org = "https://robertsspaceindustries.com/orgs";
const citizens = "https://robertsspaceindustries.com/citizens";
const organization = "https://robertsspaceindustries.com/account/organization";
const concierge = "https://robertsspaceindustries.com/account/concierge";
const referral = "https://robertsspaceindustries.com/account/referral-program";
const address = "https://robertsspaceindustries.com/account/addresses";
const hangar = "https://robertsspaceindustries.com/account/pledges";
const spectrum = "https://robertsspaceindustries.com/spectrum";
// 跳过光谱论坛 https://github.com/StarCitizenToolBox/StarCitizenBoxBrowserEx/issues/1
if (url.startsWith(spectrum)) {
return;
}
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
webview.injectLocalizationScript();
if (url.startsWith(org) || url.startsWith(citizens) || url.startsWith(organization)) {
replaceWords.add({"word": 'members', "replacement": S.current.webview_localization_name_member});
replaceWords.addAll(_getLocalizationResource("orgs"));
}
if (address.startsWith(address)) {
replaceWords.addAll(_getLocalizationResource("address"));
}
if (url.startsWith(referral)) {
replaceWords.addAll([
{"word": 'Total recruits: ', "replacement": S.current.webview_localization_total_invitations},
{"word": 'Prospects ', "replacement": S.current.webview_localization_unfinished_invitations},
{"word": 'Recruits', "replacement": S.current.webview_localization_finished_invitations},
]);
}
if (url.startsWith(concierge)) {
replaceWords.clear();
replaceWords.addAll(_getLocalizationResource("concierge"));
}
if (url.startsWith(hangar)) {
replaceWords.addAll(_getLocalizationResource("hangar"));
}
_curReplaceWords = {};
for (var element in replaceWords) {
_curReplaceWords?[element["word"] ?? ""] = element["replacement"] ?? "";
}
webview.initWebLocalization();
await Future.delayed(const Duration(milliseconds: 100));
dPrint("update replaceWords");
webview.updateReplaceWords(replaceWords, enableCapture);
/// loginMode
if (loginMode) {
dPrint("--- do rsi login ---\n run === getRSILauncherToken(\"$loginChannel\");");
await Future.delayed(const Duration(milliseconds: 200));
webview.executeRsiLogin(loginChannel);
}
} else if (url.startsWith(await _handleMirrorsUrl("https://www.erkul.games", _appVersionData))) {
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
webview.injectLocalizationScript();
dPrint("update replaceWords");
final replaceWords = _getLocalizationResource("DPS");
webview.updateReplaceWords(replaceWords, enableCapture);
} else if (url.startsWith(await _handleMirrorsUrl("https://uexcorp.space", _appVersionData))) {
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
webview.injectLocalizationScript();
dPrint("update replaceWords");
final replaceWords = _getLocalizationResource("UEX");
webview.updateReplaceWords(replaceWords, enableCapture);
}
}
Future<String> _handleMirrorsUrl(String url, AppVersionData appVersionData) async {
var finalUrl = url;
if (isEnableToolSiteMirrors) {
@@ -189,7 +206,7 @@ class WebViewModel {
}
Future<void> launch(String url, AppVersionData appVersionData) async {
webview.launch(await _handleMirrorsUrl(url, appVersionData));
webview.navigate(await _handleMirrorsUrl(url, appVersionData));
}
Future<void> initLocalization(AppWebLocalizationVersionsData v) async {
@@ -259,29 +276,19 @@ class WebViewModel {
}
void addOnWebMessageReceivedCallback(OnWebMessageReceivedCallback callback) {
webview.addOnWebMessageReceivedCallback(callback);
webview.addOnWebMessageCallback(callback);
}
void removeOnWebMessageReceivedCallback(OnWebMessageReceivedCallback callback) {
webview.removeOnWebMessageReceivedCallback(callback);
webview.removeOnWebMessageCallback(callback);
}
FutureOr<void> dispose() {
webview.removeOnUrlRequestCallback(_onUrlRequest);
webview.removeOnNavigationCompletedCallback(_onNavigationCompleted);
if (loginMode && !_loginModeSuccess) {
loginCallback?.call(null, false);
}
_isClosed = true;
}
void _onUrlRequest(String url) {
dPrint("OnUrlRequestCallback === $url");
this.url = url;
// 在页面开始加载时立即注入拦截器
if (requestInterceptorScript.isNotEmpty) {
dPrint("Injecting request interceptor for: $url");
webview.evaluateJavaScript(requestInterceptorScript);
}
webview.dispose();
}
}