feat: WASM web support

This commit is contained in:
xkeyC
2025-11-13 20:07:47 +08:00
parent 193d2c7496
commit 476c40f4cd
106 changed files with 2276 additions and 4299 deletions

View File

@@ -6,9 +6,9 @@ import 'package:starcitizen_doctor/common/utils/log.dart';
class URLConf {
/// HOME API
static String gitApiHome = "https://git.scbox.xkeyc.cn";
static String newsApiHome = "https://scbox.citizenwiki.cn";
static const String analyticsApiHome = "https://scbox.org";
static String gitApiHome = "https://ecdn.git.scbox.xkeyc.cn";
static String newsApiHome = "https://ecdn.news.scbox.xkeyc.cn";
static const String analyticsApiHome = "https://web-proxy.scbox.xkeyc.cn/analytics/analytics";
static bool isUrlCheckPass = false;
@@ -28,7 +28,7 @@ class URLConf {
static const feedbackUrl = "https://support.citizenwiki.cn/all";
static const feedbackFAQUrl = "https://support.citizenwiki.cn/t/sc-toolbox";
static String nav42KitUrl =
"https://payload.citizenwiki.cn/api/community-navs?sort=is_sponsored&depth=2&page=1&limit=1000";
"https://ecdn.42nav.xkeyc.cn/api/community-navs?sort=is_sponsored&depth=2&page=1&limit=1000";
static String get devReleaseUrl => "$gitApiHome/SCToolBox/Release/releases";

View File

@@ -1,13 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hive_ce/hive.dart';
import 'package:starcitizen_doctor/common/conf/conf.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
class SCLoggerHelper {
static Future<String?> getLogFilePath() async {
if (!Platform.isWindows) return null;
if (kIsWeb || !Platform.isWindows) return null;
Map<String, String> envVars = Platform.environment;
final appDataPath = envVars["appdata"];
if (appDataPath == null) {
@@ -31,7 +32,7 @@ class SCLoggerHelper {
}
static Future<List?> getLauncherLogList() async {
if (!Platform.isWindows) return [];
if (kIsWeb || !Platform.isWindows) return [];
try {
final jsonLogPath = await getLogFilePath();
if (jsonLogPath == null) throw "no file path";
@@ -43,9 +44,11 @@ class SCLoggerHelper {
}
}
static Future<List<String>> getGameInstallPath(List listData,
{bool checkExists = true,
List<String> withVersion = const ["LIVE"]}) async {
static Future<List<String>> getGameInstallPath(
List listData, {
bool checkExists = true,
List<String> withVersion = const ["LIVE"],
}) async {
List<String> scInstallPaths = [];
checkAndAddPath(String path, bool checkExists) async {
@@ -55,8 +58,7 @@ class SCLoggerHelper {
if (!checkExists) {
dPrint("find installPath == $path");
scInstallPaths.add(path);
} else if (await File("$path/Bin64/StarCitizen.exe").exists() &&
await File("$path/Data.p4k").exists()) {
} else if (await File("$path/Bin64/StarCitizen.exe").exists() && await File("$path/Data.p4k").exists()) {
dPrint("find installPath == $path");
scInstallPaths.add(path);
}
@@ -73,8 +75,7 @@ class SCLoggerHelper {
try {
for (var v in withVersion) {
String pattern =
r'([a-zA-Z]:\\\\[^\\\\]*\\\\[^\\\\]*\\\\StarCitizen\\\\' + v + r')';
String pattern = r'([a-zA-Z]:\\\\[^\\\\]*\\\\[^\\\\]*\\\\StarCitizen\\\\' + v + r')';
RegExp regExp = RegExp(pattern, caseSensitive: false);
for (var i = listData.length - 1; i > 0; i--) {
final line = listData[i];
@@ -91,8 +92,7 @@ class SCLoggerHelper {
for (var v in withVersion) {
if (fileName.toString().endsWith(v)) {
for (var nv in withVersion) {
final nextName =
"${fileName.toString().replaceAll("\\$v", "")}\\$nv";
final nextName = "${fileName.toString().replaceAll("\\$v", "")}\\$nv";
await checkAndAddPath(nextName, true);
}
}
@@ -121,8 +121,7 @@ class SCLoggerHelper {
if (!await logFile.exists()) {
return null;
}
return await logFile.readAsLines(
encoding: const Utf8Codec(allowMalformed: true));
return await logFile.readAsLines(encoding: const Utf8Codec(allowMalformed: true));
}
static MapEntry<String, String>? getGameRunningLogInfo(List<String> logs) {
@@ -138,47 +137,47 @@ class SCLoggerHelper {
static MapEntry<String, String>? _checkRunningLine(String line) {
if (line.contains("STATUS_CRYENGINE_OUT_OF_SYSMEM")) {
return MapEntry(S.current.doctor_game_error_low_memory,
S.current.doctor_game_error_low_memory_info);
return MapEntry(S.current.doctor_game_error_low_memory, S.current.doctor_game_error_low_memory_info);
}
if (line.contains("EXCEPTION_ACCESS_VIOLATION")) {
return MapEntry(S.current.doctor_game_error_generic_info,
"https://docs.qq.com/doc/DUURxUVhzTmZoY09Z");
return MapEntry(S.current.doctor_game_error_generic_info, "https://docs.qq.com/doc/DUURxUVhzTmZoY09Z");
}
if (line.contains("DXGI_ERROR_DEVICE_REMOVED")) {
return MapEntry(S.current.doctor_game_error_gpu_crash,
"https://www.bilibili.com/read/cv19335199");
return MapEntry(S.current.doctor_game_error_gpu_crash, "https://www.bilibili.com/read/cv19335199");
}
if (line.contains("Wakeup socket sendto error")) {
return MapEntry(S.current.doctor_game_error_socket_error,
S.current.doctor_game_error_socket_error_info);
return MapEntry(S.current.doctor_game_error_socket_error, S.current.doctor_game_error_socket_error_info);
}
if (line.contains("The requested operation requires elevated")) {
return MapEntry(S.current.doctor_game_error_permissions_error,
S.current.doctor_game_error_permissions_error_info);
return MapEntry(
S.current.doctor_game_error_permissions_error,
S.current.doctor_game_error_permissions_error_info,
);
}
if (line.contains(
"The process cannot access the file because is is being used by another process")) {
return MapEntry(S.current.doctor_game_error_game_process_error,
S.current.doctor_game_error_game_process_error_info);
if (line.contains("The process cannot access the file because is is being used by another process")) {
return MapEntry(
S.current.doctor_game_error_game_process_error,
S.current.doctor_game_error_game_process_error_info,
);
}
if (line.contains("0xc0000043")) {
return MapEntry(S.current.doctor_game_error_game_damaged_file,
S.current.doctor_game_error_game_damaged_file_info);
return MapEntry(
S.current.doctor_game_error_game_damaged_file,
S.current.doctor_game_error_game_damaged_file_info,
);
}
if (line.contains("option to verify the content of the Data.p4k file")) {
return MapEntry(S.current.doctor_game_error_game_damaged_p4k_file,
S.current.doctor_game_error_game_damaged_p4k_file_info);
return MapEntry(
S.current.doctor_game_error_game_damaged_p4k_file,
S.current.doctor_game_error_game_damaged_p4k_file_info,
);
}
if (line.contains("OUTOFMEMORY Direct3D could not allocate")) {
return MapEntry(S.current.doctor_game_error_low_gpu_memory,
S.current.doctor_game_error_low_gpu_memory_info);
return MapEntry(S.current.doctor_game_error_low_gpu_memory, S.current.doctor_game_error_low_gpu_memory_info);
}
if (line.contains(
"try disabling with r_vulkanDisableLayers = 1 in your user.cfg")) {
return MapEntry(S.current.doctor_game_error_gpu_vulkan_crash,
S.current.doctor_game_error_gpu_vulkan_crash_info);
if (line.contains("try disabling with r_vulkanDisableLayers = 1 in your user.cfg")) {
return MapEntry(S.current.doctor_game_error_gpu_vulkan_crash, S.current.doctor_game_error_gpu_vulkan_crash_info);
}
/// Unknown

View File

@@ -30,7 +30,7 @@ class SystemHelper {
"-Path",
"\"HKLM:\\SYSTEM\\CurrentControlSet\\Services\\stornvme\\Parameters\\Device\"",
"-Name",
"\"ForcedPhysicalSectorSizeInBytes\""
"\"ForcedPhysicalSectorSizeInBytes\"",
]);
dPrint("checkNvmePatchStatus result ==== ${result.stdout}");
if (result.stderr == "" && result.stdout.toString().contains("{* 4095}")) {
@@ -52,7 +52,7 @@ class SystemHelper {
"ForcedPhysicalSectorSizeInBytes",
"-PropertyType MultiString",
"-Force -Value",
"\"* 4095\""
"\"* 4095\"",
]);
dPrint("nvme_PhysicalBytes result == ${result.stdout}");
return result.stderr;
@@ -65,7 +65,7 @@ class SystemHelper {
"-Path",
"\"HKLM:\\SYSTEM\\CurrentControlSet\\Services\\stornvme\\Parameters\\Device\"",
"-Name",
"\"ForcedPhysicalSectorSizeInBytes\""
"\"ForcedPhysicalSectorSizeInBytes\"",
]);
dPrint("doRemoveNvmePath result ==== ${result.stdout}");
if (result.stderr == "") {
@@ -97,8 +97,9 @@ class SystemHelper {
"$programDataPath\\Microsoft\\Windows\\Start Menu\\Programs\\Roberts Space Industries\\RSI Launcher.lnk";
final rsiLinkFile = File(rsiFilePath);
if (await rsiLinkFile.exists()) {
final r = await Process.run(SystemHelper.powershellPath,
["(New-Object -ComObject WScript.Shell).CreateShortcut(\"$rsiFilePath\").targetpath"]);
final r = await Process.run(SystemHelper.powershellPath, [
"(New-Object -ComObject WScript.Shell).CreateShortcut(\"$rsiFilePath\").targetpath",
]);
if (r.stdout.toString().contains("RSI Launcher.exe")) {
final start = r.stdout.toString().split("RSI Launcher.exe");
if (skipEXE) {
@@ -147,22 +148,15 @@ class SystemHelper {
if (processorAffinity == null) {
Process.run(path, []);
} else {
Process.run("cmd.exe", [
'/C',
'Start',
'""',
'/High',
'/Affinity',
processorAffinity,
path,
]);
Process.run("cmd.exe", ['/C', 'Start', '""', '/High', '/Affinity', processorAffinity, path]);
}
dPrint(path);
}
static Future<int> getSystemMemorySizeGB() async {
final r = await Process.run(
powershellPath, ["(Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property capacity -Sum).sum /1gb"]);
final r = await Process.run(powershellPath, [
"(Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property capacity -Sum).sum /1gb",
]);
return int.tryParse(r.stdout.toString().trim()) ?? 0;
}
@@ -196,10 +190,9 @@ foreach ($adapter in $adapterMemory) {
}
static Future<String> getDiskInfo() async {
return (await Process.run(powershellPath, ["Get-PhysicalDisk | format-table BusType,FriendlyName,Size"]))
.stdout
.toString()
.trim();
return (await Process.run(powershellPath, [
"Get-PhysicalDisk | format-table BusType,FriendlyName,Size",
])).stdout.toString().trim();
}
static Future<int> getDirLen(String path, {List<String>? skipPath}) async {
@@ -226,8 +219,9 @@ foreach ($adapter in $adapterMemory) {
}
static Future<int> getNumberOfLogicalProcessors() async {
final cpuNumberResult =
await Process.run(powershellPath, ["(Get-WmiObject -Class Win32_Processor).NumberOfLogicalProcessors"]);
final cpuNumberResult = await Process.run(powershellPath, [
"(Get-WmiObject -Class Win32_Processor).NumberOfLogicalProcessors",
]);
if (cpuNumberResult.exitCode != 0) return 0;
return int.tryParse(cpuNumberResult.stdout.toString().trim()) ?? 0;
}
@@ -257,8 +251,10 @@ foreach ($adapter in $adapterMemory) {
static Future openDir(dynamic path, {bool isFile = false}) async {
dPrint("SystemHelper.openDir path === $path");
if (Platform.isWindows) {
await Process.run(
SystemHelper.powershellPath, ["explorer.exe", isFile ? "/select,$path" : "\"/select,\"$path\"\""]);
await Process.run(SystemHelper.powershellPath, [
"explorer.exe",
isFile ? "/select,$path" : "\"/select,\"$path\"\"",
]);
}
}

View File

@@ -1,17 +1,27 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:starcitizen_doctor/common/conf/conf.dart';
import 'package:starcitizen_doctor/common/rust/api/http_api.dart' as rust_http;
import 'package:starcitizen_doctor/common/rust/api/http_api.dart';
import 'package:starcitizen_doctor/common/rust/http_package.dart';
class RSHttp {
static late Dio _dio;
static Map<String, String> _defaultHeaders = {};
static Future<void> init() async {
await rust_http.setDefaultHeader(headers: {
_defaultHeaders = {
"User-Agent":
"SCToolBox/${ConstConf.appVersion} (${ConstConf.appVersionCode})${ConstConf.isMSE ? "" : " DEV"} RSHttp"
});
"SCToolBox/${ConstConf.appVersion} (${ConstConf.appVersionCode})${ConstConf.isMSE ? "" : " DEV"} RSHttp",
};
_dio = Dio(
BaseOptions(
headers: _defaultHeaders,
responseType: ResponseType.bytes,
validateStatus: (status) => true, // 接受所有状态码
),
);
}
static Future<RustHttpResponse> get(
@@ -20,14 +30,13 @@ class RSHttp {
String? withIpAddress,
bool? withCustomDns,
}) async {
final r = await rust_http.fetch(
method: MyMethod.gets,
return await _fetch(
method: 'GET',
url: url,
headers: headers,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns,
);
return r;
}
static Future<String> getText(
@@ -36,10 +45,7 @@ class RSHttp {
String? withIpAddress,
bool? withCustomDns,
}) async {
final r = await get(url,
headers: headers,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns);
final r = await get(url, headers: headers, withIpAddress: withIpAddress, withCustomDns: withCustomDns);
if (r.data == null) return "";
final str = utf8.decode(r.data!);
return str;
@@ -57,15 +63,14 @@ class RSHttp {
headers ??= {};
headers["Content-Type"] = contentType;
}
final r = await rust_http.fetch(
method: MyMethod.post,
return await _fetch(
method: 'POST',
url: url,
headers: headers,
inputData: data,
data: data,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns,
);
return r;
}
static Future<RustHttpResponse> head(
@@ -74,21 +79,81 @@ class RSHttp {
String? withIpAddress,
bool? withCustomDns,
}) async {
final r = await rust_http.fetch(
method: MyMethod.head,
return await _fetch(
method: 'HEAD',
url: url,
headers: headers,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns,
);
return r;
}
static Future<List<String>> dnsLookupTxt(String host) async {
return await rust_http.dnsLookupTxt(host: host);
// TXT 记录查询在 Web 平台上无法实现,返回空列表
return [];
}
static Future<List<String>> dnsLookupIps(String host) async {
return await rust_http.dnsLookupIps(host: host);
// Web 平台无法直接查询 DNS返回空列表
// 在 Web 平台上DNS 解析由浏览器自动处理
return [];
}
static Future<RustHttpResponse> _fetch({
required String method,
required String url,
Map<String, String>? headers,
Uint8List? data,
String? withIpAddress,
bool? withCustomDns,
}) async {
try {
final mergedHeaders = {..._defaultHeaders, ...?headers};
final response = await _dio.request(
url,
data: data,
options: Options(
method: method,
headers: mergedHeaders,
responseType: ResponseType.bytes,
validateStatus: (status) => true,
),
);
// 将 Dio Response 转换为 RustHttpResponse
return RustHttpResponse(
statusCode: response.statusCode ?? 0,
headers: _convertHeaders(response.headers.map),
url: response.realUri.toString(),
contentLength: response.headers.value('content-length') != null
? BigInt.from(int.parse(response.headers.value('content-length')!))
: null,
version: MyHttpVersion.httpUnknown,
remoteAddr: response.realUri.host,
data: response.data is Uint8List ? response.data : null,
);
} catch (e) {
// 发生错误时返回默认响应
return RustHttpResponse(
statusCode: 0,
headers: {},
url: url,
contentLength: null,
version: MyHttpVersion.httpUnknown,
remoteAddr: '',
data: null,
);
}
}
static Map<String, String> _convertHeaders(Map<String, List<String>> headers) {
final result = <String, String>{};
headers.forEach((key, value) {
if (value.isNotEmpty) {
result[key] = value.join(', ');
}
});
return result;
}
}

View File

@@ -1,44 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
Future<RsiLauncherAsarData> getRsiLauncherAsarData({
required String asarPath,
}) => RustLib.instance.api.crateApiAsarApiGetRsiLauncherAsarData(
asarPath: asarPath,
);
class RsiLauncherAsarData {
final String asarPath;
final String mainJsPath;
final Uint8List mainJsContent;
const RsiLauncherAsarData({
required this.asarPath,
required this.mainJsPath,
required this.mainJsContent,
});
Future<void> writeMainJs({required List<int> content}) =>
RustLib.instance.api.crateApiAsarApiRsiLauncherAsarDataWriteMainJs(
that: this,
content: content,
);
@override
int get hashCode =>
asarPath.hashCode ^ mainJsPath.hashCode ^ mainJsContent.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RsiLauncherAsarData &&
runtimeType == other.runtimeType &&
asarPath == other.asarPath &&
mainJsPath == other.mainJsPath &&
mainJsContent == other.mainJsContent;
}

View File

@@ -1,37 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import '../http_package.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `_my_method_to_hyper_method`
Future<void> setDefaultHeader({required Map<String, String> headers}) =>
RustLib.instance.api.crateApiHttpApiSetDefaultHeader(headers: headers);
Future<RustHttpResponse> fetch({
required MyMethod method,
required String url,
Map<String, String>? headers,
Uint8List? inputData,
String? withIpAddress,
bool? withCustomDns,
}) => RustLib.instance.api.crateApiHttpApiFetch(
method: method,
url: url,
headers: headers,
inputData: inputData,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns,
);
Future<List<String>> dnsLookupTxt({required String host}) =>
RustLib.instance.api.crateApiHttpApiDnsLookupTxt(host: host);
Future<List<String>> dnsLookupIps({required String host}) =>
RustLib.instance.api.crateApiHttpApiDnsLookupIps(host: host);
enum MyMethod { options, gets, post, put, delete, head, trace, connect, patch }

View File

@@ -1,50 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `_process_output`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `RsProcess`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`
Stream<RsProcessStreamData> start({
required String executable,
required List<String> arguments,
required String workingDirectory,
}) => RustLib.instance.api.crateApiRsProcessStart(
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
);
Future<void> write({required int rsPid, required String data}) =>
RustLib.instance.api.crateApiRsProcessWrite(rsPid: rsPid, data: data);
class RsProcessStreamData {
final RsProcessStreamDataType dataType;
final String data;
final int rsPid;
const RsProcessStreamData({
required this.dataType,
required this.data,
required this.rsPid,
});
@override
int get hashCode => dataType.hashCode ^ data.hashCode ^ rsPid.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RsProcessStreamData &&
runtimeType == other.runtimeType &&
dataType == other.dataType &&
data == other.data &&
rsPid == other.rsPid;
}
enum RsProcessStreamDataType { output, error, exit }

View File

@@ -1,24 +1,15 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// Web platform stub for win32_api
// Windows API 在 Web 平台上不可用,提供空实现
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
/// 发送系统通知Web 平台使用控制台输出)
Future<void> sendNotify({String? summary, String? body, String? appName, String? appId}) async {
print('Notification: $summary - $body');
// Web 平台可以使用浏览器 Notification API
// 但需要用户授权,这里简化处理
}
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
Future<void> sendNotify({
String? summary,
String? body,
String? appName,
String? appId,
}) => RustLib.instance.api.crateApiWin32ApiSendNotify(
summary: summary,
body: body,
appName: appName,
appId: appId,
);
Future<bool> setForegroundWindow({required String windowName}) => RustLib
.instance
.api
.crateApiWin32ApiSetForegroundWindow(windowName: windowName);
/// 设置窗口为前台Web 平台不支持)
Future<bool> setForegroundWindow({required String windowName}) async {
print('setForegroundWindow not supported on web platform');
return false;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,7 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// HTTP 响应数据类
// 原先由 flutter_rust_bridge 生成,现改为纯 Dart 实现
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'dart:typed_data';
enum MyHttpVersion { http09, http10, http11, http2, http3, httpUnknown }

View File

@@ -0,0 +1,14 @@
// Stub file for web platform - not used
class FileCacheUtils {
static Future<dynamic> getFile(String url) async {
throw UnsupportedError('File operations are not supported on web platform');
}
static Future<bool> clearCache(String url) async {
throw UnsupportedError('File operations are not supported on web platform');
}
static Future<void> clearAllCache() async {
throw UnsupportedError('File operations are not supported on web platform');
}
}