// ignore_for_file: use_build_context_synchronously import 'dart:async'; import 'dart:convert'; import 'dart:io'; 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 RustWebViewController webview; final BuildContext context; bool _isClosed = false; bool get isClosed => _isClosed; WebViewModel(this.context, {this.loginMode = false, this.loginCallback, this.loginChannel = "LIVE"}); String url = ""; bool canGoBack = false; final localizationResource = {}; var localizationScript = ""; bool enableCapture = false; bool isEnableToolSiteMirrors = false; Map? _curReplaceWords; Map? get curReplaceWords => _curReplaceWords; final bool loginMode; final String loginChannel; bool _loginModeSuccess = false; final RsiLoginCallback? loginCallback; late AppVersionData _appVersionData; Future initWebView({ String title = "", required String applicationSupportDir, required AppVersionData appVersionData, }) async { try { final userBox = await Hive.openBox("app_conf"); isEnableToolSiteMirrors = userBox.get("isEnableToolSiteMirrors", defaultValue: false); _appVersionData = appVersionData; webview = await RustWebViewController.create( title: Platform.isMacOS ? "" : title, width: loginMode ? 960 : 1920, height: loginMode ? 720 : 1080, userDataFolder: "$applicationSupportDir/webview_data", enableDevtools: kDebugMode, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0", ); // 添加导航完成回调(用于注入脚本) webview.addOnNavigationCompletedCallback(_onNavigationCompleted); // 添加关闭回调 webview.addOnCloseCallback(dispose); if (loginMode) { 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"); } }); } } catch (e) { showToast(context, S.current.app_init_failed_with_reason(e)); } } Future _onNavigationCompleted(String newUrl) async { dPrint("Navigation completed: $newUrl"); url = newUrl; 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 _handleMirrorsUrl(String url, AppVersionData appVersionData) async { var finalUrl = url; if (isEnableToolSiteMirrors) { for (var kv in appVersionData.webMirrors!.entries) { if (url.startsWith(kv.key)) { finalUrl = url.replaceFirst(kv.key, kv.value); AnalyticsApi.touch("webLocalization_with_boost_mirror"); } } } return finalUrl; } Future launch(String url, AppVersionData appVersionData) async { webview.navigate(await _handleMirrorsUrl(url, appVersionData)); } Future initLocalization(AppWebLocalizationVersionsData v) async { localizationScript = await rootBundle.loadString('assets/web_script.js'); /// https://github.com/CxJuice/Uex_Chinese_Translate // get versions final hostUrl = URLConf.webTranslateHomeUrl; dPrint("AppWebLocalizationVersionsData === ${v.toJson()}"); localizationResource["zh-CN"] = await _getJson("$hostUrl/zh-CN-rsi.json", cacheKey: "rsi", version: v.rsi); localizationResource["concierge"] = await _getJson( "$hostUrl/concierge.json", cacheKey: "concierge", version: v.concierge, ); localizationResource["orgs"] = await _getJson("$hostUrl/orgs.json", cacheKey: "orgs", version: v.orgs); localizationResource["address"] = await _getJson( "$hostUrl/addresses.json", cacheKey: "addresses", version: v.addresses, ); localizationResource["hangar"] = await _getJson("$hostUrl/hangar.json", cacheKey: "hangar", version: v.hangar); localizationResource["UEX"] = await _getJson("$hostUrl/zh-CN-uex.json", cacheKey: "uex", version: v.uex); localizationResource["DPS"] = await _getJson("$hostUrl/zh-CN-dps.json", cacheKey: "dps", version: v.dps); } List> _getLocalizationResource(String key) { final List> localizations = []; final dict = localizationResource[key]; if (dict is Map) { for (var element in dict.entries) { final k = element.key .toString() .trim() .toLowerCase() .replaceAll(RegExp("/\xa0/g"), ' ') .replaceAll(RegExp("/s{2,}/g"), ' '); localizations.add({"word": k, "replacement": element.value.toString().trim()}); } } return localizations; } Future _getJson(String url, {String cacheKey = "", String? version}) async { final box = await Hive.openBox("web_localization_cache_data"); if (cacheKey.isNotEmpty) { final localVersion = box.get("${cacheKey}_version}", defaultValue: ""); var data = box.get(cacheKey, defaultValue: {}); if (data is Map && data.isNotEmpty && localVersion == version) { return data; } } final startTime = DateTime.now(); final r = await RSHttp.getText(url); final endTime = DateTime.now(); final data = json.decode(r); if (cacheKey.isNotEmpty) { dPrint( "update $cacheKey v == $version time == ${(endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch) / 1000 / 1000}s", ); await box.put(cacheKey, data); await box.put("${cacheKey}_version}", version); } return data; } void addOnWebMessageReceivedCallback(OnWebMessageReceivedCallback callback) { webview.addOnWebMessageCallback(callback); } void removeOnWebMessageReceivedCallback(OnWebMessageReceivedCallback callback) { webview.removeOnWebMessageCallback(callback); } FutureOr dispose() { webview.removeOnNavigationCompletedCallback(_onNavigationCompleted); if (loginMode && !_loginModeSuccess) { loginCallback?.call(null, false); } _isClosed = true; webview.dispose(); } }