Compare commits

...

14 Commits

Author SHA1 Message Date
xkeyC
2b7be216a8 bump: WEB 3.0.0 2025-12-23 17:23:30 +08:00
xkeyC
9e324c01fb fix: web zip 2025-12-23 17:22:52 +08:00
xkeyC
79cc157e04 feat: update 2025-12-20 17:31:36 +08:00
xkeyC
6d2bb89c64 feat: deply config 2025-12-20 16:40:53 +08:00
xkeyC
66ab76e784 feat: web yearly_report 2025-12-20 16:04:51 +08:00
xkeyC
acea7bc68c fix: icon 2025-11-13 20:52:37 +08:00
xkeyC
4a8b18fed0 fix: lang 2025-11-13 20:48:29 +08:00
xkeyC
16cc835f23 fix: icon 2025-11-13 20:33:10 +08:00
xkeyC
6984da58b8 fix: update icon 2025-11-13 20:29:43 +08:00
xkeyC
71c3b61bdd feat: AnalyticsApi disable firstLaunch 2025-11-13 20:24:09 +08:00
xkeyC
c31b31516f fix: RSI Status 2025-11-13 20:19:22 +08:00
xkeyC
476c40f4cd feat: WASM web support 2025-11-13 20:07:47 +08:00
xkeyC
193d2c7496 feat: web support 2025-11-13 17:29:46 +08:00
xkeyC
334ed424e9 feat: web support 2025-11-13 17:29:27 +08:00
170 changed files with 8768 additions and 7481 deletions

9
.gitignore vendored
View File

@@ -47,4 +47,11 @@ app.*.map.json
/lib/generated/l10n_temp_fix.json
# FVM Version Cache
.fvm/
.fvm/
# Web-only branch: ignore other platform folders
/android/
/ios/
/linux/
/macos/
/windows/

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3"
revision: "a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7"
channel: "stable"
project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: android
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: ios
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: linux
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: macos
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
create_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
base_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
- platform: web
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: windows
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
create_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
base_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
# User provided section

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

View File

@@ -7,15 +7,13 @@ import 'package:starcitizen_doctor/common/utils/log.dart';
class AnalyticsApi {
static Future<void> touch(String key) async {
if (kDebugMode || kProfileMode) {
if (kDebugMode || kProfileMode || kIsWeb) {
dPrint("AnalyticsApi.touch === $key skip");
return;
}
dPrint("AnalyticsApi.touch === $key start");
try {
final r = await RSHttp.postData(
"${URLConf.analyticsApiHome}/analytics/$key",
data: null);
final r = await RSHttp.postData("${URLConf.analyticsApiHome}/analytics/$key", data: null);
dPrint("AnalyticsApi.touch === $key over statusCode == ${r.statusCode}");
} catch (e) {
dPrint("AnalyticsApi.touch === $key Error:$e");

View File

@@ -73,7 +73,7 @@ class Api {
}
static Future<List> getScServerStatus() async {
final r = await RSHttp.getText("https://status.robertsspaceindustries.com/index.json");
final r = await RSHttp.getText("https:///web-proxy.scbox.xkeyc.cn/rsi_status/index.json");
final map = json.decode(r);
return map["systems"];
}

View File

@@ -2,7 +2,8 @@ import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart';
import 'package:hexcolor/hexcolor.dart';
@@ -14,18 +15,12 @@ import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/guide/guide_ui.dart';
import 'package:starcitizen_doctor/ui/home/performance/performance_ui.dart';
import 'package:starcitizen_doctor/ui/splash_ui.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart';
import 'api/analytics.dart';
import 'api/api.dart';
import 'common/conf/url_conf.dart';
import 'common/helper/system_helper.dart';
import 'common/io/rs_http.dart';
import 'common/rust/frb_generated.dart';
import 'common/rust/api/win32_api.dart' as win32;
// import 'common/rust/api/win32_api.dart' as win32; // Web 不支持
import 'data/app_version_data.dart';
import 'generated/no_l10n_strings.dart';
import 'ui/home/downloader/home_downloader_ui.dart';
@@ -34,6 +29,7 @@ import 'ui/home/localization/advanced_localization_ui.dart';
import 'ui/index_ui.dart';
import 'ui/settings/upgrade_dialog.dart';
import 'ui/tools/unp4kc/unp4kc_ui.dart';
import 'ui/tools/yearly_report/yearly_report_entry.dart';
part 'app.g.dart';
@@ -49,24 +45,24 @@ abstract class AppGlobalState with _$AppGlobalState {
@Default(ThemeConf()) ThemeConf themeConf,
Locale? appLocale,
Box? appConfBox,
@Default("assets/backgrounds/SC_01_Wallpaper_3840x2160.webp") String backgroundImageAssetsPath,
}) = _AppGlobalState;
}
@riverpod
GoRouter router(Ref ref) {
return GoRouter(
initialLocation: '/splash',
routes: [
GoRoute(path: '/splash', pageBuilder: (context, state) => myPageBuilder(context, state, const SplashUI())),
GoRoute(
path: '/',
pageBuilder: (context, state) => myPageBuilder(context, state, const SplashUI()),
),
GoRoute(
path: '/index',
pageBuilder: (context, state) => myPageBuilder(context, state, const IndexUI()),
routes: [
GoRoute(
path: "downloader",
pageBuilder: (context, state) => myPageBuilder(context, state, const HomeDownloaderUI())),
path: "downloader",
pageBuilder: (context, state) => myPageBuilder(context, state, const HomeDownloaderUI()),
),
GoRoute(
path: 'game_doctor',
pageBuilder: (context, state) => myPageBuilder(context, state, const HomeGameDoctorUI()),
@@ -76,17 +72,23 @@ GoRouter router(Ref ref) {
pageBuilder: (context, state) => myPageBuilder(context, state, const HomePerformanceUI()),
),
GoRoute(
path: 'advanced_localization',
pageBuilder: (context, state) => myPageBuilder(context, state, const AdvancedLocalizationUI()))
path: 'advanced_localization',
pageBuilder: (context, state) => myPageBuilder(context, state, const AdvancedLocalizationUI()),
),
],
),
GoRoute(path: '/tools', builder: (_, _) => const SizedBox(), routes: [
GoRoute(
path: 'unp4kc',
pageBuilder: (context, state) => myPageBuilder(context, state, const UnP4kcUI()),
),
]),
GoRoute(path: '/guide', pageBuilder: (context, state) => myPageBuilder(context, state, const GuideUI()))
GoRoute(
path: '/tools',
builder: (_, _) => const SizedBox(),
routes: [
GoRoute(path: 'unp4kc', pageBuilder: (context, state) => myPageBuilder(context, state, const UnP4kcUI())),
GoRoute(
path: 'yearly_report',
pageBuilder: (context, state) => myPageBuilder(context, state, const YearlyReportEntryUIRoute()),
),
],
),
GoRoute(path: '/guide', pageBuilder: (context, state) => myPageBuilder(context, state, const GuideUI())),
],
);
}
@@ -94,13 +96,13 @@ GoRouter router(Ref ref) {
@riverpod
class AppGlobalModel extends _$AppGlobalModel {
static Map<Locale, String> get appLocaleSupport => {
const Locale("auto"): S.current.settings_app_language_auto,
const Locale("zh", "CN"): NoL10n.langZHS,
const Locale("zh", "TW"): NoL10n.langZHT,
const Locale("en"): NoL10n.langEn,
const Locale("ja"): NoL10n.langJa,
const Locale("ru"): NoL10n.langRU,
};
const Locale("auto"): S.current.settings_app_language_auto,
const Locale("zh", "CN"): NoL10n.langZHS,
const Locale("zh", "TW"): NoL10n.langZHT,
const Locale("en"): NoL10n.langEn,
const Locale("ja"): NoL10n.langJa,
const Locale("ru"): NoL10n.langRU,
};
@override
AppGlobalState build() {
@@ -113,21 +115,18 @@ class AppGlobalModel extends _$AppGlobalModel {
if (_initialized) return;
// init Data
final applicationSupportDir = await _initAppDir();
// init Rust bridge
await RustLib.init();
await RSHttp.init();
dPrint("---- rust bridge init -----");
// init Hive
try {
Hive.init("$applicationSupportDir/db");
if (!kIsWeb) Hive.init("$applicationSupportDir/db");
final box = await Hive.openBox("app_conf");
state = state.copyWith(appConfBox: box);
if (box.get("install_id", defaultValue: "") == "") {
await box.put("install_id", const Uuid().v4());
AnalyticsApi.touch("firstLaunch");
}
// if (box.get("install_id", defaultValue: "") == "") {
// await box.put("install_id", const Uuid().v4());
// AnalyticsApi.touch("firstLaunch");
// }
final deviceUUID = box.get("install_id", defaultValue: "");
final localeCode = box.get("app_locale", defaultValue: null);
Locale? locale;
@@ -141,13 +140,16 @@ class AppGlobalModel extends _$AppGlobalModel {
}
state = state.copyWith(deviceUUID: deviceUUID, appLocale: locale);
} catch (e) {
await win32.setForegroundWindow(windowName: "SCToolBox");
// Web 平台不支持 win32 API
if (!kIsWeb) {
// await win32.setForegroundWindow(windowName: "SCToolBox");
}
dPrint("exit: db is locking ...");
exit(0);
if (!kIsWeb) exit(0);
}
// init powershell
if (Platform.isWindows) {
if (!kIsWeb && Platform.isWindows) {
try {
await SystemHelper.initPowershellPath();
dPrint("---- Powershell init -----");
@@ -157,35 +159,63 @@ class AppGlobalModel extends _$AppGlobalModel {
}
// get windows info
WindowsDeviceInfo? windowsDeviceInfo;
try {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
windowsDeviceInfo = await deviceInfo.windowsInfo;
} catch (e) {
dPrint("DeviceInfo.windowsInfo error: $e");
}
// WindowsDeviceInfo? windowsDeviceInfo;
// try {
// DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
// windowsDeviceInfo = await deviceInfo.windowsInfo;
// } catch (e) {
// dPrint("DeviceInfo.windowsInfo error: $e");
// }
// init windows
windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setTitle("SCToolBox");
await windowManager.setSkipTaskbar(false);
await windowManager.show();
if (Platform.isWindows) {
await Window.initialize();
await Window.hideWindowControls();
if (windowsDeviceInfo?.productName.contains("Windows 11") ?? false) {
await Window.setEffect(
effect: WindowEffect.acrylic,
);
}
}
});
// if (!kIsWeb) {
// windowManager.waitUntilReadyToShow().then((_) async {
// await windowManager.setTitle("SCToolBox");
// await windowManager.setSkipTaskbar(false);
// await windowManager.show();
// if (Platform.isWindows) {
// await Window.initialize();
// await Window.hideWindowControls();
// if (windowsDeviceInfo?.productName.contains("Windows 11") ?? false) {
// await Window.setEffect(effect: WindowEffect.acrylic);
// }
// }
// });
// }
dPrint("---- Window init -----");
if (kIsWeb) {
_startBackgroundLoop();
}
_initialized = true;
ref.keepAlive();
}
Timer? _loopTimer;
void _startBackgroundLoop() async {
_loopTimer?.cancel();
_loopTimer = null;
final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle);
final imageAssetsList = assetManifest
.listAssets()
.where((string) => string.startsWith("assets/backgrounds"))
.toList();
void rollImage() {
final random = DateTime.now().millisecondsSinceEpoch % imageAssetsList.length;
final image = imageAssetsList[random];
state = state.copyWith(backgroundImageAssetsPath: image);
dPrint("rollImage: [$random] $image");
}
rollImage();
// 使用 timer 每 30 秒 更换一次随机图片
_loopTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
rollImage();
});
}
String getUpgradePath() {
return "${state.applicationSupportDir}/._upgrade";
}
@@ -194,7 +224,7 @@ class AppGlobalModel extends _$AppGlobalModel {
// ignore: avoid_build_context_in_providers
Future<bool> checkUpdate(BuildContext context) async {
if (!ConstConf.isMSE) {
if (!kIsWeb && !ConstConf.isMSE) {
final dir = Directory(getUpgradePath());
if (await dir.exists()) {
dir.delete(recursive: true);
@@ -226,18 +256,25 @@ class AppGlobalModel extends _$AppGlobalModel {
if (state.networkVersionData == null) {
if (!context.mounted) return false;
await showToast(
context, S.current.app_common_network_error(ConstConf.appVersionDate, checkUpdateError.toString()));
context,
S.current.app_common_network_error(ConstConf.appVersionDate, checkUpdateError.toString()),
);
return false;
}
if (kIsWeb) return false; // Web 版本不支持自动更新
if (!Platform.isWindows) return false;
final lastVersion =
ConstConf.isMSE ? state.networkVersionData?.mSELastVersionCode : state.networkVersionData?.lastVersionCode;
final lastVersion = ConstConf.isMSE
? state.networkVersionData?.mSELastVersionCode
: state.networkVersionData?.lastVersionCode;
if ((lastVersion ?? 0) > ConstConf.appVersionCode) {
// need update
if (!context.mounted) return false;
final r =
await showDialog(dismissWithEsc: false, context: context, builder: (context) => const UpgradeDialogUI());
final r = await showDialog(
dismissWithEsc: false,
context: context,
builder: (context) => const UpgradeDialogUI(),
);
if (r != true) {
if (!context.mounted) return false;
@@ -264,8 +301,10 @@ class AppGlobalModel extends _$AppGlobalModel {
dPrint("now == $now start == $startTime end == $endTime");
if (now < startTime) {
_activityThemeColorTimer =
Timer(Duration(milliseconds: startTime - now), () => checkActivityThemeColor(networkVersionData));
_activityThemeColorTimer = Timer(
Duration(milliseconds: startTime - now),
() => checkActivityThemeColor(networkVersionData),
);
dPrint("start Timer ....");
} else if (now >= startTime && now <= endTime) {
dPrint("update Color ....");
@@ -280,8 +319,10 @@ class AppGlobalModel extends _$AppGlobalModel {
);
// wait for end
_activityThemeColorTimer =
Timer(Duration(milliseconds: endTime - now), () => checkActivityThemeColor(networkVersionData));
_activityThemeColorTimer = Timer(
Duration(milliseconds: endTime - now),
() => checkActivityThemeColor(networkVersionData),
);
} else {
dPrint("reset Color ....");
state = state.copyWith(
@@ -302,8 +343,9 @@ class AppGlobalModel extends _$AppGlobalModel {
await appConfBox.put("app_locale", null);
return;
}
final localeCode =
value.countryCode != null ? "${value.languageCode}_${value.countryCode ?? ""}" : value.languageCode;
final localeCode = value.countryCode != null
? "${value.languageCode}_${value.countryCode ?? ""}"
: value.languageCode;
dPrint("changeLocale == $value localeCode=== $localeCode");
await appConfBox.put("app_locale", localeCode);
state = state.copyWith(appLocale: value);
@@ -311,6 +353,12 @@ class AppGlobalModel extends _$AppGlobalModel {
}
Future<String> _initAppDir() async {
if (kIsWeb) {
await Future.delayed(const Duration(milliseconds: 10));
// Web 版本不需要本地目录
state = state.copyWith(applicationSupportDir: "", applicationBinaryModuleDir: "");
return Future.value("");
}
if (Platform.isWindows) {
final userProfileDir = Platform.environment["USERPROFILE"];
final applicationSupportDir = (await getApplicationSupportDirectory()).absolute.path;

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppGlobalState {
String? get deviceUUID; String? get applicationSupportDir; String? get applicationBinaryModuleDir; AppVersionData? get networkVersionData; ThemeConf get themeConf; Locale? get appLocale; Box? get appConfBox;
String? get deviceUUID; String? get applicationSupportDir; String? get applicationBinaryModuleDir; AppVersionData? get networkVersionData; ThemeConf get themeConf; Locale? get appLocale; Box? get appConfBox; String get backgroundImageAssetsPath;
/// Create a copy of AppGlobalState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $AppGlobalStateCopyWith<AppGlobalState> get copyWith => _$AppGlobalStateCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppGlobalState&&(identical(other.deviceUUID, deviceUUID) || other.deviceUUID == deviceUUID)&&(identical(other.applicationSupportDir, applicationSupportDir) || other.applicationSupportDir == applicationSupportDir)&&(identical(other.applicationBinaryModuleDir, applicationBinaryModuleDir) || other.applicationBinaryModuleDir == applicationBinaryModuleDir)&&(identical(other.networkVersionData, networkVersionData) || other.networkVersionData == networkVersionData)&&(identical(other.themeConf, themeConf) || other.themeConf == themeConf)&&(identical(other.appLocale, appLocale) || other.appLocale == appLocale)&&(identical(other.appConfBox, appConfBox) || other.appConfBox == appConfBox));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppGlobalState&&(identical(other.deviceUUID, deviceUUID) || other.deviceUUID == deviceUUID)&&(identical(other.applicationSupportDir, applicationSupportDir) || other.applicationSupportDir == applicationSupportDir)&&(identical(other.applicationBinaryModuleDir, applicationBinaryModuleDir) || other.applicationBinaryModuleDir == applicationBinaryModuleDir)&&(identical(other.networkVersionData, networkVersionData) || other.networkVersionData == networkVersionData)&&(identical(other.themeConf, themeConf) || other.themeConf == themeConf)&&(identical(other.appLocale, appLocale) || other.appLocale == appLocale)&&(identical(other.appConfBox, appConfBox) || other.appConfBox == appConfBox)&&(identical(other.backgroundImageAssetsPath, backgroundImageAssetsPath) || other.backgroundImageAssetsPath == backgroundImageAssetsPath));
}
@override
int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox);
int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox,backgroundImageAssetsPath);
@override
String toString() {
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox)';
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox, backgroundImageAssetsPath: $backgroundImageAssetsPath)';
}
@@ -45,7 +45,7 @@ abstract mixin class $AppGlobalStateCopyWith<$Res> {
factory $AppGlobalStateCopyWith(AppGlobalState value, $Res Function(AppGlobalState) _then) = _$AppGlobalStateCopyWithImpl;
@useResult
$Res call({
String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox
String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath
});
@@ -62,7 +62,7 @@ class _$AppGlobalStateCopyWithImpl<$Res>
/// Create a copy of AppGlobalState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? deviceUUID = freezed,Object? applicationSupportDir = freezed,Object? applicationBinaryModuleDir = freezed,Object? networkVersionData = freezed,Object? themeConf = null,Object? appLocale = freezed,Object? appConfBox = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? deviceUUID = freezed,Object? applicationSupportDir = freezed,Object? applicationBinaryModuleDir = freezed,Object? networkVersionData = freezed,Object? themeConf = null,Object? appLocale = freezed,Object? appConfBox = freezed,Object? backgroundImageAssetsPath = null,}) {
return _then(_self.copyWith(
deviceUUID: freezed == deviceUUID ? _self.deviceUUID : deviceUUID // ignore: cast_nullable_to_non_nullable
as String?,applicationSupportDir: freezed == applicationSupportDir ? _self.applicationSupportDir : applicationSupportDir // ignore: cast_nullable_to_non_nullable
@@ -71,7 +71,8 @@ as String?,networkVersionData: freezed == networkVersionData ? _self.networkVers
as AppVersionData?,themeConf: null == themeConf ? _self.themeConf : themeConf // ignore: cast_nullable_to_non_nullable
as ThemeConf,appLocale: freezed == appLocale ? _self.appLocale : appLocale // ignore: cast_nullable_to_non_nullable
as Locale?,appConfBox: freezed == appConfBox ? _self.appConfBox : appConfBox // ignore: cast_nullable_to_non_nullable
as Box?,
as Box?,backgroundImageAssetsPath: null == backgroundImageAssetsPath ? _self.backgroundImageAssetsPath : backgroundImageAssetsPath // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of AppGlobalState
@@ -165,10 +166,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppGlobalState() when $default != null:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox);case _:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.backgroundImageAssetsPath);case _:
return orElse();
}
@@ -186,10 +187,10 @@ return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBi
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath) $default,) {final _that = this;
switch (_that) {
case _AppGlobalState():
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox);case _:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.backgroundImageAssetsPath);case _:
throw StateError('Unexpected subclass');
}
@@ -206,10 +207,10 @@ return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBi
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath)? $default,) {final _that = this;
switch (_that) {
case _AppGlobalState() when $default != null:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox);case _:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.backgroundImageAssetsPath);case _:
return null;
}
@@ -221,7 +222,7 @@ return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBi
class _AppGlobalState implements AppGlobalState {
const _AppGlobalState({this.deviceUUID, this.applicationSupportDir, this.applicationBinaryModuleDir, this.networkVersionData, this.themeConf = const ThemeConf(), this.appLocale, this.appConfBox});
const _AppGlobalState({this.deviceUUID, this.applicationSupportDir, this.applicationBinaryModuleDir, this.networkVersionData, this.themeConf = const ThemeConf(), this.appLocale, this.appConfBox, this.backgroundImageAssetsPath = "assets/backgrounds/SC_01_Wallpaper_3840x2160.webp"});
@override final String? deviceUUID;
@@ -231,6 +232,7 @@ class _AppGlobalState implements AppGlobalState {
@override@JsonKey() final ThemeConf themeConf;
@override final Locale? appLocale;
@override final Box? appConfBox;
@override@JsonKey() final String backgroundImageAssetsPath;
/// Create a copy of AppGlobalState
/// with the given fields replaced by the non-null parameter values.
@@ -242,16 +244,16 @@ _$AppGlobalStateCopyWith<_AppGlobalState> get copyWith => __$AppGlobalStateCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppGlobalState&&(identical(other.deviceUUID, deviceUUID) || other.deviceUUID == deviceUUID)&&(identical(other.applicationSupportDir, applicationSupportDir) || other.applicationSupportDir == applicationSupportDir)&&(identical(other.applicationBinaryModuleDir, applicationBinaryModuleDir) || other.applicationBinaryModuleDir == applicationBinaryModuleDir)&&(identical(other.networkVersionData, networkVersionData) || other.networkVersionData == networkVersionData)&&(identical(other.themeConf, themeConf) || other.themeConf == themeConf)&&(identical(other.appLocale, appLocale) || other.appLocale == appLocale)&&(identical(other.appConfBox, appConfBox) || other.appConfBox == appConfBox));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppGlobalState&&(identical(other.deviceUUID, deviceUUID) || other.deviceUUID == deviceUUID)&&(identical(other.applicationSupportDir, applicationSupportDir) || other.applicationSupportDir == applicationSupportDir)&&(identical(other.applicationBinaryModuleDir, applicationBinaryModuleDir) || other.applicationBinaryModuleDir == applicationBinaryModuleDir)&&(identical(other.networkVersionData, networkVersionData) || other.networkVersionData == networkVersionData)&&(identical(other.themeConf, themeConf) || other.themeConf == themeConf)&&(identical(other.appLocale, appLocale) || other.appLocale == appLocale)&&(identical(other.appConfBox, appConfBox) || other.appConfBox == appConfBox)&&(identical(other.backgroundImageAssetsPath, backgroundImageAssetsPath) || other.backgroundImageAssetsPath == backgroundImageAssetsPath));
}
@override
int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox);
int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox,backgroundImageAssetsPath);
@override
String toString() {
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox)';
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox, backgroundImageAssetsPath: $backgroundImageAssetsPath)';
}
@@ -262,7 +264,7 @@ abstract mixin class _$AppGlobalStateCopyWith<$Res> implements $AppGlobalStateCo
factory _$AppGlobalStateCopyWith(_AppGlobalState value, $Res Function(_AppGlobalState) _then) = __$AppGlobalStateCopyWithImpl;
@override @useResult
$Res call({
String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox
String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath
});
@@ -279,7 +281,7 @@ class __$AppGlobalStateCopyWithImpl<$Res>
/// Create a copy of AppGlobalState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? deviceUUID = freezed,Object? applicationSupportDir = freezed,Object? applicationBinaryModuleDir = freezed,Object? networkVersionData = freezed,Object? themeConf = null,Object? appLocale = freezed,Object? appConfBox = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? deviceUUID = freezed,Object? applicationSupportDir = freezed,Object? applicationBinaryModuleDir = freezed,Object? networkVersionData = freezed,Object? themeConf = null,Object? appLocale = freezed,Object? appConfBox = freezed,Object? backgroundImageAssetsPath = null,}) {
return _then(_AppGlobalState(
deviceUUID: freezed == deviceUUID ? _self.deviceUUID : deviceUUID // ignore: cast_nullable_to_non_nullable
as String?,applicationSupportDir: freezed == applicationSupportDir ? _self.applicationSupportDir : applicationSupportDir // ignore: cast_nullable_to_non_nullable
@@ -288,7 +290,8 @@ as String?,networkVersionData: freezed == networkVersionData ? _self.networkVers
as AppVersionData?,themeConf: null == themeConf ? _self.themeConf : themeConf // ignore: cast_nullable_to_non_nullable
as ThemeConf,appLocale: freezed == appLocale ? _self.appLocale : appLocale // ignore: cast_nullable_to_non_nullable
as Locale?,appConfBox: freezed == appConfBox ? _self.appConfBox : appConfBox // ignore: cast_nullable_to_non_nullable
as Box?,
as Box?,backgroundImageAssetsPath: null == backgroundImageAssetsPath ? _self.backgroundImageAssetsPath : backgroundImageAssetsPath // ignore: cast_nullable_to_non_nullable
as String,
));
}

View File

@@ -48,7 +48,7 @@ final class RouterProvider
}
}
String _$routerHash() => r'62dd494daf9b176547e30da83b1923ccdea13b4f';
String _$routerHash() => r'eb81af4202a3a92cf95447ecfb6a698f9a8cd122';
@ProviderFor(AppGlobalModel)
const appGlobalModelProvider = AppGlobalModelProvider._();
@@ -82,7 +82,7 @@ final class AppGlobalModelProvider
}
}
String _$appGlobalModelHash() => r'53dd9ed5e197333b509d282eb01073f15572820c';
String _$appGlobalModelHash() => r'8bd5efc6fb95ad9d06875e6fb18bf46ae222fafa';
abstract class _$AppGlobalModel extends $Notifier<AppGlobalState> {
AppGlobalState build();

View File

@@ -1,15 +1,8 @@
class ConstConf {
static const String appVersion = "2.15.0";
static const int appVersionCode = 70;
static const String appVersionDate = "2025-11-8";
static const _gameChannels = [
"LIVE",
"4.0_PREVIEW",
"PTU",
"EPTU",
"TECH-PREVIEW",
"HOTFIX",
];
static const String appVersion = "3.0.0";
static const int appVersionCode = 79;
static const String appVersionDate = "2025-12-23";
static const _gameChannels = ["LIVE", "4.0_PREVIEW", "PTU", "EPTU", "TECH-PREVIEW", "HOTFIX"];
static const isMSE = String.fromEnvironment("MSE", defaultValue: "false") == "true";
static const win32AppId = isMSE
? "56575xkeyC.MSE_bsn1nexg8e4qe!starcitizendoctor"

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

@@ -0,0 +1,434 @@
import 'package:intl/intl.dart';
import 'package:starcitizen_doctor/generated/l10n.dart';
/// 日志分析结果数据类
class LogAnalyzeLineData {
final String type;
final String title;
final String? data;
final String? dateTime;
final String? tag;
final String? victimId;
final String? location;
final String? area;
final String? playerName;
const LogAnalyzeLineData({
required this.type,
required this.title,
this.data,
this.dateTime,
this.tag,
this.victimId,
this.location,
this.area,
this.playerName,
});
@override
String toString() {
return 'LogAnalyzeLineData(type: $type, title: $title, data: $data, dateTime: $dateTime)';
}
}
/// 日志分析统计数据
class LogAnalyzeStatistics {
final String playerName;
final int killCount;
final int deathCount;
final int selfKillCount;
final int vehicleDestructionCount;
final int vehicleDestructionCountHard;
final DateTime? gameStartTime;
final int gameCrashLineNumber;
final String? latestLocation;
const LogAnalyzeStatistics({
required this.playerName,
required this.killCount,
required this.deathCount,
required this.selfKillCount,
required this.vehicleDestructionCount,
required this.vehicleDestructionCountHard,
this.gameStartTime,
required this.gameCrashLineNumber,
this.latestLocation,
});
}
/// 游戏日志分析器
class GameLogAnalyzer {
static const String unknownValue = "<Unknown>";
static final _baseRegExp = RegExp(r'\[Notice\]\s+<([^>]+)>');
static final _gameLoadingRegExp = RegExp(
r'<[^>]+>\s+Loading screen for\s+(\w+)\s+:\s+SC_Frontend closed after\s+(\d+\.\d+)\s+seconds',
);
static final _logDateTimeRegExp = RegExp(r'<(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)>');
static final DateFormat _dateTimeFormatter = DateFormat('yyyy-MM-dd HH:mm:ss:SSS');
static final _fatalCollisionPatterns = {
'vehicle': RegExp(r'Fatal Collision occured for vehicle\s+(\S+)'),
'zone': RegExp(r'Zone:\s*([^,\]]+)'),
'player_pilot': RegExp(r'PlayerPilot:\s*(\d)'),
'hit_entity': RegExp(r'hitting entity:\s*(\w+)'),
'distance': RegExp(r'Distance:\s*([\d.]+)'),
};
static final _vehicleDestructionPattern = RegExp(
r"Vehicle\s+'([^']+)'.*?"
r"in zone\s+'([^']+)'.*?"
r"destroy level \d+ to (\d+).*?"
r"caused by\s+'([^']+)'",
);
static final _actorDeathPattern = RegExp(
r"Actor '([^']+)'.*?"
r"ejected from zone '([^']+)'.*?"
r"to zone '([^']+)'",
);
static final _characterNamePattern = RegExp(r"name\s+([^-]+)");
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
static final vehicleControlPattern = RegExp(r"granted control token for '([^']+)'\s+\[(\d+)\]");
static DateTime? getLogLineDateTime(String line) => _getLogLineDateTime(line);
static String? getLogLineDateTimeString(String line) => _getLogLineDateTimeString(line);
static String removeVehicleId(String vehicleName) {
final regex = RegExp(r'_\d+$');
return vehicleName.replaceAll(regex, '');
}
/// 从字符串内容分析日志
static (List<LogAnalyzeLineData>, LogAnalyzeStatistics) analyzeLogContent(String content, {DateTime? startTime}) {
final logLines = content.split("\n");
return _analyzeLogLines(logLines, startTime: startTime);
}
static (List<LogAnalyzeLineData>, LogAnalyzeStatistics) _analyzeLogLines(
List<String> logLines, {
DateTime? startTime,
}) {
final results = <LogAnalyzeLineData>[];
String playerName = "";
int killCount = 0;
int deathCount = 0;
int selfKillCount = 0;
int vehicleDestructionCount = 0;
int vehicleDestructionCountHard = 0;
DateTime? gameStartTime;
String? latestLocation;
int gameCrashLineNumber = -1;
bool shouldCount = startTime == null;
for (var i = 0; i < logLines.length; i++) {
final line = logLines[i];
if (line.isEmpty) continue;
if (startTime != null && !shouldCount) {
final lineTime = _getLogLineDateTime(line);
if (lineTime != null && lineTime.isAfter(startTime)) {
shouldCount = true;
}
}
if (gameStartTime == null) {
gameStartTime = _getLogLineDateTime(line);
if (gameStartTime != null) {
results.add(LogAnalyzeLineData(type: "info", title: S.current.log_analyzer_game_start, tag: "game_start"));
}
}
final gameLoading = _parseGameLoading(line);
if (gameLoading != null) {
results.add(
LogAnalyzeLineData(
type: "info",
title: S.current.log_analyzer_game_loading,
data: S.current.log_analyzer_mode_loading_time(gameLoading.$1, gameLoading.$2),
dateTime: _getLogLineDateTimeString(line),
),
);
continue;
}
final baseEvent = _parseBaseEvent(line);
if (baseEvent != null) {
LogAnalyzeLineData? data;
switch (baseEvent) {
case "AccountLoginCharacterStatus_Character":
data = _parseCharacterName(line);
if (data != null && data.playerName != null) {
playerName = data.playerName!;
}
break;
case "FatalCollision":
data = _parseFatalCollision(line);
break;
case "Vehicle Destruction":
data = _parseVehicleDestruction(line, playerName, shouldCount, (isHard) {
if (isHard) {
vehicleDestructionCountHard++;
} else {
vehicleDestructionCount++;
}
});
break;
case "[ActorState] Dead":
data = _parseActorDeath(line, playerName, shouldCount, (isKill, isDeath, isSelfKill) {
if (isSelfKill) {
selfKillCount++;
} else {
if (isKill) killCount++;
if (isDeath) deathCount++;
}
});
break;
case "RequestLocationInventory":
data = _parseRequestLocationInventory(line);
if (data != null && data.location != null) {
latestLocation = data.location;
}
break;
}
if (data != null) {
results.add(data);
continue;
}
}
if (line.contains("[CIG] CCIGBroker::FastShutdown")) {
results.add(
LogAnalyzeLineData(
type: "info",
title: S.current.log_analyzer_game_close,
dateTime: _getLogLineDateTimeString(line),
),
);
continue;
}
if (line.contains("Cloud Imperium Games public crash handler")) {
gameCrashLineNumber = i;
}
}
if (gameCrashLineNumber > 0) {
final lastLineDateTime = gameStartTime != null
? _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20")))
: null;
final crashInfo = logLines.sublist(gameCrashLineNumber);
results.add(
LogAnalyzeLineData(
type: "game_crash",
title: S.current.log_analyzer_game_crash,
data: crashInfo.join("\n"),
dateTime: lastLineDateTime != null ? _dateTimeFormatter.format(lastLineDateTime) : null,
),
);
}
if (killCount > 0 || deathCount > 0) {
results.add(
LogAnalyzeLineData(
type: "statistics",
title: S.current.log_analyzer_kill_summary,
data: S.current.log_analyzer_kill_death_suicide_count(
killCount,
deathCount,
selfKillCount,
vehicleDestructionCount,
vehicleDestructionCountHard,
),
),
);
}
if (gameStartTime != null) {
final lastLineDateTime = _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20"), orElse: () => ""));
if (lastLineDateTime != null) {
final duration = lastLineDateTime.difference(gameStartTime);
results.add(
LogAnalyzeLineData(
type: "statistics",
title: S.current.log_analyzer_play_time,
data: S.current.log_analyzer_play_time_format(
duration.inHours,
duration.inMinutes.remainder(60),
duration.inSeconds.remainder(60),
),
),
);
}
}
final statistics = LogAnalyzeStatistics(
playerName: playerName,
killCount: killCount,
deathCount: deathCount,
selfKillCount: selfKillCount,
vehicleDestructionCount: vehicleDestructionCount,
vehicleDestructionCountHard: vehicleDestructionCountHard,
gameStartTime: gameStartTime,
gameCrashLineNumber: gameCrashLineNumber,
latestLocation: latestLocation,
);
return (results, statistics);
}
static String? _parseBaseEvent(String line) {
final match = _baseRegExp.firstMatch(line);
return match?.group(1);
}
static (String, String)? _parseGameLoading(String line) {
final match = _gameLoadingRegExp.firstMatch(line);
if (match != null) {
return (match.group(1) ?? "-", match.group(2) ?? "-");
}
return null;
}
static DateTime? _getLogLineDateTime(String line) {
final match = _logDateTimeRegExp.firstMatch(line);
if (match != null) {
final dateTimeString = match.group(1);
if (dateTimeString != null) {
return DateTime.parse(dateTimeString).toLocal();
}
}
return null;
}
static String? _getLogLineDateTimeString(String line) {
final dateTime = _getLogLineDateTime(line);
if (dateTime != null) {
return _dateTimeFormatter.format(dateTime);
}
return null;
}
static String? _safeExtract(RegExp pattern, String line) => pattern.firstMatch(line)?.group(1)?.trim();
static LogAnalyzeLineData? _parseFatalCollision(String line) {
final vehicle = _safeExtract(_fatalCollisionPatterns['vehicle']!, line) ?? unknownValue;
final zone = _safeExtract(_fatalCollisionPatterns['zone']!, line) ?? unknownValue;
final playerPilot = (_safeExtract(_fatalCollisionPatterns['player_pilot']!, line) ?? '0') == '1';
final hitEntity = _safeExtract(_fatalCollisionPatterns['hit_entity']!, line) ?? unknownValue;
final distance = double.tryParse(_safeExtract(_fatalCollisionPatterns['distance']!, line) ?? '') ?? 0.0;
return LogAnalyzeLineData(
type: "fatal_collision",
title: S.current.log_analyzer_filter_fatal_collision,
data: S.current.log_analyzer_collision_details(
zone,
playerPilot ? '' : '',
hitEntity,
vehicle,
distance.toStringAsFixed(2),
),
dateTime: _getLogLineDateTimeString(line),
);
}
static LogAnalyzeLineData? _parseVehicleDestruction(
String line,
String playerName,
bool shouldCount,
void Function(bool isHard) onDestruction,
) {
final match = _vehicleDestructionPattern.firstMatch(line);
if (match != null) {
final vehicleModel = match.group(1) ?? unknownValue;
final zone = match.group(2) ?? unknownValue;
final destructionLevel = int.tryParse(match.group(3) ?? '') ?? 0;
final causedBy = match.group(4) ?? unknownValue;
final destructionLevelMap = {1: S.current.log_analyzer_soft_death, 2: S.current.log_analyzer_disintegration};
if (shouldCount && causedBy.trim() == playerName) {
onDestruction(destructionLevel == 2);
}
return LogAnalyzeLineData(
type: "vehicle_destruction",
title: S.current.log_analyzer_filter_vehicle_damaged,
data: S.current.log_analyzer_vehicle_damage_details(
vehicleModel,
zone,
destructionLevel.toString(),
destructionLevelMap[destructionLevel] ?? unknownValue,
causedBy,
),
dateTime: _getLogLineDateTimeString(line),
);
}
return null;
}
static LogAnalyzeLineData? _parseActorDeath(
String line,
String playerName,
bool shouldCount,
void Function(bool isKill, bool isDeath, bool isSelfKill) onDeath,
) {
final match = _actorDeathPattern.firstMatch(line);
if (match != null) {
final victimId = match.group(1) ?? unknownValue;
final fromZone = match.group(2) ?? unknownValue;
final toZone = match.group(3) ?? unknownValue;
if (shouldCount) {
final isDeath = victimId.trim() == playerName;
if (isDeath) {
onDeath(false, true, false);
}
}
return LogAnalyzeLineData(
type: "actor_death",
title: S.current.log_analyzer_filter_character_death,
data: S.current.log_analyzer_death_details(victimId, unknownValue, unknownValue, "$fromZone -> $toZone"),
dateTime: _getLogLineDateTimeString(line),
location: fromZone,
area: toZone,
victimId: victimId,
playerName: playerName,
);
}
return null;
}
static LogAnalyzeLineData? _parseCharacterName(String line) {
final match = _characterNamePattern.firstMatch(line);
if (match != null) {
final characterName = match.group(1)?.trim() ?? unknownValue;
return LogAnalyzeLineData(
type: "player_login",
title: S.current.log_analyzer_player_login(characterName),
dateTime: _getLogLineDateTimeString(line),
playerName: characterName,
);
}
return null;
}
static LogAnalyzeLineData? _parseRequestLocationInventory(String line) {
final match = _requestLocationInventoryPattern.firstMatch(line);
if (match != null) {
final playerId = match.group(1) ?? unknownValue;
final location = match.group(2) ?? unknownValue;
return LogAnalyzeLineData(
type: "request_location_inventory",
title: S.current.log_analyzer_view_local_inventory,
data: S.current.log_analyzer_player_location(playerId, location),
dateTime: _getLogLineDateTimeString(line),
location: location,
);
}
return null;
}
}

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

@@ -0,0 +1,678 @@
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
/// 年度报告数据类
class YearlyReportData {
// 基础统计
final int totalLaunchCount;
final Duration totalPlayTime;
final int yearlyLaunchCount;
final Duration yearlyPlayTime;
final int totalCrashCount;
final int yearlyCrashCount;
// 时间统计
final DateTime? yearlyFirstLaunchTime;
final DateTime? earliestPlayDate;
final DateTime? latestPlayDate;
// 游玩时长统计
final Duration? longestSession;
final DateTime? longestSessionDate;
final Duration? shortestSession;
final DateTime? shortestSessionDate;
final Duration? averageSessionTime;
// 载具统计
final int yearlyVehicleDestructionCount;
final String? mostDestroyedVehicle;
final int mostDestroyedVehicleCount;
final String? mostPilotedVehicle;
final int mostPilotedVehicleCount;
// 账号统计
final int accountCount;
final String? mostPlayedAccount;
final int mostPlayedAccountSessionCount;
// 地点统计
final List<MapEntry<String, int>> topLocations;
// 击杀统计 (K/D)
final int yearlyKillCount;
final int yearlyDeathCount;
final int yearlySelfKillCount;
// 月份统计
final int? mostPlayedMonth;
final int mostPlayedMonthCount;
final int? leastPlayedMonth;
final int leastPlayedMonthCount;
// 连续游玩/离线统计
final int longestPlayStreak;
final DateTime? playStreakStartDate;
final DateTime? playStreakEndDate;
final int longestOfflineStreak;
final DateTime? offlineStreakStartDate;
final DateTime? offlineStreakEndDate;
// 详细数据
final Map<String, int> vehiclePilotedDetails;
final Map<String, int> accountSessionDetails;
final Map<String, int> locationDetails;
const YearlyReportData({
required this.totalLaunchCount,
required this.totalPlayTime,
required this.yearlyLaunchCount,
required this.yearlyPlayTime,
required this.totalCrashCount,
required this.yearlyCrashCount,
this.yearlyFirstLaunchTime,
this.earliestPlayDate,
this.latestPlayDate,
this.longestSession,
this.longestSessionDate,
this.shortestSession,
this.shortestSessionDate,
this.averageSessionTime,
required this.yearlyVehicleDestructionCount,
this.mostDestroyedVehicle,
required this.mostDestroyedVehicleCount,
this.mostPilotedVehicle,
required this.mostPilotedVehicleCount,
required this.accountCount,
this.mostPlayedAccount,
required this.mostPlayedAccountSessionCount,
required this.topLocations,
required this.yearlyKillCount,
required this.yearlyDeathCount,
required this.yearlySelfKillCount,
this.mostPlayedMonth,
required this.mostPlayedMonthCount,
this.leastPlayedMonth,
required this.leastPlayedMonthCount,
required this.longestPlayStreak,
this.playStreakStartDate,
this.playStreakEndDate,
required this.longestOfflineStreak,
this.offlineStreakStartDate,
this.offlineStreakEndDate,
required this.vehiclePilotedDetails,
required this.accountSessionDetails,
required this.locationDetails,
});
static int? _toUtcTimestamp(DateTime? dateTime) {
if (dateTime == null) return null;
return dateTime.toUtc().millisecondsSinceEpoch;
}
Map<String, dynamic> toJson() {
final now = DateTime.now();
final offset = now.timeZoneOffset;
return {
'generatedAtUtc': _toUtcTimestamp(now),
'timezoneOffsetMinutes': offset.inMinutes,
'totalLaunchCount': totalLaunchCount,
'totalPlayTimeMs': totalPlayTime.inMilliseconds,
'yearlyLaunchCount': yearlyLaunchCount,
'yearlyPlayTimeMs': yearlyPlayTime.inMilliseconds,
'totalCrashCount': totalCrashCount,
'yearlyCrashCount': yearlyCrashCount,
'yearlyFirstLaunchTimeUtc': _toUtcTimestamp(yearlyFirstLaunchTime),
'earliestPlayDateUtc': _toUtcTimestamp(earliestPlayDate),
'latestPlayDateUtc': _toUtcTimestamp(latestPlayDate),
'longestSessionMs': longestSession?.inMilliseconds,
'longestSessionDateUtc': _toUtcTimestamp(longestSessionDate),
'shortestSessionMs': shortestSession?.inMilliseconds,
'shortestSessionDateUtc': _toUtcTimestamp(shortestSessionDate),
'averageSessionTimeMs': averageSessionTime?.inMilliseconds,
'yearlyVehicleDestructionCount': yearlyVehicleDestructionCount,
'mostDestroyedVehicle': mostDestroyedVehicle,
'mostDestroyedVehicleCount': mostDestroyedVehicleCount,
'mostPilotedVehicle': mostPilotedVehicle,
'mostPilotedVehicleCount': mostPilotedVehicleCount,
'accountCount': accountCount,
'mostPlayedAccount': mostPlayedAccount,
'mostPlayedAccountSessionCount': mostPlayedAccountSessionCount,
'topLocations': topLocations.map((e) => {'location': e.key, 'count': e.value}).toList(),
'yearlyKillCount': yearlyKillCount,
'yearlyDeathCount': yearlyDeathCount,
'yearlySelfKillCount': yearlySelfKillCount,
'mostPlayedMonth': mostPlayedMonth,
'mostPlayedMonthCount': mostPlayedMonthCount,
'leastPlayedMonth': leastPlayedMonth,
'leastPlayedMonthCount': leastPlayedMonthCount,
'longestPlayStreak': longestPlayStreak,
'playStreakStartDateUtc': _toUtcTimestamp(playStreakStartDate),
'playStreakEndDateUtc': _toUtcTimestamp(playStreakEndDate),
'longestOfflineStreak': longestOfflineStreak,
'offlineStreakStartDateUtc': _toUtcTimestamp(offlineStreakStartDate),
'offlineStreakEndDateUtc': _toUtcTimestamp(offlineStreakEndDate),
'vehiclePilotedDetails': vehiclePilotedDetails,
'accountSessionDetails': accountSessionDetails,
'locationDetails': locationDetails,
};
}
@override
String toString() {
return '''YearlyReportData(
totalLaunchCount: $totalLaunchCount,
totalPlayTime: $totalPlayTime,
yearlyLaunchCount: $yearlyLaunchCount,
yearlyPlayTime: $yearlyPlayTime,
totalCrashCount: $totalCrashCount,
yearlyCrashCount: $yearlyCrashCount,
yearlyFirstLaunchTime: $yearlyFirstLaunchTime,
earliestPlayDate: $earliestPlayDate,
latestPlayDate: $latestPlayDate,
longestSession: $longestSession (on $longestSessionDate),
shortestSession: $shortestSession (on $shortestSessionDate),
averageSessionTime: $averageSessionTime,
yearlyVehicleDestructionCount: $yearlyVehicleDestructionCount,
mostDestroyedVehicle: $mostDestroyedVehicle ($mostDestroyedVehicleCount),
mostPilotedVehicle: $mostPilotedVehicle ($mostPilotedVehicleCount),
accountCount: $accountCount,
mostPlayedAccount: $mostPlayedAccount ($mostPlayedAccountSessionCount),
topLocations: ${topLocations.take(5).map((e) => '${e.key}: ${e.value}').join(', ')},
)''';
}
}
/// 单个日志文件的统计结果 (内部使用)
class _LogFileStats {
DateTime? startTime;
DateTime? endTime;
bool hasCrash = false;
int killCount = 0;
int deathCount = 0;
int selfKillCount = 0;
Set<String> playerNames = {};
String? currentPlayerName;
String? firstPlayerName;
Map<String, int> vehicleDestruction = {};
Map<String, int> vehiclePiloted = {};
Map<String, int> locationVisits = {};
DateTime? _lastDeathTime;
List<_SessionInfo> yearlySessions = [];
String? get uniqueKey {
if (startTime == null) return null;
final timeKey = startTime!.toUtc().toIso8601String();
final playerKey = firstPlayerName ?? 'unknown';
return '$timeKey|$playerKey';
}
}
/// 单次游玩会话信息
class _SessionInfo {
final DateTime startTime;
final DateTime endTime;
_SessionInfo({required this.startTime, required this.endTime});
Duration get duration => endTime.difference(startTime);
}
/// 年度报告分析器 (Web 版本)
class YearlyReportAnalyzer {
static final _characterNamePattern = RegExp(r'name\s+([^-]+)');
static final _vehicleDestructionPattern = RegExp(
r"Vehicle\s+'([^']+)'.*?"
r"in zone\s+'([^']+)'.*?"
r"destroy level \d+ to (\d+).*?"
r"caused by\s+'([^']+)'",
);
static final _actorDeathPattern = RegExp(
r"Actor '([^']+)'.*?"
r"ejected from zone '([^']+)'.*?"
r"to zone '([^']+)'",
);
static final _legacyActorDeathPattern = RegExp(
r"CActor::Kill: '([^']+)'.*?"
r"in zone '([^']+)'.*?"
r"killed by '([^']+)'.*?"
r"with damage type '([^']+)'",
);
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
/// 分析单个日志文件内容
static _LogFileStats _analyzeLogContent(String content, int targetYear) {
final stats = _LogFileStats();
try {
final lines = content.split('\n');
for (final line in lines) {
if (line.isEmpty) continue;
final lineTime = GameLogAnalyzer.getLogLineDateTime(line);
if (stats.startTime == null && lineTime != null) {
stats.startTime = lineTime;
}
if (lineTime != null) {
stats.endTime = lineTime;
}
if (line.contains("Cloud Imperium Games public crash handler")) {
stats.hasCrash = true;
}
if (line.contains('AccountLoginCharacterStatus_Character')) {
final nameMatch = _characterNamePattern.firstMatch(line);
if (nameMatch != null) {
final playerName = nameMatch.group(1)?.trim();
if (playerName != null &&
playerName.isNotEmpty &&
!playerName.contains(' ') &&
!playerName.contains('/') &&
!playerName.contains(r'\\') &&
!playerName.contains('.')) {
stats.currentPlayerName = playerName;
if (!stats.playerNames.any((n) => n.toLowerCase() == playerName.toLowerCase())) {
stats.playerNames.add(playerName);
}
stats.firstPlayerName ??= playerName;
}
}
}
if (lineTime != null && lineTime.year == targetYear) {
final destructionMatch = _vehicleDestructionPattern.firstMatch(line);
if (destructionMatch != null) {
final vehicleModel = destructionMatch.group(1);
final causedBy = destructionMatch.group(4)?.trim();
if (vehicleModel != null &&
causedBy != null &&
stats.currentPlayerName != null &&
causedBy == stats.currentPlayerName) {
final cleanVehicleName = GameLogAnalyzer.removeVehicleId(vehicleModel);
stats.vehicleDestruction[cleanVehicleName] = (stats.vehicleDestruction[cleanVehicleName] ?? 0) + 1;
}
}
final controlMatch = GameLogAnalyzer.vehicleControlPattern.firstMatch(line);
if (controlMatch != null) {
final vehicleName = controlMatch.group(1);
if (vehicleName != null) {
final cleanVehicleName = GameLogAnalyzer.removeVehicleId(vehicleName);
if (cleanVehicleName != 'Default') {
stats.vehiclePiloted[cleanVehicleName] = (stats.vehiclePiloted[cleanVehicleName] ?? 0) + 1;
}
}
}
var deathMatch = _actorDeathPattern.firstMatch(line);
if (deathMatch != null) {
final victimId = deathMatch.group(1)?.trim();
if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) {
if (stats._lastDeathTime == null || lineTime.difference(stats._lastDeathTime!).abs().inSeconds > 2) {
stats.deathCount++;
stats._lastDeathTime = lineTime;
}
}
}
final legacyDeathMatch = _legacyActorDeathPattern.firstMatch(line);
if (legacyDeathMatch != null) {
final victimId = legacyDeathMatch.group(1)?.trim();
final killerId = legacyDeathMatch.group(3)?.trim();
if (victimId != null && stats.currentPlayerName != null) {
bool isRecent =
stats._lastDeathTime != null && lineTime.difference(stats._lastDeathTime!).abs().inSeconds <= 2;
if (victimId == killerId) {
if (victimId == stats.currentPlayerName) {
if (isRecent) {
stats.selfKillCount++;
stats._lastDeathTime = lineTime;
} else {
stats.deathCount++;
stats.selfKillCount++;
stats._lastDeathTime = lineTime;
}
}
} else {
if (victimId == stats.currentPlayerName) {
if (!isRecent) {
stats.deathCount++;
stats._lastDeathTime = lineTime;
}
}
if (killerId == stats.currentPlayerName) {
stats.killCount++;
}
}
}
}
final locationMatch = _requestLocationInventoryPattern.firstMatch(line);
if (locationMatch != null) {
final location = locationMatch.group(2)?.trim();
if (location != null && location.isNotEmpty) {
final cleanLocation = _cleanLocationName(location);
stats.locationVisits[cleanLocation] = (stats.locationVisits[cleanLocation] ?? 0) + 1;
}
}
}
}
if (stats.startTime != null && stats.endTime != null && stats.startTime!.year == targetYear) {
stats.yearlySessions.add(_SessionInfo(startTime: stats.startTime!, endTime: stats.endTime!));
}
} catch (e) {
// Error handled silently
}
return stats;
}
static String _cleanLocationName(String location) {
final cleanPattern = RegExp(r'_\d{6,}$');
return location.replaceAll(cleanPattern, '');
}
/// 从日志文件内容列表生成年度报告 (Web 版本)
static Future<YearlyReportData> generateReportFromContents(List<String> logContents, int targetYear) async {
final allStats = <_LogFileStats>[];
final seenKeys = <String>{};
for (final content in logContents) {
try {
final stats = _analyzeLogContent(content, targetYear);
final key = stats.uniqueKey;
if (key == null) {
allStats.add(stats);
} else if (!seenKeys.contains(key)) {
seenKeys.add(key);
allStats.add(stats);
}
} catch (_) {
// 忽略单个文件分析错误
}
}
return _generateReportFromStats(allStats, targetYear);
}
static YearlyReportData _generateReportFromStats(List<_LogFileStats> allStats, int targetYear) {
int totalLaunchCount = allStats.length;
Duration totalPlayTime = Duration.zero;
int yearlyLaunchCount = 0;
Duration yearlyPlayTime = Duration.zero;
int totalCrashCount = 0;
int yearlyCrashCount = 0;
DateTime? yearlyFirstLaunchTime;
DateTime? earliestPlayDate;
DateTime? latestPlayDate;
Duration? longestSession;
DateTime? longestSessionDate;
Duration? shortestSession;
DateTime? shortestSessionDate;
List<Duration> allSessionDurations = [];
int yearlyKillCount = 0;
int yearlyDeathCount = 0;
int yearlySelfKillCount = 0;
final Map<String, int> vehicleDestructionDetails = {};
final Map<String, int> vehiclePilotedDetails = {};
final Map<String, int> accountSessionDetails = {};
final Map<String, int> locationDetails = {};
for (final stats in allStats) {
if (stats.startTime != null && stats.endTime != null) {
totalPlayTime += stats.endTime!.difference(stats.startTime!);
}
if (stats.hasCrash) {
totalCrashCount++;
if (stats.endTime != null && stats.endTime!.year == targetYear) {
yearlyCrashCount++;
}
}
for (final session in stats.yearlySessions) {
yearlyLaunchCount++;
final sessionDuration = session.duration;
yearlyPlayTime += sessionDuration;
allSessionDurations.add(sessionDuration);
if (yearlyFirstLaunchTime == null || session.startTime.isBefore(yearlyFirstLaunchTime)) {
yearlyFirstLaunchTime = session.startTime;
}
if (session.startTime.hour >= 5) {
if (earliestPlayDate == null || _timeOfDayIsEarlier(session.startTime, earliestPlayDate)) {
earliestPlayDate = session.startTime;
}
}
if (session.endTime.hour <= 4) {
if (latestPlayDate == null || _timeOfDayIsLater(session.endTime, latestPlayDate)) {
latestPlayDate = session.endTime;
}
}
if (longestSession == null || sessionDuration > longestSession) {
longestSession = sessionDuration;
longestSessionDate = session.startTime;
}
if (sessionDuration.inMinutes >= 5) {
if (shortestSession == null || sessionDuration < shortestSession) {
shortestSession = sessionDuration;
shortestSessionDate = session.startTime;
}
}
}
for (final entry in stats.vehicleDestruction.entries) {
if (!entry.key.contains('PU_')) {
vehicleDestructionDetails[entry.key] = (vehicleDestructionDetails[entry.key] ?? 0) + entry.value;
}
}
for (final entry in stats.vehiclePiloted.entries) {
vehiclePilotedDetails[entry.key] = (vehiclePilotedDetails[entry.key] ?? 0) + entry.value;
}
yearlyKillCount += stats.killCount;
yearlyDeathCount += stats.deathCount;
yearlySelfKillCount += stats.selfKillCount;
for (final playerName in stats.playerNames) {
if (playerName.length > 16) continue;
String targetKey = playerName;
for (final key in accountSessionDetails.keys) {
if (key.toLowerCase() == playerName.toLowerCase()) {
targetKey = key;
break;
}
}
accountSessionDetails[targetKey] = (accountSessionDetails[targetKey] ?? 0) + 1;
}
for (final entry in stats.locationVisits.entries) {
locationDetails[entry.key] = (locationDetails[entry.key] ?? 0) + entry.value;
}
}
Duration? averageSessionTime;
if (allSessionDurations.isNotEmpty) {
final totalMs = allSessionDurations.fold<int>(0, (sum, d) => sum + d.inMilliseconds);
averageSessionTime = Duration(milliseconds: totalMs ~/ allSessionDurations.length);
}
final yearlyVehicleDestructionCount = vehicleDestructionDetails.values.fold(0, (a, b) => a + b);
String? mostDestroyedVehicle;
int mostDestroyedVehicleCount = 0;
for (final entry in vehicleDestructionDetails.entries) {
if (entry.value > mostDestroyedVehicleCount) {
mostDestroyedVehicle = entry.key;
mostDestroyedVehicleCount = entry.value;
}
}
String? mostPilotedVehicle;
int mostPilotedVehicleCount = 0;
for (final entry in vehiclePilotedDetails.entries) {
if (entry.value > mostPilotedVehicleCount) {
mostPilotedVehicle = entry.key;
mostPilotedVehicleCount = entry.value;
}
}
String? mostPlayedAccount;
int mostPlayedAccountSessionCount = 0;
for (final entry in accountSessionDetails.entries) {
if (entry.value > mostPlayedAccountSessionCount) {
mostPlayedAccount = entry.key;
mostPlayedAccountSessionCount = entry.value;
}
}
final sortedLocations = locationDetails.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
final topLocations = sortedLocations.take(10).toList();
final Map<int, int> monthlyPlayCount = {};
final Set<DateTime> playDates = {};
for (final stats in allStats) {
for (final session in stats.yearlySessions) {
final month = session.startTime.month;
monthlyPlayCount[month] = (monthlyPlayCount[month] ?? 0) + 1;
playDates.add(DateTime(session.startTime.year, session.startTime.month, session.startTime.day));
}
}
int? mostPlayedMonth;
int mostPlayedMonthCount = 0;
int? leastPlayedMonth;
int leastPlayedMonthCount = 0;
if (monthlyPlayCount.isNotEmpty) {
for (final entry in monthlyPlayCount.entries) {
if (entry.value > mostPlayedMonthCount) {
mostPlayedMonth = entry.key;
mostPlayedMonthCount = entry.value;
}
}
leastPlayedMonthCount = monthlyPlayCount.values.first;
for (final entry in monthlyPlayCount.entries) {
if (entry.value <= leastPlayedMonthCount) {
leastPlayedMonth = entry.key;
leastPlayedMonthCount = entry.value;
}
}
}
int longestPlayStreak = 0;
DateTime? playStreakStartDate;
DateTime? playStreakEndDate;
int longestOfflineStreak = 0;
DateTime? offlineStreakStartDate;
DateTime? offlineStreakEndDate;
if (playDates.isNotEmpty) {
final sortedDates = playDates.toList()..sort();
int currentStreak = 1;
DateTime streakStart = sortedDates.first;
for (int i = 1; i < sortedDates.length; i++) {
final diff = sortedDates[i].difference(sortedDates[i - 1]).inDays;
if (diff == 1) {
currentStreak++;
} else {
if (currentStreak > longestPlayStreak) {
longestPlayStreak = currentStreak;
playStreakStartDate = streakStart;
playStreakEndDate = sortedDates[i - 1];
}
currentStreak = 1;
streakStart = sortedDates[i];
}
}
if (currentStreak > longestPlayStreak) {
longestPlayStreak = currentStreak;
playStreakStartDate = streakStart;
playStreakEndDate = sortedDates.last;
}
for (int i = 1; i < sortedDates.length; i++) {
final gapDays = sortedDates[i].difference(sortedDates[i - 1]).inDays - 1;
if (gapDays > longestOfflineStreak) {
longestOfflineStreak = gapDays;
offlineStreakStartDate = sortedDates[i - 1].add(const Duration(days: 1));
offlineStreakEndDate = sortedDates[i].subtract(const Duration(days: 1));
}
}
}
return YearlyReportData(
totalLaunchCount: totalLaunchCount,
totalPlayTime: totalPlayTime,
yearlyLaunchCount: yearlyLaunchCount,
yearlyPlayTime: yearlyPlayTime,
totalCrashCount: totalCrashCount,
yearlyCrashCount: yearlyCrashCount,
yearlyFirstLaunchTime: yearlyFirstLaunchTime,
earliestPlayDate: earliestPlayDate,
latestPlayDate: latestPlayDate,
longestSession: longestSession,
longestSessionDate: longestSessionDate,
shortestSession: shortestSession,
shortestSessionDate: shortestSessionDate,
averageSessionTime: averageSessionTime,
yearlyVehicleDestructionCount: yearlyVehicleDestructionCount,
mostDestroyedVehicle: mostDestroyedVehicle,
mostDestroyedVehicleCount: mostDestroyedVehicleCount,
mostPilotedVehicle: mostPilotedVehicle,
mostPilotedVehicleCount: mostPilotedVehicleCount,
accountCount: accountSessionDetails.length,
mostPlayedAccount: mostPlayedAccount,
mostPlayedAccountSessionCount: mostPlayedAccountSessionCount,
topLocations: topLocations,
yearlyKillCount: yearlyKillCount,
yearlyDeathCount: yearlyDeathCount,
yearlySelfKillCount: yearlySelfKillCount,
mostPlayedMonth: mostPlayedMonth,
mostPlayedMonthCount: mostPlayedMonthCount,
leastPlayedMonth: leastPlayedMonth,
leastPlayedMonthCount: leastPlayedMonthCount,
longestPlayStreak: longestPlayStreak,
playStreakStartDate: playStreakStartDate,
playStreakEndDate: playStreakEndDate,
longestOfflineStreak: longestOfflineStreak,
offlineStreakStartDate: offlineStreakStartDate,
offlineStreakEndDate: offlineStreakEndDate,
vehiclePilotedDetails: vehiclePilotedDetails,
accountSessionDetails: accountSessionDetails,
locationDetails: locationDetails,
);
}
static bool _timeOfDayIsEarlier(DateTime a, DateTime b) {
if (a.hour < b.hour) return true;
if (a.hour > b.hour) return false;
return a.minute < b.minute;
}
static bool _timeOfDayIsLater(DateTime a, DateTime b) {
if (a.hour > b.hour) return true;
if (a.hour < b.hour) return false;
return a.minute > b.minute;
}
}

View File

@@ -1,17 +1,24 @@
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: {
"User-Agent":
"SCToolBox/${ConstConf.appVersion} (${ConstConf.appVersionCode})${ConstConf.isMSE ? "" : " DEV"} RSHttp"
});
_defaultHeaders = {};
_dio = Dio(
BaseOptions(
headers: _defaultHeaders,
responseType: ResponseType.bytes,
validateStatus: (status) => true, // 接受所有状态码
),
);
}
static Future<RustHttpResponse> get(
@@ -20,14 +27,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 +42,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 +60,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 +76,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');
}
}

View File

@@ -200,6 +200,54 @@ class MessageLookup extends MessageLookupByLibrary {
static String m75(v0) => "P4K Viewer -> ${v0}";
static String m76(v0) => "Logged in ${v0} times";
static String m77(v0) => "Detected ${v0} accounts in total";
static String m78(year) =>
"View your Star Citizen gameplay statistics for ${year}. Data is from local logs, please check on your main computer.";
static String m79(year) => "${year} Yearly Report (Limited Time)";
static String m80(v0, v1, v2, v3) => "${v0}/${v1} - ${v2}/${v3}";
static String m81(v0, v1) => "${v0} hours ${v1} minutes";
static String m82(v0) => "${v0} minutes";
static String m83(v0, v1) =>
"You started your space journey at dawn on ${v0}/${v1}";
static String m84(v0, v1) =>
"Late night on ${v0}/${v1}, you were still exploring the universe";
static String m85(v0) => "${v0} times";
static String m86(v0) => "Month ${v0}";
static String m87(v0) => "Only launched ${v0} times";
static String m88(v0) => "Launched ${v0} times";
static String m89(v0) => "${v0} hours";
static String m90(v0, v1) => "${v0}/${v1}";
static String m91(year) =>
"In ${year}, together we created\ncountless wonderful memories in Star Citizen";
static String m92(nextYear) => "Looking forward to ${nextYear} with you!";
static String m93(year) => "Star Citizen ${year} Yearly Report";
static String m94(v0) => "Destroyed ${v0} times";
static String m95(v0) => "Piloted ${v0} times";
static String m96(v0) => "View all ${v0} vehicles";
static String m97(year) => "${year} Yearly Report";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about_action_btn_faq": MessageLookupByLibrary.simpleMessage("FAQ"),
@@ -366,6 +414,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_action_rsi_launcher_log": MessageLookupByLibrary.simpleMessage(
"RSI Launcher log",
),
"doctor_action_select_log_file": MessageLookupByLibrary.simpleMessage(
"Select Log File",
),
"doctor_action_tip_checking_game_log": MessageLookupByLibrary.simpleMessage(
"Checking: Game.log",
),
@@ -435,6 +486,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_game_rescue_service_note": MessageLookupByLibrary.simpleMessage(
"You are about to access the game anomaly rescue service provided by Deep Space Treatment Center (QQ Group: 536454632), which mainly solves game installation failures and frequent crashes. Please do not join the group for gameplay issues.",
),
"doctor_info_log_file_selected": MessageLookupByLibrary.simpleMessage(
"Log file selected",
),
"doctor_info_need_help": MessageLookupByLibrary.simpleMessage(
"Need help? Click to join the group for free human support!",
),
@@ -491,6 +545,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_tool_check_result_note": MessageLookupByLibrary.simpleMessage(
"Note: The detection results of this tool are for reference only. If you do not understand the following operations, please provide screenshots to experienced players!",
),
"doctor_info_web_select_log_file": MessageLookupByLibrary.simpleMessage(
"Web platform: Please manually select the Game.log file for diagnosis",
),
"doctor_tip_title_select_game_directory":
MessageLookupByLibrary.simpleMessage(
"Please select the game installation directory on the home page.",
@@ -1705,5 +1762,230 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Total invitations:"),
"webview_localization_unfinished_invitations":
MessageLookupByLibrary.simpleMessage("Unfinished invitations"),
"yearly_report_account_count": m76,
"yearly_report_account_expand": MessageLookupByLibrary.simpleMessage(
"View all accounts",
),
"yearly_report_account_most": MessageLookupByLibrary.simpleMessage(
"Most used account",
),
"yearly_report_account_title": MessageLookupByLibrary.simpleMessage(
"Account Statistics",
),
"yearly_report_account_total": m77,
"yearly_report_analyzing_logs": MessageLookupByLibrary.simpleMessage(
"Analyzing game log data",
),
"yearly_report_card_desc": m78,
"yearly_report_card_title": m79,
"yearly_report_crash_desc": MessageLookupByLibrary.simpleMessage(
"Unstable moments this year",
),
"yearly_report_crash_label": MessageLookupByLibrary.simpleMessage(
"Total Crashes",
),
"yearly_report_crash_note_high": MessageLookupByLibrary.simpleMessage(
"Hope it\'s more stable next year!",
),
"yearly_report_crash_note_low": MessageLookupByLibrary.simpleMessage(
"Lucky you!",
),
"yearly_report_crash_title": MessageLookupByLibrary.simpleMessage(
"Game Crash Count",
),
"yearly_report_date_range": m80,
"yearly_report_disclaimer": MessageLookupByLibrary.simpleMessage(
"Data is generated from your local logs and will not be sent to any third party. Due to significant log changes across versions, data may be incomplete. For entertainment purposes only.",
),
"yearly_report_duration_hours_minutes": m81,
"yearly_report_duration_minutes": m82,
"yearly_report_earliest_play_desc": m83,
"yearly_report_earliest_play_title": MessageLookupByLibrary.simpleMessage(
"Earliest Play Session",
),
"yearly_report_error_description": MessageLookupByLibrary.simpleMessage(
"Please ensure the game directory is correct and log files exist",
),
"yearly_report_error_title": MessageLookupByLibrary.simpleMessage(
"Unable to generate yearly report",
),
"yearly_report_generating": MessageLookupByLibrary.simpleMessage(
"Generating your yearly report...",
),
"yearly_report_kd_death": MessageLookupByLibrary.simpleMessage("Deaths"),
"yearly_report_kd_kill": MessageLookupByLibrary.simpleMessage("Kills"),
"yearly_report_kd_no_record": MessageLookupByLibrary.simpleMessage(
"No kill/death records detected this year",
),
"yearly_report_kd_suicide": MessageLookupByLibrary.simpleMessage(
"Suicides",
),
"yearly_report_kd_title": MessageLookupByLibrary.simpleMessage(
"Kill Statistics",
),
"yearly_report_latest_play_desc": m84,
"yearly_report_latest_play_title": MessageLookupByLibrary.simpleMessage(
"Latest Play Session",
),
"yearly_report_launch_count_desc": MessageLookupByLibrary.simpleMessage(
"This year you launched the game",
),
"yearly_report_launch_count_label": MessageLookupByLibrary.simpleMessage(
"Total Launches",
),
"yearly_report_launch_count_title": MessageLookupByLibrary.simpleMessage(
"Game Launch Count",
),
"yearly_report_launch_count_value": m85,
"yearly_report_location_frequent": MessageLookupByLibrary.simpleMessage(
"Frequent Locations",
),
"yearly_report_location_no_record": MessageLookupByLibrary.simpleMessage(
"No location visit records",
),
"yearly_report_location_note": MessageLookupByLibrary.simpleMessage(
"Based on inventory viewing records",
),
"yearly_report_location_title": MessageLookupByLibrary.simpleMessage(
"Location Statistics",
),
"yearly_report_menu_title": MessageLookupByLibrary.simpleMessage("Report"),
"yearly_report_month_format": m86,
"yearly_report_monthly_least": MessageLookupByLibrary.simpleMessage(
"Least played",
),
"yearly_report_monthly_least_count": m87,
"yearly_report_monthly_most": MessageLookupByLibrary.simpleMessage(
"Most played",
),
"yearly_report_monthly_most_count": m88,
"yearly_report_monthly_title": MessageLookupByLibrary.simpleMessage(
"Monthly Statistics",
),
"yearly_report_nav_next": MessageLookupByLibrary.simpleMessage("Continue"),
"yearly_report_nav_prev": MessageLookupByLibrary.simpleMessage(
"Previous Page",
),
"yearly_report_no_data": MessageLookupByLibrary.simpleMessage(
"No data available",
),
"yearly_report_play_time_desc": MessageLookupByLibrary.simpleMessage(
"This year you explored the universe for",
),
"yearly_report_play_time_label": MessageLookupByLibrary.simpleMessage(
"Total Playtime",
),
"yearly_report_play_time_title": MessageLookupByLibrary.simpleMessage(
"Play Time",
),
"yearly_report_play_time_unit": MessageLookupByLibrary.simpleMessage(
"hours",
),
"yearly_report_play_time_value": m89,
"yearly_report_powered_by": MessageLookupByLibrary.simpleMessage(
"Presented by SCToolbox | github.com/StarCitizenToolBox/app",
),
"yearly_report_session_average": MessageLookupByLibrary.simpleMessage(
"Average",
),
"yearly_report_session_date": m90,
"yearly_report_session_longest": MessageLookupByLibrary.simpleMessage(
"Longest",
),
"yearly_report_session_note": MessageLookupByLibrary.simpleMessage(
"(Shortest only counts sessions over 5 minutes)",
),
"yearly_report_session_shortest": MessageLookupByLibrary.simpleMessage(
"Shortest",
),
"yearly_report_session_title": MessageLookupByLibrary.simpleMessage(
"Session Time Details",
),
"yearly_report_streak_day_unit": MessageLookupByLibrary.simpleMessage(
"days",
),
"yearly_report_streak_offline": MessageLookupByLibrary.simpleMessage(
"Consecutive Offline",
),
"yearly_report_streak_play": MessageLookupByLibrary.simpleMessage(
"Consecutive Play",
),
"yearly_report_streak_title": MessageLookupByLibrary.simpleMessage(
"Streak Records",
),
"yearly_report_summary_earliest_time": MessageLookupByLibrary.simpleMessage(
"Earliest Time",
),
"yearly_report_summary_favorite_vehicle":
MessageLookupByLibrary.simpleMessage("Favorite Vehicle"),
"yearly_report_summary_frequent_location":
MessageLookupByLibrary.simpleMessage("Frequent Location"),
"yearly_report_summary_hottest_month": MessageLookupByLibrary.simpleMessage(
"Hottest Month",
),
"yearly_report_summary_latest_time": MessageLookupByLibrary.simpleMessage(
"Latest Time",
),
"yearly_report_summary_launch_game": MessageLookupByLibrary.simpleMessage(
"Launch Game",
),
"yearly_report_summary_longest_online":
MessageLookupByLibrary.simpleMessage("Longest Online"),
"yearly_report_summary_respawn_count": MessageLookupByLibrary.simpleMessage(
"Respawn Count",
),
"yearly_report_thanks_message": m91,
"yearly_report_thanks_next": m92,
"yearly_report_thanks_title": MessageLookupByLibrary.simpleMessage(
"Thank You for Being With Us",
),
"yearly_report_title": m93,
"yearly_report_vehicle_destruction_count": m94,
"yearly_report_vehicle_destruction_desc":
MessageLookupByLibrary.simpleMessage("This year you destroyed"),
"yearly_report_vehicle_destruction_most":
MessageLookupByLibrary.simpleMessage("Most destroyed ship"),
"yearly_report_vehicle_destruction_title":
MessageLookupByLibrary.simpleMessage("Vehicle Destruction Statistics"),
"yearly_report_vehicle_destruction_unit":
MessageLookupByLibrary.simpleMessage("ships"),
"yearly_report_vehicle_pilot_collapse":
MessageLookupByLibrary.simpleMessage("Collapse details"),
"yearly_report_vehicle_pilot_count": m95,
"yearly_report_vehicle_pilot_expand": m96,
"yearly_report_vehicle_pilot_most": MessageLookupByLibrary.simpleMessage(
"Most piloted vehicle",
),
"yearly_report_vehicle_pilot_title": MessageLookupByLibrary.simpleMessage(
"Vehicle Piloting Statistics",
),
"yearly_report_web_browser_not_supported": MessageLookupByLibrary.simpleMessage(
"Your browser is not supported. Please use Chrome, Edge, or another Chromium-based browser.",
),
"yearly_report_web_generate": MessageLookupByLibrary.simpleMessage(
"Generate Report",
),
"yearly_report_web_no_logs_found": MessageLookupByLibrary.simpleMessage(
"No game logs found",
),
"yearly_report_web_reading_files": MessageLookupByLibrary.simpleMessage(
"Reading log files...",
),
"yearly_report_web_select_folder": MessageLookupByLibrary.simpleMessage(
"Select Game Folder",
),
"yearly_report_web_select_folder_desc": MessageLookupByLibrary.simpleMessage(
"Please select your Star Citizen game folder (the parent folder containing LIVE directory)",
),
"yearly_report_web_select_year": MessageLookupByLibrary.simpleMessage(
"Select Year",
),
"yearly_report_welcome_hint": MessageLookupByLibrary.simpleMessage(
"Scroll down or click the button below to start",
),
"yearly_report_welcome_subtitle": MessageLookupByLibrary.simpleMessage(
"Relive your memorable moments in Star Citizen",
),
"yearly_report_welcome_title": m97,
};
}

View File

@@ -191,6 +191,52 @@ class MessageLookup extends MessageLookupByLibrary {
static String m75(v0) => "P4Kビューア -> ${v0}";
static String m76(v0) => "${v0} 回ログイン";
static String m77(v0) => "合計 ${v0} アカウントを検出";
static String m78(year) =>
"${year}年のStar Citizenプレイ統計を表示します。データはローカルログからのものです。メインのコンピュータで確認してください。";
static String m79(year) => "${year} 年間レポート(期間限定)";
static String m80(v0, v1, v2, v3) => "${v0}${v1}日 - ${v2}${v3}";
static String m81(v0, v1) => "${v0} 時間 ${v1}";
static String m82(v0) => "${v0}";
static String m83(v0, v1) => "あなたは ${v0}${v1}日 の夜明けに宇宙の旅を始めました";
static String m84(v0, v1) => "${v0}${v1}日 の深夜、あなたはまだ宇宙を探索していました";
static String m85(v0) => "${v0}";
static String m86(v0) => "${v0}";
static String m87(v0) => "わずか ${v0} 回起動";
static String m88(v0) => "${v0} 回起動";
static String m89(v0) => "${v0} 時間";
static String m90(v0, v1) => "${v0}${v1}";
static String m91(year) =>
"${year} 年、私たちはStar Citizenで\n数え切れないほどの素晴らしい思い出を作りました";
static String m92(nextYear) => "${nextYear} 年もよろしくお願いします!";
static String m93(year) => "Star Citizen ${year} 年間レポート";
static String m94(v0) => "${v0} 回破壊";
static String m95(v0) => "${v0} 回操縦";
static String m96(v0) => "${v0} 車両を表示";
static String m97(year) => "${year} 年間レポート";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about_action_btn_faq": MessageLookupByLibrary.simpleMessage("よくある質問"),
@@ -344,6 +390,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_action_rsi_launcher_log": MessageLookupByLibrary.simpleMessage(
"RSIランチャーログ",
),
"doctor_action_select_log_file": MessageLookupByLibrary.simpleMessage(
"ログファイルを選択",
),
"doctor_action_tip_checking_game_log": MessageLookupByLibrary.simpleMessage(
"確認中Game.log",
),
@@ -408,6 +457,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_game_rescue_service_note": MessageLookupByLibrary.simpleMessage(
"深宇宙治療センターQQグループ番号536454632によるゲーム異常救済サービスにアクセスしようとしています。これは主にゲームのインストール失敗や頻繁なクラッシュに対応するものです。ゲームプレイの問題に関しては、グループに参加しないでください。",
),
"doctor_info_log_file_selected": MessageLookupByLibrary.simpleMessage(
"ログファイルが選択されました",
),
"doctor_info_need_help": MessageLookupByLibrary.simpleMessage(
"助けが必要ですか?クリックしてグループに参加し、無料のサポートを受けましょう!",
),
@@ -463,6 +515,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_tool_check_result_note": MessageLookupByLibrary.simpleMessage(
"注意:このツールの検出結果は参考用です。以下の操作が理解できない場合は、スクリーンショットを経験豊富なプレイヤーに提供してください!",
),
"doctor_info_web_select_log_file": MessageLookupByLibrary.simpleMessage(
"WebプラットフォームGame.logファイルを手動で選択して診断してください",
),
"doctor_tip_title_select_game_directory":
MessageLookupByLibrary.simpleMessage(
"ホームページでゲームインストールディレクトリを選択してください。",
@@ -1531,5 +1586,208 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("招待総数:"),
"webview_localization_unfinished_invitations":
MessageLookupByLibrary.simpleMessage("未完了の招待"),
"yearly_report_account_count": m76,
"yearly_report_account_expand": MessageLookupByLibrary.simpleMessage(
"すべてのアカウントを表示",
),
"yearly_report_account_most": MessageLookupByLibrary.simpleMessage(
"最も使用されたアカウント",
),
"yearly_report_account_title": MessageLookupByLibrary.simpleMessage(
"アカウント統計",
),
"yearly_report_account_total": m77,
"yearly_report_analyzing_logs": MessageLookupByLibrary.simpleMessage(
"ゲームログデータを分析中",
),
"yearly_report_card_desc": m78,
"yearly_report_card_title": m79,
"yearly_report_crash_desc": MessageLookupByLibrary.simpleMessage(
"今年の不安定な瞬間",
),
"yearly_report_crash_label": MessageLookupByLibrary.simpleMessage(
"合計クラッシュ数",
),
"yearly_report_crash_note_high": MessageLookupByLibrary.simpleMessage(
"来年はもっと安定しますように!",
),
"yearly_report_crash_note_low": MessageLookupByLibrary.simpleMessage(
"ラッキーですね!",
),
"yearly_report_crash_title": MessageLookupByLibrary.simpleMessage(
"ゲームクラッシュ回数",
),
"yearly_report_date_range": m80,
"yearly_report_disclaimer": MessageLookupByLibrary.simpleMessage(
"データはローカルログから生成され、第三者に送信されることはありません。バージョン間のログの大幅な変更により、データが不完全な場合があります。娯楽目的のみ。",
),
"yearly_report_duration_hours_minutes": m81,
"yearly_report_duration_minutes": m82,
"yearly_report_earliest_play_desc": m83,
"yearly_report_earliest_play_title": MessageLookupByLibrary.simpleMessage(
"最も早いプレイ",
),
"yearly_report_error_description": MessageLookupByLibrary.simpleMessage(
"ゲームディレクトリが正しいか、ログファイルが存在するか確認してください",
),
"yearly_report_error_title": MessageLookupByLibrary.simpleMessage(
"年間レポートを作成できません",
),
"yearly_report_generating": MessageLookupByLibrary.simpleMessage(
"年間レポートを作成中...",
),
"yearly_report_kd_death": MessageLookupByLibrary.simpleMessage("デス"),
"yearly_report_kd_kill": MessageLookupByLibrary.simpleMessage("キル"),
"yearly_report_kd_no_record": MessageLookupByLibrary.simpleMessage(
"今年はキル/デス記録が検出されませんでした",
),
"yearly_report_kd_suicide": MessageLookupByLibrary.simpleMessage("自殺"),
"yearly_report_kd_title": MessageLookupByLibrary.simpleMessage("キル統計"),
"yearly_report_latest_play_desc": m84,
"yearly_report_latest_play_title": MessageLookupByLibrary.simpleMessage(
"最も遅いプレイ",
),
"yearly_report_launch_count_desc": MessageLookupByLibrary.simpleMessage(
"今年あなたはゲームを起動しました",
),
"yearly_report_launch_count_label": MessageLookupByLibrary.simpleMessage(
"合計起動回数",
),
"yearly_report_launch_count_title": MessageLookupByLibrary.simpleMessage(
"ゲーム起動回数",
),
"yearly_report_launch_count_value": m85,
"yearly_report_location_frequent": MessageLookupByLibrary.simpleMessage(
"よく行く場所",
),
"yearly_report_location_no_record": MessageLookupByLibrary.simpleMessage(
"場所訪問記録なし",
),
"yearly_report_location_note": MessageLookupByLibrary.simpleMessage(
"インベントリ閲覧記録に基づく",
),
"yearly_report_location_title": MessageLookupByLibrary.simpleMessage(
"場所統計",
),
"yearly_report_menu_title": MessageLookupByLibrary.simpleMessage("レポート"),
"yearly_report_month_format": m86,
"yearly_report_monthly_least": MessageLookupByLibrary.simpleMessage(
"最少プレイ",
),
"yearly_report_monthly_least_count": m87,
"yearly_report_monthly_most": MessageLookupByLibrary.simpleMessage("最多プレイ"),
"yearly_report_monthly_most_count": m88,
"yearly_report_monthly_title": MessageLookupByLibrary.simpleMessage("月間統計"),
"yearly_report_nav_next": MessageLookupByLibrary.simpleMessage("次へ"),
"yearly_report_nav_prev": MessageLookupByLibrary.simpleMessage("前のページ"),
"yearly_report_no_data": MessageLookupByLibrary.simpleMessage("データなし"),
"yearly_report_play_time_desc": MessageLookupByLibrary.simpleMessage(
"今年あなたは宇宙を探索しました",
),
"yearly_report_play_time_label": MessageLookupByLibrary.simpleMessage(
"合計プレイ時間",
),
"yearly_report_play_time_title": MessageLookupByLibrary.simpleMessage(
"プレイ時間",
),
"yearly_report_play_time_unit": MessageLookupByLibrary.simpleMessage("時間"),
"yearly_report_play_time_value": m89,
"yearly_report_powered_by": MessageLookupByLibrary.simpleMessage(
"SCToolbox 提供 | github.com/StarCitizenToolBox/app",
),
"yearly_report_session_average": MessageLookupByLibrary.simpleMessage("平均"),
"yearly_report_session_date": m90,
"yearly_report_session_longest": MessageLookupByLibrary.simpleMessage("最長"),
"yearly_report_session_note": MessageLookupByLibrary.simpleMessage(
"(最短は5分以上のセッションのみ集計)",
),
"yearly_report_session_shortest": MessageLookupByLibrary.simpleMessage(
"最短",
),
"yearly_report_session_title": MessageLookupByLibrary.simpleMessage(
"セッション時間詳細",
),
"yearly_report_streak_day_unit": MessageLookupByLibrary.simpleMessage(""),
"yearly_report_streak_offline": MessageLookupByLibrary.simpleMessage(
"連続オフライン",
),
"yearly_report_streak_play": MessageLookupByLibrary.simpleMessage("連続プレイ"),
"yearly_report_streak_title": MessageLookupByLibrary.simpleMessage("連続記録"),
"yearly_report_summary_earliest_time": MessageLookupByLibrary.simpleMessage(
"最も早い時間",
),
"yearly_report_summary_favorite_vehicle":
MessageLookupByLibrary.simpleMessage("お気に入りの乗り物"),
"yearly_report_summary_frequent_location":
MessageLookupByLibrary.simpleMessage("頻繁な場所"),
"yearly_report_summary_hottest_month": MessageLookupByLibrary.simpleMessage(
"最も活発な月",
),
"yearly_report_summary_latest_time": MessageLookupByLibrary.simpleMessage(
"最も遅い時間",
),
"yearly_report_summary_launch_game": MessageLookupByLibrary.simpleMessage(
"ゲーム起動",
),
"yearly_report_summary_longest_online":
MessageLookupByLibrary.simpleMessage("最長オンライン"),
"yearly_report_summary_respawn_count": MessageLookupByLibrary.simpleMessage(
"リスポーン回数",
),
"yearly_report_thanks_message": m91,
"yearly_report_thanks_next": m92,
"yearly_report_thanks_title": MessageLookupByLibrary.simpleMessage(
"ご愛顧ありがとうございます",
),
"yearly_report_title": m93,
"yearly_report_vehicle_destruction_count": m94,
"yearly_report_vehicle_destruction_desc":
MessageLookupByLibrary.simpleMessage("今年あなたは破壊しました"),
"yearly_report_vehicle_destruction_most":
MessageLookupByLibrary.simpleMessage("最も破壊された船"),
"yearly_report_vehicle_destruction_title":
MessageLookupByLibrary.simpleMessage("ビークル破壊統計"),
"yearly_report_vehicle_destruction_unit":
MessageLookupByLibrary.simpleMessage("隻の船"),
"yearly_report_vehicle_pilot_collapse":
MessageLookupByLibrary.simpleMessage("詳細を折りたたむ"),
"yearly_report_vehicle_pilot_count": m95,
"yearly_report_vehicle_pilot_expand": m96,
"yearly_report_vehicle_pilot_most": MessageLookupByLibrary.simpleMessage(
"最も操縦したビークル",
),
"yearly_report_vehicle_pilot_title": MessageLookupByLibrary.simpleMessage(
"ビークル操縦統計",
),
"yearly_report_web_browser_not_supported":
MessageLookupByLibrary.simpleMessage(
"お使いのブラウザはサポートされていません。Chrome、Edgeまたはその他のChromiumベースのブラウザをご使用ください。",
),
"yearly_report_web_generate": MessageLookupByLibrary.simpleMessage(
"レポートを生成",
),
"yearly_report_web_no_logs_found": MessageLookupByLibrary.simpleMessage(
"ゲームログが見つかりません",
),
"yearly_report_web_reading_files": MessageLookupByLibrary.simpleMessage(
"ログファイルを読み込み中...",
),
"yearly_report_web_select_folder": MessageLookupByLibrary.simpleMessage(
"ゲームフォルダを選択",
),
"yearly_report_web_select_folder_desc":
MessageLookupByLibrary.simpleMessage(
"Star Citizenゲームフォルダを選択してくださいLIVEディレクトリを含む親フォルダ",
),
"yearly_report_web_select_year": MessageLookupByLibrary.simpleMessage(
"年を選択",
),
"yearly_report_welcome_hint": MessageLookupByLibrary.simpleMessage(
"下にスクロールするか、下のボタンをクリックして開始",
),
"yearly_report_welcome_subtitle": MessageLookupByLibrary.simpleMessage(
"Star Citizenでの思い出深い瞬間を振り返る",
),
"yearly_report_welcome_title": m97,
};
}

View File

@@ -199,6 +199,52 @@ class MessageLookup extends MessageLookupByLibrary {
static String m75(v0) => "Просмотрщик P4K -> ${v0}";
static String m76(v0) => "Вход выполнен ${v0} раз";
static String m77(v0) => "Всего обнаружено ${v0} аккаунтов";
static String m78(year) =>
"Посмотрите статистику вашей игры в Star Citizen за ${year} год. Данные из локальных логов, пожалуйста, проверяйте на основном компьютере.";
static String m79(year) => "Ежегодный отчет ${year} (Ограниченное время)";
static String m80(v0, v1, v2, v3) => "${v0}/${v1} - ${v2}/${v3}";
static String m81(v0, v1) => "${v0} ч ${v1} мин";
static String m82(v0) => "${v0} мин";
static String m83(v0, v1) =>
"Вы начали свое космическое путешествие на рассвете ${v0}/${v1}";
static String m84(v0, v1) =>
"Поздно ночью ${v0}/${v1} вы все еще исследовали вселенную";
static String m85(v0) => "${v0} раз";
static String m86(v0) => "Месяц ${v0}";
static String m87(v0) => "Запущено только ${v0} раз";
static String m88(v0) => "Запущено ${v0} раз";
static String m89(v0) => "${v0} часов";
static String m90(v0, v1) => "${v0}/${v1}";
static String m91(year) =>
"В ${year} году мы вместе создали\nбесчисленное количество прекрасных воспоминаний в Star Citizen";
static String m92(nextYear) => "Ждем встречи с вами в ${nextYear} году!";
static String m94(v0) => "Уничтожено ${v0} раз";
static String m95(v0) => "Пилотировался ${v0} раз";
static String m96(v0) => "Показать все ${v0} тс";
static String m97(year) => "Ежегодный отчет ${year}";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about_action_btn_faq": MessageLookupByLibrary.simpleMessage(
@@ -371,6 +417,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_action_rsi_launcher_log": MessageLookupByLibrary.simpleMessage(
"Лог RSI Launcher",
),
"doctor_action_select_log_file": MessageLookupByLibrary.simpleMessage(
"Выбрать файл журнала",
),
"doctor_action_tip_checking_game_log": MessageLookupByLibrary.simpleMessage(
"Проверка: Game.log",
),
@@ -440,6 +489,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_game_rescue_service_note": MessageLookupByLibrary.simpleMessage(
"Вы собираетесь перейти к сервису спасения игры, предоставляемому Центром глубокого космического лечения (QQ группа: 536454632), который в основном решает проблемы с неудачной установкой и частыми вылетами. Не присоединяйтесь к группе для вопросов по игровому процессу.",
),
"doctor_info_log_file_selected": MessageLookupByLibrary.simpleMessage(
"Файл журнала выбран",
),
"doctor_info_need_help": MessageLookupByLibrary.simpleMessage(
"Нужна помощь? Нажмите, чтобы присоединиться к группе для бесплатной поддержки!",
),
@@ -496,6 +548,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_tool_check_result_note": MessageLookupByLibrary.simpleMessage(
"Примечание: Результаты проверки этим инструментом предоставляются только для справки. Если вы не понимаете следующие операции, пожалуйста, предоставьте снимок экрана опытным игрокам!",
),
"doctor_info_web_select_log_file": MessageLookupByLibrary.simpleMessage(
"Web-платформа: Пожалуйста, выберите файл Game.log вручную для диагностики",
),
"doctor_tip_title_select_game_directory":
MessageLookupByLibrary.simpleMessage(
"Пожалуйста, выберите директорию установки игры на главной странице.",
@@ -1723,5 +1778,227 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Всего приглашений:"),
"webview_localization_unfinished_invitations":
MessageLookupByLibrary.simpleMessage("Незавершённые приглашения"),
"yearly_report_account_count": m76,
"yearly_report_account_expand": MessageLookupByLibrary.simpleMessage(
"Показать все аккаунты",
),
"yearly_report_account_most": MessageLookupByLibrary.simpleMessage(
"Самый используемый аккаунт",
),
"yearly_report_account_title": MessageLookupByLibrary.simpleMessage(
"Статистика аккаунта",
),
"yearly_report_account_total": m77,
"yearly_report_analyzing_logs": MessageLookupByLibrary.simpleMessage(
"Анализ данных игровых журналов",
),
"yearly_report_card_desc": m78,
"yearly_report_card_title": m79,
"yearly_report_crash_desc": MessageLookupByLibrary.simpleMessage(
"Нестабильные моменты этого года",
),
"yearly_report_crash_label": MessageLookupByLibrary.simpleMessage(
"Всего сбоев",
),
"yearly_report_crash_note_high": MessageLookupByLibrary.simpleMessage(
"Надеемся, в следующем году будет стабильнее!",
),
"yearly_report_crash_note_low": MessageLookupByLibrary.simpleMessage(
"Вам повезло!",
),
"yearly_report_crash_title": MessageLookupByLibrary.simpleMessage(
"Количество сбоев игры",
),
"yearly_report_date_range": m80,
"yearly_report_disclaimer": MessageLookupByLibrary.simpleMessage(
"Данные генерируются из ваших локальных логов и не отправляются третьим лицам. Из-за значительных изменений логов в разных версиях данные могут быть неполными. Только для развлечения.",
),
"yearly_report_duration_hours_minutes": m81,
"yearly_report_duration_minutes": m82,
"yearly_report_earliest_play_desc": m83,
"yearly_report_earliest_play_title": MessageLookupByLibrary.simpleMessage(
"Самая ранняя игровая сессия",
),
"yearly_report_error_description": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, убедитесь, что путь к игре верен и файлы логов существуют",
),
"yearly_report_error_title": MessageLookupByLibrary.simpleMessage(
"Не удалось создать ежегодный отчет",
),
"yearly_report_generating": MessageLookupByLibrary.simpleMessage(
"Генерация вашего ежегодного отчета...",
),
"yearly_report_kd_death": MessageLookupByLibrary.simpleMessage("Смерти"),
"yearly_report_kd_kill": MessageLookupByLibrary.simpleMessage("Убийства"),
"yearly_report_kd_no_record": MessageLookupByLibrary.simpleMessage(
"В этом году записей об убийствах/смертях не обнаружено",
),
"yearly_report_kd_suicide": MessageLookupByLibrary.simpleMessage(
"Самоубийства",
),
"yearly_report_kd_title": MessageLookupByLibrary.simpleMessage(
"Статистика убийств",
),
"yearly_report_latest_play_desc": m84,
"yearly_report_latest_play_title": MessageLookupByLibrary.simpleMessage(
"Самая поздняя игровая сессия",
),
"yearly_report_launch_count_desc": MessageLookupByLibrary.simpleMessage(
"В этом году вы запускали игру",
),
"yearly_report_launch_count_label": MessageLookupByLibrary.simpleMessage(
"Всего запусков",
),
"yearly_report_launch_count_title": MessageLookupByLibrary.simpleMessage(
"Количество запусков игры",
),
"yearly_report_launch_count_value": m85,
"yearly_report_location_frequent": MessageLookupByLibrary.simpleMessage(
"Частые локации",
),
"yearly_report_location_no_record": MessageLookupByLibrary.simpleMessage(
"Нет записей о посещении локаций",
),
"yearly_report_location_note": MessageLookupByLibrary.simpleMessage(
"Основано на записях просмотра инвентаря",
),
"yearly_report_location_title": MessageLookupByLibrary.simpleMessage(
"Статистика локаций",
),
"yearly_report_menu_title": MessageLookupByLibrary.simpleMessage("Отчет"),
"yearly_report_month_format": m86,
"yearly_report_monthly_least": MessageLookupByLibrary.simpleMessage(
"Меньше всего игр",
),
"yearly_report_monthly_least_count": m87,
"yearly_report_monthly_most": MessageLookupByLibrary.simpleMessage(
"Больше всего игр",
),
"yearly_report_monthly_most_count": m88,
"yearly_report_monthly_title": MessageLookupByLibrary.simpleMessage(
"Ежемесячная статистика",
),
"yearly_report_nav_next": MessageLookupByLibrary.simpleMessage(
"Продолжить",
),
"yearly_report_nav_prev": MessageLookupByLibrary.simpleMessage("Назад"),
"yearly_report_no_data": MessageLookupByLibrary.simpleMessage("Нет данных"),
"yearly_report_play_time_desc": MessageLookupByLibrary.simpleMessage(
"В этом году вы исследовали вселенную в течение",
),
"yearly_report_play_time_label": MessageLookupByLibrary.simpleMessage(
"Общее время игры",
),
"yearly_report_play_time_title": MessageLookupByLibrary.simpleMessage(
"Время игры",
),
"yearly_report_play_time_unit": MessageLookupByLibrary.simpleMessage(
"часов",
),
"yearly_report_play_time_value": m89,
"yearly_report_powered_by": MessageLookupByLibrary.simpleMessage(
"Представлено SCToolbox | github.com/StarCitizenToolBox/app",
),
"yearly_report_session_average": MessageLookupByLibrary.simpleMessage(
"Среднее",
),
"yearly_report_session_date": m90,
"yearly_report_session_longest": MessageLookupByLibrary.simpleMessage(
"Самое долгое",
),
"yearly_report_session_note": MessageLookupByLibrary.simpleMessage(
"(Учитываются только сессии более 5 минут)",
),
"yearly_report_session_shortest": MessageLookupByLibrary.simpleMessage(
"Самое короткое",
),
"yearly_report_session_title": MessageLookupByLibrary.simpleMessage(
"Детали времени сессии",
),
"yearly_report_streak_day_unit": MessageLookupByLibrary.simpleMessage(
"дн.",
),
"yearly_report_streak_offline": MessageLookupByLibrary.simpleMessage(
"Серия оффлайна",
),
"yearly_report_streak_play": MessageLookupByLibrary.simpleMessage(
"Серия игр",
),
"yearly_report_streak_title": MessageLookupByLibrary.simpleMessage(
"Рекорды серий",
),
"yearly_report_summary_earliest_time": MessageLookupByLibrary.simpleMessage(
"Самое раннее время",
),
"yearly_report_summary_favorite_vehicle":
MessageLookupByLibrary.simpleMessage("Любимый транспорт"),
"yearly_report_summary_frequent_location":
MessageLookupByLibrary.simpleMessage("Частая локация"),
"yearly_report_summary_hottest_month": MessageLookupByLibrary.simpleMessage(
"Самый жаркий месяц",
),
"yearly_report_summary_latest_time": MessageLookupByLibrary.simpleMessage(
"Самое позднее время",
),
"yearly_report_summary_launch_game": MessageLookupByLibrary.simpleMessage(
"Запуск игры",
),
"yearly_report_summary_longest_online":
MessageLookupByLibrary.simpleMessage("Дольше всего онлайн"),
"yearly_report_summary_respawn_count": MessageLookupByLibrary.simpleMessage(
"Количество возрождений",
),
"yearly_report_thanks_message": m91,
"yearly_report_thanks_next": m92,
"yearly_report_thanks_title": MessageLookupByLibrary.simpleMessage(
"Спасибо, что вы с нами",
),
"yearly_report_vehicle_destruction_count": m94,
"yearly_report_vehicle_destruction_desc":
MessageLookupByLibrary.simpleMessage("В этом году вы уничтожили"),
"yearly_report_vehicle_destruction_most":
MessageLookupByLibrary.simpleMessage("Самый уничтожаемый корабль"),
"yearly_report_vehicle_destruction_title":
MessageLookupByLibrary.simpleMessage("Статистика уничтожения техники"),
"yearly_report_vehicle_destruction_unit":
MessageLookupByLibrary.simpleMessage("кораблей"),
"yearly_report_vehicle_pilot_collapse":
MessageLookupByLibrary.simpleMessage("Свернуть детали"),
"yearly_report_vehicle_pilot_count": m95,
"yearly_report_vehicle_pilot_expand": m96,
"yearly_report_vehicle_pilot_most": MessageLookupByLibrary.simpleMessage(
"Самый пилотируемый транспорт",
),
"yearly_report_vehicle_pilot_title": MessageLookupByLibrary.simpleMessage(
"Статистика пилотирования",
),
"yearly_report_web_browser_not_supported": MessageLookupByLibrary.simpleMessage(
"Ваш браузер не поддерживается. Используйте Chrome, Edge или другой браузер на базе Chromium.",
),
"yearly_report_web_generate": MessageLookupByLibrary.simpleMessage(
"Создать отчет",
),
"yearly_report_web_no_logs_found": MessageLookupByLibrary.simpleMessage(
"Игровые логи не найдены",
),
"yearly_report_web_reading_files": MessageLookupByLibrary.simpleMessage(
"Чтение файлов логов...",
),
"yearly_report_web_select_folder": MessageLookupByLibrary.simpleMessage(
"Выберите папку игры",
),
"yearly_report_web_select_folder_desc": MessageLookupByLibrary.simpleMessage(
"Выберите папку игры Star Citizen (родительская папка, содержащая директорию LIVE)",
),
"yearly_report_web_select_year": MessageLookupByLibrary.simpleMessage(
"Выберите год",
),
"yearly_report_welcome_hint": MessageLookupByLibrary.simpleMessage(
"Прокрутите вниз или нажмите кнопку ниже, чтобы начать",
),
"yearly_report_welcome_subtitle": MessageLookupByLibrary.simpleMessage(
"Вспомните свои незабываемые моменты в Star Citizen",
),
"yearly_report_welcome_title": m97,
};
}

View File

@@ -185,6 +185,50 @@ class MessageLookup extends MessageLookupByLibrary {
static String m75(v0) => "P4K 查看器 -> ${v0}";
static String m76(v0) => "登录了 ${v0}";
static String m77(v0) => "共检测到 ${v0} 个账号";
static String m78(year) => "查看您在${year}年的星际公民游玩统计,数据来自本地 log ,请确保在常用电脑上查看。";
static String m79(year) => "${year} 年度报告(限时)";
static String m80(v0, v1, v2, v3) => "${v0}${v1}日 - ${v2}${v3}";
static String m81(v0, v1) => "${v0} 小时 ${v1} 分钟";
static String m82(v0) => "${v0} 分钟";
static String m83(v0, v1) => "您在清晨 ${v0}${v1} 日开始了星际之旅";
static String m84(v0, v1) => "深夜 ${v0}${v1} 日还在探索宇宙";
static String m85(v0) => "${v0}";
static String m86(v0) => "${v0}";
static String m87(v0) => "仅启动 ${v0}";
static String m88(v0) => "启动了 ${v0}";
static String m89(v0) => "${v0} 小时";
static String m90(v0, v1) => "${v0}${v1}";
static String m91(year) => "${year} 年,我们一起在星际公民中\n创造了无数精彩回忆";
static String m92(nextYear) => "期待 ${nextYear} 年继续与您相伴!";
static String m93(year) => "星际公民 ${year} 年度报告";
static String m94(v0) => "炸了 ${v0}";
static String m95(v0) => "驾驶了 ${v0}";
static String m96(v0) => "查看全部 ${v0} 个载具";
static String m97(year) => "${year} 年度报告";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about_action_btn_faq": MessageLookupByLibrary.simpleMessage("常见问题"),
@@ -320,6 +364,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_action_rsi_launcher_log": MessageLookupByLibrary.simpleMessage(
"RSI启动器log",
),
"doctor_action_select_log_file": MessageLookupByLibrary.simpleMessage(
"选择 log 文件",
),
"doctor_action_tip_checking_game_log": MessageLookupByLibrary.simpleMessage(
"正在检查Game.log",
),
@@ -381,6 +428,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_game_rescue_service_note": MessageLookupByLibrary.simpleMessage(
"您即将前往由 深空治疗中心QQ群号536454632 提供的游戏异常救援服务,主要解决游戏安装失败与频繁闪退,如游戏玩法问题,请勿加群。",
),
"doctor_info_log_file_selected": MessageLookupByLibrary.simpleMessage(
"已选择日志文件",
),
"doctor_info_need_help": MessageLookupByLibrary.simpleMessage(
"需要帮助? 点击加群寻求免费人工支援!",
),
@@ -432,6 +482,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_tool_check_result_note": MessageLookupByLibrary.simpleMessage(
"注意:本工具检测结果仅供参考,若您不理解以下操作,请提供截图给有经验的玩家!",
),
"doctor_info_web_select_log_file": MessageLookupByLibrary.simpleMessage(
"Web 平台请手动选择 Game.log 文件进行诊断",
),
"doctor_tip_title_select_game_directory":
MessageLookupByLibrary.simpleMessage("请在首页选择游戏安装目录。"),
"doctor_title_one_click_diagnosis": m18,
@@ -1469,5 +1522,196 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("总邀请数:"),
"webview_localization_unfinished_invitations":
MessageLookupByLibrary.simpleMessage("未完成的邀请"),
"yearly_report_account_count": m76,
"yearly_report_account_expand": MessageLookupByLibrary.simpleMessage(
"查看全部账号",
),
"yearly_report_account_most": MessageLookupByLibrary.simpleMessage(
"最常使用的账号",
),
"yearly_report_account_title": MessageLookupByLibrary.simpleMessage("账号统计"),
"yearly_report_account_total": m77,
"yearly_report_analyzing_logs": MessageLookupByLibrary.simpleMessage(
"正在分析游戏日志数据",
),
"yearly_report_card_desc": m78,
"yearly_report_card_title": m79,
"yearly_report_crash_desc": MessageLookupByLibrary.simpleMessage(
"今年游戏不太稳定的时刻",
),
"yearly_report_crash_label": MessageLookupByLibrary.simpleMessage("累计崩溃"),
"yearly_report_crash_note_high": MessageLookupByLibrary.simpleMessage(
"希望明年能更稳定!",
),
"yearly_report_crash_note_low": MessageLookupByLibrary.simpleMessage(
"运气不错!",
),
"yearly_report_crash_title": MessageLookupByLibrary.simpleMessage("游戏崩溃次数"),
"yearly_report_date_range": m80,
"yearly_report_disclaimer": MessageLookupByLibrary.simpleMessage(
"数据使用您的本地日志生成,不会发送到任何第三方。因跨版本 Log 改动较大,数据可能不完整,仅供娱乐。",
),
"yearly_report_duration_hours_minutes": m81,
"yearly_report_duration_minutes": m82,
"yearly_report_earliest_play_desc": m83,
"yearly_report_earliest_play_title": MessageLookupByLibrary.simpleMessage(
"最早的一次游玩",
),
"yearly_report_error_description": MessageLookupByLibrary.simpleMessage(
"请确保游戏目录正确且存在日志文件",
),
"yearly_report_error_title": MessageLookupByLibrary.simpleMessage(
"无法生成年度报告",
),
"yearly_report_generating": MessageLookupByLibrary.simpleMessage(
"正在生成您的年度报告...",
),
"yearly_report_kd_death": MessageLookupByLibrary.simpleMessage("死亡"),
"yearly_report_kd_kill": MessageLookupByLibrary.simpleMessage("击杀"),
"yearly_report_kd_no_record": MessageLookupByLibrary.simpleMessage(
"今年没有检测到击杀/死亡记录",
),
"yearly_report_kd_suicide": MessageLookupByLibrary.simpleMessage("自杀"),
"yearly_report_kd_title": MessageLookupByLibrary.simpleMessage("击杀统计"),
"yearly_report_latest_play_desc": m84,
"yearly_report_latest_play_title": MessageLookupByLibrary.simpleMessage(
"最晚的一次游玩",
),
"yearly_report_launch_count_desc": MessageLookupByLibrary.simpleMessage(
"今年您启动了游戏",
),
"yearly_report_launch_count_label": MessageLookupByLibrary.simpleMessage(
"累计启动",
),
"yearly_report_launch_count_title": MessageLookupByLibrary.simpleMessage(
"游戏启动次数",
),
"yearly_report_launch_count_value": m85,
"yearly_report_location_frequent": MessageLookupByLibrary.simpleMessage(
"常去的地点",
),
"yearly_report_location_no_record": MessageLookupByLibrary.simpleMessage(
"暂无地点访问记录",
),
"yearly_report_location_note": MessageLookupByLibrary.simpleMessage(
"基于库存查看记录统计",
),
"yearly_report_location_title": MessageLookupByLibrary.simpleMessage(
"地点统计",
),
"yearly_report_menu_title": MessageLookupByLibrary.simpleMessage("报告"),
"yearly_report_month_format": m86,
"yearly_report_monthly_least": MessageLookupByLibrary.simpleMessage("游玩最少"),
"yearly_report_monthly_least_count": m87,
"yearly_report_monthly_most": MessageLookupByLibrary.simpleMessage("游玩最多"),
"yearly_report_monthly_most_count": m88,
"yearly_report_monthly_title": MessageLookupByLibrary.simpleMessage("月份统计"),
"yearly_report_nav_next": MessageLookupByLibrary.simpleMessage("继续查看"),
"yearly_report_nav_prev": MessageLookupByLibrary.simpleMessage("上一页"),
"yearly_report_no_data": MessageLookupByLibrary.simpleMessage("暂无数据"),
"yearly_report_play_time_desc": MessageLookupByLibrary.simpleMessage(
"今年您在宇宙中遨游了",
),
"yearly_report_play_time_label": MessageLookupByLibrary.simpleMessage(
"累计游玩",
),
"yearly_report_play_time_title": MessageLookupByLibrary.simpleMessage(
"游玩时长",
),
"yearly_report_play_time_unit": MessageLookupByLibrary.simpleMessage("小时"),
"yearly_report_play_time_value": m89,
"yearly_report_powered_by": MessageLookupByLibrary.simpleMessage(
"由 SC 汉化盒子为您呈现 | github.com/StarCitizenToolBox/app",
),
"yearly_report_session_average": MessageLookupByLibrary.simpleMessage("平均"),
"yearly_report_session_date": m90,
"yearly_report_session_longest": MessageLookupByLibrary.simpleMessage("最长"),
"yearly_report_session_note": MessageLookupByLibrary.simpleMessage(
"(最短仅统计超过 5 分钟的游戏)",
),
"yearly_report_session_shortest": MessageLookupByLibrary.simpleMessage(
"最短",
),
"yearly_report_session_title": MessageLookupByLibrary.simpleMessage(
"游玩时长详情",
),
"yearly_report_streak_day_unit": MessageLookupByLibrary.simpleMessage(""),
"yearly_report_streak_offline": MessageLookupByLibrary.simpleMessage(
"连续离线",
),
"yearly_report_streak_play": MessageLookupByLibrary.simpleMessage("连续游玩"),
"yearly_report_streak_title": MessageLookupByLibrary.simpleMessage("连续记录"),
"yearly_report_summary_earliest_time": MessageLookupByLibrary.simpleMessage(
"最早时刻",
),
"yearly_report_summary_favorite_vehicle":
MessageLookupByLibrary.simpleMessage("最爱载具"),
"yearly_report_summary_frequent_location":
MessageLookupByLibrary.simpleMessage("常去位置"),
"yearly_report_summary_hottest_month": MessageLookupByLibrary.simpleMessage(
"最热月",
),
"yearly_report_summary_latest_time": MessageLookupByLibrary.simpleMessage(
"最晚时刻",
),
"yearly_report_summary_launch_game": MessageLookupByLibrary.simpleMessage(
"启动游戏",
),
"yearly_report_summary_longest_online":
MessageLookupByLibrary.simpleMessage("最长在线"),
"yearly_report_summary_respawn_count": MessageLookupByLibrary.simpleMessage(
"重开次数",
),
"yearly_report_thanks_message": m91,
"yearly_report_thanks_next": m92,
"yearly_report_thanks_title": MessageLookupByLibrary.simpleMessage(
"感谢您的陪伴",
),
"yearly_report_title": m93,
"yearly_report_vehicle_destruction_count": m94,
"yearly_report_vehicle_destruction_desc":
MessageLookupByLibrary.simpleMessage("今年您共炸了"),
"yearly_report_vehicle_destruction_most":
MessageLookupByLibrary.simpleMessage("炸的最多的船"),
"yearly_report_vehicle_destruction_title":
MessageLookupByLibrary.simpleMessage("载具损毁统计"),
"yearly_report_vehicle_destruction_unit":
MessageLookupByLibrary.simpleMessage("艘船"),
"yearly_report_vehicle_pilot_collapse":
MessageLookupByLibrary.simpleMessage("收起详情"),
"yearly_report_vehicle_pilot_count": m95,
"yearly_report_vehicle_pilot_expand": m96,
"yearly_report_vehicle_pilot_most": MessageLookupByLibrary.simpleMessage(
"最常驾驶的载具",
),
"yearly_report_vehicle_pilot_title": MessageLookupByLibrary.simpleMessage(
"载具驾驶统计",
),
"yearly_report_web_browser_not_supported":
MessageLookupByLibrary.simpleMessage(
"您的浏览器不受支持,请使用 Chrome、Edge 或其他基于 Chromium 的浏览器。",
),
"yearly_report_web_generate": MessageLookupByLibrary.simpleMessage("生成报告"),
"yearly_report_web_no_logs_found": MessageLookupByLibrary.simpleMessage(
"未找到游戏日志",
),
"yearly_report_web_reading_files": MessageLookupByLibrary.simpleMessage(
"正在读取日志文件...",
),
"yearly_report_web_select_folder": MessageLookupByLibrary.simpleMessage(
"选择游戏文件夹",
),
"yearly_report_web_select_folder_desc":
MessageLookupByLibrary.simpleMessage("请选择您的星际公民游戏文件夹(包含 LIVE 目录的父文件夹)"),
"yearly_report_web_select_year": MessageLookupByLibrary.simpleMessage(
"选择年份",
),
"yearly_report_welcome_hint": MessageLookupByLibrary.simpleMessage(
"向下滚动或点击下方按钮开始",
),
"yearly_report_welcome_subtitle": MessageLookupByLibrary.simpleMessage(
"回顾您在星际公民中的精彩时刻",
),
"yearly_report_welcome_title": m97,
};
}

View File

@@ -185,6 +185,50 @@ class MessageLookup extends MessageLookupByLibrary {
static String m75(v0) => "P4K 查看器 -> ${v0}";
static String m76(v0) => "登入了 ${v0}";
static String m77(v0) => "共檢測到 ${v0} 個帳號";
static String m78(year) => "查看您在${year}年的星際公民遊玩統計,數據來自本地 log ,請確保在常用電腦上查看。";
static String m79(year) => "${year} 年度報告(限時)";
static String m80(v0, v1, v2, v3) => "${v0}${v1}日 - ${v2}${v3}";
static String m81(v0, v1) => "${v0} 小時 ${v1} 分鐘";
static String m82(v0) => "${v0} 分鐘";
static String m83(v0, v1) => "您在清晨 ${v0}${v1} 日開始了星際之旅";
static String m84(v0, v1) => "深夜 ${v0}${v1} 日還在探索宇宙";
static String m85(v0) => "${v0}";
static String m86(v0) => "${v0}";
static String m87(v0) => "僅啟動 ${v0}";
static String m88(v0) => "啟動了 ${v0}";
static String m89(v0) => "${v0} 小時";
static String m90(v0, v1) => "${v0}${v1}";
static String m91(year) => "${year} 年,我們一起在星際公民中\n創造了無數精彩回憶";
static String m92(nextYear) => "期待 ${nextYear} 年繼續與您相伴!";
static String m93(year) => "星際公民 ${year} 年度報告";
static String m94(v0) => "炸了 ${v0}";
static String m95(v0) => "駕駛了 ${v0}";
static String m96(v0) => "查看全部 ${v0} 個載具";
static String m97(year) => "${year} 年度報告";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about_action_btn_faq": MessageLookupByLibrary.simpleMessage("常見問題"),
@@ -322,6 +366,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_action_rsi_launcher_log": MessageLookupByLibrary.simpleMessage(
"RSI啟動器log",
),
"doctor_action_select_log_file": MessageLookupByLibrary.simpleMessage(
"選擇 log 檔案",
),
"doctor_action_tip_checking_game_log": MessageLookupByLibrary.simpleMessage(
"正在檢查Game.log",
),
@@ -387,6 +434,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_game_rescue_service_note": MessageLookupByLibrary.simpleMessage(
"您即將前往由 深空治療中心QQ群號536454632 提供的遊戲異常救援服務,主要解決遊戲安裝失敗與頻繁閃退,如遊戲玩法問題,請勿加群。",
),
"doctor_info_log_file_selected": MessageLookupByLibrary.simpleMessage(
"已選擇日誌檔案",
),
"doctor_info_need_help": MessageLookupByLibrary.simpleMessage(
"需要幫助? 點擊加群尋求免費人工支援!",
),
@@ -438,6 +488,9 @@ class MessageLookup extends MessageLookupByLibrary {
"doctor_info_tool_check_result_note": MessageLookupByLibrary.simpleMessage(
"注意:本工具檢測結果僅供參考,若您不理解以下操作,請提供截圖給有經驗的玩家!",
),
"doctor_info_web_select_log_file": MessageLookupByLibrary.simpleMessage(
"Web 平台請手動選擇 Game.log 檔案進行診斷",
),
"doctor_tip_title_select_game_directory":
MessageLookupByLibrary.simpleMessage("請在首頁選擇遊戲安裝目錄。"),
"doctor_title_one_click_diagnosis": m18,
@@ -1472,5 +1525,196 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("總邀請數:"),
"webview_localization_unfinished_invitations":
MessageLookupByLibrary.simpleMessage("未完成的邀請"),
"yearly_report_account_count": m76,
"yearly_report_account_expand": MessageLookupByLibrary.simpleMessage(
"查看全部帳號",
),
"yearly_report_account_most": MessageLookupByLibrary.simpleMessage(
"最常使用的帳號",
),
"yearly_report_account_title": MessageLookupByLibrary.simpleMessage("帳號統計"),
"yearly_report_account_total": m77,
"yearly_report_analyzing_logs": MessageLookupByLibrary.simpleMessage(
"正在分析遊戲日誌數據",
),
"yearly_report_card_desc": m78,
"yearly_report_card_title": m79,
"yearly_report_crash_desc": MessageLookupByLibrary.simpleMessage(
"今年遊戲不太穩定的時刻",
),
"yearly_report_crash_label": MessageLookupByLibrary.simpleMessage("累計崩潰"),
"yearly_report_crash_note_high": MessageLookupByLibrary.simpleMessage(
"希望明年能更穩定!",
),
"yearly_report_crash_note_low": MessageLookupByLibrary.simpleMessage(
"運氣不錯!",
),
"yearly_report_crash_title": MessageLookupByLibrary.simpleMessage("遊戲崩潰次數"),
"yearly_report_date_range": m80,
"yearly_report_disclaimer": MessageLookupByLibrary.simpleMessage(
"數據使用您的本地日誌生成,不會發送到任何第三方。因跨版本 Log 改動較大,數據可能不完整,僅供娛樂。",
),
"yearly_report_duration_hours_minutes": m81,
"yearly_report_duration_minutes": m82,
"yearly_report_earliest_play_desc": m83,
"yearly_report_earliest_play_title": MessageLookupByLibrary.simpleMessage(
"最早的一次遊玩",
),
"yearly_report_error_description": MessageLookupByLibrary.simpleMessage(
"請確保遊戲目錄正確且存在日誌文件",
),
"yearly_report_error_title": MessageLookupByLibrary.simpleMessage(
"無法生成年度報告",
),
"yearly_report_generating": MessageLookupByLibrary.simpleMessage(
"正在生成您的年度報告...",
),
"yearly_report_kd_death": MessageLookupByLibrary.simpleMessage("死亡"),
"yearly_report_kd_kill": MessageLookupByLibrary.simpleMessage("擊殺"),
"yearly_report_kd_no_record": MessageLookupByLibrary.simpleMessage(
"今年沒有檢測到擊殺/死亡記錄",
),
"yearly_report_kd_suicide": MessageLookupByLibrary.simpleMessage("自殺"),
"yearly_report_kd_title": MessageLookupByLibrary.simpleMessage("擊殺統計"),
"yearly_report_latest_play_desc": m84,
"yearly_report_latest_play_title": MessageLookupByLibrary.simpleMessage(
"最晚的一次遊玩",
),
"yearly_report_launch_count_desc": MessageLookupByLibrary.simpleMessage(
"今年您啟動了遊戲",
),
"yearly_report_launch_count_label": MessageLookupByLibrary.simpleMessage(
"累計啟動",
),
"yearly_report_launch_count_title": MessageLookupByLibrary.simpleMessage(
"遊戲啟動次數",
),
"yearly_report_launch_count_value": m85,
"yearly_report_location_frequent": MessageLookupByLibrary.simpleMessage(
"常去的地點",
),
"yearly_report_location_no_record": MessageLookupByLibrary.simpleMessage(
"暫無地點訪問記錄",
),
"yearly_report_location_note": MessageLookupByLibrary.simpleMessage(
"基於庫存查看記錄統計",
),
"yearly_report_location_title": MessageLookupByLibrary.simpleMessage(
"地點統計",
),
"yearly_report_menu_title": MessageLookupByLibrary.simpleMessage("報告"),
"yearly_report_month_format": m86,
"yearly_report_monthly_least": MessageLookupByLibrary.simpleMessage("遊玩最少"),
"yearly_report_monthly_least_count": m87,
"yearly_report_monthly_most": MessageLookupByLibrary.simpleMessage("遊玩最多"),
"yearly_report_monthly_most_count": m88,
"yearly_report_monthly_title": MessageLookupByLibrary.simpleMessage("月份統計"),
"yearly_report_nav_next": MessageLookupByLibrary.simpleMessage("繼續查看"),
"yearly_report_nav_prev": MessageLookupByLibrary.simpleMessage("上一頁"),
"yearly_report_no_data": MessageLookupByLibrary.simpleMessage("暫無數據"),
"yearly_report_play_time_desc": MessageLookupByLibrary.simpleMessage(
"今年您在宇宙中遨遊了",
),
"yearly_report_play_time_label": MessageLookupByLibrary.simpleMessage(
"累計遊玩",
),
"yearly_report_play_time_title": MessageLookupByLibrary.simpleMessage(
"遊玩時長",
),
"yearly_report_play_time_unit": MessageLookupByLibrary.simpleMessage("小時"),
"yearly_report_play_time_value": m89,
"yearly_report_powered_by": MessageLookupByLibrary.simpleMessage(
"由 SC工具箱為您呈現 | github.com/StarCitizenToolBox/app",
),
"yearly_report_session_average": MessageLookupByLibrary.simpleMessage("平均"),
"yearly_report_session_date": m90,
"yearly_report_session_longest": MessageLookupByLibrary.simpleMessage("最長"),
"yearly_report_session_note": MessageLookupByLibrary.simpleMessage(
"(最短僅統計超過 5 分鐘的遊戲)",
),
"yearly_report_session_shortest": MessageLookupByLibrary.simpleMessage(
"最短",
),
"yearly_report_session_title": MessageLookupByLibrary.simpleMessage(
"遊玩時長詳情",
),
"yearly_report_streak_day_unit": MessageLookupByLibrary.simpleMessage(""),
"yearly_report_streak_offline": MessageLookupByLibrary.simpleMessage(
"連續離線",
),
"yearly_report_streak_play": MessageLookupByLibrary.simpleMessage("連續遊玩"),
"yearly_report_streak_title": MessageLookupByLibrary.simpleMessage("連續記錄"),
"yearly_report_summary_earliest_time": MessageLookupByLibrary.simpleMessage(
"最早時刻",
),
"yearly_report_summary_favorite_vehicle":
MessageLookupByLibrary.simpleMessage("最愛載具"),
"yearly_report_summary_frequent_location":
MessageLookupByLibrary.simpleMessage("常去位置"),
"yearly_report_summary_hottest_month": MessageLookupByLibrary.simpleMessage(
"最熱月",
),
"yearly_report_summary_latest_time": MessageLookupByLibrary.simpleMessage(
"最晚時刻",
),
"yearly_report_summary_launch_game": MessageLookupByLibrary.simpleMessage(
"啟動遊戲",
),
"yearly_report_summary_longest_online":
MessageLookupByLibrary.simpleMessage("最長在線"),
"yearly_report_summary_respawn_count": MessageLookupByLibrary.simpleMessage(
"重開次數",
),
"yearly_report_thanks_message": m91,
"yearly_report_thanks_next": m92,
"yearly_report_thanks_title": MessageLookupByLibrary.simpleMessage(
"感謝您的陪伴",
),
"yearly_report_title": m93,
"yearly_report_vehicle_destruction_count": m94,
"yearly_report_vehicle_destruction_desc":
MessageLookupByLibrary.simpleMessage("今年您共炸了"),
"yearly_report_vehicle_destruction_most":
MessageLookupByLibrary.simpleMessage("炸的最多的船"),
"yearly_report_vehicle_destruction_title":
MessageLookupByLibrary.simpleMessage("載具損毀統計"),
"yearly_report_vehicle_destruction_unit":
MessageLookupByLibrary.simpleMessage("艘船"),
"yearly_report_vehicle_pilot_collapse":
MessageLookupByLibrary.simpleMessage("收起詳情"),
"yearly_report_vehicle_pilot_count": m95,
"yearly_report_vehicle_pilot_expand": m96,
"yearly_report_vehicle_pilot_most": MessageLookupByLibrary.simpleMessage(
"最常駕駛的載具",
),
"yearly_report_vehicle_pilot_title": MessageLookupByLibrary.simpleMessage(
"載具駕駛統計",
),
"yearly_report_web_browser_not_supported":
MessageLookupByLibrary.simpleMessage(
"您的瀏覽器不受支援,請使用 Chrome、Edge 或其他基於 Chromium 的瀏覽器。",
),
"yearly_report_web_generate": MessageLookupByLibrary.simpleMessage("生成報告"),
"yearly_report_web_no_logs_found": MessageLookupByLibrary.simpleMessage(
"未找到遊戲記錄檔",
),
"yearly_report_web_reading_files": MessageLookupByLibrary.simpleMessage(
"正在讀取記錄檔...",
),
"yearly_report_web_select_folder": MessageLookupByLibrary.simpleMessage(
"選擇遊戲資料夾",
),
"yearly_report_web_select_folder_desc":
MessageLookupByLibrary.simpleMessage("請選擇您的星際公民遊戲資料夾(包含 LIVE 目錄的父資料夾)"),
"yearly_report_web_select_year": MessageLookupByLibrary.simpleMessage(
"選擇年份",
),
"yearly_report_welcome_hint": MessageLookupByLibrary.simpleMessage(
"向下滾動或點擊下方按鈕開始",
),
"yearly_report_welcome_subtitle": MessageLookupByLibrary.simpleMessage(
"回顧您在星際公民中的精彩時刻",
),
"yearly_report_welcome_title": m97,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -141,6 +141,12 @@
"@doctor_action_rsi_launcher_log": {},
"doctor_action_game_run_log": "Game run log",
"@doctor_action_game_run_log": {},
"doctor_action_select_log_file": "Select Log File",
"@doctor_action_select_log_file": {},
"doctor_info_web_select_log_file": "Web platform: Please manually select the Game.log file for diagnosis",
"@doctor_info_web_select_log_file": {},
"doctor_info_log_file_selected": "Log file selected",
"@doctor_info_log_file_selected": {},
"doctor_info_scan_complete_no_issues": "Scan complete, no issues found!",
"@doctor_info_scan_complete_no_issues": {},
"doctor_info_processing": "Processing...",
@@ -1178,5 +1184,195 @@
"tools_vehicle_sorting_search": "Search Vehicles",
"@tools_vehicle_sorting_search": {},
"tools_vehicle_sorting_sorted": "Sorted Vehicles",
"@tools_vehicle_sorting_sorted": {}
"@tools_vehicle_sorting_sorted": {},
"yearly_report_title": "Star Citizen {year} Yearly Report",
"@yearly_report_title": {},
"yearly_report_generating": "Generating your yearly report...",
"@yearly_report_generating": {},
"yearly_report_analyzing_logs": "Analyzing game log data",
"@yearly_report_analyzing_logs": {},
"yearly_report_error_title": "Unable to generate yearly report",
"@yearly_report_error_title": {},
"yearly_report_error_description": "Please ensure the game directory is correct and log files exist",
"@yearly_report_error_description": {},
"yearly_report_nav_prev": "Previous Page",
"@yearly_report_nav_prev": {},
"yearly_report_nav_next": "Continue",
"@yearly_report_nav_next": {},
"yearly_report_welcome_title": "{year} Yearly Report",
"@yearly_report_welcome_title": {},
"yearly_report_welcome_subtitle": "Relive your memorable moments in Star Citizen",
"@yearly_report_welcome_subtitle": {},
"yearly_report_welcome_hint": "Scroll down or click the button below to start",
"@yearly_report_welcome_hint": {},
"yearly_report_launch_count_title": "Game Launch Count",
"@yearly_report_launch_count_title": {},
"yearly_report_launch_count_desc": "This year you launched the game",
"@yearly_report_launch_count_desc": {},
"yearly_report_launch_count_label": "Total Launches",
"@yearly_report_launch_count_label": {},
"yearly_report_launch_count_value": "{v0} times",
"@yearly_report_launch_count_value": {},
"yearly_report_play_time_title": "Play Time",
"@yearly_report_play_time_title": {},
"yearly_report_play_time_desc": "This year you explored the universe for",
"@yearly_report_play_time_desc": {},
"yearly_report_play_time_unit": "hours",
"@yearly_report_play_time_unit": {},
"yearly_report_play_time_label": "Total Playtime",
"@yearly_report_play_time_label": {},
"yearly_report_play_time_value": "{v0} hours",
"@yearly_report_play_time_value": {},
"yearly_report_crash_title": "Game Crash Count",
"@yearly_report_crash_title": {},
"yearly_report_crash_desc": "Unstable moments this year",
"@yearly_report_crash_desc": {},
"yearly_report_crash_label": "Total Crashes",
"@yearly_report_crash_label": {},
"yearly_report_crash_note_high": "Hope it's more stable next year!",
"@yearly_report_crash_note_high": {},
"yearly_report_crash_note_low": "Lucky you!",
"@yearly_report_crash_note_low": {},
"yearly_report_kd_title": "Kill Statistics",
"@yearly_report_kd_title": {},
"yearly_report_kd_kill": "Kills",
"@yearly_report_kd_kill": {},
"yearly_report_kd_death": "Deaths",
"@yearly_report_kd_death": {},
"yearly_report_kd_suicide": "Suicides",
"@yearly_report_kd_suicide": {},
"yearly_report_kd_no_record": "No kill/death records detected this year",
"@yearly_report_kd_no_record": {},
"yearly_report_no_data": "No data available",
"@yearly_report_no_data": {},
"yearly_report_earliest_play_title": "Earliest Play Session",
"@yearly_report_earliest_play_title": {},
"yearly_report_earliest_play_desc": "You started your space journey at dawn on {v0}/{v1}",
"@yearly_report_earliest_play_desc": {},
"yearly_report_latest_play_title": "Latest Play Session",
"@yearly_report_latest_play_title": {},
"yearly_report_latest_play_desc": "Late night on {v0}/{v1}, you were still exploring the universe",
"@yearly_report_latest_play_desc": {},
"yearly_report_vehicle_destruction_title": "Vehicle Destruction Statistics",
"@yearly_report_vehicle_destruction_title": {},
"yearly_report_vehicle_destruction_desc": "This year you destroyed",
"@yearly_report_vehicle_destruction_desc": {},
"yearly_report_vehicle_destruction_unit": "ships",
"@yearly_report_vehicle_destruction_unit": {},
"yearly_report_vehicle_destruction_most": "Most destroyed ship",
"@yearly_report_vehicle_destruction_most": {},
"yearly_report_vehicle_destruction_count": "Destroyed {v0} times",
"@yearly_report_vehicle_destruction_count": {},
"yearly_report_vehicle_pilot_title": "Vehicle Piloting Statistics",
"@yearly_report_vehicle_pilot_title": {},
"yearly_report_vehicle_pilot_most": "Most piloted vehicle",
"@yearly_report_vehicle_pilot_most": {},
"yearly_report_vehicle_pilot_count": "Piloted {v0} times",
"@yearly_report_vehicle_pilot_count": {},
"yearly_report_vehicle_pilot_collapse": "Collapse details",
"@yearly_report_vehicle_pilot_collapse": {},
"yearly_report_vehicle_pilot_expand": "View all {v0} vehicles",
"@yearly_report_vehicle_pilot_expand": {},
"yearly_report_account_title": "Account Statistics",
"@yearly_report_account_title": {},
"yearly_report_account_most": "Most used account",
"@yearly_report_account_most": {},
"yearly_report_account_count": "Logged in {v0} times",
"@yearly_report_account_count": {},
"yearly_report_account_total": "Detected {v0} accounts in total",
"@yearly_report_account_total": {},
"yearly_report_account_expand": "View all accounts",
"@yearly_report_account_expand": {},
"yearly_report_duration_hours_minutes": "{v0} hours {v1} minutes",
"@yearly_report_duration_hours_minutes": {},
"yearly_report_duration_minutes": "{v0} minutes",
"@yearly_report_duration_minutes": {},
"yearly_report_session_title": "Session Time Details",
"@yearly_report_session_title": {},
"yearly_report_session_average": "Average",
"@yearly_report_session_average": {},
"yearly_report_session_longest": "Longest",
"@yearly_report_session_longest": {},
"yearly_report_session_date": "{v0}/{v1}",
"@yearly_report_session_date": {},
"yearly_report_session_shortest": "Shortest",
"@yearly_report_session_shortest": {},
"yearly_report_session_note": "(Shortest only counts sessions over 5 minutes)",
"@yearly_report_session_note": {},
"yearly_report_month_format": "Month {v0}",
"@yearly_report_month_format": {},
"yearly_report_monthly_title": "Monthly Statistics",
"@yearly_report_monthly_title": {},
"yearly_report_monthly_most": "Most played",
"@yearly_report_monthly_most": {},
"yearly_report_monthly_most_count": "Launched {v0} times",
"@yearly_report_monthly_most_count": {},
"yearly_report_monthly_least": "Least played",
"@yearly_report_monthly_least": {},
"yearly_report_monthly_least_count": "Only launched {v0} times",
"@yearly_report_monthly_least_count": {},
"yearly_report_date_range": "{v0}/{v1} - {v2}/{v3}",
"@yearly_report_date_range": {},
"yearly_report_streak_title": "Streak Records",
"@yearly_report_streak_title": {},
"yearly_report_streak_play": "Consecutive Play",
"@yearly_report_streak_play": {},
"yearly_report_streak_day_unit": "days",
"@yearly_report_streak_day_unit": {},
"yearly_report_streak_offline": "Consecutive Offline",
"@yearly_report_streak_offline": {},
"yearly_report_location_title": "Location Statistics",
"@yearly_report_location_title": {},
"yearly_report_location_no_record": "No location visit records",
"@yearly_report_location_no_record": {},
"yearly_report_location_frequent": "Frequent Locations",
"@yearly_report_location_frequent": {},
"yearly_report_location_note": "Based on inventory viewing records",
"@yearly_report_location_note": {},
"yearly_report_thanks_title": "Thank You for Being With Us",
"@yearly_report_thanks_title": {},
"yearly_report_thanks_message": "In {year}, together we created\ncountless wonderful memories in Star Citizen",
"@yearly_report_thanks_message": {},
"yearly_report_thanks_next": "Looking forward to {nextYear} with you!",
"@yearly_report_thanks_next": {},
"yearly_report_summary_launch_game": "Launch Game",
"@yearly_report_summary_launch_game": {},
"yearly_report_summary_longest_online": "Longest Online",
"@yearly_report_summary_longest_online": {},
"yearly_report_summary_earliest_time": "Earliest Time",
"@yearly_report_summary_earliest_time": {},
"yearly_report_summary_latest_time": "Latest Time",
"@yearly_report_summary_latest_time": {},
"yearly_report_summary_respawn_count": "Respawn Count",
"@yearly_report_summary_respawn_count": {},
"yearly_report_summary_hottest_month": "Hottest Month",
"@yearly_report_summary_hottest_month": {},
"yearly_report_summary_frequent_location": "Frequent Location",
"@yearly_report_summary_frequent_location": {},
"yearly_report_summary_favorite_vehicle": "Favorite Vehicle",
"@yearly_report_summary_favorite_vehicle": {},
"yearly_report_powered_by": "Presented by SCToolbox | github.com/StarCitizenToolBox/app",
"@yearly_report_powered_by": {},
"yearly_report_disclaimer": "Data is generated from your local logs and will not be sent to any third party. Due to significant log changes across versions, data may be incomplete. For entertainment purposes only.",
"@yearly_report_disclaimer": {},
"yearly_report_card_title": "{year} Yearly Report (Limited Time)",
"@yearly_report_card_title": {},
"yearly_report_card_desc": "View your Star Citizen gameplay statistics for {year}. Data is from local logs, please check on your main computer.",
"@yearly_report_card_desc": {},
"yearly_report_web_select_folder": "Select Game Folder",
"@yearly_report_web_select_folder": {},
"yearly_report_web_select_folder_desc": "Please select your Star Citizen game folder (the parent folder containing LIVE directory)",
"@yearly_report_web_select_folder_desc": {},
"yearly_report_web_select_year": "Select Year",
"@yearly_report_web_select_year": {},
"yearly_report_web_generate": "Generate Report",
"@yearly_report_web_generate": {},
"yearly_report_web_reading_files": "Reading log files...",
"@yearly_report_web_reading_files": {},
"yearly_report_web_no_logs_found": "No game logs found",
"@yearly_report_web_no_logs_found": {},
"yearly_report_web_browser_not_supported": "Your browser is not supported. Please use Chrome, Edge, or another Chromium-based browser.",
"@yearly_report_web_browser_not_supported": {},
"yearly_report_menu_title": "Report",
"@yearly_report_menu_title": {}
}

View File

@@ -141,6 +141,12 @@
"@doctor_action_rsi_launcher_log": {},
"doctor_action_game_run_log": "ゲーム実行ログ",
"@doctor_action_game_run_log": {},
"doctor_action_select_log_file": "ログファイルを選択",
"@doctor_action_select_log_file": {},
"doctor_info_web_select_log_file": "WebプラットフォームGame.logファイルを手動で選択して診断してください",
"@doctor_info_web_select_log_file": {},
"doctor_info_log_file_selected": "ログファイルが選択されました",
"@doctor_info_log_file_selected": {},
"doctor_info_scan_complete_no_issues": "スキャン完了、問題は見つかりませんでした!",
"@doctor_info_scan_complete_no_issues": {},
"doctor_info_processing": "処理中...",
@@ -1157,5 +1163,186 @@
"log_analyzer_description": "プレイ記録を分析(ログイン、死亡、キルなどの情報)",
"@log_analyzer_description": {},
"log_analyzer_window_title": "SCToolbox: logアナライザ",
"@log_analyzer_window_title": {}
"@log_analyzer_window_title": {},
"yearly_report_title": "Star Citizen {year} 年間レポート",
"@yearly_report_title": {},
"yearly_report_generating": "年間レポートを作成中...",
"@yearly_report_generating": {},
"yearly_report_analyzing_logs": "ゲームログデータを分析中",
"@yearly_report_analyzing_logs": {},
"yearly_report_error_title": "年間レポートを作成できません",
"@yearly_report_error_title": {},
"yearly_report_error_description": "ゲームディレクトリが正しいか、ログファイルが存在するか確認してください",
"@yearly_report_error_description": {},
"yearly_report_nav_prev": "前のページ",
"@yearly_report_nav_prev": {},
"yearly_report_nav_next": "次へ",
"@yearly_report_nav_next": {},
"yearly_report_welcome_title": "{year} 年間レポート",
"@yearly_report_welcome_title": {},
"yearly_report_welcome_subtitle": "Star Citizenでの思い出深い瞬間を振り返る",
"@yearly_report_welcome_subtitle": {},
"yearly_report_welcome_hint": "下にスクロールするか、下のボタンをクリックして開始",
"@yearly_report_welcome_hint": {},
"yearly_report_launch_count_title": "ゲーム起動回数",
"@yearly_report_launch_count_title": {},
"yearly_report_launch_count_desc": "今年あなたはゲームを起動しました",
"@yearly_report_launch_count_desc": {},
"yearly_report_launch_count_label": "合計起動回数",
"@yearly_report_launch_count_label": {},
"yearly_report_launch_count_value": "{v0} 回",
"@yearly_report_launch_count_value": {},
"yearly_report_play_time_title": "プレイ時間",
"@yearly_report_play_time_title": {},
"yearly_report_play_time_desc": "今年あなたは宇宙を探索しました",
"@yearly_report_play_time_desc": {},
"yearly_report_play_time_unit": "時間",
"@yearly_report_play_time_unit": {},
"yearly_report_play_time_label": "合計プレイ時間",
"@yearly_report_play_time_label": {},
"yearly_report_play_time_value": "{v0} 時間",
"@yearly_report_play_time_value": {},
"yearly_report_crash_title": "ゲームクラッシュ回数",
"@yearly_report_crash_title": {},
"yearly_report_crash_desc": "今年の不安定な瞬間",
"@yearly_report_crash_desc": {},
"yearly_report_crash_label": "合計クラッシュ数",
"@yearly_report_crash_label": {},
"yearly_report_crash_note_high": "来年はもっと安定しますように!",
"@yearly_report_crash_note_high": {},
"yearly_report_crash_note_low": "ラッキーですね!",
"@yearly_report_crash_note_low": {},
"yearly_report_kd_title": "キル統計",
"@yearly_report_kd_title": {},
"yearly_report_kd_kill": "キル",
"@yearly_report_kd_kill": {},
"yearly_report_kd_death": "デス",
"@yearly_report_kd_death": {},
"yearly_report_kd_suicide": "自殺",
"@yearly_report_kd_suicide": {},
"yearly_report_kd_no_record": "今年はキル/デス記録が検出されませんでした",
"@yearly_report_kd_no_record": {},
"yearly_report_no_data": "データなし",
"@yearly_report_no_data": {},
"yearly_report_earliest_play_title": "最も早いプレイ",
"@yearly_report_earliest_play_title": {},
"yearly_report_earliest_play_desc": "あなたは {v0}月{v1}日 の夜明けに宇宙の旅を始めました",
"@yearly_report_earliest_play_desc": {},
"yearly_report_latest_play_title": "最も遅いプレイ",
"@yearly_report_latest_play_title": {},
"yearly_report_latest_play_desc": "{v0}月{v1}日 の深夜、あなたはまだ宇宙を探索していました",
"@yearly_report_latest_play_desc": {},
"yearly_report_vehicle_destruction_title": "ビークル破壊統計",
"@yearly_report_vehicle_destruction_title": {},
"yearly_report_vehicle_destruction_desc": "今年あなたは破壊しました",
"@yearly_report_vehicle_destruction_desc": {},
"yearly_report_vehicle_destruction_unit": "隻の船",
"@yearly_report_vehicle_destruction_unit": {},
"yearly_report_vehicle_destruction_most": "最も破壊された船",
"@yearly_report_vehicle_destruction_most": {},
"yearly_report_vehicle_destruction_count": "{v0} 回破壊",
"@yearly_report_vehicle_destruction_count": {},
"yearly_report_vehicle_pilot_title": "ビークル操縦統計",
"@yearly_report_vehicle_pilot_title": {},
"yearly_report_vehicle_pilot_most": "最も操縦したビークル",
"@yearly_report_vehicle_pilot_most": {},
"yearly_report_vehicle_pilot_count": "{v0} 回操縦",
"@yearly_report_vehicle_pilot_count": {},
"yearly_report_vehicle_pilot_collapse": "詳細を折りたたむ",
"@yearly_report_vehicle_pilot_collapse": {},
"yearly_report_vehicle_pilot_expand": "全 {v0} 車両を表示",
"@yearly_report_vehicle_pilot_expand": {},
"yearly_report_account_title": "アカウント統計",
"@yearly_report_account_title": {},
"yearly_report_account_most": "最も使用されたアカウント",
"@yearly_report_account_most": {},
"yearly_report_account_count": "{v0} 回ログイン",
"@yearly_report_account_count": {},
"yearly_report_account_total": "合計 {v0} アカウントを検出",
"@yearly_report_account_total": {},
"yearly_report_account_expand": "すべてのアカウントを表示",
"@yearly_report_account_expand": {},
"yearly_report_duration_hours_minutes": "{v0} 時間 {v1} 分",
"@yearly_report_duration_hours_minutes": {},
"yearly_report_duration_minutes": "{v0} 分",
"@yearly_report_duration_minutes": {},
"yearly_report_session_title": "セッション時間詳細",
"@yearly_report_session_title": {},
"yearly_report_session_average": "平均",
"@yearly_report_session_average": {},
"yearly_report_session_longest": "最長",
"@yearly_report_session_longest": {},
"yearly_report_session_date": "{v0}月{v1}日",
"@yearly_report_session_date": {},
"yearly_report_session_shortest": "最短",
"@yearly_report_session_shortest": {},
"yearly_report_session_note": "(最短は5分以上のセッションのみ集計)",
"@yearly_report_session_note": {},
"yearly_report_month_format": "{v0}月",
"@yearly_report_month_format": {},
"yearly_report_monthly_title": "月間統計",
"@yearly_report_monthly_title": {},
"yearly_report_monthly_most": "最多プレイ",
"@yearly_report_monthly_most": {},
"yearly_report_monthly_most_count": "{v0} 回起動",
"@yearly_report_monthly_most_count": {},
"yearly_report_monthly_least": "最少プレイ",
"@yearly_report_monthly_least": {},
"yearly_report_monthly_least_count": "わずか {v0} 回起動",
"@yearly_report_monthly_least_count": {},
"yearly_report_date_range": "{v0}月{v1}日 - {v2}月{v3}日",
"@yearly_report_date_range": {},
"yearly_report_streak_title": "連続記録",
"@yearly_report_streak_title": {},
"yearly_report_streak_play": "連続プレイ",
"@yearly_report_streak_play": {},
"yearly_report_streak_day_unit": "日",
"@yearly_report_streak_day_unit": {},
"yearly_report_streak_offline": "連続オフライン",
"@yearly_report_streak_offline": {},
"yearly_report_location_title": "場所統計",
"@yearly_report_location_title": {},
"yearly_report_location_no_record": "場所訪問記録なし",
"@yearly_report_location_no_record": {},
"yearly_report_location_frequent": "よく行く場所",
"@yearly_report_location_frequent": {},
"yearly_report_location_note": "インベントリ閲覧記録に基づく",
"@yearly_report_location_note": {},
"yearly_report_thanks_title": "ご愛顧ありがとうございます",
"@yearly_report_thanks_title": {},
"yearly_report_thanks_message": "{year} 年、私たちはStar Citizenで\n数え切れないほどの素晴らしい思い出を作りました",
"@yearly_report_thanks_message": {},
"yearly_report_thanks_next": "{nextYear} 年もよろしくお願いします!",
"@yearly_report_thanks_next": {},
"yearly_report_summary_launch_game": "ゲーム起動",
"@yearly_report_summary_launch_game": {},
"yearly_report_summary_longest_online": "最長オンライン",
"@yearly_report_summary_longest_online": {},
"yearly_report_summary_earliest_time": "最も早い時間",
"@yearly_report_summary_earliest_time": {},
"yearly_report_summary_latest_time": "最も遅い時間",
"@yearly_report_summary_latest_time": {},
"yearly_report_summary_respawn_count": "リスポーン回数",
"@yearly_report_summary_respawn_count": {},
"yearly_report_summary_hottest_month": "最も活発な月",
"@yearly_report_summary_hottest_month": {},
"yearly_report_summary_frequent_location": "頻繁な場所",
"@yearly_report_summary_frequent_location": {},
"yearly_report_summary_favorite_vehicle": "お気に入りの乗り物",
"@yearly_report_summary_favorite_vehicle": {},
"yearly_report_powered_by": "SCToolbox 提供 | github.com/StarCitizenToolBox/app",
"@yearly_report_powered_by": {},
"yearly_report_disclaimer": "データはローカルログから生成され、第三者に送信されることはありません。バージョン間のログの大幅な変更により、データが不完全な場合があります。娯楽目的のみ。",
"@yearly_report_disclaimer": {},
"yearly_report_card_title": "{year} 年間レポート(期間限定)",
"@yearly_report_card_title": {},
"yearly_report_card_desc": "{year}年のStar Citizenプレイ統計を表示します。データはローカルログからのものです。メインのコンピュータで確認してください。",
"yearly_report_web_select_folder": "ゲームフォルダを選択",
"yearly_report_web_select_folder_desc": "Star Citizenゲームフォルダを選択してくださいLIVEディレクトリを含む親フォルダ",
"yearly_report_web_select_year": "年を選択",
"yearly_report_web_generate": "レポートを生成",
"yearly_report_web_reading_files": "ログファイルを読み込み中...",
"yearly_report_web_no_logs_found": "ゲームログが見つかりません",
"yearly_report_web_browser_not_supported": "お使いのブラウザはサポートされていません。Chrome、Edgeまたはその他のChromiumベースのブラウザをご使用ください。",
"yearly_report_menu_title": "レポート"
}

View File

@@ -141,6 +141,12 @@
"@doctor_action_rsi_launcher_log": {},
"doctor_action_game_run_log": "Лог запуска игры",
"@doctor_action_game_run_log": {},
"doctor_action_select_log_file": "Выбрать файл журнала",
"@doctor_action_select_log_file": {},
"doctor_info_web_select_log_file": "Web-платформа: Пожалуйста, выберите файл Game.log вручную для диагностики",
"@doctor_info_web_select_log_file": {},
"doctor_info_log_file_selected": "Файл журнала выбран",
"@doctor_info_log_file_selected": {},
"doctor_info_scan_complete_no_issues": "Сканирование завершено, проблем не обнаружено!",
"@doctor_info_scan_complete_no_issues": {},
"doctor_info_processing": "Обработка...",
@@ -1157,5 +1163,186 @@
"log_analyzer_description": "Анализ ваших игровых записей (логин, смерти, убийства и другая информация)",
"@log_analyzer_description": {},
"log_analyzer_window_title": "SCToolbox: Анализатор логов",
"@log_analyzer_window_title": {}
"@log_analyzer_window_title": {},
"@yearly_report_title": {},
"yearly_report_generating": "Генерация вашего ежегодного отчета...",
"@yearly_report_generating": {},
"yearly_report_analyzing_logs": "Анализ данных игровых журналов",
"@yearly_report_analyzing_logs": {},
"yearly_report_error_title": "Не удалось создать ежегодный отчет",
"@yearly_report_error_title": {},
"yearly_report_error_description": "Пожалуйста, убедитесь, что путь к игре верен и файлы логов существуют",
"@yearly_report_error_description": {},
"yearly_report_nav_prev": "Назад",
"@yearly_report_nav_prev": {},
"yearly_report_nav_next": "Продолжить",
"@yearly_report_nav_next": {},
"yearly_report_welcome_title": "Ежегодный отчет {year}",
"@yearly_report_welcome_title": {},
"yearly_report_welcome_subtitle": "Вспомните свои незабываемые моменты в Star Citizen",
"@yearly_report_welcome_subtitle": {},
"yearly_report_welcome_hint": "Прокрутите вниз или нажмите кнопку ниже, чтобы начать",
"@yearly_report_welcome_hint": {},
"yearly_report_launch_count_title": "Количество запусков игры",
"@yearly_report_launch_count_title": {},
"yearly_report_launch_count_desc": "В этом году вы запускали игру",
"@yearly_report_launch_count_desc": {},
"yearly_report_launch_count_label": "Всего запусков",
"@yearly_report_launch_count_label": {},
"yearly_report_launch_count_value": "{v0} раз",
"@yearly_report_launch_count_value": {},
"yearly_report_play_time_title": "Время игры",
"@yearly_report_play_time_title": {},
"yearly_report_play_time_desc": "В этом году вы исследовали вселенную в течение",
"@yearly_report_play_time_desc": {},
"yearly_report_play_time_unit": "часов",
"@yearly_report_play_time_unit": {},
"yearly_report_play_time_label": "Общее время игры",
"@yearly_report_play_time_label": {},
"yearly_report_play_time_value": "{v0} часов",
"@yearly_report_play_time_value": {},
"yearly_report_crash_title": "Количество сбоев игры",
"@yearly_report_crash_title": {},
"yearly_report_crash_desc": "Нестабильные моменты этого года",
"@yearly_report_crash_desc": {},
"yearly_report_crash_label": "Всего сбоев",
"@yearly_report_crash_label": {},
"yearly_report_crash_note_high": "Надеемся, в следующем году будет стабильнее!",
"@yearly_report_crash_note_high": {},
"yearly_report_crash_note_low": "Вам повезло!",
"@yearly_report_crash_note_low": {},
"yearly_report_kd_title": "Статистика убийств",
"@yearly_report_kd_title": {},
"yearly_report_kd_kill": "Убийства",
"@yearly_report_kd_kill": {},
"yearly_report_kd_death": "Смерти",
"@yearly_report_kd_death": {},
"yearly_report_kd_suicide": "Самоубийства",
"@yearly_report_kd_suicide": {},
"yearly_report_kd_no_record": "В этом году записей об убийствах/смертях не обнаружено",
"@yearly_report_kd_no_record": {},
"yearly_report_no_data": "Нет данных",
"@yearly_report_no_data": {},
"yearly_report_earliest_play_title": "Самая ранняя игровая сессия",
"@yearly_report_earliest_play_title": {},
"yearly_report_earliest_play_desc": "Вы начали свое космическое путешествие на рассвете {v0}/{v1}",
"@yearly_report_earliest_play_desc": {},
"yearly_report_latest_play_title": "Самая поздняя игровая сессия",
"@yearly_report_latest_play_title": {},
"yearly_report_latest_play_desc": "Поздно ночью {v0}/{v1} вы все еще исследовали вселенную",
"@yearly_report_latest_play_desc": {},
"yearly_report_vehicle_destruction_title": "Статистика уничтожения техники",
"@yearly_report_vehicle_destruction_title": {},
"yearly_report_vehicle_destruction_desc": "В этом году вы уничтожили",
"@yearly_report_vehicle_destruction_desc": {},
"yearly_report_vehicle_destruction_unit": "кораблей",
"@yearly_report_vehicle_destruction_unit": {},
"yearly_report_vehicle_destruction_most": "Самый уничтожаемый корабль",
"@yearly_report_vehicle_destruction_most": {},
"yearly_report_vehicle_destruction_count": "Уничтожено {v0} раз",
"@yearly_report_vehicle_destruction_count": {},
"yearly_report_vehicle_pilot_title": "Статистика пилотирования",
"@yearly_report_vehicle_pilot_title": {},
"yearly_report_vehicle_pilot_most": "Самый пилотируемый транспорт",
"@yearly_report_vehicle_pilot_most": {},
"yearly_report_vehicle_pilot_count": "Пилотировался {v0} раз",
"@yearly_report_vehicle_pilot_count": {},
"yearly_report_vehicle_pilot_collapse": "Свернуть детали",
"@yearly_report_vehicle_pilot_collapse": {},
"yearly_report_vehicle_pilot_expand": "Показать все {v0} тс",
"@yearly_report_vehicle_pilot_expand": {},
"yearly_report_account_title": "Статистика аккаунта",
"@yearly_report_account_title": {},
"yearly_report_account_most": "Самый используемый аккаунт",
"@yearly_report_account_most": {},
"yearly_report_account_count": "Вход выполнен {v0} раз",
"@yearly_report_account_count": {},
"yearly_report_account_total": "Всего обнаружено {v0} аккаунтов",
"@yearly_report_account_total": {},
"yearly_report_account_expand": "Показать все аккаунты",
"@yearly_report_account_expand": {},
"yearly_report_duration_hours_minutes": "{v0} ч {v1} мин",
"@yearly_report_duration_hours_minutes": {},
"yearly_report_duration_minutes": "{v0} мин",
"@yearly_report_duration_minutes": {},
"yearly_report_session_title": "Детали времени сессии",
"@yearly_report_session_title": {},
"yearly_report_session_average": "Среднее",
"@yearly_report_session_average": {},
"yearly_report_session_longest": "Самое долгое",
"@yearly_report_session_longest": {},
"yearly_report_session_date": "{v0}/{v1}",
"@yearly_report_session_date": {},
"yearly_report_session_shortest": "Самое короткое",
"@yearly_report_session_shortest": {},
"yearly_report_session_note": "(Учитываются только сессии более 5 минут)",
"@yearly_report_session_note": {},
"yearly_report_month_format": "Месяц {v0}",
"@yearly_report_month_format": {},
"yearly_report_monthly_title": "Ежемесячная статистика",
"@yearly_report_monthly_title": {},
"yearly_report_monthly_most": "Больше всего игр",
"@yearly_report_monthly_most": {},
"yearly_report_monthly_most_count": "Запущено {v0} раз",
"@yearly_report_monthly_most_count": {},
"yearly_report_monthly_least": "Меньше всего игр",
"@yearly_report_monthly_least": {},
"yearly_report_monthly_least_count": "Запущено только {v0} раз",
"@yearly_report_monthly_least_count": {},
"yearly_report_date_range": "{v0}/{v1} - {v2}/{v3}",
"@yearly_report_date_range": {},
"yearly_report_streak_title": "Рекорды серий",
"@yearly_report_streak_title": {},
"yearly_report_streak_play": "Серия игр",
"@yearly_report_streak_play": {},
"yearly_report_streak_day_unit": "дн.",
"@yearly_report_streak_day_unit": {},
"yearly_report_streak_offline": "Серия оффлайна",
"@yearly_report_streak_offline": {},
"yearly_report_location_title": "Статистика локаций",
"@yearly_report_location_title": {},
"yearly_report_location_no_record": "Нет записей о посещении локаций",
"@yearly_report_location_no_record": {},
"yearly_report_location_frequent": "Частые локации",
"@yearly_report_location_frequent": {},
"yearly_report_location_note": "Основано на записях просмотра инвентаря",
"@yearly_report_location_note": {},
"yearly_report_thanks_title": "Спасибо, что вы с нами",
"@yearly_report_thanks_title": {},
"yearly_report_thanks_message": "В {year} году мы вместе создали\nбесчисленное количество прекрасных воспоминаний в Star Citizen",
"@yearly_report_thanks_message": {},
"yearly_report_thanks_next": "Ждем встречи с вами в {nextYear} году!",
"@yearly_report_thanks_next": {},
"yearly_report_summary_launch_game": "Запуск игры",
"@yearly_report_summary_launch_game": {},
"yearly_report_summary_longest_online": "Дольше всего онлайн",
"@yearly_report_summary_longest_online": {},
"yearly_report_summary_earliest_time": "Самое раннее время",
"@yearly_report_summary_earliest_time": {},
"yearly_report_summary_latest_time": "Самое позднее время",
"@yearly_report_summary_latest_time": {},
"yearly_report_summary_respawn_count": "Количество возрождений",
"@yearly_report_summary_respawn_count": {},
"yearly_report_summary_hottest_month": "Самый жаркий месяц",
"@yearly_report_summary_hottest_month": {},
"yearly_report_summary_frequent_location": "Частая локация",
"@yearly_report_summary_frequent_location": {},
"yearly_report_summary_favorite_vehicle": "Любимый транспорт",
"@yearly_report_summary_favorite_vehicle": {},
"yearly_report_powered_by": "Представлено SCToolbox | github.com/StarCitizenToolBox/app",
"@yearly_report_powered_by": {},
"yearly_report_disclaimer": "Данные генерируются из ваших локальных логов и не отправляются третьим лицам. Из-за значительных изменений логов в разных версиях данные могут быть неполными. Только для развлечения.",
"@yearly_report_disclaimer": {},
"yearly_report_card_title": "Ежегодный отчет {year} (Ограниченное время)",
"@yearly_report_card_title": {},
"yearly_report_card_desc": "Посмотрите статистику вашей игры в Star Citizen за {year} год. Данные из локальных логов, пожалуйста, проверяйте на основном компьютере.",
"@yearly_report_card_desc": {},
"yearly_report_web_select_folder": "Выберите папку игры",
"yearly_report_web_select_folder_desc": "Выберите папку игры Star Citizen (родительская папка, содержащая директорию LIVE)",
"yearly_report_web_select_year": "Выберите год",
"yearly_report_web_generate": "Создать отчет",
"yearly_report_web_reading_files": "Чтение файлов логов...",
"yearly_report_web_no_logs_found": "Игровые логи не найдены",
"yearly_report_web_browser_not_supported": "Ваш браузер не поддерживается. Используйте Chrome, Edge или другой браузер на базе Chromium.",
"yearly_report_menu_title": "Отчет"
}

View File

@@ -140,6 +140,12 @@
"@doctor_action_rsi_launcher_log": {},
"doctor_action_game_run_log": "游戏运行log",
"@doctor_action_game_run_log": {},
"doctor_action_select_log_file": "选择 log 文件",
"@doctor_action_select_log_file": {},
"doctor_info_web_select_log_file": "Web 平台请手动选择 Game.log 文件进行诊断",
"@doctor_info_web_select_log_file": {},
"doctor_info_log_file_selected": "已选择日志文件",
"@doctor_info_log_file_selected": {},
"doctor_info_scan_complete_no_issues": "扫描完毕,没有找到问题!",
"@doctor_info_scan_complete_no_issues": {},
"doctor_info_processing": "正在处理...",
@@ -904,5 +910,116 @@
"tools_vehicle_sorting_info": "将左侧载具拖动到右侧列表中,这将会为载具名称增加 001、002 .. 等前缀,方便您在游戏内 UI 快速定位载具。在右侧列表上下拖动可以调整载具的顺序。",
"tools_vehicle_sorting_vehicle": "载具",
"tools_vehicle_sorting_search": "搜索载具",
"tools_vehicle_sorting_sorted": "已排序载具"
"tools_vehicle_sorting_sorted": "已排序载具",
"yearly_report_title": "星际公民 {year} 年度报告",
"yearly_report_generating": "正在生成您的年度报告...",
"yearly_report_analyzing_logs": "正在分析游戏日志数据",
"yearly_report_error_title": "无法生成年度报告",
"yearly_report_error_description": "请确保游戏目录正确且存在日志文件",
"yearly_report_nav_prev": "上一页",
"yearly_report_nav_next": "继续查看",
"yearly_report_welcome_title": "{year} 年度报告",
"yearly_report_welcome_subtitle": "回顾您在星际公民中的精彩时刻",
"yearly_report_welcome_hint": "向下滚动或点击下方按钮开始",
"yearly_report_launch_count_title": "游戏启动次数",
"yearly_report_launch_count_desc": "今年您启动了游戏",
"yearly_report_launch_count_label": "累计启动",
"yearly_report_launch_count_value": "{v0} 次",
"@yearly_report_launch_count_value": {},
"yearly_report_play_time_title": "游玩时长",
"yearly_report_play_time_desc": "今年您在宇宙中遨游了",
"yearly_report_play_time_unit": "小时",
"yearly_report_play_time_label": "累计游玩",
"yearly_report_play_time_value": "{v0} 小时",
"@yearly_report_play_time_value": {},
"yearly_report_crash_title": "游戏崩溃次数",
"yearly_report_crash_desc": "今年游戏不太稳定的时刻",
"yearly_report_crash_label": "累计崩溃",
"yearly_report_crash_note_high": "希望明年能更稳定!",
"yearly_report_crash_note_low": "运气不错!",
"yearly_report_kd_title": "击杀统计",
"yearly_report_kd_kill": "击杀",
"yearly_report_kd_death": "死亡",
"yearly_report_kd_suicide": "自杀",
"yearly_report_kd_no_record": "今年没有检测到击杀/死亡记录",
"yearly_report_no_data": "暂无数据",
"yearly_report_earliest_play_title": "最早的一次游玩",
"yearly_report_earliest_play_desc": "您在清晨 {v0} 月 {v1} 日开始了星际之旅",
"@yearly_report_earliest_play_desc": {},
"yearly_report_latest_play_title": "最晚的一次游玩",
"yearly_report_latest_play_desc": "深夜 {v0} 月 {v1} 日还在探索宇宙",
"@yearly_report_latest_play_desc": {},
"yearly_report_vehicle_destruction_title": "载具损毁统计",
"yearly_report_vehicle_destruction_desc": "今年您共炸了",
"yearly_report_vehicle_destruction_unit": "艘船",
"yearly_report_vehicle_destruction_most": "炸的最多的船",
"yearly_report_vehicle_destruction_count": "炸了 {v0} 次",
"@yearly_report_vehicle_destruction_count": {},
"yearly_report_vehicle_pilot_title": "载具驾驶统计",
"yearly_report_vehicle_pilot_most": "最常驾驶的载具",
"yearly_report_vehicle_pilot_count": "驾驶了 {v0} 次",
"@yearly_report_vehicle_pilot_count": {},
"yearly_report_vehicle_pilot_collapse": "收起详情",
"yearly_report_vehicle_pilot_expand": "查看全部 {v0} 个载具",
"@yearly_report_vehicle_pilot_expand": {},
"yearly_report_account_title": "账号统计",
"yearly_report_account_most": "最常使用的账号",
"yearly_report_account_count": "登录了 {v0} 次",
"@yearly_report_account_count": {},
"yearly_report_account_total": "共检测到 {v0} 个账号",
"@yearly_report_account_total": {},
"yearly_report_account_expand": "查看全部账号",
"yearly_report_duration_hours_minutes": "{v0} 小时 {v1} 分钟",
"@yearly_report_duration_hours_minutes": {},
"yearly_report_duration_minutes": "{v0} 分钟",
"@yearly_report_duration_minutes": {},
"yearly_report_session_title": "游玩时长详情",
"yearly_report_session_average": "平均",
"yearly_report_session_longest": "最长",
"yearly_report_session_date": "{v0}月{v1}日",
"@yearly_report_session_date": {},
"yearly_report_session_shortest": "最短",
"yearly_report_session_note": "(最短仅统计超过 5 分钟的游戏)",
"yearly_report_month_format": "{v0}月",
"@yearly_report_month_format": {},
"yearly_report_monthly_title": "月份统计",
"yearly_report_monthly_most": "游玩最多",
"yearly_report_monthly_most_count": "启动了 {v0} 次",
"@yearly_report_monthly_most_count": {},
"yearly_report_monthly_least": "游玩最少",
"yearly_report_monthly_least_count": "仅启动 {v0} 次",
"@yearly_report_monthly_least_count": {},
"yearly_report_date_range": "{v0}月{v1}日 - {v2}月{v3}日",
"@yearly_report_date_range": {},
"yearly_report_streak_title": "连续记录",
"yearly_report_streak_play": "连续游玩",
"yearly_report_streak_day_unit": "天",
"yearly_report_streak_offline": "连续离线",
"yearly_report_location_title": "地点统计",
"yearly_report_location_no_record": "暂无地点访问记录",
"yearly_report_location_frequent": "常去的地点",
"yearly_report_location_note": "基于库存查看记录统计",
"yearly_report_thanks_title": "感谢您的陪伴",
"yearly_report_thanks_message": "{year} 年,我们一起在星际公民中\n创造了无数精彩回忆",
"yearly_report_thanks_next": "期待 {nextYear} 年继续与您相伴!",
"yearly_report_summary_launch_game": "启动游戏",
"yearly_report_summary_longest_online": "最长在线",
"yearly_report_summary_earliest_time": "最早时刻",
"yearly_report_summary_latest_time": "最晚时刻",
"yearly_report_summary_respawn_count": "重开次数",
"yearly_report_summary_hottest_month": "最热月",
"yearly_report_summary_frequent_location": "常去位置",
"yearly_report_summary_favorite_vehicle": "最爱载具",
"yearly_report_powered_by": "由 SC 汉化盒子为您呈现 | github.com/StarCitizenToolBox/app",
"yearly_report_disclaimer": "数据使用您的本地日志生成,不会发送到任何第三方。因跨版本 Log 改动较大,数据可能不完整,仅供娱乐。",
"yearly_report_card_title": "{year} 年度报告(限时)",
"yearly_report_card_desc": "查看您在{year}年的星际公民游玩统计,数据来自本地 log ,请确保在常用电脑上查看。",
"yearly_report_web_select_folder": "选择游戏文件夹",
"yearly_report_web_select_folder_desc": "请选择您的星际公民游戏文件夹(包含 LIVE 目录的父文件夹)",
"yearly_report_web_select_year": "选择年份",
"yearly_report_web_generate": "生成报告",
"yearly_report_web_reading_files": "正在读取日志文件...",
"yearly_report_web_no_logs_found": "未找到游戏日志",
"yearly_report_web_browser_not_supported": "您的浏览器不受支持,请使用 Chrome、Edge 或其他基于 Chromium 的浏览器。",
"yearly_report_menu_title": "报告"
}

View File

@@ -140,6 +140,12 @@
"@doctor_action_rsi_launcher_log": {},
"doctor_action_game_run_log": "遊戲執行log",
"@doctor_action_game_run_log": {},
"doctor_action_select_log_file": "選擇 log 檔案",
"@doctor_action_select_log_file": {},
"doctor_info_web_select_log_file": "Web 平台請手動選擇 Game.log 檔案進行診斷",
"@doctor_info_web_select_log_file": {},
"doctor_info_log_file_selected": "已選擇日誌檔案",
"@doctor_info_log_file_selected": {},
"doctor_info_scan_complete_no_issues": "掃描完畢,沒有找到問題!",
"@doctor_info_scan_complete_no_issues": {},
"doctor_info_processing": "正在處理...",
@@ -905,5 +911,100 @@
"tools_vehicle_sorting_info": "將左側載具拖動到右側列表中,這將會為載具名稱增加 001、002 .. 等前綴,方便您在遊戲內 UI 快速定位載具。在右側列表上下拖動可以調整載具的順序。",
"tools_vehicle_sorting_vehicle": "載具",
"tools_vehicle_sorting_search": "搜索載具",
"tools_vehicle_sorting_sorted": "已排序載具"
"tools_vehicle_sorting_sorted": "已排序載具",
"yearly_report_title": "星際公民 {year} 年度報告",
"yearly_report_generating": "正在生成您的年度報告...",
"yearly_report_analyzing_logs": "正在分析遊戲日誌數據",
"yearly_report_error_title": "無法生成年度報告",
"yearly_report_error_description": "請確保遊戲目錄正確且存在日誌文件",
"yearly_report_nav_prev": "上一頁",
"yearly_report_nav_next": "繼續查看",
"yearly_report_welcome_title": "{year} 年度報告",
"yearly_report_welcome_subtitle": "回顧您在星際公民中的精彩時刻",
"yearly_report_welcome_hint": "向下滾動或點擊下方按鈕開始",
"yearly_report_launch_count_title": "遊戲啟動次數",
"yearly_report_launch_count_desc": "今年您啟動了遊戲",
"yearly_report_launch_count_label": "累計啟動",
"yearly_report_launch_count_value": "{v0} 次",
"yearly_report_play_time_title": "遊玩時長",
"yearly_report_play_time_desc": "今年您在宇宙中遨遊了",
"yearly_report_play_time_unit": "小時",
"yearly_report_play_time_label": "累計遊玩",
"yearly_report_play_time_value": "{v0} 小時",
"yearly_report_crash_title": "遊戲崩潰次數",
"yearly_report_crash_desc": "今年遊戲不太穩定的時刻",
"yearly_report_crash_label": "累計崩潰",
"yearly_report_crash_note_high": "希望明年能更穩定!",
"yearly_report_crash_note_low": "運氣不錯!",
"yearly_report_kd_title": "擊殺統計",
"yearly_report_kd_kill": "擊殺",
"yearly_report_kd_death": "死亡",
"yearly_report_kd_suicide": "自殺",
"yearly_report_kd_no_record": "今年沒有檢測到擊殺/死亡記錄",
"yearly_report_no_data": "暫無數據",
"yearly_report_earliest_play_title": "最早的一次遊玩",
"yearly_report_earliest_play_desc": "您在清晨 {v0} 月 {v1} 日開始了星際之旅",
"yearly_report_latest_play_title": "最晚的一次遊玩",
"yearly_report_latest_play_desc": "深夜 {v0} 月 {v1} 日還在探索宇宙",
"yearly_report_vehicle_destruction_title": "載具損毀統計",
"yearly_report_vehicle_destruction_desc": "今年您共炸了",
"yearly_report_vehicle_destruction_unit": "艘船",
"yearly_report_vehicle_destruction_most": "炸的最多的船",
"yearly_report_vehicle_destruction_count": "炸了 {v0} 次",
"yearly_report_vehicle_pilot_title": "載具駕駛統計",
"yearly_report_vehicle_pilot_most": "最常駕駛的載具",
"yearly_report_vehicle_pilot_count": "駕駛了 {v0} 次",
"yearly_report_vehicle_pilot_collapse": "收起詳情",
"yearly_report_vehicle_pilot_expand": "查看全部 {v0} 個載具",
"yearly_report_account_title": "帳號統計",
"yearly_report_account_most": "最常使用的帳號",
"yearly_report_account_count": "登入了 {v0} 次",
"yearly_report_account_total": "共檢測到 {v0} 個帳號",
"yearly_report_account_expand": "查看全部帳號",
"yearly_report_duration_hours_minutes": "{v0} 小時 {v1} 分鐘",
"yearly_report_duration_minutes": "{v0} 分鐘",
"yearly_report_session_title": "遊玩時長詳情",
"yearly_report_session_average": "平均",
"yearly_report_session_longest": "最長",
"yearly_report_session_date": "{v0}月{v1}日",
"yearly_report_session_shortest": "最短",
"yearly_report_session_note": "(最短僅統計超過 5 分鐘的遊戲)",
"yearly_report_month_format": "{v0}月",
"yearly_report_monthly_title": "月份統計",
"yearly_report_monthly_most": "遊玩最多",
"yearly_report_monthly_most_count": "啟動了 {v0} 次",
"yearly_report_monthly_least": "遊玩最少",
"yearly_report_monthly_least_count": "僅啟動 {v0} 次",
"yearly_report_date_range": "{v0}月{v1}日 - {v2}月{v3}日",
"yearly_report_streak_title": "連續記錄",
"yearly_report_streak_play": "連續遊玩",
"yearly_report_streak_day_unit": "天",
"yearly_report_streak_offline": "連續離線",
"yearly_report_location_title": "地點統計",
"yearly_report_location_no_record": "暫無地點訪問記錄",
"yearly_report_location_frequent": "常去的地點",
"yearly_report_location_note": "基於庫存查看記錄統計",
"yearly_report_thanks_title": "感謝您的陪伴",
"yearly_report_thanks_message": "{year} 年,我們一起在星際公民中\n創造了無數精彩回憶",
"yearly_report_thanks_next": "期待 {nextYear} 年繼續與您相伴!",
"yearly_report_summary_launch_game": "啟動遊戲",
"yearly_report_summary_longest_online": "最長在線",
"yearly_report_summary_earliest_time": "最早時刻",
"yearly_report_summary_latest_time": "最晚時刻",
"yearly_report_summary_respawn_count": "重開次數",
"yearly_report_summary_hottest_month": "最熱月",
"yearly_report_summary_frequent_location": "常去位置",
"yearly_report_summary_favorite_vehicle": "最愛載具",
"yearly_report_powered_by": "由 SC工具箱為您呈現 | github.com/StarCitizenToolBox/app",
"yearly_report_disclaimer": "數據使用您的本地日誌生成,不會發送到任何第三方。因跨版本 Log 改動較大,數據可能不完整,僅供娛樂。",
"yearly_report_card_title": "{year} 年度報告(限時)",
"yearly_report_card_desc": "查看您在{year}年的星際公民遊玩統計,數據來自本地 log ,請確保在常用電腦上查看。",
"yearly_report_web_select_folder": "選擇遊戲資料夾",
"yearly_report_web_select_folder_desc": "請選擇您的星際公民遊戲資料夾(包含 LIVE 目錄的父資料夾)",
"yearly_report_web_select_year": "選擇年份",
"yearly_report_web_generate": "生成報告",
"yearly_report_web_reading_files": "正在讀取記錄檔...",
"yearly_report_web_no_logs_found": "未找到遊戲記錄檔",
"yearly_report_web_browser_not_supported": "您的瀏覽器不受支援,請使用 Chrome、Edge 或其他基於 Chromium 的瀏覽器。",
"yearly_report_menu_title": "報告"
}

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/generated/l10n.dart';
@@ -12,9 +13,20 @@ import 'app.dart';
import 'common/utils/multi_window_manager.dart';
void main(List<String> args) async {
if (kIsWeb) {
// Web 版本直接运行
WidgetsFlutterBinding.ensureInitialized();
runApp(const ProviderScope(child: App()));
return;
}
// Desktop 版本
// webview window
if (runWebViewTitleBarWidget(args,
backgroundColor: const Color.fromRGBO(19, 36, 49, 1), builder: _defaultWebviewTitleBar)) {
if (runWebViewTitleBarWidget(
args,
backgroundColor: const Color.fromRGBO(19, 36, 49, 1),
builder: _defaultWebviewTitleBar,
)) {
return;
}
if (args.firstOrNull == 'multi_window') {
@@ -29,10 +41,7 @@ void main(List<String> args) async {
Future<void> _initWindow() async {
await windowManager.ensureInitialized();
await windowManager.setTitleBarStyle(
TitleBarStyle.hidden,
windowButtonVisibility: false,
);
await windowManager.setTitleBarStyle(TitleBarStyle.hidden, windowButtonVisibility: false);
await windowManager.setSize(const Size(1280, 810));
await windowManager.setMinimumSize(const Size(1280, 810));
await windowManager.center(animate: true);
@@ -47,10 +56,14 @@ class App extends HookConsumerWidget with WindowListener {
final appState = ref.watch(appGlobalModelProvider);
useEffect(() {
windowManager.addListener(this);
windowManager.setPreventClose(true);
if (!kIsWeb) {
windowManager.addListener(this);
windowManager.setPreventClose(true);
}
return () async {
windowManager.removeListener(this);
if (!kIsWeb) {
windowManager.removeListener(this);
}
};
}, const []);
@@ -73,18 +86,22 @@ class App extends HookConsumerWidget with WindowListener {
);
},
theme: FluentThemeData(
brightness: Brightness.dark,
fontFamily: "SourceHanSansCN-Regular",
navigationPaneTheme: NavigationPaneThemeData(
backgroundColor: appState.themeConf.backgroundColor,
brightness: Brightness.dark,
fontFamily: "SourceHanSansCN-Regular",
navigationPaneTheme: NavigationPaneThemeData(backgroundColor: appState.themeConf.backgroundColor),
menuColor: appState.themeConf.menuColor,
micaBackgroundColor: appState.themeConf.micaColor,
buttonTheme: ButtonThemeData(
defaultButtonStyle: ButtonStyle(
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(color: Colors.white.withValues(alpha: .01)),
),
),
),
menuColor: appState.themeConf.menuColor,
micaBackgroundColor: appState.themeConf.micaColor,
buttonTheme: ButtonThemeData(
defaultButtonStyle: ButtonStyle(
shape: WidgetStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4), side: BorderSide(color: Colors.white.withValues(alpha: .01)))),
))),
),
),
locale: appState.appLocale,
debugShowCheckedModeBanner: false,
routeInformationParser: router.routeInformationParser,
@@ -96,7 +113,7 @@ class App extends HookConsumerWidget with WindowListener {
@override
Future<void> onWindowClose() async {
debugPrint("onWindowClose");
if (await windowManager.isPreventClose()) {
if (!kIsWeb && await windowManager.isPreventClose()) {
final windows = await DesktopMultiWindow.getAllSubWindowIds();
for (final id in windows) {
await WindowController.fromWindowId(id).close();
@@ -112,42 +129,28 @@ Widget _defaultWebviewTitleBar(BuildContext context) {
final state = TitleBarWebViewState.of(context);
final controller = TitleBarWebViewController.of(context);
return FluentTheme(
data: FluentThemeData.dark(),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (Platform.isMacOS) const SizedBox(width: 96),
IconButton(
onPressed: !state.canGoBack ? null : controller.back,
icon: const Icon(FluentIcons.chevron_left),
),
const SizedBox(width: 12),
IconButton(
onPressed: !state.canGoForward ? null : controller.forward,
icon: const Icon(FluentIcons.chevron_right),
),
const SizedBox(width: 12),
if (state.isLoading)
IconButton(
onPressed: controller.stop,
icon: const Icon(FluentIcons.chrome_close),
)
else
IconButton(
onPressed: controller.reload,
icon: const Icon(FluentIcons.refresh),
),
const SizedBox(width: 12),
(state.isLoading)
? const SizedBox(
width: 24,
height: 24,
child: ProgressRing(),
)
: const SizedBox(width: 24),
const SizedBox(width: 12),
SelectableText(state.url ?? ""),
const Spacer()
],
));
data: FluentThemeData.dark(),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (Platform.isMacOS) const SizedBox(width: 96),
IconButton(onPressed: !state.canGoBack ? null : controller.back, icon: const Icon(FluentIcons.chevron_left)),
const SizedBox(width: 12),
IconButton(
onPressed: !state.canGoForward ? null : controller.forward,
icon: const Icon(FluentIcons.chevron_right),
),
const SizedBox(width: 12),
if (state.isLoading)
IconButton(onPressed: controller.stop, icon: const Icon(FluentIcons.chrome_close))
else
IconButton(onPressed: controller.reload, icon: const Icon(FluentIcons.refresh)),
const SizedBox(width: 12),
(state.isLoading) ? const SizedBox(width: 24, height: 24, child: ProgressRing()) : const SizedBox(width: 24),
const SizedBox(width: 12),
SelectableText(state.url ?? ""),
const Spacer(),
],
),
);
}

View File

@@ -5,11 +5,8 @@ import 'dart:io';
import 'dart:math';
import 'package:aria2/aria2.dart';
import 'package:flutter/foundation.dart';
import 'package:hive_ce/hive.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/rust/api/rs_process.dart'
as rs_process;
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart';
@@ -20,11 +17,8 @@ part 'aria2c.freezed.dart';
@freezed
abstract class Aria2cModelState with _$Aria2cModelState {
const factory Aria2cModelState({
required String aria2cDir,
Aria2c? aria2c,
Aria2GlobalStat? aria2globalStat,
}) = _Aria2cModelState;
const factory Aria2cModelState({required String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat}) =
_Aria2cModelState;
}
extension Aria2cModelExt on Aria2cModelState {
@@ -32,24 +26,17 @@ extension Aria2cModelExt on Aria2cModelState {
bool get hasDownloadTask => aria2globalStat != null && aria2TotalTaskNum > 0;
int get aria2TotalTaskNum => aria2globalStat == null
? 0
: ((aria2globalStat!.numActive ?? 0) +
(aria2globalStat!.numWaiting ?? 0));
int get aria2TotalTaskNum =>
aria2globalStat == null ? 0 : ((aria2globalStat!.numActive ?? 0) + (aria2globalStat!.numWaiting ?? 0));
}
@riverpod
class Aria2cModel extends _$Aria2cModel {
bool _disposed = false;
@override
Aria2cModelState build() {
if (appGlobalState.applicationBinaryModuleDir == null) {
throw Exception("applicationBinaryModuleDir is null");
}
ref.onDispose(() {
_disposed = true;
});
ref.keepAlive();
final aria2cDir = "${appGlobalState.applicationBinaryModuleDir}\\aria2c";
// LazyLoad init
@@ -57,8 +44,7 @@ class Aria2cModel extends _$Aria2cModel {
try {
final sessionFile = File("$aria2cDir\\aria2.session");
// 有下载任务则第一时间初始化
if (await sessionFile.exists() &&
(await sessionFile.readAsString()).trim().isNotEmpty) {
if (await sessionFile.exists() && (await sessionFile.readAsString()).trim().isNotEmpty) {
dPrint("launch Aria2c daemon");
await launchDaemon(appGlobalState.applicationBinaryModuleDir!);
} else {
@@ -74,8 +60,7 @@ class Aria2cModel extends _$Aria2cModel {
Future launchDaemon(String applicationBinaryModuleDir) async {
if (state.aria2c != null) return;
await BinaryModuleConf.extractModule(
["aria2c"], applicationBinaryModuleDir);
await BinaryModuleConf.extractModule(["aria2c"], applicationBinaryModuleDir);
/// skip for debug hot reload
if (kDebugMode) {
@@ -90,7 +75,7 @@ class Aria2cModel extends _$Aria2cModel {
await sessionFile.create(recursive: true);
}
final exePath = "${state.aria2cDir}\\aria2c.exe";
// final exePath = "${state.aria2cDir}\\aria2c.exe";
final port = await getFreePort();
final pwd = generateRandomPassword(16);
dPrint("pwd === $pwd");
@@ -98,53 +83,53 @@ class Aria2cModel extends _$Aria2cModel {
dPrint("trackerList === $trackerList");
dPrint("Aria2cManager .----- aria2c start $port------");
final stream = rs_process.start(
executable: exePath,
arguments: [
"-V",
"-c",
"-x 16",
"--dir=${state.aria2cDir}\\downloads",
"--disable-ipv6",
"--enable-rpc",
"--pause",
"--rpc-listen-port=$port",
"--rpc-secret=$pwd",
"--input-file=${sessionFile.absolute.path.trim()}",
"--save-session=${sessionFile.absolute.path.trim()}",
"--save-session-interval=60",
"--file-allocation=trunc",
"--seed-time=0",
],
workingDirectory: state.aria2cDir);
// final stream = rs_process.start(
// executable: exePath,
// arguments: [
// "-V",
// "-c",
// "-x 16",
// "--dir=${state.aria2cDir}\\downloads",
// "--disable-ipv6",
// "--enable-rpc",
// "--pause",
// "--rpc-listen-port=$port",
// "--rpc-secret=$pwd",
// "--input-file=${sessionFile.absolute.path.trim()}",
// "--save-session=${sessionFile.absolute.path.trim()}",
// "--save-session-interval=60",
// "--file-allocation=trunc",
// "--seed-time=0",
// ],
// workingDirectory: state.aria2cDir,
// );
String launchError = "";
// String launchError = "";
stream.listen((event) {
dPrint(
"Aria2cManager.rs_process event === [${event.rsPid}] ${event.dataType} >> ${event.data}");
switch (event.dataType) {
case rs_process.RsProcessStreamDataType.output:
if (event.data.contains("IPv4 RPC: listening on TCP port")) {
_onLaunch(port, pwd, trackerList);
}
break;
case rs_process.RsProcessStreamDataType.error:
launchError = event.data;
state = state.copyWith(aria2c: null);
break;
case rs_process.RsProcessStreamDataType.exit:
launchError = event.data;
state = state.copyWith(aria2c: null);
break;
}
});
// stream.listen((event) {
// dPrint("Aria2cManager.rs_process event === [${event.rsPid}] ${event.dataType} >> ${event.data}");
// switch (event.dataType) {
// case rs_process.RsProcessStreamDataType.output:
// if (event.data.contains("IPv4 RPC: listening on TCP port")) {
// _onLaunch(port, pwd, trackerList);
// }
// break;
// case rs_process.RsProcessStreamDataType.error:
// launchError = event.data;
// state = state.copyWith(aria2c: null);
// break;
// case rs_process.RsProcessStreamDataType.exit:
// launchError = event.data;
// state = state.copyWith(aria2c: null);
// break;
// }
// });
while (true) {
if (state.aria2c != null) return;
if (launchError.isNotEmpty) throw launchError;
await Future.delayed(const Duration(milliseconds: 100));
}
// while (true) {
// if (state.aria2c != null) return;
// if (launchError.isNotEmpty) throw launchError;
// await Future.delayed(const Duration(milliseconds: 100));
// }
}
Future<int> getFreePort() async {
@@ -155,8 +140,7 @@ class Aria2cModel extends _$Aria2cModel {
}
String generateRandomPassword(int length) {
const String charset =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const String charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = Random();
StringBuffer buffer = StringBuffer();
for (int i = 0; i < length; i++) {
@@ -182,36 +166,37 @@ class Aria2cModel extends _$Aria2cModel {
return 0;
}
Future<void> _onLaunch(int port, String pwd, String trackerList) async {
final aria2c = Aria2c("ws://127.0.0.1:$port/jsonrpc", "websocket", pwd);
state = state.copyWith(aria2c: aria2c);
aria2c.getVersion().then((value) {
dPrint("Aria2cManager.connected! version == ${value.version}");
_listenState(aria2c);
});
final box = await Hive.openBox("app_conf");
aria2c.changeGlobalOption(Aria2Option()
..maxOverallUploadLimit =
textToByte(box.get("downloader_up_limit", defaultValue: "0"))
..maxOverallDownloadLimit =
textToByte(box.get("downloader_down_limit", defaultValue: "0"))
..btTracker = trackerList);
}
// Future<void> _onLaunch(int port, String pwd, String trackerList) async {
// final aria2c = Aria2c("ws://127.0.0.1:$port/jsonrpc", "websocket", pwd);
// state = state.copyWith(aria2c: aria2c);
// aria2c.getVersion().then((value) {
// dPrint("Aria2cManager.connected! version == ${value.version}");
// _listenState(aria2c);
// });
// final box = await Hive.openBox("app_conf");
// aria2c.changeGlobalOption(
// Aria2Option()
// ..maxOverallUploadLimit = textToByte(box.get("downloader_up_limit", defaultValue: "0"))
// ..maxOverallDownloadLimit = textToByte(box.get("downloader_down_limit", defaultValue: "0"))
// ..btTracker = trackerList,
// );
// }
Future<void> _listenState(Aria2c aria2c) async {
dPrint("Aria2cModel._listenState start");
while (true) {
if (_disposed || state.aria2c == null) {
dPrint("Aria2cModel._listenState end");
return;
}
try {
final aria2globalStat = await aria2c.getGlobalStat();
state = state.copyWith(aria2globalStat: aria2globalStat);
} catch (e) {
dPrint("aria2globalStat update error:$e");
}
await Future.delayed(const Duration(seconds: 1));
}
}
// Future<void> _listenState(Aria2c aria2c) async {
// dPrint("Aria2cModel._listenState start");
// while (true) {
// if (_disposed || state.aria2c == null) {
// dPrint("Aria2cModel._listenState end");
// return;
// }
// try {
// final aria2globalStat = await aria2c.getGlobalStat();
// state = state.copyWith(aria2globalStat: aria2globalStat);
// } catch (e) {
// dPrint("aria2globalStat update error:$e");
// }
// await Future.delayed(const Duration(seconds: 1));
// }
// }
// }
}

View File

@@ -41,7 +41,7 @@ final class Aria2cModelProvider
}
}
String _$aria2cModelHash() => r'3d51aeefd92e5291dca1f01db961f9c5496ec24f';
String _$aria2cModelHash() => r'6fe18708151958dc19a01782f053f3f581fd7913';
abstract class _$Aria2cModel extends $Notifier<Aria2cModelState> {
Aria2cModelState build();

View File

@@ -9,13 +9,10 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/common/conf/binary_conf.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/rust/api/rs_process.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart';
import 'package:starcitizen_doctor/data/app_unp4k_p4k_item_data.dart';
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
import 'package:starcitizen_doctor/common/rust/api/rs_process.dart'
as rs_process;
part 'unp4kc.freezed.dart';
@@ -40,10 +37,7 @@ class Unp4kCModel extends _$Unp4kCModel {
@override
Unp4kcState build() {
state = Unp4kcState(
startUp: false,
curPath: '\\',
endMessage: S.current.tools_unp4k_msg_init);
state = Unp4kcState(startUp: false, curPath: '\\', endMessage: S.current.tools_unp4k_msg_init);
_init();
return state;
}
@@ -52,48 +46,45 @@ class Unp4kCModel extends _$Unp4kCModel {
String getGamePath() => _toolsState.scInstalledPath;
bool _hasUnp4kRunTimeError = false;
final bool _hasUnp4kRunTimeError = false;
void _init() async {
final execDir = "${appGlobalState.applicationBinaryModuleDir}\\unp4kc";
await BinaryModuleConf.extractModule(
["unp4kc"], appGlobalState.applicationBinaryModuleDir!);
await BinaryModuleConf.extractModule(["unp4kc"], appGlobalState.applicationBinaryModuleDir!);
final exec = "$execDir\\unp4kc.exe";
final stream = rs_process.start(
executable: exec, arguments: [], workingDirectory: execDir);
// final stream = rs_process.start(executable: exec, arguments: [], workingDirectory: execDir);
stream.listen((event) async {
switch (event.dataType) {
case RsProcessStreamDataType.output:
_rsPid = event.rsPid;
try {
final eventJson = await compute(json.decode, event.data);
_handleMessage(eventJson, event.rsPid);
} catch (e) {
dPrint("[unp4kc] json error: $e");
}
break;
case RsProcessStreamDataType.error:
dPrint("[unp4kc] stderr: ${event.data}");
if (state.errorMessage.isEmpty) {
state = state.copyWith(errorMessage: event.data);
} else {
state = state.copyWith(
errorMessage: "${state.errorMessage}\n${event.data}");
}
if (!_hasUnp4kRunTimeError) {
if (checkRunTimeError(state.errorMessage)) {
_hasUnp4kRunTimeError = true;
AnalyticsApi.touch("unp4k_no_runtime");
}
}
break;
case RsProcessStreamDataType.exit:
dPrint("[unp4kc] exit: ${event.data}");
break;
}
});
// stream.listen((event) async {
// switch (event.dataType) {
// case RsProcessStreamDataType.output:
// _rsPid = event.rsPid;
// try {
// final eventJson = await compute(json.decode, event.data);
// _handleMessage(eventJson, event.rsPid);
// } catch (e) {
// dPrint("[unp4kc] json error: $e");
// }
// break;
// case RsProcessStreamDataType.error:
// dPrint("[unp4kc] stderr: ${event.data}");
// if (state.errorMessage.isEmpty) {
// state = state.copyWith(errorMessage: event.data);
// } else {
// state = state.copyWith(errorMessage: "${state.errorMessage}\n${event.data}");
// }
// if (!_hasUnp4kRunTimeError) {
// if (checkRunTimeError(state.errorMessage)) {
// _hasUnp4kRunTimeError = true;
// AnalyticsApi.touch("unp4k_no_runtime");
// }
// }
// break;
// case RsProcessStreamDataType.exit:
// dPrint("[unp4kc] exit: ${event.data}");
// break;
// }
// });
ref.onDispose(() {
state = state.copyWith(fs: null);
@@ -113,7 +104,7 @@ class Unp4kCModel extends _$Unp4kCModel {
final gameP4kPath = "$gamePath\\Data.p4k";
switch (action.toString().trim()) {
case "info: startup":
rs_process.write(rsPid: rsPid, data: "$gameP4kPath\n");
// rs_process.write(rsPid: rsPid, data: "$gameP4kPath\n");
break;
case "info: Reading_p4k_file":
_loadStartTime = DateTime.now();
@@ -132,23 +123,22 @@ class Unp4kCModel extends _$Unp4kCModel {
final item = AppUnp4kP4kItemData.fromJson(p4kFiles[i]);
item.name = "${item.name}";
files["\\${item.name}"] = item;
await fs
.file(item.name?.replaceAll("\\", "/") ?? "")
.create(recursive: true);
await fs.file(item.name?.replaceAll("\\", "/") ?? "").create(recursive: true);
if (i == nextAwait) {
state = state.copyWith(
endMessage:
S.current.tools_unp4k_msg_reading3(i, p4kFiles.length));
state = state.copyWith(endMessage: S.current.tools_unp4k_msg_reading3(i, p4kFiles.length));
await Future.delayed(Duration.zero);
nextAwait += 20000;
}
}
final endTime = DateTime.now();
state = state.copyWith(
files: files,
fs: fs,
endMessage: S.current.tools_unp4k_msg_read_completed(files.length,
endTime.difference(_loadStartTime!).inMilliseconds));
files: files,
fs: fs,
endMessage: S.current.tools_unp4k_msg_read_completed(
files.length,
endTime.difference(_loadStartTime!).inMilliseconds,
),
);
_loadStartTime = null;
break;
case "info: Extracted_Open":
@@ -168,8 +158,9 @@ class Unp4kCModel extends _$Unp4kCModel {
}
}
state = state.copyWith(
tempOpenFile: MapEntry(openType, filePath),
endMessage: S.current.tools_unp4k_msg_open_file(filePath));
tempOpenFile: MapEntry(openType, filePath),
endMessage: S.current.tools_unp4k_msg_open_file(filePath),
);
break;
default:
dPrint("[unp4kc] unknown action: $action");
@@ -204,8 +195,7 @@ class Unp4kCModel extends _$Unp4kCModel {
result.add(f);
}
} else {
result.add(AppUnp4kP4kItemData(
name: file.path.replaceAll("/", "\\"), isDirectory: true));
result.add(AppUnp4kP4kItemData(name: file.path.replaceAll("/", "\\"), isDirectory: true));
}
}
return result;
@@ -221,16 +211,15 @@ class Unp4kCModel extends _$Unp4kCModel {
Future<void> openFile(String filePath) async {
final tempDir = await getTemporaryDirectory();
final tempPath =
"${tempDir.absolute.path}\\SCToolbox_unp4kc\\${SCLoggerHelper.getGameChannelID(getGamePath())}\\";
final tempPath = "${tempDir.absolute.path}\\SCToolbox_unp4kc\\${SCLoggerHelper.getGameChannelID(getGamePath())}\\";
state = state.copyWith(
tempOpenFile: const MapEntry("loading", ""),
endMessage: S.current.tools_unp4k_msg_open_file(filePath));
tempOpenFile: const MapEntry("loading", ""),
endMessage: S.current.tools_unp4k_msg_open_file(filePath),
);
extractFile(filePath, tempPath, mode: "extract_open");
}
Future<void> extractFile(String filePath, String outputPath,
{String mode = "extract"}) async {
Future<void> extractFile(String filePath, String outputPath, {String mode = "extract"}) async {
// remove first \\
if (filePath.startsWith("\\")) {
filePath = filePath.substring(1);
@@ -238,35 +227,28 @@ class Unp4kCModel extends _$Unp4kCModel {
outputPath = "$outputPath$filePath";
dPrint("extractFile .... $filePath");
if (_rsPid != null) {
rs_process.write(
rsPid: _rsPid!, data: "$mode<:,:>$filePath<:,:>$outputPath\n");
// rs_process.write(rsPid: _rsPid!, data: "$mode<:,:>$filePath<:,:>$outputPath\n");
}
}
static bool checkRunTimeError(String errorMessage) {
if (errorMessage
.contains("You must install .NET to run this application") ||
errorMessage.contains(
"You must install or update .NET to run this application") ||
errorMessage.contains(
"It was not possible to find any compatible framework version")) {
if (errorMessage.contains("You must install .NET to run this application") ||
errorMessage.contains("You must install or update .NET to run this application") ||
errorMessage.contains("It was not possible to find any compatible framework version")) {
AnalyticsApi.touch("unp4k_no_runtime");
return true;
}
return false;
}
static Future<Uint8List> unp4kTools(
String applicationBinaryModuleDir, List<String> args) async {
await BinaryModuleConf.extractModule(
["unp4kc"], applicationBinaryModuleDir);
static Future<Uint8List> unp4kTools(String applicationBinaryModuleDir, List<String> args) async {
await BinaryModuleConf.extractModule(["unp4kc"], applicationBinaryModuleDir);
final execDir = "$applicationBinaryModuleDir\\unp4kc";
final exec = "$execDir\\unp4kc.exe";
final r = await Process.run(exec, args);
if (r.exitCode != 0) {
Process.killPid(r.pid);
throw Exception(
"error: ${r.exitCode} , info= ${r.stdout} , err= ${r.stderr}");
throw Exception("error: ${r.exitCode} , info= ${r.stdout} , err= ${r.stderr}");
}
final eventJson = await compute(json.decode, r.stdout.toString());
if (eventJson["action"] == "data: Uint8List") {

View File

@@ -41,7 +41,7 @@ final class Unp4kCModelProvider
}
}
String _$unp4kCModelHash() => r'410461980f6173fdbb5d92cbaa3f4c2f57c1ad8d';
String _$unp4kCModelHash() => r'5ea79d8084a79353742a3344102fb5725b50211b';
abstract class _$Unp4kCModel extends $Notifier<Unp4kcState> {
Unp4kcState build();

View File

@@ -1,4 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@@ -45,7 +46,7 @@ class AboutUI extends HookConsumerWidget {
const SizedBox(height: 12),
Button(
onPressed: () => _onCheckUpdate(context, ref),
child: Padding(padding: const EdgeInsets.all(4), child: Text(S.current.about_check_update)),
child: Padding(padding: const EdgeInsets.all(4), child: Text("获取完整版")),
),
const SizedBox(height: 32),
Container(
@@ -598,8 +599,10 @@ class AboutUI extends HookConsumerWidget {
}
Future<void> _onCheckUpdate(BuildContext context, WidgetRef ref) async {
if (ConstConf.isMSE) {
launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1");
if (ConstConf.isMSE || kIsWeb) {
launchUrlString(
"https://citizenwiki.cn/Localization#%E6%96%B9%E5%BC%8F2%EF%BC%9ASC%E6%B1%89%E5%8C%96%E7%9B%92%E5%AD%90%E4%B8%80%E9%94%AE%E6%B1%89%E5%8C%96:~:text=%E6%97%A0%E6%B3%95%E8%BE%93%E5%85%A5%E4%B8%AD%E6%96%87-,%E6%96%B9%E5%BC%8F2%EF%BC%9ASC%E6%B1%89%E5%8C%96%E7%9B%92%E5%AD%90%E4%B8%80%E9%94%AE%E6%B1%89%E5%8C%96,-SC%E6%B1%89%E5%8C%96%E7%9B%92%E5%AD%90",
);
return;
} else {
final hasUpdate = await ref.read(appGlobalModelProvider.notifier).checkUpdate(context);

View File

@@ -1,8 +1,12 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
@@ -26,20 +30,53 @@ class HomeGameDoctorUI extends HookConsumerWidget {
AnalyticsApi.touch("auto_scan_issues");
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
dPrint("HomeGameDoctorUI useEffect doCheck timeStamp === $timeStamp");
model.doCheck(context);
if (kIsWeb) {
// Web 平台自动弹出文件选择器
_selectLogFile(context, model);
} else {
model.doCheck(context);
}
});
return null;
}, []);
return makeDefaultPage(context,
title: S.current
.doctor_title_one_click_diagnosis(homeState.scInstalledPath ?? ""),
useBodyContainer: true,
content: Stack(
children: [
Column(
children: [
const SizedBox(height: 12),
return makeDefaultPage(
context,
title: S.current.doctor_title_one_click_diagnosis(homeState.scInstalledPath ?? ""),
useBodyContainer: true,
content: Stack(
children: [
Column(
children: [
const SizedBox(height: 12),
if (kIsWeb)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
FilledButton(
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
const Icon(FluentIcons.folder_open),
const SizedBox(width: 6),
Text(S.current.doctor_action_select_log_file),
],
),
),
onPressed: () => _selectLogFile(context, model),
),
if (state.customLogFilePath != null) ...[
const SizedBox(width: 12),
Expanded(
child: Text(S.current.doctor_info_log_file_selected, style: TextStyle(color: Colors.green)),
),
],
],
),
)
else
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
@@ -50,118 +87,102 @@ class HomeGameDoctorUI extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.only(left: 6, right: 6),
child: Button(
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
const Icon(FluentIcons.folder_open),
const SizedBox(width: 6),
Text(item.value),
],
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
const Icon(FluentIcons.folder_open),
const SizedBox(width: 6),
Text(item.value),
],
),
onPressed: () =>
_onTapButton(context, item.key, homeState)),
),
onPressed: () => _onTapButton(context, item.key, homeState),
),
),
],
),
if (state.isChecking)
Expanded(
child: Center(
if (state.isChecking)
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(state.lastScreenInfo)
],
children: [const ProgressRing(), const SizedBox(height: 12), Text(state.lastScreenInfo)],
),
))
else if (state.checkResult == null ||
state.checkResult!.isEmpty) ...[
Expanded(
child: Center(
),
)
else if (state.checkResult == null || state.checkResult!.isEmpty) ...[
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
Text(S.current.doctor_info_scan_complete_no_issues,
maxLines: 1),
Text(S.current.doctor_info_scan_complete_no_issues, maxLines: 1),
const SizedBox(height: 64),
],
),
))
] else
...makeResult(context, state, model),
],
),
if (state.isFixing)
Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(150),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(state.isFixingString.isNotEmpty
? state.isFixingString
: S.current.doctor_info_processing),
],
),
),
] else
...makeResult(context, state, model),
],
),
if (state.isFixing)
Container(
decoration: BoxDecoration(color: Colors.black.withAlpha(150)),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(state.isFixingString.isNotEmpty ? state.isFixingString : S.current.doctor_info_processing),
],
),
),
Positioned(
bottom: 20,
right: 20,
child: makeRescueBanner(context),
)
],
));
),
Positioned(bottom: 20, right: 20, child: makeRescueBanner(context)),
],
),
);
}
Widget makeRescueBanner(BuildContext context) {
return GestureDetector(
onTap: () async {
await showToast(
context, S.current.doctor_info_game_rescue_service_note);
await showToast(context, S.current.doctor_info_game_rescue_service_note);
launchUrlString(
"https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=-M4wEme_bCXbUGT4LFKLH0bAYTFt70Ad&authKey=vHVr0TNgRmKu%2BHwywoJV6EiLa7La2VX74Vkyixr05KA0H9TqB6qWlCdY%2B9jLQ4Ha&noverify=0&group_code=536454632");
"https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=-M4wEme_bCXbUGT4LFKLH0bAYTFt70Ad&authKey=vHVr0TNgRmKu%2BHwywoJV6EiLa7La2VX74Vkyixr05KA0H9TqB6qWlCdY%2B9jLQ4Ha&noverify=0&group_code=536454632",
);
},
child: Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .2),
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset("assets/rescue.png", width: 24, height: 24),
const SizedBox(width: 12),
Text(S.current.doctor_info_need_help),
],
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset("assets/rescue.png", width: 24, height: 24),
const SizedBox(width: 12),
Text(S.current.doctor_info_need_help),
],
),
)),
),
),
),
);
}
List<Widget> makeResult(BuildContext context, HomeGameDoctorState state,
HomeGameDoctorUIModel model) {
List<Widget> makeResult(BuildContext context, HomeGameDoctorState state, HomeGameDoctorUIModel model) {
return [
const SizedBox(height: 24),
Text(state.lastScreenInfo, maxLines: 1),
const SizedBox(height: 12),
Text(
S.current.doctor_info_tool_check_result_note,
style: TextStyle(color: Colors.red, fontSize: 16),
),
Text(S.current.doctor_info_tool_check_result_note, style: TextStyle(color: Colors.red, fontSize: 16)),
const SizedBox(height: 24),
ListView.builder(
itemCount: state.checkResult!.length,
@@ -176,43 +197,54 @@ class HomeGameDoctorUI extends HookConsumerWidget {
];
}
Widget makeResultItem(BuildContext context, MapEntry<String, String> item,
HomeGameDoctorState state, HomeGameDoctorUIModel model) {
Widget makeResultItem(
BuildContext context,
MapEntry<String, String> item,
HomeGameDoctorState state,
HomeGameDoctorUIModel model,
) {
final errorNames = {
"unSupport_system": MapEntry(S.current.doctor_info_result_unsupported_os,
S.current.doctor_info_result_upgrade_system(item.value)),
"no_live_path": MapEntry(S.current.doctor_info_result_missing_live_folder,
S.current.doctor_info_result_create_live_folder(item.value)),
"unSupport_system": MapEntry(
S.current.doctor_info_result_unsupported_os,
S.current.doctor_info_result_upgrade_system(item.value),
),
"no_live_path": MapEntry(
S.current.doctor_info_result_missing_live_folder,
S.current.doctor_info_result_create_live_folder(item.value),
),
"nvme_PhysicalBytes": MapEntry(
S.current.doctor_info_result_incompatible_nvme_device,
S.current.doctor_info_result_add_registry_value(item.value)),
S.current.doctor_info_result_incompatible_nvme_device,
S.current.doctor_info_result_add_registry_value(item.value),
),
"eac_file_miss": MapEntry(
S.current.doctor_info_result_missing_easyanticheat_files,
S.current.doctor_info_result_verify_files_with_rsi_launcher),
S.current.doctor_info_result_missing_easyanticheat_files,
S.current.doctor_info_result_verify_files_with_rsi_launcher,
),
"eac_not_install": MapEntry(
S.current.doctor_info_result_easyanticheat_not_installed,
S.current.doctor_info_result_install_easyanticheat),
"cn_user_name": MapEntry(S.current.doctor_info_result_chinese_username,
S.current.doctor_info_result_chinese_username_error),
S.current.doctor_info_result_easyanticheat_not_installed,
S.current.doctor_info_result_install_easyanticheat,
),
"cn_user_name": MapEntry(
S.current.doctor_info_result_chinese_username,
S.current.doctor_info_result_chinese_username_error,
),
"cn_install_path": MapEntry(
S.current.doctor_info_result_chinese_install_path,
S.current.doctor_info_result_chinese_install_path_error(item.value)),
"low_ram": MapEntry(S.current.doctor_info_result_low_physical_memory,
S.current.doctor_info_result_memory_requirement(item.value)),
S.current.doctor_info_result_chinese_install_path,
S.current.doctor_info_result_chinese_install_path_error(item.value),
),
"low_ram": MapEntry(
S.current.doctor_info_result_low_physical_memory,
S.current.doctor_info_result_memory_requirement(item.value),
),
};
bool isCheckedError = errorNames.containsKey(item.key);
if (isCheckedError) {
return Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
),
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor),
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(
errorNames[item.key]?.key ?? "",
style: const TextStyle(fontSize: 18),
),
title: Text(errorNames[item.key]?.key ?? "", style: const TextStyle(fontSize: 18)),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4),
child: Column(
@@ -220,10 +252,9 @@ class HomeGameDoctorUI extends HookConsumerWidget {
const SizedBox(height: 4),
Text(
S.current.doctor_info_result_fix_suggestion(
errorNames[item.key]?.value ??
S.current.doctor_info_result_no_solution),
style: TextStyle(
fontSize: 14, color: Colors.white.withValues(alpha: .7)),
errorNames[item.key]?.value ?? S.current.doctor_info_result_no_solution,
),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
),
],
),
@@ -235,8 +266,7 @@ class HomeGameDoctorUI extends HookConsumerWidget {
await model.doFix(context, item);
},
child: Padding(
padding:
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
child: Text(S.current.doctor_info_action_fix),
),
),
@@ -247,26 +277,16 @@ class HomeGameDoctorUI extends HookConsumerWidget {
final isSubTitleUrl = item.value.startsWith("https://");
return Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
),
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor),
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(
item.key,
style: const TextStyle(fontSize: 18),
),
title: Text(item.key, style: const TextStyle(fontSize: 18)),
subtitle: isSubTitleUrl
? null
: Column(
children: [
const SizedBox(height: 4),
Text(
item.value,
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: .7)),
),
Text(item.value, style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7))),
],
),
trailing: isSubTitleUrl
@@ -275,8 +295,7 @@ class HomeGameDoctorUI extends HookConsumerWidget {
launchUrlString(item.value);
},
child: Padding(
padding: const EdgeInsets.only(
left: 8, right: 8, top: 4, bottom: 4),
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
child: Text(S.current.doctor_action_view_solution),
),
)
@@ -285,8 +304,7 @@ class HomeGameDoctorUI extends HookConsumerWidget {
);
}
Future<void> _onTapButton(
BuildContext context, String key, HomeUIModelState homeState) async {
Future<void> _onTapButton(BuildContext context, String key, HomeUIModelState homeState) async {
switch (key) {
case "rsi_log":
final path = await SCLoggerHelper.getLogFilePath();
@@ -294,8 +312,7 @@ class HomeGameDoctorUI extends HookConsumerWidget {
SystemHelper.openDir(path);
return;
case "game_log":
if (homeState.scInstalledPath == "not_install" ||
homeState.scInstalledPath == null) {
if (homeState.scInstalledPath == "not_install" || homeState.scInstalledPath == null) {
showToast(context, S.current.doctor_tip_title_select_game_directory);
return;
}
@@ -303,4 +320,37 @@ class HomeGameDoctorUI extends HookConsumerWidget {
return;
}
}
Future<void> _selectLogFile(BuildContext context, HomeGameDoctorUIModel model) async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['log'],
dialogTitle: S.current.doctor_action_select_log_file,
);
if (result != null && result.files.isNotEmpty) {
final file = result.files.first;
// 在 web 平台,使用 bytes 读取文件内容
if (kIsWeb && file.bytes != null) {
final content = String.fromCharCodes(file.bytes!);
final lines = content.split('\n');
model.setCustomLogFile(file.name, lines);
if (!context.mounted) return;
// Web 平台选择文件后自动开始诊断
model.doCheck(context);
} else if (!kIsWeb && file.path != null) {
// 桌面平台使用路径读取
if (!kIsWeb) {
final logFile = File(file.path!);
final lines = await logFile.readAsLines();
model.setCustomLogFile(file.path!, lines);
}
}
}
} catch (e) {
dPrint("Error selecting log file: $e");
}
}
}

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
@@ -23,6 +24,8 @@ abstract class HomeGameDoctorState with _$HomeGameDoctorState {
@Default("") String lastScreenInfo,
@Default("") String isFixingString,
List<MapEntry<String, String>>? checkResult,
String? customLogFilePath,
List<String>? customLogFileContent,
}) = _HomeGameDoctorState;
}
@@ -34,12 +37,16 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
return state;
}
void setCustomLogFile(String? filePath, List<String>? content) {
state = state.copyWith(customLogFilePath: filePath, customLogFileContent: content);
}
Future<void> doFix(
// ignore: avoid_build_context_in_providers
BuildContext context,
MapEntry<String, String> item) async {
final checkResult =
List<MapEntry<String, String>>.from(state.checkResult ?? []);
// ignore: avoid_build_context_in_providers
BuildContext context,
MapEntry<String, String> item,
) async {
final checkResult = List<MapEntry<String, String>>.from(state.checkResult ?? []);
state = state.copyWith(isFixing: true, isFixingString: "");
switch (item.key) {
case "unSupport_system":
@@ -49,13 +56,11 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
try {
await Directory(item.value).create(recursive: true);
if (!context.mounted) break;
showToast(
context, S.current.doctor_action_result_create_folder_success);
showToast(context, S.current.doctor_action_result_create_folder_success);
checkResult.remove(item);
state = state.copyWith(checkResult: checkResult);
} catch (e) {
showToast(context,
S.current.doctor_action_result_create_folder_fail(item.value, e));
showToast(context, S.current.doctor_action_result_create_folder_fail(item.value, e));
}
break;
case "nvme_PhysicalBytes":
@@ -71,8 +76,7 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
}
break;
case "eac_file_miss":
showToast(context,
S.current.doctor_info_result_verify_files_with_rsi_launcher);
showToast(context, S.current.doctor_info_result_verify_files_with_rsi_launcher);
break;
case "eac_not_install":
final eacJsonPath = "${item.value}\\Settings.json";
@@ -80,19 +84,16 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
final Map eacJson = json.decode(utf8.decode(eacJsonData));
final eacID = eacJson["productid"];
try {
var result = await Process.run(
"${item.value}\\EasyAntiCheat_EOS_Setup.exe", ["install", eacID]);
var result = await Process.run("${item.value}\\EasyAntiCheat_EOS_Setup.exe", ["install", eacID]);
dPrint("${item.value}\\EasyAntiCheat_EOS_Setup.exe install $eacID");
if (result.stderr == "") {
if (!context.mounted) break;
showToast(
context, S.current.doctor_action_result_game_start_success);
showToast(context, S.current.doctor_action_result_game_start_success);
checkResult.remove(item);
state = state.copyWith(checkResult: checkResult);
} else {
if (!context.mounted) break;
showToast(context,
S.current.doctor_action_result_fix_fail(result.stderr));
showToast(context, S.current.doctor_action_result_fix_fail(result.stderr));
}
} catch (e) {
if (!context.mounted) break;
@@ -102,8 +103,7 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
case "cn_user_name":
showToast(context, S.current.doctor_action_result_redirect_warning);
await Future.delayed(const Duration(milliseconds: 300));
launchUrlString(
"https://jingyan.baidu.com/article/59703552a318a08fc0074021.html");
launchUrlString("https://jingyan.baidu.com/article/59703552a318a08fc0074021.html");
break;
default:
showToast(context, S.current.doctor_action_result_issue_not_supported);
@@ -115,8 +115,7 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
// ignore: avoid_build_context_in_providers
Future<void> doCheck(BuildContext context) async {
if (state.isChecking) return;
state = state.copyWith(
isChecking: true, lastScreenInfo: S.current.doctor_action_analyzing);
state = state.copyWith(isChecking: true, lastScreenInfo: S.current.doctor_action_analyzing);
dPrint("-------- start docker check -----");
if (!context.mounted) return;
await _statCheck(context);
@@ -134,62 +133,73 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
// checkResult?.add(MapEntry("nvme_PhysicalBytes", "C"));
// checkResult?.add(MapEntry("no_live_path", ""));
await _checkPreInstall(context, scInstalledPath, checkResult);
if (!context.mounted) return;
await _checkEAC(context, scInstalledPath, checkResult);
if (!context.mounted) return;
// Web 平台仅检查日志文件,跳过系统环境和 EAC 检查
if (!kIsWeb) {
await _checkPreInstall(context, scInstalledPath, checkResult);
if (!context.mounted) return;
await _checkEAC(context, scInstalledPath, checkResult);
if (!context.mounted) return;
}
await _checkGameRunningLog(context, scInstalledPath, checkResult);
if (checkResult.isEmpty) {
final lastScreenInfo = S.current.doctor_action_result_analysis_no_issue;
state = state.copyWith(checkResult: null, lastScreenInfo: lastScreenInfo);
} else {
final lastScreenInfo = S.current
.doctor_action_result_analysis_issues_found(
checkResult.length.toString());
state = state.copyWith(
checkResult: checkResult, lastScreenInfo: lastScreenInfo);
final lastScreenInfo = S.current.doctor_action_result_analysis_issues_found(checkResult.length.toString());
state = state.copyWith(checkResult: checkResult, lastScreenInfo: lastScreenInfo);
}
if (scInstalledPath == "not_install" && (checkResult.isEmpty)) {
// Web 平台不显示 not_install 提示
if (!kIsWeb && scInstalledPath == "not_install" && (checkResult.isEmpty)) {
if (!context.mounted) return;
showToast(context, S.current.doctor_action_result_toast_scan_no_issue);
}
}
// ignore: avoid_build_context_in_providers
Future _checkGameRunningLog(BuildContext context, String scInstalledPath,
List<MapEntry<String, String>> checkResult) async {
if (scInstalledPath == "not_install") return;
Future _checkGameRunningLog(
BuildContext context,
String scInstalledPath,
List<MapEntry<String, String>> checkResult,
) async {
if (scInstalledPath == "not_install" && state.customLogFileContent == null) return;
final lastScreenInfo = S.current.doctor_action_tip_checking_game_log;
state = state.copyWith(lastScreenInfo: lastScreenInfo);
final logs = await SCLoggerHelper.getGameRunningLogs(scInstalledPath);
// 优先使用自定义 log 文件内容(用于 web 平台)
List<String>? logs;
if (state.customLogFileContent != null) {
logs = state.customLogFileContent;
} else {
logs = await SCLoggerHelper.getGameRunningLogs(scInstalledPath);
}
if (logs == null) return;
final info = SCLoggerHelper.getGameRunningLogInfo(logs);
if (info != null) {
if (info.key != "_") {
checkResult.add(MapEntry(
S.current.doctor_action_info_game_abnormal_exit(info.key),
info.value));
checkResult.add(MapEntry(S.current.doctor_action_info_game_abnormal_exit(info.key), info.value));
} else {
checkResult.add(MapEntry(
checkResult.add(
MapEntry(
S.current.doctor_action_info_game_abnormal_exit_unknown,
S.current.doctor_action_info_info_feedback(info.value)));
S.current.doctor_action_info_info_feedback(info.value),
),
);
}
}
}
// ignore: avoid_build_context_in_providers
Future _checkEAC(BuildContext context, String scInstalledPath,
List<MapEntry<String, String>> checkResult) async {
Future _checkEAC(BuildContext context, String scInstalledPath, List<MapEntry<String, String>> checkResult) async {
if (scInstalledPath == "not_install") return;
final lastScreenInfo = S.current.doctor_action_info_checking_eac;
state = state.copyWith(lastScreenInfo: lastScreenInfo);
final eacPath = "$scInstalledPath\\EasyAntiCheat";
final eacJsonPath = "$eacPath\\Settings.json";
if (!await Directory(eacPath).exists() ||
!await File(eacJsonPath).exists()) {
if (!await Directory(eacPath).exists() || !await File(eacJsonPath).exists()) {
checkResult.add(const MapEntry("eac_file_miss", ""));
return;
}
@@ -212,17 +222,18 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
final _cnExp = RegExp(r"[^\x00-\xff]");
// ignore: avoid_build_context_in_providers
Future _checkPreInstall(BuildContext context, String scInstalledPath,
List<MapEntry<String, String>> checkResult) async {
Future _checkPreInstall(
BuildContext context,
String scInstalledPath,
List<MapEntry<String, String>> checkResult,
) async {
final lastScreenInfo = S.current.doctor_action_info_checking_runtime;
state = state.copyWith(lastScreenInfo: lastScreenInfo);
if (!(Platform.operatingSystemVersion.contains("Windows 10") ||
Platform.operatingSystemVersion.contains("Windows 11"))) {
checkResult
.add(MapEntry("unSupport_system", Platform.operatingSystemVersion));
final lastScreenInfo = S.current.doctor_action_result_info_unsupported_os(
Platform.operatingSystemVersion);
checkResult.add(MapEntry("unSupport_system", Platform.operatingSystemVersion));
final lastScreenInfo = S.current.doctor_action_result_info_unsupported_os(Platform.operatingSystemVersion);
state = state.copyWith(lastScreenInfo: lastScreenInfo);
await showToast(context, lastScreenInfo);
}
@@ -236,12 +247,10 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
if (ramSize < 16) {
checkResult.add(MapEntry("low_ram", "$ramSize"));
}
state = state.copyWith(
lastScreenInfo: S.current.doctor_action_info_checking_install_info);
state = state.copyWith(lastScreenInfo: S.current.doctor_action_info_checking_install_info);
// 检查安装分区
try {
final listData = await SCLoggerHelper.getGameInstallPath(
await SCLoggerHelper.getLauncherLogList() ?? []);
final listData = await SCLoggerHelper.getGameInstallPath(await SCLoggerHelper.getLauncherLogList() ?? []);
final p = [];
final checkedPath = [];
for (var installPath in listData) {
@@ -265,10 +274,9 @@ class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
// call check
for (var element in p) {
var result = await Process.run('powershell', [
"(fsutil fsinfo sectorinfo $element: | Select-String 'PhysicalBytesPerSectorForPerformance').ToString().Split(':')[1].Trim()"
"(fsutil fsinfo sectorinfo $element: | Select-String 'PhysicalBytesPerSectorForPerformance').ToString().Split(':')[1].Trim()",
]);
dPrint(
"fsutil info sector info: ->>> ${result.stdout.toString().trim()}");
dPrint("fsutil info sector info: ->>> ${result.stdout.toString().trim()}");
if (result.stderr == "") {
final rs = result.stdout.toString().trim();
final physicalBytesPerSectorForPerformance = (int.tryParse(rs) ?? 0);

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$HomeGameDoctorState {
bool get isChecking; bool get isFixing; String get lastScreenInfo; String get isFixingString; List<MapEntry<String, String>>? get checkResult;
bool get isChecking; bool get isFixing; String get lastScreenInfo; String get isFixingString; List<MapEntry<String, String>>? get checkResult; String? get customLogFilePath; List<String>? get customLogFileContent;
/// Create a copy of HomeGameDoctorState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $HomeGameDoctorStateCopyWith<HomeGameDoctorState> get copyWith => _$HomeGameDoct
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is HomeGameDoctorState&&(identical(other.isChecking, isChecking) || other.isChecking == isChecking)&&(identical(other.isFixing, isFixing) || other.isFixing == isFixing)&&(identical(other.lastScreenInfo, lastScreenInfo) || other.lastScreenInfo == lastScreenInfo)&&(identical(other.isFixingString, isFixingString) || other.isFixingString == isFixingString)&&const DeepCollectionEquality().equals(other.checkResult, checkResult));
return identical(this, other) || (other.runtimeType == runtimeType&&other is HomeGameDoctorState&&(identical(other.isChecking, isChecking) || other.isChecking == isChecking)&&(identical(other.isFixing, isFixing) || other.isFixing == isFixing)&&(identical(other.lastScreenInfo, lastScreenInfo) || other.lastScreenInfo == lastScreenInfo)&&(identical(other.isFixingString, isFixingString) || other.isFixingString == isFixingString)&&const DeepCollectionEquality().equals(other.checkResult, checkResult)&&(identical(other.customLogFilePath, customLogFilePath) || other.customLogFilePath == customLogFilePath)&&const DeepCollectionEquality().equals(other.customLogFileContent, customLogFileContent));
}
@override
int get hashCode => Object.hash(runtimeType,isChecking,isFixing,lastScreenInfo,isFixingString,const DeepCollectionEquality().hash(checkResult));
int get hashCode => Object.hash(runtimeType,isChecking,isFixing,lastScreenInfo,isFixingString,const DeepCollectionEquality().hash(checkResult),customLogFilePath,const DeepCollectionEquality().hash(customLogFileContent));
@override
String toString() {
return 'HomeGameDoctorState(isChecking: $isChecking, isFixing: $isFixing, lastScreenInfo: $lastScreenInfo, isFixingString: $isFixingString, checkResult: $checkResult)';
return 'HomeGameDoctorState(isChecking: $isChecking, isFixing: $isFixing, lastScreenInfo: $lastScreenInfo, isFixingString: $isFixingString, checkResult: $checkResult, customLogFilePath: $customLogFilePath, customLogFileContent: $customLogFileContent)';
}
@@ -45,7 +45,7 @@ abstract mixin class $HomeGameDoctorStateCopyWith<$Res> {
factory $HomeGameDoctorStateCopyWith(HomeGameDoctorState value, $Res Function(HomeGameDoctorState) _then) = _$HomeGameDoctorStateCopyWithImpl;
@useResult
$Res call({
bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult
bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult, String? customLogFilePath, List<String>? customLogFileContent
});
@@ -62,14 +62,16 @@ class _$HomeGameDoctorStateCopyWithImpl<$Res>
/// Create a copy of HomeGameDoctorState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isChecking = null,Object? isFixing = null,Object? lastScreenInfo = null,Object? isFixingString = null,Object? checkResult = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isChecking = null,Object? isFixing = null,Object? lastScreenInfo = null,Object? isFixingString = null,Object? checkResult = freezed,Object? customLogFilePath = freezed,Object? customLogFileContent = freezed,}) {
return _then(_self.copyWith(
isChecking: null == isChecking ? _self.isChecking : isChecking // ignore: cast_nullable_to_non_nullable
as bool,isFixing: null == isFixing ? _self.isFixing : isFixing // ignore: cast_nullable_to_non_nullable
as bool,lastScreenInfo: null == lastScreenInfo ? _self.lastScreenInfo : lastScreenInfo // ignore: cast_nullable_to_non_nullable
as String,isFixingString: null == isFixingString ? _self.isFixingString : isFixingString // ignore: cast_nullable_to_non_nullable
as String,checkResult: freezed == checkResult ? _self.checkResult : checkResult // ignore: cast_nullable_to_non_nullable
as List<MapEntry<String, String>>?,
as List<MapEntry<String, String>>?,customLogFilePath: freezed == customLogFilePath ? _self.customLogFilePath : customLogFilePath // ignore: cast_nullable_to_non_nullable
as String?,customLogFileContent: freezed == customLogFileContent ? _self.customLogFileContent : customLogFileContent // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
@@ -154,10 +156,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult, String? customLogFilePath, List<String>? customLogFileContent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _HomeGameDoctorState() when $default != null:
return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFixingString,_that.checkResult);case _:
return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFixingString,_that.checkResult,_that.customLogFilePath,_that.customLogFileContent);case _:
return orElse();
}
@@ -175,10 +177,10 @@ return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFix
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult, String? customLogFilePath, List<String>? customLogFileContent) $default,) {final _that = this;
switch (_that) {
case _HomeGameDoctorState():
return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFixingString,_that.checkResult);case _:
return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFixingString,_that.checkResult,_that.customLogFilePath,_that.customLogFileContent);case _:
throw StateError('Unexpected subclass');
}
@@ -195,10 +197,10 @@ return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFix
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult, String? customLogFilePath, List<String>? customLogFileContent)? $default,) {final _that = this;
switch (_that) {
case _HomeGameDoctorState() when $default != null:
return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFixingString,_that.checkResult);case _:
return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFixingString,_that.checkResult,_that.customLogFilePath,_that.customLogFileContent);case _:
return null;
}
@@ -210,7 +212,7 @@ return $default(_that.isChecking,_that.isFixing,_that.lastScreenInfo,_that.isFix
class _HomeGameDoctorState implements HomeGameDoctorState {
_HomeGameDoctorState({this.isChecking = false, this.isFixing = false, this.lastScreenInfo = "", this.isFixingString = "", final List<MapEntry<String, String>>? checkResult}): _checkResult = checkResult;
_HomeGameDoctorState({this.isChecking = false, this.isFixing = false, this.lastScreenInfo = "", this.isFixingString = "", final List<MapEntry<String, String>>? checkResult, this.customLogFilePath, final List<String>? customLogFileContent}): _checkResult = checkResult,_customLogFileContent = customLogFileContent;
@override@JsonKey() final bool isChecking;
@@ -226,6 +228,16 @@ class _HomeGameDoctorState implements HomeGameDoctorState {
return EqualUnmodifiableListView(value);
}
@override final String? customLogFilePath;
final List<String>? _customLogFileContent;
@override List<String>? get customLogFileContent {
final value = _customLogFileContent;
if (value == null) return null;
if (_customLogFileContent is EqualUnmodifiableListView) return _customLogFileContent;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Create a copy of HomeGameDoctorState
/// with the given fields replaced by the non-null parameter values.
@@ -237,16 +249,16 @@ _$HomeGameDoctorStateCopyWith<_HomeGameDoctorState> get copyWith => __$HomeGameD
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HomeGameDoctorState&&(identical(other.isChecking, isChecking) || other.isChecking == isChecking)&&(identical(other.isFixing, isFixing) || other.isFixing == isFixing)&&(identical(other.lastScreenInfo, lastScreenInfo) || other.lastScreenInfo == lastScreenInfo)&&(identical(other.isFixingString, isFixingString) || other.isFixingString == isFixingString)&&const DeepCollectionEquality().equals(other._checkResult, _checkResult));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HomeGameDoctorState&&(identical(other.isChecking, isChecking) || other.isChecking == isChecking)&&(identical(other.isFixing, isFixing) || other.isFixing == isFixing)&&(identical(other.lastScreenInfo, lastScreenInfo) || other.lastScreenInfo == lastScreenInfo)&&(identical(other.isFixingString, isFixingString) || other.isFixingString == isFixingString)&&const DeepCollectionEquality().equals(other._checkResult, _checkResult)&&(identical(other.customLogFilePath, customLogFilePath) || other.customLogFilePath == customLogFilePath)&&const DeepCollectionEquality().equals(other._customLogFileContent, _customLogFileContent));
}
@override
int get hashCode => Object.hash(runtimeType,isChecking,isFixing,lastScreenInfo,isFixingString,const DeepCollectionEquality().hash(_checkResult));
int get hashCode => Object.hash(runtimeType,isChecking,isFixing,lastScreenInfo,isFixingString,const DeepCollectionEquality().hash(_checkResult),customLogFilePath,const DeepCollectionEquality().hash(_customLogFileContent));
@override
String toString() {
return 'HomeGameDoctorState(isChecking: $isChecking, isFixing: $isFixing, lastScreenInfo: $lastScreenInfo, isFixingString: $isFixingString, checkResult: $checkResult)';
return 'HomeGameDoctorState(isChecking: $isChecking, isFixing: $isFixing, lastScreenInfo: $lastScreenInfo, isFixingString: $isFixingString, checkResult: $checkResult, customLogFilePath: $customLogFilePath, customLogFileContent: $customLogFileContent)';
}
@@ -257,7 +269,7 @@ abstract mixin class _$HomeGameDoctorStateCopyWith<$Res> implements $HomeGameDoc
factory _$HomeGameDoctorStateCopyWith(_HomeGameDoctorState value, $Res Function(_HomeGameDoctorState) _then) = __$HomeGameDoctorStateCopyWithImpl;
@override @useResult
$Res call({
bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult
bool isChecking, bool isFixing, String lastScreenInfo, String isFixingString, List<MapEntry<String, String>>? checkResult, String? customLogFilePath, List<String>? customLogFileContent
});
@@ -274,14 +286,16 @@ class __$HomeGameDoctorStateCopyWithImpl<$Res>
/// Create a copy of HomeGameDoctorState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isChecking = null,Object? isFixing = null,Object? lastScreenInfo = null,Object? isFixingString = null,Object? checkResult = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isChecking = null,Object? isFixing = null,Object? lastScreenInfo = null,Object? isFixingString = null,Object? checkResult = freezed,Object? customLogFilePath = freezed,Object? customLogFileContent = freezed,}) {
return _then(_HomeGameDoctorState(
isChecking: null == isChecking ? _self.isChecking : isChecking // ignore: cast_nullable_to_non_nullable
as bool,isFixing: null == isFixing ? _self.isFixing : isFixing // ignore: cast_nullable_to_non_nullable
as bool,lastScreenInfo: null == lastScreenInfo ? _self.lastScreenInfo : lastScreenInfo // ignore: cast_nullable_to_non_nullable
as String,isFixingString: null == isFixingString ? _self.isFixingString : isFixingString // ignore: cast_nullable_to_non_nullable
as String,checkResult: freezed == checkResult ? _self._checkResult : checkResult // ignore: cast_nullable_to_non_nullable
as List<MapEntry<String, String>>?,
as List<MapEntry<String, String>>?,customLogFilePath: freezed == customLogFilePath ? _self.customLogFilePath : customLogFilePath // ignore: cast_nullable_to_non_nullable
as String?,customLogFileContent: freezed == customLogFileContent ? _self._customLogFileContent : customLogFileContent // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}

View File

@@ -42,7 +42,7 @@ final class HomeGameDoctorUIModelProvider
}
String _$homeGameDoctorUIModelHash() =>
r'7035b501860e9d8c3fdfb91370311760120af115';
r'8b969c4638fb07b8224dd60b3f04fa29742c4ae8';
abstract class _$HomeGameDoctorUIModel extends $Notifier<HomeGameDoctorState> {
HomeGameDoctorState build();

View File

@@ -11,7 +11,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/generated/no_l10n_strings.dart';
import 'package:starcitizen_doctor/ui/guide/guide_ui.dart';
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -32,7 +31,7 @@ class HomeUI extends HookConsumerWidget {
ref.watch(localizationUIModelProvider);
useEffect(() {
_checkGuide(context, model);
// _checkGuide(context, model);
return null;
}, const []);
@@ -118,25 +117,25 @@ class HomeUI extends HookConsumerWidget {
children: [
Text(S.current.home_install_location),
const SizedBox(width: 12),
Button(
onPressed: () async {
await context.push("/guide");
await model.reScanPath();
},
child: const Padding(padding: EdgeInsets.all(6), child: Icon(FluentIcons.settings)),
),
const SizedBox(width: 6),
// Button(
// onPressed: () async {
// await context.push("/guide");
// await model.reScanPath();
// },
// child: const Padding(padding: EdgeInsets.all(6), child: Icon(FluentIcons.settings)),
// ),
// const SizedBox(width: 6),
Expanded(
child: ComboBox<String>(
value: homeState.scInstalledPath,
isExpanded: true,
items: [
ComboBoxItem(value: "not_install", child: Text(S.current.home_not_installed_or_failed)),
for (final path in homeState.scInstallPaths)
ComboBoxItem(
value: path,
child: Row(children: [Text(path)]),
),
ComboBoxItem(value: "not_install", child: Text("您的电脑")),
// for (final path in homeState.scInstallPaths)
// ComboBoxItem(
// value: path,
// child: Row(children: [Text(path)]),
// ),
],
onChanged: model.onChangeInstallPath,
),
@@ -714,13 +713,8 @@ class HomeUI extends HookConsumerWidget {
}
Future<void> _onMenuTap(BuildContext context, String key, HomeUIModelState homeState, WidgetRef ref) async {
String gameInstallReqInfo = S.current.home_action_info_valid_install_location_required;
switch (key) {
case "localization":
if (homeState.scInstalledPath == "not_install") {
ToolsUIModel.rsiEnhance(context, showNotGameInstallMsg: true);
break;
}
final model = ref.watch(homeUIModelProvider.notifier);
model.checkLocalizationUpdate();
await showDialog(
@@ -731,14 +725,10 @@ class HomeUI extends HookConsumerWidget {
model.checkLocalizationUpdate(skipReload: true);
break;
case "performance":
if (homeState.scInstalledPath == "not_install") {
showToast(context, gameInstallReqInfo);
break;
}
context.push("/index/$key");
context.push("/$key");
break;
default:
context.push("/index/$key");
context.push("/$key");
}
}
@@ -771,42 +761,7 @@ class HomeUI extends HookConsumerWidget {
HomeUIModel model,
WidgetRef ref,
) async {
final localizationState = ref.read(localizationUIModelProvider);
if (localizationState.communityInputMethodLanguageData == null) {
showToast(context, S.current.input_method_feature_maintenance);
return;
}
if (localizationState.installedCommunityInputMethodSupportVersion == null) {
final userOK = await showConfirmDialogs(
context,
S.current.input_method_community_input_method_not_installed,
Text(S.current.input_method_install_community_input_method_prompt),
);
if (userOK) {
if (!context.mounted) return;
() async {
await _onMenuTap(context, 'localization', homeState, ref);
final localizationState = ref.read(localizationUIModelProvider);
if (localizationState.installedCommunityInputMethodSupportVersion != null) {
await Future.delayed(Duration(milliseconds: 300));
if (!context.mounted) return;
await _goInputMethod(context, model);
return;
}
}();
await Future.delayed(Duration(milliseconds: 300));
final localizationModel = ref.read(localizationUIModelProvider.notifier);
if (!context.mounted) return;
localizationModel.checkReinstall(context);
}
return;
}
await _goInputMethod(context, model);
}
Future<void> _goInputMethod(BuildContext context, HomeUIModel model) async {
await showDialog(context: context, builder: (context) => const InputMethodDialogUI());
showToast(context, "请使用完整版体验");
}
}

View File

@@ -120,7 +120,12 @@ class HomeUIModel extends _$HomeUIModel {
for (var node in h.body!.nodes) {
if (node is html_dom.Element) {
if (node.localName == "img") {
return node.attributes["src"]?.trim() ?? "";
final image = node.attributes["src"]?.trim() ?? "";
var updatedImage = image.replaceAllMapped(
RegExp(r'http(s)?://i(\d+)\.hdslb\.com/bfs/'),
(match) => 'https://web-proxy.scbox.xkeyc.cn/bfs${match[2]}/bfs/',
);
return updatedImage;
}
}
}

View File

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

View File

@@ -47,7 +47,7 @@ final class AdvancedLocalizationUIModelProvider
}
String _$advancedLocalizationUIModelHash() =>
r'2f890c854bc56e506c441acabc2014438a163617';
r'c7cca8935ac7df2281e83297b11b6b82d94f7a59';
abstract class _$AdvancedLocalizationUIModel
extends $Notifier<AdvancedLocalizationUIState> {

View File

@@ -30,13 +30,9 @@ class LocalizationDialogUI extends HookConsumerWidget {
return ContentDialog(
title: makeTitle(context, model, state),
constraints: BoxConstraints(
maxWidth: MediaQuery
.of(context)
.size
.width * .7, minHeight: MediaQuery
.of(context)
.size
.height * .9),
maxWidth: MediaQuery.of(context).size.width * .7,
minHeight: MediaQuery.of(context).size.height * .9,
),
content: Padding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
child: SingleChildScrollView(
@@ -44,132 +40,57 @@ class LocalizationDialogUI extends HookConsumerWidget {
children: [
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: state.patchStatus?.key == true &&
state.patchStatus?.value == S.current.home_action_info_game_built_in
child:
state.patchStatus?.key == true &&
state.patchStatus?.value == S.current.home_action_info_game_built_in
? Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InfoBar(
title: Text(S.current.home_action_info_warning),
content: Text(S.current.localization_info_machine_translation_warning),
severity: InfoBarSeverity.info,
style: InfoBarThemeData(decoration: (severity) {
return const BoxDecoration(color: Color.fromRGBO(155, 7, 7, 1.0));
}, iconColor: (severity) {
return Colors.white;
}),
),
)
: SizedBox(
width: MediaQuery
.of(context)
.size
.width,
),
padding: const EdgeInsets.only(bottom: 12),
child: InfoBar(
title: Text(S.current.home_action_info_warning),
content: Text(S.current.localization_info_machine_translation_warning),
severity: InfoBarSeverity.info,
style: InfoBarThemeData(
decoration: (severity) {
return const BoxDecoration(color: Color.fromRGBO(155, 7, 7, 1.0));
},
iconColor: (severity) {
return Colors.white;
},
),
),
)
: SizedBox(width: MediaQuery.of(context).size.width),
),
makeToolsListContainer(context, model, state),
makeListContainer(
S.current.localization_info_translation,
[
if (state.patchStatus == null)
makeLoading(context)
else
...[
const SizedBox(height: 6),
Row(
children: [
Center(
child: Text(S.current.localization_info_enabled(
LocalizationUIModel.languageSupport[state.selectedLanguage] ?? "")),
),
const Spacer(),
ToggleSwitch(
checked: state.patchStatus?.key == true,
onChanged: model.updateLangCfg,
)
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Row(
children: [
Text(S.current.localization_info_installed_version(
"${state.patchStatus?.value ?? ""} ${(state.isInstalledAdvanced ?? false) ? S
.current.home_localization_msg_version_advanced : ""}")),
SizedBox(width: 24),
if (state.installedCommunityInputMethodSupportVersion != null)
Text(
S.current.input_method_community_input_method_support_version(
state.installedCommunityInputMethodSupportVersion ?? "?"),
)
],
),
),
if (state.patchStatus?.value != S.current.home_action_info_game_built_in)
Row(
children: [
Button(
onPressed: model.goFeedback,
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
const Icon(FluentIcons.feedback),
const SizedBox(width: 6),
Text(S.current.localization_action_translation_feedback),
],
),
)),
const SizedBox(width: 16),
Button(
onPressed: model.doDelIniFile(),
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
const Icon(FluentIcons.delete),
const SizedBox(width: 6),
Text(S.current.localization_action_uninstall_translation),
],
),
)),
],
),
],
),
const SizedBox(height: 12),
Container(
color: Colors.white.withValues(alpha: .1),
height: 1,
),
const SizedBox(height: 12),
if (state.apiLocalizationData == null)
makeLoading(context)
else
if (state.apiLocalizationData!.isEmpty)
Center(
child: Text(
S.current.localization_info_no_translation_available,
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .8)),
),
)
else
AlignedGridView.count(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
itemBuilder: (BuildContext context, int index) {
final item = state.apiLocalizationData!.entries.elementAt(index);
return makeRemoteList(context, model, item, state, index);
},
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: state.apiLocalizationData?.length ?? 0,
)
],
],
context),
makeListContainer(S.current.localization_info_translation, [
if (state.patchStatus == null)
makeLoading(context)
else ...[
const SizedBox(height: 6),
if (state.apiLocalizationData == null)
makeLoading(context)
else if (state.apiLocalizationData!.isEmpty)
Center(
child: Text(
S.current.localization_info_no_translation_available,
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .8)),
),
)
else
AlignedGridView.count(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
itemBuilder: (BuildContext context, int index) {
final item = state.apiLocalizationData!.entries.elementAt(index);
return makeRemoteList(context, model, item, state, index);
},
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: state.apiLocalizationData?.length ?? 0,
),
],
], context),
],
),
),
@@ -177,8 +98,13 @@ class LocalizationDialogUI extends HookConsumerWidget {
);
}
Widget makeRemoteList(BuildContext context, LocalizationUIModel model, MapEntry<String, ScLocalizationData> item,
LocalizationUIState state, int index) {
Widget makeRemoteList(
BuildContext context,
LocalizationUIModel model,
MapEntry<String, ScLocalizationData> item,
LocalizationUIState state,
int index,
) {
final isWorking = state.workingVersion.isNotEmpty;
final isMineWorking = state.workingVersion == item.key;
final isInstalled = state.patchStatus?.value == item.key;
@@ -207,10 +133,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${item.value.info}",
style: const TextStyle(fontSize: 19),
),
Text("${item.value.info}", style: const TextStyle(fontSize: 19)),
const SizedBox(height: 4),
Text(
S.current.localization_info_version_number(item.value.versionName ?? ""),
@@ -230,40 +153,30 @@ class LocalizationDialogUI extends HookConsumerWidget {
),
),
if (isMineWorking)
const Padding(
padding: EdgeInsets.only(right: 12),
child: ProgressRing(),
)
else
...[
Icon(
isInstalled
? FluentIcons.check_mark
: isItemEnabled
? FluentIcons.download
: FluentIcons.disable_updates,
color: Colors.white.withValues(alpha: .8),
size: 18,
),
const SizedBox(width: 6),
Text(
isInstalled
? S.current.localization_info_installed
: (isItemEnabled
? S.current.localization_action_install
: S.current.localization_info_unavailable),
style: TextStyle(
color: Colors.white.withValues(alpha: .8),
),
),
const SizedBox(width: 6),
if ((!isInstalled) && isItemEnabled)
Icon(
FluentIcons.chevron_right,
size: 14,
color: Colors.white.withValues(alpha: .6),
)
]
const Padding(padding: EdgeInsets.only(right: 12), child: ProgressRing())
else ...[
Icon(
isInstalled
? FluentIcons.check_mark
: isItemEnabled
? FluentIcons.download
: FluentIcons.disable_updates,
color: Colors.white.withValues(alpha: .8),
size: 18,
),
const SizedBox(width: 6),
Text(
isInstalled
? S.current.localization_info_installed
: (isItemEnabled
? S.current.localization_action_install
: S.current.localization_info_unavailable),
style: TextStyle(color: Colors.white.withValues(alpha: .8)),
),
const SizedBox(width: 6),
if ((!isInstalled) && isItemEnabled)
Icon(FluentIcons.chevron_right, size: 14, color: Colors.white.withValues(alpha: .6)),
],
],
),
if (item.value.note != null) ...[
@@ -272,10 +185,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
"${item.value.note}",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white.withValues(alpha: .4),
fontSize: 13,
),
style: TextStyle(color: Colors.white.withValues(alpha: .4), fontSize: 13),
),
],
],
@@ -286,16 +196,20 @@ class LocalizationDialogUI extends HookConsumerWidget {
);
}
Widget makeListContainer(String title, List<Widget> children, BuildContext context,
{List<Widget> actions = const [], bool gridViewMode = false, int gridViewCrossAxisCount = 2}) {
Widget makeListContainer(
String title,
List<Widget> children,
BuildContext context, {
List<Widget> actions = const [],
bool gridViewMode = false,
int gridViewCrossAxisCount = 2,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Container(
decoration: BoxDecoration(color: FluentTheme
.of(context)
.cardColor, borderRadius: BorderRadius.circular(7)),
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor, borderRadius: BorderRadius.circular(7)),
child: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 12, left: 24, right: 24),
child: Column(
@@ -303,21 +217,13 @@ class LocalizationDialogUI extends HookConsumerWidget {
children: [
Row(
children: [
Text(
title,
style: const TextStyle(fontSize: 22),
),
Text(title, style: const TextStyle(fontSize: 22)),
const Spacer(),
if (actions.isNotEmpty) ...actions,
],
),
const SizedBox(
height: 6,
),
Container(
color: Colors.white.withValues(alpha: .1),
height: 1,
),
const SizedBox(height: 6),
Container(color: Colors.white.withValues(alpha: .1), height: 1),
const SizedBox(height: 12),
if (gridViewMode)
AlignedGridView.count(
@@ -332,7 +238,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
itemCount: children.length,
)
else
...children
...children,
],
),
),
@@ -344,55 +250,54 @@ class LocalizationDialogUI extends HookConsumerWidget {
Widget makeTitle(BuildContext context, LocalizationUIModel model, LocalizationUIState state) {
return Row(
children: [
IconButton(
icon: const Icon(
FluentIcons.back,
size: 22,
),
onPressed: model.onBack(context)),
IconButton(icon: const Icon(FluentIcons.back, size: 22), onPressed: model.onBack(context)),
const SizedBox(width: 12),
Text(S.current.home_action_localization_management),
const SizedBox(width: 24),
Text(
"${model.getScInstallPath()}",
style: const TextStyle(fontSize: 13),
),
const Spacer(),
SizedBox(
height: 36,
child: Row(
children: [
Text(
S.current.localization_info_language,
style: const TextStyle(fontSize: 16),
),
Text(S.current.localization_info_language, style: const TextStyle(fontSize: 16)),
const SizedBox(width: 12),
ComboBox<String>(
value: state.selectedLanguage,
items: [
for (final lang in LocalizationUIModel.languageSupport.entries)
ComboBoxItem(
value: lang.key,
child: Text(lang.value),
)
ComboBoxItem(value: lang.key, child: Text(lang.value)),
],
onChanged: state.workingVersion.isNotEmpty
? null
: (v) {
if (v == null) return;
model.selectLang(v);
},
)
if (v == null) return;
model.selectLang(v);
},
),
const SizedBox(width: 12),
const Text("频道选择", style: TextStyle(fontSize: 16)),
const SizedBox(width: 12),
ComboBox<String>(
value: state.selectedChannel,
items: const [
ComboBoxItem(value: "LIVE", child: Text("LIVE")),
ComboBoxItem(value: "PTU", child: Text("PTU")),
],
onChanged: state.workingVersion.isNotEmpty
? null
: (v) {
if (v == null) return;
model.selectChannel(v);
},
),
],
),
),
const SizedBox(width: 12),
Button(
onPressed: model.doRefresh(),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.refresh),
)),
onPressed: model.doRefresh(),
child: const Padding(padding: EdgeInsets.all(6), child: Icon(FluentIcons.refresh)),
),
],
);
}
@@ -400,85 +305,77 @@ class LocalizationDialogUI extends HookConsumerWidget {
Widget makeToolsListContainer(BuildContext context, LocalizationUIModel model, LocalizationUIState state) {
final toolsMenu = {
"launcher_mod": (
const Icon(FluentIcons.c_plus_plus, size: 24),
(S.current.home_localization_action_rsi_launcher_localization),
),
"advanced": (
const Icon(FluentIcons.queue_advanced, size: 24),
(S.current.home_localization_action_advanced),
const Icon(FluentIcons.c_plus_plus, size: 24),
(S.current.home_localization_action_rsi_launcher_localization),
),
"advanced": (const Icon(FluentIcons.queue_advanced, size: 24), (S.current.home_localization_action_advanced)),
"custom_files": (
const Icon(FluentIcons.custom_activity, size: 24),
(S.current.home_localization_action_install_customize),
const Icon(FluentIcons.custom_activity, size: 24),
(S.current.home_localization_action_install_customize),
),
};
final enableTap = state.workingVersion.isEmpty;
return makeListContainer(
S.current.home_localization_title_localization_tools,
[
for (final item in toolsMenu.entries)
Tilt(
disable: !enableTap,
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(7),
child: GestureDetector(
onTap: enableTap
? () async {
switch (item.key) {
case "launcher_mod":
ToolsUIModel.rsiEnhance(context);
break;
case "advanced":
context.push("/index/advanced_localization");
break;
case "custom_files":
final sb = await showDialog(
context: context,
builder: (BuildContext context) => const LocalizationFromFileDialogUI(),
);
if (sb is (StringBuffer, bool)) {
if (!context.mounted) return;
await model.installFormString(
sb.$1,
S.current.localization_info_custom_files,
isEnableCommunityInputMethod: sb.$2,
context: context,
);
S.current.home_localization_title_localization_tools,
[
for (final item in toolsMenu.entries)
Tilt(
disable: !enableTap,
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(7),
child: GestureDetector(
onTap: enableTap
? () async {
switch (item.key) {
case "launcher_mod":
ToolsUIModel.rsiEnhance(context);
break;
case "advanced":
context.push("/advanced_localization");
break;
case "custom_files":
final sb = await showDialog(
context: context,
builder: (BuildContext context) => const LocalizationFromFileDialogUI(),
);
if (sb is (StringBuffer, bool)) {
if (!context.mounted) return;
await model.installFormString(
sb.$1,
S.current.localization_info_custom_files,
isEnableCommunityInputMethod: sb.$2,
context: context,
);
}
break;
}
break;
}
}
: null,
child: Container(
decoration: BoxDecoration(
color: FluentTheme
.of(context)
.cardColor,
borderRadius: BorderRadius.circular(7),
),
padding: const EdgeInsets.all(12),
child: Row(
children: [
item.value.$1,
const SizedBox(width: 12),
Text(item.value.$2),
const SizedBox(width: 12),
const Spacer(),
Icon(
FluentIcons.chevron_right,
size: 14,
color: Colors.white.withValues(alpha: .6),
)
],
),
}
: null,
child: Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(7),
),
padding: const EdgeInsets.all(12),
child: Row(
children: [
item.value.$1,
const SizedBox(width: 12),
Text(item.value.$2),
const SizedBox(width: 12),
const Spacer(),
Icon(FluentIcons.chevron_right, size: 14, color: Colors.white.withValues(alpha: .6)),
],
),
),
),
],
context,
gridViewMode: true,
gridViewCrossAxisCount: 3);
),
],
context,
gridViewMode: true,
gridViewCrossAxisCount: 3,
);
}
}

View File

@@ -2,10 +2,11 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:archive/archive_io.dart';
import 'package:archive/archive.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/foundation.dart' show kIsWeb, compute;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_ce/hive.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -23,8 +24,8 @@ import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/ui/tools/dialogs/vehicle_sorting_dialog_ui.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:starcitizen_doctor/common/rust/api/win32_api.dart' as win32;
import 'dart:js_interop';
import 'package:web/web.dart' as web;
part 'localization_ui_model.g.dart';
@@ -34,6 +35,7 @@ part 'localization_ui_model.freezed.dart';
abstract class LocalizationUIState with _$LocalizationUIState {
factory LocalizationUIState({
String? selectedLanguage,
@Default("LIVE") String selectedChannel,
String? installedCommunityInputMethodSupportVersion,
InputMethodApiLanguageData? communityInputMethodLanguageData,
Map<String, ScLocalizationData>? apiLocalizationData,
@@ -46,10 +48,7 @@ abstract class LocalizationUIState with _$LocalizationUIState {
@riverpod
class LocalizationUIModel extends _$LocalizationUIModel {
static const languageSupport = {
"chinese_(simplified)": NoL10n.langZHS,
"chinese_(traditional)": NoL10n.langZHT,
};
static const languageSupport = {"chinese_(simplified)": NoL10n.langZHS, "chinese_(traditional)": NoL10n.langZHT};
Directory get _downloadDir => Directory("${appGlobalState.applicationSupportDir}\\Localizations");
@@ -71,7 +70,7 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<void> _init() async {
if (_scInstallPath == "not_install") {
if (!kIsWeb && _scInstallPath == "not_install") {
return;
}
ref.onDispose(() {
@@ -108,7 +107,16 @@ class LocalizationUIModel extends _$LocalizationUIModel {
Future<void> _loadData() async {
_allVersionLocalizationData.clear();
await _updateStatus();
if (!kIsWeb) {
await _updateStatus();
} else {
await Future.delayed(Duration(milliseconds: 100));
state = state.copyWith(
patchStatus: MapEntry(false, S.current.home_action_info_game_built_in),
isInstalledAdvanced: false,
installedCommunityInputMethodSupportVersion: null,
);
}
await _loadCommunityInputMethodData();
for (var lang in languageSupport.keys) {
final l = await Api.getScLocalizationData(lang).unwrap();
@@ -116,7 +124,7 @@ class LocalizationUIModel extends _$LocalizationUIModel {
if (lang == state.selectedLanguage) {
final apiLocalizationData = <String, ScLocalizationData>{};
for (var element in l) {
final isPTU = !_scInstallPath.contains("LIVE");
final isPTU = !state.selectedChannel.contains("LIVE");
if (isPTU && element.gameChannel == "PTU") {
apiLocalizationData[element.versionName ?? ""] = element;
} else if (!isPTU && element.gameChannel == "PU") {
@@ -135,14 +143,20 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
void checkUserCfg(BuildContext context) async {
// Web 版本不支持此功能
if (kIsWeb) return;
final userCfgFile = File("$_scInstallPath\\USER.cfg");
if (await userCfgFile.exists()) {
final cfgString = await userCfgFile.readAsString();
if (cfgString.contains("g_language") && !cfgString.contains("g_language=${state.selectedLanguage}")) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(context, S.current.localization_info_remove_incompatible_translation_params,
Text(S.current.localization_info_incompatible_translation_params_warning),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35));
final ok = await showConfirmDialogs(
context,
S.current.localization_info_remove_incompatible_translation_params,
Text(S.current.localization_info_incompatible_translation_params_warning),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35),
);
if (ok == true) {
var finalString = "";
for (var item in cfgString.split("\n")) {
@@ -160,6 +174,9 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<void> updateLangCfg(bool enable) async {
// Web 版本不支持此功能
if (kIsWeb) return;
final selectedLanguage = state.selectedLanguage!;
final status = await _getLangCfgEnableLang(lang: selectedLanguage);
final exists = await _cfgFile.exists();
@@ -216,7 +233,25 @@ class LocalizationUIModel extends _$LocalizationUIModel {
launchUrlString(URLConf.feedbackUrl);
}
Future<String> genLangCfg() async {
final selectedLanguage = state.selectedLanguage!;
StringBuffer newStr = StringBuffer();
if (!newStr.toString().contains("sys_languages=$selectedLanguage")) {
newStr.writeln("sys_languages=$selectedLanguage");
}
if (!newStr.toString().contains("g_language=$selectedLanguage")) {
newStr.writeln("g_language=$selectedLanguage");
}
if (!newStr.toString().contains("g_languageAudio")) {
newStr.writeln("g_languageAudio=english");
}
return newStr.toString();
}
VoidCallback? doDelIniFile() {
// Web 版本不支持此功能
if (kIsWeb) return null;
return () async {
final iniFile = File("${_scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini");
if (await iniFile.exists()) await iniFile.delete();
@@ -238,7 +273,7 @@ class LocalizationUIModel extends _$LocalizationUIModel {
BuildContext? context,
}) async {
dPrint("LocalizationUIModel -> installFormString $versionName");
final iniFile = File("${_scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini");
if (versionName.isNotEmpty) {
if (!globalIni.toString().endsWith("\n")) {
globalIni.write("\n");
@@ -258,8 +293,9 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
if (communityInputMethodVersion != null) {
globalIni
.write("_starcitizen_doctor_localization_community_input_method_version=$communityInputMethodVersion\n");
globalIni.write(
"_starcitizen_doctor_localization_community_input_method_version=$communityInputMethodVersion\n",
);
}
if (communityInputMethodSupportData != null) {
for (var line in communityInputMethodSupportData.split("\n")) {
@@ -274,17 +310,29 @@ class LocalizationUIModel extends _$LocalizationUIModel {
var iniStringData = globalIni.toString().trim();
// 载具排序对话框 - 在生成最终文件前执行
if ((context?.mounted ?? false) && isEnableVehicleSorting) {
if (!context!.mounted) return;
final iniStringDataVN = ValueNotifier(iniStringData);
final ok = await showConfirmDialogs(context, S.current.tools_vehicle_sorting_title, VehicleSortingDialogUi(iniStringData: iniStringDataVN),constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .75,
));
final ok = await showConfirmDialogs(
context,
S.current.tools_vehicle_sorting_title,
VehicleSortingDialogUi(iniStringData: iniStringDataVN),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .75),
);
if (ok) {
iniStringData = iniStringDataVN.value;
}
}
// Web 版本生成下载文件
if (kIsWeb) {
await _generateAndDownloadWebFile(iniStringData, versionName);
return;
}
final iniFile = File("${_scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini");
/// write ini
if (await iniFile.exists()) {
await iniFile.delete();
@@ -295,6 +343,28 @@ class LocalizationUIModel extends _$LocalizationUIModel {
await _updateStatus();
}
Future<void> _generateAndDownloadWebFile(String iniStringData, String versionName) async {
final selectedLanguage = state.selectedLanguage!;
final iniFileString = "\uFEFF$iniStringData";
final cfg = await genLangCfg();
final archive = Archive();
final iniFileBytes = utf8.encode(iniFileString);
final cfgBytes = utf8.encode(cfg);
archive.addFile(ArchiveFile("data/Localization/$selectedLanguage/global.ini", iniFileBytes.length, iniFileBytes));
archive.addFile(ArchiveFile("data/system.cfg", cfgBytes.length, cfgBytes));
final zip = await compute(_encodeZipFile, archive);
if (zip == null) return;
final blob = Blob.fromBytes(zip, opt: {"type": "application/zip"});
final url = web.URL.createObjectURL(blob);
await Future.delayed(const Duration(seconds: 1));
jsDownloadBlobFile(url, "Localization_$versionName.zip");
}
List<int>? _encodeZipFile(Archive archive) {
final zip = ZipEncoder().encode(archive);
return zip;
}
Future<Map<String, String>?> getCommunityInputMethodSupportData() async {
final iniPath = "${_scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini";
final iniFile = File(iniPath);
@@ -348,10 +418,41 @@ class LocalizationUIModel extends _$LocalizationUIModel {
return ("", "");
}
Future? doRemoteInstall(BuildContext context, ScLocalizationData value,
{bool isEnableCommunityInputMethod = false, bool isEnableVehicleSorting = false}) async {
Future? doRemoteInstall(
BuildContext context,
ScLocalizationData value, {
bool isEnableCommunityInputMethod = false,
bool isEnableVehicleSorting = false,
}) async {
AnalyticsApi.touch("install_localization");
// Web 版本直接下载并生成 ZIP
if (kIsWeb) {
try {
state = state.copyWith(workingVersion: value.versionName!);
final data = await downloadLocalizationFileForWeb(value);
await Future.delayed(const Duration(milliseconds: 300));
// check file
final globalIni = await compute(readArchiveFromBytes, data);
if (globalIni.isEmpty) {
throw S.current.localization_info_corrupted_file;
}
if (!context.mounted) return;
await installFormString(
globalIni,
value.versionName ?? "",
isEnableCommunityInputMethod: isEnableCommunityInputMethod,
isEnableVehicleSorting: isEnableVehicleSorting,
context: context,
);
} catch (e) {
if (!context.mounted) return;
await showToast(context, S.current.localization_info_installation_error(e));
}
state = state.copyWith(workingVersion: "");
return;
}
final savePath = File("${_downloadDir.absolute.path}\\${value.versionName}.sclang");
try {
state = state.copyWith(workingVersion: value.versionName!);
@@ -385,7 +486,8 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<String> downloadOrGetCachedCommunityInputMethodSupportFile(
InputMethodApiLanguageData communityInputMethodData) async {
InputMethodApiLanguageData communityInputMethodData,
) async {
final lang = state.selectedLanguage ?? "_";
final box = await Hive.openBox("community_input_method_data");
final cachedVersion = box.get("${lang}_version");
@@ -410,6 +512,18 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
}
// Web 版本下载方法,返回字节数据
Future<Uint8List> downloadLocalizationFileForWeb(ScLocalizationData value) async {
dPrint("downloading file for web");
final downloadUrl = "${URLConf.gitlabLocalizationUrl}/archive/${value.versionName}.tar.gz";
final r = await RSHttp.get(downloadUrl);
if (r.statusCode == 200 && r.data != null) {
return r.data!;
} else {
throw "statusCode Error : ${r.statusCode}";
}
}
static StringBuffer readArchive(String savePath) {
final inputStream = InputFileStream(savePath);
final output = GZipDecoder().decodeBytes(inputStream.toUint8List());
@@ -429,6 +543,25 @@ class LocalizationUIModel extends _$LocalizationUIModel {
return globalIni;
}
// Web 版本从字节数据读取 Archive
static StringBuffer readArchiveFromBytes(Uint8List data) {
final output = GZipDecoder().decodeBytes(data);
final archive = TarDecoder().decodeBytes(output);
StringBuffer globalIni = StringBuffer("");
for (var element in archive.files) {
if (element.name.contains("global.ini")) {
if (element.rawContent == null) continue;
final stringContent = utf8.decode(element.rawContent!.readBytes());
for (var value in (stringContent).split("\n")) {
final tv = value.trim();
if (tv.isNotEmpty) globalIni.writeln(value);
}
}
}
archive.clear();
return globalIni;
}
String? getScInstallPath() {
return ref.read(homeUIModelProvider).scInstalledPath;
}
@@ -440,6 +573,13 @@ class LocalizationUIModel extends _$LocalizationUIModel {
await appConfBox.put("localization_selectedLanguage", v);
}
void selectChannel(String v) async {
state = state.copyWith(selectedChannel: v);
_loadData();
final appConfBox = await Hive.openBox("app_conf");
await appConfBox.put("localization_selectedChannel", v);
}
VoidCallback? onBack(BuildContext context) {
if (state.workingVersion.isNotEmpty) return null;
return () {
@@ -456,14 +596,20 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<void> _updateStatus() async {
// Web 版本不支持此功能
if (kIsWeb) return;
final iniPath = "${_scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini";
final patchStatus =
MapEntry(await _getLangCfgEnableLang(lang: state.selectedLanguage!), await _getInstalledIniVersion(iniPath));
final patchStatus = MapEntry(
await _getLangCfgEnableLang(lang: state.selectedLanguage!),
await _getInstalledIniVersion(iniPath),
);
final isInstalledAdvanced = await _checkAdvancedStatus(iniPath);
final installedCommunityInputMethodSupportVersion = await getInstalledCommunityInputMethodSupportVersion(iniPath);
dPrint(
"_updateStatus updateStatus: $patchStatus , isInstalledAdvanced: $isInstalledAdvanced ,installedCommunityInputMethodSupportVersion: $installedCommunityInputMethodSupportVersion");
"_updateStatus updateStatus: $patchStatus , isInstalledAdvanced: $isInstalledAdvanced ,installedCommunityInputMethodSupportVersion: $installedCommunityInputMethodSupportVersion",
);
state = state.copyWith(
patchStatus: patchStatus,
@@ -473,6 +619,9 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<String?> getInstalledCommunityInputMethodSupportVersion(String path) async {
// Web 版本不支持此功能
if (kIsWeb) return null;
final iniFile = File(path);
if (!await iniFile.exists()) {
return null;
@@ -488,6 +637,9 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<bool> _checkAdvancedStatus(String path) async {
// Web 版本不支持此功能
if (kIsWeb) return false;
final iniFile = File(path);
if (!await iniFile.exists()) {
return false;
@@ -497,6 +649,9 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<bool> _getLangCfgEnableLang({String lang = "", String gamePath = ""}) async {
// Web 版本不支持此功能
if (kIsWeb) return false;
if (gamePath.isEmpty) {
gamePath = _scInstallPath;
}
@@ -509,6 +664,9 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
static Future<String> _getInstalledIniVersion(String iniPath) async {
// Web 版本不支持此功能
if (kIsWeb) return S.current.home_action_info_game_built_in;
final iniFile = File(iniPath);
if (!await iniFile.exists()) {
return S.current.home_action_info_game_built_in;
@@ -524,6 +682,9 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<List<String>> checkLangUpdate({bool skipReload = false}) async {
// Web 版本不支持此功能
if (kIsWeb) return [];
if (_scInstallPath == "not_install") {
return [];
}
@@ -571,6 +732,7 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<void> checkCommunityInputMethodUpdate() async {
if (kIsWeb) return; // Web 版本不支持自动更新检查
final cloudVersion = state.communityInputMethodLanguageData?.version;
final localVersion = state.installedCommunityInputMethodSupportVersion;
if (cloudVersion == null || localVersion == null) return;
@@ -582,11 +744,14 @@ class LocalizationUIModel extends _$LocalizationUIModel {
return;
}
await installFormString(StringBuffer(localIniString), versioName, isEnableCommunityInputMethod: true);
await win32.sendNotify(
summary: S.current.input_method_support_updated,
body: S.current.input_method_support_updated_to_version(cloudVersion),
appName: S.current.home_title_app_name,
appId: ConstConf.win32AppId);
if (!kIsWeb) {
// Web 版本不支持系统通知
// await win32.sendNotify(
// summary: S.current.input_method_support_updated,
// body: S.current.input_method_support_updated_to_version(cloudVersion),
// appName: S.current.home_title_app_name,
// appId: ConstConf.win32AppId);
}
}
}
@@ -595,7 +760,10 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
Future<void> onRemoteInsTall(
BuildContext context, MapEntry<String, ScLocalizationData> item, LocalizationUIState state) async {
BuildContext context,
MapEntry<String, ScLocalizationData> item,
LocalizationUIState state,
) async {
final appBox = Hive.box("app_conf");
bool enableCommunityInputMethod = state.communityInputMethodLanguageData != null;
bool isEnableVehicleSorting = appBox.get("vehicle_sorting", defaultValue: false) ?? false;
@@ -646,9 +814,7 @@ class LocalizationUIModel extends _$LocalizationUIModel {
SizedBox(height: 12),
Row(
children: [
Text(
S.current.input_method_install_community_input_method_support,
),
Text(S.current.input_method_install_community_input_method_support),
Spacer(),
StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
@@ -662,15 +828,13 @@ class LocalizationUIModel extends _$LocalizationUIModel {
},
);
},
)
),
],
),
SizedBox(height: 12),
Row(
children: [
Text(
S.current.tools_vehicle_sorting_title,
),
Text(S.current.tools_vehicle_sorting_title),
Spacer(),
StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
@@ -683,7 +847,7 @@ class LocalizationUIModel extends _$LocalizationUIModel {
},
);
},
)
),
],
),
],
@@ -717,4 +881,21 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}
}
}
}
}
// Web 平台支持 - JS 互操作
@JS("Blob")
extension type Blob._(JSObject _) implements JSObject {
external factory Blob(JSArray<JSArrayBuffer> blobParts, JSAny? options);
factory Blob.fromBytes(List<int> bytes, {Map? opt}) {
final data = Uint8List.fromList(bytes).buffer.toJS;
return Blob([data].toJS, opt?.jsify());
}
external JSArrayBuffer? get blobParts;
external JSObject? get options;
}
@JS()
external void jsDownloadBlobFile(String blobUrl, String filename);

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LocalizationUIState {
String? get selectedLanguage; String? get installedCommunityInputMethodSupportVersion; InputMethodApiLanguageData? get communityInputMethodLanguageData; Map<String, ScLocalizationData>? get apiLocalizationData; String get workingVersion; MapEntry<bool, String>? get patchStatus; bool? get isInstalledAdvanced; List<String>? get customizeList;
String? get selectedLanguage; String get selectedChannel; String? get installedCommunityInputMethodSupportVersion; InputMethodApiLanguageData? get communityInputMethodLanguageData; Map<String, ScLocalizationData>? get apiLocalizationData; String get workingVersion; MapEntry<bool, String>? get patchStatus; bool? get isInstalledAdvanced; List<String>? get customizeList;
/// Create a copy of LocalizationUIState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LocalizationUIStateCopyWith<LocalizationUIState> get copyWith => _$Localization
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LocalizationUIState&&(identical(other.selectedLanguage, selectedLanguage) || other.selectedLanguage == selectedLanguage)&&(identical(other.installedCommunityInputMethodSupportVersion, installedCommunityInputMethodSupportVersion) || other.installedCommunityInputMethodSupportVersion == installedCommunityInputMethodSupportVersion)&&(identical(other.communityInputMethodLanguageData, communityInputMethodLanguageData) || other.communityInputMethodLanguageData == communityInputMethodLanguageData)&&const DeepCollectionEquality().equals(other.apiLocalizationData, apiLocalizationData)&&(identical(other.workingVersion, workingVersion) || other.workingVersion == workingVersion)&&(identical(other.patchStatus, patchStatus) || other.patchStatus == patchStatus)&&(identical(other.isInstalledAdvanced, isInstalledAdvanced) || other.isInstalledAdvanced == isInstalledAdvanced)&&const DeepCollectionEquality().equals(other.customizeList, customizeList));
return identical(this, other) || (other.runtimeType == runtimeType&&other is LocalizationUIState&&(identical(other.selectedLanguage, selectedLanguage) || other.selectedLanguage == selectedLanguage)&&(identical(other.selectedChannel, selectedChannel) || other.selectedChannel == selectedChannel)&&(identical(other.installedCommunityInputMethodSupportVersion, installedCommunityInputMethodSupportVersion) || other.installedCommunityInputMethodSupportVersion == installedCommunityInputMethodSupportVersion)&&(identical(other.communityInputMethodLanguageData, communityInputMethodLanguageData) || other.communityInputMethodLanguageData == communityInputMethodLanguageData)&&const DeepCollectionEquality().equals(other.apiLocalizationData, apiLocalizationData)&&(identical(other.workingVersion, workingVersion) || other.workingVersion == workingVersion)&&(identical(other.patchStatus, patchStatus) || other.patchStatus == patchStatus)&&(identical(other.isInstalledAdvanced, isInstalledAdvanced) || other.isInstalledAdvanced == isInstalledAdvanced)&&const DeepCollectionEquality().equals(other.customizeList, customizeList));
}
@override
int get hashCode => Object.hash(runtimeType,selectedLanguage,installedCommunityInputMethodSupportVersion,communityInputMethodLanguageData,const DeepCollectionEquality().hash(apiLocalizationData),workingVersion,patchStatus,isInstalledAdvanced,const DeepCollectionEquality().hash(customizeList));
int get hashCode => Object.hash(runtimeType,selectedLanguage,selectedChannel,installedCommunityInputMethodSupportVersion,communityInputMethodLanguageData,const DeepCollectionEquality().hash(apiLocalizationData),workingVersion,patchStatus,isInstalledAdvanced,const DeepCollectionEquality().hash(customizeList));
@override
String toString() {
return 'LocalizationUIState(selectedLanguage: $selectedLanguage, installedCommunityInputMethodSupportVersion: $installedCommunityInputMethodSupportVersion, communityInputMethodLanguageData: $communityInputMethodLanguageData, apiLocalizationData: $apiLocalizationData, workingVersion: $workingVersion, patchStatus: $patchStatus, isInstalledAdvanced: $isInstalledAdvanced, customizeList: $customizeList)';
return 'LocalizationUIState(selectedLanguage: $selectedLanguage, selectedChannel: $selectedChannel, installedCommunityInputMethodSupportVersion: $installedCommunityInputMethodSupportVersion, communityInputMethodLanguageData: $communityInputMethodLanguageData, apiLocalizationData: $apiLocalizationData, workingVersion: $workingVersion, patchStatus: $patchStatus, isInstalledAdvanced: $isInstalledAdvanced, customizeList: $customizeList)';
}
@@ -45,7 +45,7 @@ abstract mixin class $LocalizationUIStateCopyWith<$Res> {
factory $LocalizationUIStateCopyWith(LocalizationUIState value, $Res Function(LocalizationUIState) _then) = _$LocalizationUIStateCopyWithImpl;
@useResult
$Res call({
String? selectedLanguage, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList
String? selectedLanguage, String selectedChannel, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList
});
@@ -62,10 +62,11 @@ class _$LocalizationUIStateCopyWithImpl<$Res>
/// Create a copy of LocalizationUIState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? selectedLanguage = freezed,Object? installedCommunityInputMethodSupportVersion = freezed,Object? communityInputMethodLanguageData = freezed,Object? apiLocalizationData = freezed,Object? workingVersion = null,Object? patchStatus = freezed,Object? isInstalledAdvanced = freezed,Object? customizeList = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? selectedLanguage = freezed,Object? selectedChannel = null,Object? installedCommunityInputMethodSupportVersion = freezed,Object? communityInputMethodLanguageData = freezed,Object? apiLocalizationData = freezed,Object? workingVersion = null,Object? patchStatus = freezed,Object? isInstalledAdvanced = freezed,Object? customizeList = freezed,}) {
return _then(_self.copyWith(
selectedLanguage: freezed == selectedLanguage ? _self.selectedLanguage : selectedLanguage // ignore: cast_nullable_to_non_nullable
as String?,installedCommunityInputMethodSupportVersion: freezed == installedCommunityInputMethodSupportVersion ? _self.installedCommunityInputMethodSupportVersion : installedCommunityInputMethodSupportVersion // ignore: cast_nullable_to_non_nullable
as String?,selectedChannel: null == selectedChannel ? _self.selectedChannel : selectedChannel // ignore: cast_nullable_to_non_nullable
as String,installedCommunityInputMethodSupportVersion: freezed == installedCommunityInputMethodSupportVersion ? _self.installedCommunityInputMethodSupportVersion : installedCommunityInputMethodSupportVersion // ignore: cast_nullable_to_non_nullable
as String?,communityInputMethodLanguageData: freezed == communityInputMethodLanguageData ? _self.communityInputMethodLanguageData : communityInputMethodLanguageData // ignore: cast_nullable_to_non_nullable
as InputMethodApiLanguageData?,apiLocalizationData: freezed == apiLocalizationData ? _self.apiLocalizationData : apiLocalizationData // ignore: cast_nullable_to_non_nullable
as Map<String, ScLocalizationData>?,workingVersion: null == workingVersion ? _self.workingVersion : workingVersion // ignore: cast_nullable_to_non_nullable
@@ -157,10 +158,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? selectedLanguage, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? selectedLanguage, String selectedChannel, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LocalizationUIState() when $default != null:
return $default(_that.selectedLanguage,_that.installedCommunityInputMethodSupportVersion,_that.communityInputMethodLanguageData,_that.apiLocalizationData,_that.workingVersion,_that.patchStatus,_that.isInstalledAdvanced,_that.customizeList);case _:
return $default(_that.selectedLanguage,_that.selectedChannel,_that.installedCommunityInputMethodSupportVersion,_that.communityInputMethodLanguageData,_that.apiLocalizationData,_that.workingVersion,_that.patchStatus,_that.isInstalledAdvanced,_that.customizeList);case _:
return orElse();
}
@@ -178,10 +179,10 @@ return $default(_that.selectedLanguage,_that.installedCommunityInputMethodSuppor
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? selectedLanguage, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? selectedLanguage, String selectedChannel, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList) $default,) {final _that = this;
switch (_that) {
case _LocalizationUIState():
return $default(_that.selectedLanguage,_that.installedCommunityInputMethodSupportVersion,_that.communityInputMethodLanguageData,_that.apiLocalizationData,_that.workingVersion,_that.patchStatus,_that.isInstalledAdvanced,_that.customizeList);case _:
return $default(_that.selectedLanguage,_that.selectedChannel,_that.installedCommunityInputMethodSupportVersion,_that.communityInputMethodLanguageData,_that.apiLocalizationData,_that.workingVersion,_that.patchStatus,_that.isInstalledAdvanced,_that.customizeList);case _:
throw StateError('Unexpected subclass');
}
@@ -198,10 +199,10 @@ return $default(_that.selectedLanguage,_that.installedCommunityInputMethodSuppor
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? selectedLanguage, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? selectedLanguage, String selectedChannel, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList)? $default,) {final _that = this;
switch (_that) {
case _LocalizationUIState() when $default != null:
return $default(_that.selectedLanguage,_that.installedCommunityInputMethodSupportVersion,_that.communityInputMethodLanguageData,_that.apiLocalizationData,_that.workingVersion,_that.patchStatus,_that.isInstalledAdvanced,_that.customizeList);case _:
return $default(_that.selectedLanguage,_that.selectedChannel,_that.installedCommunityInputMethodSupportVersion,_that.communityInputMethodLanguageData,_that.apiLocalizationData,_that.workingVersion,_that.patchStatus,_that.isInstalledAdvanced,_that.customizeList);case _:
return null;
}
@@ -213,10 +214,11 @@ return $default(_that.selectedLanguage,_that.installedCommunityInputMethodSuppor
class _LocalizationUIState implements LocalizationUIState {
_LocalizationUIState({this.selectedLanguage, this.installedCommunityInputMethodSupportVersion, this.communityInputMethodLanguageData, final Map<String, ScLocalizationData>? apiLocalizationData, this.workingVersion = "", this.patchStatus, this.isInstalledAdvanced, final List<String>? customizeList}): _apiLocalizationData = apiLocalizationData,_customizeList = customizeList;
_LocalizationUIState({this.selectedLanguage, this.selectedChannel = "LIVE", this.installedCommunityInputMethodSupportVersion, this.communityInputMethodLanguageData, final Map<String, ScLocalizationData>? apiLocalizationData, this.workingVersion = "", this.patchStatus, this.isInstalledAdvanced, final List<String>? customizeList}): _apiLocalizationData = apiLocalizationData,_customizeList = customizeList;
@override final String? selectedLanguage;
@override@JsonKey() final String selectedChannel;
@override final String? installedCommunityInputMethodSupportVersion;
@override final InputMethodApiLanguageData? communityInputMethodLanguageData;
final Map<String, ScLocalizationData>? _apiLocalizationData;
@@ -251,16 +253,16 @@ _$LocalizationUIStateCopyWith<_LocalizationUIState> get copyWith => __$Localizat
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocalizationUIState&&(identical(other.selectedLanguage, selectedLanguage) || other.selectedLanguage == selectedLanguage)&&(identical(other.installedCommunityInputMethodSupportVersion, installedCommunityInputMethodSupportVersion) || other.installedCommunityInputMethodSupportVersion == installedCommunityInputMethodSupportVersion)&&(identical(other.communityInputMethodLanguageData, communityInputMethodLanguageData) || other.communityInputMethodLanguageData == communityInputMethodLanguageData)&&const DeepCollectionEquality().equals(other._apiLocalizationData, _apiLocalizationData)&&(identical(other.workingVersion, workingVersion) || other.workingVersion == workingVersion)&&(identical(other.patchStatus, patchStatus) || other.patchStatus == patchStatus)&&(identical(other.isInstalledAdvanced, isInstalledAdvanced) || other.isInstalledAdvanced == isInstalledAdvanced)&&const DeepCollectionEquality().equals(other._customizeList, _customizeList));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocalizationUIState&&(identical(other.selectedLanguage, selectedLanguage) || other.selectedLanguage == selectedLanguage)&&(identical(other.selectedChannel, selectedChannel) || other.selectedChannel == selectedChannel)&&(identical(other.installedCommunityInputMethodSupportVersion, installedCommunityInputMethodSupportVersion) || other.installedCommunityInputMethodSupportVersion == installedCommunityInputMethodSupportVersion)&&(identical(other.communityInputMethodLanguageData, communityInputMethodLanguageData) || other.communityInputMethodLanguageData == communityInputMethodLanguageData)&&const DeepCollectionEquality().equals(other._apiLocalizationData, _apiLocalizationData)&&(identical(other.workingVersion, workingVersion) || other.workingVersion == workingVersion)&&(identical(other.patchStatus, patchStatus) || other.patchStatus == patchStatus)&&(identical(other.isInstalledAdvanced, isInstalledAdvanced) || other.isInstalledAdvanced == isInstalledAdvanced)&&const DeepCollectionEquality().equals(other._customizeList, _customizeList));
}
@override
int get hashCode => Object.hash(runtimeType,selectedLanguage,installedCommunityInputMethodSupportVersion,communityInputMethodLanguageData,const DeepCollectionEquality().hash(_apiLocalizationData),workingVersion,patchStatus,isInstalledAdvanced,const DeepCollectionEquality().hash(_customizeList));
int get hashCode => Object.hash(runtimeType,selectedLanguage,selectedChannel,installedCommunityInputMethodSupportVersion,communityInputMethodLanguageData,const DeepCollectionEquality().hash(_apiLocalizationData),workingVersion,patchStatus,isInstalledAdvanced,const DeepCollectionEquality().hash(_customizeList));
@override
String toString() {
return 'LocalizationUIState(selectedLanguage: $selectedLanguage, installedCommunityInputMethodSupportVersion: $installedCommunityInputMethodSupportVersion, communityInputMethodLanguageData: $communityInputMethodLanguageData, apiLocalizationData: $apiLocalizationData, workingVersion: $workingVersion, patchStatus: $patchStatus, isInstalledAdvanced: $isInstalledAdvanced, customizeList: $customizeList)';
return 'LocalizationUIState(selectedLanguage: $selectedLanguage, selectedChannel: $selectedChannel, installedCommunityInputMethodSupportVersion: $installedCommunityInputMethodSupportVersion, communityInputMethodLanguageData: $communityInputMethodLanguageData, apiLocalizationData: $apiLocalizationData, workingVersion: $workingVersion, patchStatus: $patchStatus, isInstalledAdvanced: $isInstalledAdvanced, customizeList: $customizeList)';
}
@@ -271,7 +273,7 @@ abstract mixin class _$LocalizationUIStateCopyWith<$Res> implements $Localizatio
factory _$LocalizationUIStateCopyWith(_LocalizationUIState value, $Res Function(_LocalizationUIState) _then) = __$LocalizationUIStateCopyWithImpl;
@override @useResult
$Res call({
String? selectedLanguage, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList
String? selectedLanguage, String selectedChannel, String? installedCommunityInputMethodSupportVersion, InputMethodApiLanguageData? communityInputMethodLanguageData, Map<String, ScLocalizationData>? apiLocalizationData, String workingVersion, MapEntry<bool, String>? patchStatus, bool? isInstalledAdvanced, List<String>? customizeList
});
@@ -288,10 +290,11 @@ class __$LocalizationUIStateCopyWithImpl<$Res>
/// Create a copy of LocalizationUIState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? selectedLanguage = freezed,Object? installedCommunityInputMethodSupportVersion = freezed,Object? communityInputMethodLanguageData = freezed,Object? apiLocalizationData = freezed,Object? workingVersion = null,Object? patchStatus = freezed,Object? isInstalledAdvanced = freezed,Object? customizeList = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? selectedLanguage = freezed,Object? selectedChannel = null,Object? installedCommunityInputMethodSupportVersion = freezed,Object? communityInputMethodLanguageData = freezed,Object? apiLocalizationData = freezed,Object? workingVersion = null,Object? patchStatus = freezed,Object? isInstalledAdvanced = freezed,Object? customizeList = freezed,}) {
return _then(_LocalizationUIState(
selectedLanguage: freezed == selectedLanguage ? _self.selectedLanguage : selectedLanguage // ignore: cast_nullable_to_non_nullable
as String?,installedCommunityInputMethodSupportVersion: freezed == installedCommunityInputMethodSupportVersion ? _self.installedCommunityInputMethodSupportVersion : installedCommunityInputMethodSupportVersion // ignore: cast_nullable_to_non_nullable
as String?,selectedChannel: null == selectedChannel ? _self.selectedChannel : selectedChannel // ignore: cast_nullable_to_non_nullable
as String,installedCommunityInputMethodSupportVersion: freezed == installedCommunityInputMethodSupportVersion ? _self.installedCommunityInputMethodSupportVersion : installedCommunityInputMethodSupportVersion // ignore: cast_nullable_to_non_nullable
as String?,communityInputMethodLanguageData: freezed == communityInputMethodLanguageData ? _self.communityInputMethodLanguageData : communityInputMethodLanguageData // ignore: cast_nullable_to_non_nullable
as InputMethodApiLanguageData?,apiLocalizationData: freezed == apiLocalizationData ? _self._apiLocalizationData : apiLocalizationData // ignore: cast_nullable_to_non_nullable
as Map<String, ScLocalizationData>?,workingVersion: null == workingVersion ? _self.workingVersion : workingVersion // ignore: cast_nullable_to_non_nullable

View File

@@ -42,7 +42,7 @@ final class LocalizationUIModelProvider
}
String _$localizationUIModelHash() =>
r'd3797a7ff3d31dd1d4b05aed4a9969f4be6853c5';
r'93c4172a4048a083e941a9011067dc8b17c33ac5';
abstract class _$LocalizationUIModel extends $Notifier<LocalizationUIState> {
LocalizationUIState build();

View File

@@ -1,4 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
@@ -30,12 +31,8 @@ class HomePerformanceUI extends HookConsumerWidget {
children: [
if (state.showGraphicsPerformanceTip)
InfoBar(
title: Text(S.current
.performance_info_graphic_optimization_hint),
content: Text(
S.current
.performance_info_graphic_optimization_warning,
),
title: Text(S.current.performance_info_graphic_optimization_hint),
content: Text(S.current.performance_info_graphic_optimization_warning),
onClose: () => model.closeTip(),
),
const SizedBox(height: 16),
@@ -43,65 +40,74 @@ class HomePerformanceUI extends HookConsumerWidget {
children: [
Text(
S.current.performance_info_current_status(
state.enabled
? S.current.performance_info_applied
: S.current.performance_info_not_applied),
state.enabled
? S.current.performance_info_applied
: S.current.performance_info_not_applied,
),
style: const TextStyle(fontSize: 18),
),
const SizedBox(width: 32),
Text(
S.current.performance_action_preset,
style: const TextStyle(fontSize: 18),
),
Text(S.current.performance_action_preset, style: const TextStyle(fontSize: 18)),
for (final item in {
"low": S.current.performance_action_low,
"medium": S.current.performance_action_medium,
"high": S.current.performance_action_high,
"ultra": S.current.performance_action_super
"ultra": S.current.performance_action_super,
}.entries)
Padding(
padding: const EdgeInsets.only(left: 6, right: 6),
child: Button(
child: Padding(
padding: const EdgeInsets.only(
top: 2, bottom: 2, left: 4, right: 4),
child: Text(item.value),
),
onPressed: () =>
model.onChangePreProfile(item.key)),
child: Padding(
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 4, right: 4),
child: Text(item.value),
),
onPressed: () => model.onChangePreProfile(item.key),
),
),
Text(S.current
.performance_action_info_preset_only_changes_graphics),
Text(S.current.performance_action_info_preset_only_changes_graphics),
const Spacer(),
Button(
onPressed: () => model.refresh(),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.refresh),
),
child: const Padding(padding: EdgeInsets.all(6), child: Icon(FluentIcons.refresh)),
),
const SizedBox(width: 12),
Button(
if (!kIsWeb)
Button(
child: Text(
S.current.performance_action_reset_to_default,
style: const TextStyle(fontSize: 16),
),
onPressed: () => model.clean(context)),
const SizedBox(width: 24),
Button(
onPressed: () => model.clean(context),
),
if (!kIsWeb) const SizedBox(width: 24),
if (kIsWeb)
// Web 平台:下载配置文件
Button(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(FluentIcons.download, size: 16),
const SizedBox(width: 6),
Text("下载配置", style: const TextStyle(fontSize: 16)),
],
),
onPressed: () => model.downloadConfigForWeb(),
)
else ...[
// 桌面平台:应用配置
Button(
child: Text(S.current.performance_action_apply, style: const TextStyle(fontSize: 16)),
onPressed: () => model.applyProfile(false),
),
const SizedBox(width: 6),
Button(
child: Text(
S.current.performance_action_apply,
S.current.performance_action_apply_and_clear_shaders,
style: const TextStyle(fontSize: 16),
),
onPressed: () => model.applyProfile(false)),
const SizedBox(width: 6),
Button(
child: Text(
S.current
.performance_action_apply_and_clear_shaders,
style: const TextStyle(fontSize: 16),
),
onPressed: () => model.applyProfile(true)),
onPressed: () => model.applyProfile(true),
),
],
],
),
const SizedBox(height: 16),
@@ -109,73 +115,60 @@ class HomePerformanceUI extends HookConsumerWidget {
),
),
Expanded(
child: MasonryGridView.count(
crossAxisCount: 2,
mainAxisSpacing: 1,
crossAxisSpacing: 1,
itemCount: state.performanceMap!.length,
itemBuilder: (context, index) {
return makeItemGroup(context,
state.performanceMap!.entries.elementAt(index), model);
},
)),
child: MasonryGridView.count(
crossAxisCount: 2,
mainAxisSpacing: 1,
crossAxisSpacing: 1,
itemCount: state.performanceMap!.length,
itemBuilder: (context, index) {
return makeItemGroup(context, state.performanceMap!.entries.elementAt(index), model);
},
),
),
],
),
),
if (state.workingString.isNotEmpty)
Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(150),
),
decoration: BoxDecoration(color: Colors.black.withAlpha(150)),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(state.workingString),
],
children: [const ProgressRing(), const SizedBox(height: 12), Text(state.workingString)],
),
),
)
),
],
);
}
return makeDefaultPage(context,
title:
S.current.performance_title_performance_optimization(model.scPath),
useBodyContainer: true,
content: content);
return makeDefaultPage(
context,
title: S.current.performance_title_performance_optimization(model.scPath),
useBodyContainer: true,
content: content,
);
}
Widget makeItemGroup(
BuildContext context,
MapEntry<String?, List<GamePerformanceData>> group,
HomePerformanceUIModel model) {
BuildContext context,
MapEntry<String?, List<GamePerformanceData>> group,
HomePerformanceUIModel model,
) {
return Padding(
padding: const EdgeInsets.all(12),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: FluentTheme.of(context).cardColor,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: FluentTheme.of(context).cardColor),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${group.key}",
style: const TextStyle(fontSize: 20),
),
Text("${group.key}", style: const TextStyle(fontSize: 20)),
const SizedBox(height: 6),
Container(
color:
FluentTheme.of(context).cardColor.withValues(alpha: .2),
height: 1),
Container(color: FluentTheme.of(context).cardColor.withValues(alpha: .2), height: 1),
const SizedBox(height: 6),
for (final item in group.value) makeItem(context, item, model)
for (final item in group.value) makeItem(context, item, model),
],
),
),
@@ -183,17 +176,13 @@ class HomePerformanceUI extends HookConsumerWidget {
);
}
Widget makeItem(BuildContext context, GamePerformanceData item,
HomePerformanceUIModel model) {
Widget makeItem(BuildContext context, GamePerformanceData item, HomePerformanceUIModel model) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${item.name}",
style: const TextStyle(fontSize: 16),
),
Text("${item.name}", style: const TextStyle(fontSize: 16)),
const SizedBox(height: 12),
if (item.type == "int")
Column(
@@ -209,9 +198,7 @@ class HomePerformanceUI extends HookConsumerWidget {
dPrint(str);
if (str.isEmpty) return;
final v = int.tryParse(str);
if (v != null &&
v < (item.max ?? 0) &&
v >= (item.min ?? 0)) {
if (v != null && v < (item.max ?? 0) && v >= (item.min ?? 0)) {
item.value = v;
}
model.updateState();
@@ -233,9 +220,9 @@ class HomePerformanceUI extends HookConsumerWidget {
model.updateState();
},
),
)
),
],
)
),
],
)
else if (item.type == "bool")
@@ -247,7 +234,7 @@ class HomePerformanceUI extends HookConsumerWidget {
item.value = value ? 1 : 0;
model.updateState();
},
)
),
],
)
else if (item.type == "customize")
@@ -258,11 +245,7 @@ class HomePerformanceUI extends HookConsumerWidget {
),
if (item.info != null && item.info!.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
"${item.info}",
style: TextStyle(
fontSize: 14, color: Colors.white.withValues(alpha: .6)),
),
Text("${item.info}", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6))),
],
const SizedBox(height: 12),
if (item.type != "customize")
@@ -270,16 +253,13 @@ class HomePerformanceUI extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
S.current.performance_info_min_max_values(
item.key ?? "", item.min ?? "", item.max ?? ""),
S.current.performance_info_min_max_values(item.key ?? "", item.min ?? "", item.max ?? ""),
style: TextStyle(color: Colors.white.withValues(alpha: .6)),
)
),
],
),
const SizedBox(height: 6),
Container(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
height: 1),
Container(color: FluentTheme.of(context).cardColor.withValues(alpha: .1), height: 1),
],
),
);

View File

@@ -1,7 +1,12 @@
// ignore_for_file: avoid_build_context_in_providers, avoid_public_notifier_properties
import 'dart:io';
import 'dart:js_interop';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart' show kIsWeb, compute;
import 'package:web/web.dart' as web;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_ce/hive.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -36,7 +41,7 @@ class HomePerformanceUIModel extends _$HomePerformanceUIModel {
final List<String> _inAppKeys = [];
late final confFile = File("$scPath\\USER.cfg");
late final confFile = kIsWeb ? null : File("$scPath\\USER.cfg");
static const _graphicsPerformanceTipVersion = 1;
@@ -66,7 +71,7 @@ class HomePerformanceUIModel extends _$HomePerformanceUIModel {
}
state = state.copyWith(performanceMap: performanceMap);
if (await confFile.exists()) {
if (!kIsWeb && await confFile!.exists()) {
await _readConf();
} else {
state = state.copyWith(enabled: false);
@@ -78,10 +83,10 @@ class HomePerformanceUIModel extends _$HomePerformanceUIModel {
}
Future<void> _readConf() async {
if (state.performanceMap == null) return;
if (state.performanceMap == null || kIsWeb) return;
state = state.copyWith(enabled: true);
final confString = await confFile.readAsString();
final confString = await confFile!.readAsString();
for (var value in confString.split("\n")) {
final kv = value.split("=");
for (var m in state.performanceMap!.entries) {
@@ -151,9 +156,15 @@ class HomePerformanceUIModel extends _$HomePerformanceUIModel {
}
Future<void> clean(BuildContext context) async {
if (kIsWeb) {
// Web 平台只需要重置状态
await _init();
return;
}
state = state.copyWith(workingString: S.current.performance_info_delete_config_file);
if (await confFile.exists()) {
await confFile.delete(recursive: true);
if (await confFile!.exists()) {
await confFile!.delete(recursive: true);
}
state = state.copyWith(workingString: S.current.performance_action_clear_shaders);
if (!context.mounted) return;
@@ -181,6 +192,12 @@ class HomePerformanceUIModel extends _$HomePerformanceUIModel {
}
Future<void> applyProfile(bool cleanShader) async {
if (kIsWeb) {
// Web 平台使用下载功能
await downloadConfigForWeb();
return;
}
if (state.performanceMap == null) return;
AnalyticsApi.touch("performance_apply");
state = state.copyWith(workingString: S.current.performance_info_generate_config_file);
@@ -203,11 +220,11 @@ class HomePerformanceUIModel extends _$HomePerformanceUIModel {
}
}
state = state.copyWith(workingString: S.current.performance_info_write_out_config_file);
if (await confFile.exists()) {
await confFile.delete();
if (await confFile!.exists()) {
await confFile!.delete();
}
await confFile.create();
await confFile.writeAsString(conf);
await confFile!.create();
await confFile!.writeAsString(conf);
if (cleanShader) {
state = state.copyWith(workingString: S.current.performance_action_clear_shaders);
await cleanShaderCache(null);
@@ -221,4 +238,71 @@ class HomePerformanceUIModel extends _$HomePerformanceUIModel {
void updateState() {
state = state.copyWith();
}
/// Web 平台下载配置文件
Future<void> downloadConfigForWeb() async {
if (!kIsWeb) return;
if (state.performanceMap == null) return;
AnalyticsApi.touch("performance_download_web");
state = state.copyWith(workingString: S.current.performance_info_generate_config_file);
String conf = "";
for (var v in state.performanceMap!.entries) {
for (var c in v.value) {
if (c.key != "customize") {
conf = "$conf${c.key}=${c.value}\n";
}
}
}
if (customizeCtrl.text.trim().isNotEmpty) {
final lines = customizeCtrl.text.split("\n");
for (var value in lines) {
final sp = value.split("=");
// 忽略无效的配置文件
if (sp.length == 2) {
conf = "$conf${sp[0].trim()}=${sp[1].trim()}\n";
}
}
}
await _generateAndDownloadWebFile(conf);
state = state.copyWith(workingString: "");
}
Future<void> _generateAndDownloadWebFile(String confContent) async {
final archive = Archive();
final confBytes = confContent.codeUnits;
archive.addFile(ArchiveFile("USER.cfg", confBytes.length, confBytes));
final zip = await compute(_encodeZipFile, archive);
if (zip == null) return;
final blob = Blob.fromBytes(zip, opt: {"type": "application/zip"});
final url = web.URL.createObjectURL(blob);
jsDownloadBlobFile(url, "StarCitizen_Performance_Config.zip");
}
List<int>? _encodeZipFile(Archive archive) {
final zip = ZipEncoder().encode(archive);
return zip;
}
}
// Web 平台支持 - JS 互操作
@JS("Blob")
extension type Blob._(JSObject _) implements JSObject {
external factory Blob(JSArray<JSArrayBuffer> blobParts, JSAny? options);
factory Blob.fromBytes(List<int> bytes, {Map? opt}) {
final data = Uint8List.fromList(bytes).buffer.toJS;
return Blob([data].toJS, opt?.jsify());
}
external JSArrayBuffer? get blobParts;
external JSObject? get options;
}
@JS()
external void jsDownloadBlobFile(String blobUrl, String filename);

View File

@@ -42,7 +42,7 @@ final class HomePerformanceUIModelProvider
}
String _$homePerformanceUIModelHash() =>
r'c3c55c0470ef8c8be4915a1878deba332653ecde';
r'4c5c33fe7d85dc8f6bf0d019c1b870d285d594ff';
abstract class _$HomePerformanceUIModel
extends $Notifier<HomePerformanceUIState> {

View File

@@ -1,4 +1,6 @@
import 'package:extended_image/extended_image.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -15,6 +17,7 @@ import 'home/home_ui.dart';
import 'nav/nav_ui.dart';
import 'settings/settings_ui.dart';
import 'tools/tools_ui.dart';
import 'tools/yearly_report/yearly_report_entry.dart';
class IndexUI extends HookConsumerWidget {
const IndexUI({super.key});
@@ -24,55 +27,110 @@ class IndexUI extends HookConsumerWidget {
// pre init child
ref.watch(homeUIModelProvider.select((value) => null));
ref.watch(settingsUIModelProvider.select((value) => null));
ref.watch(appGlobalModelProvider);
final globalState = ref.watch(appGlobalModelProvider);
final curIndex = useState(0);
// Web 版本使用背景图片布局
if (kIsWeb) {
return Stack(
children: [
AnimatedSwitcher(
duration: const Duration(seconds: 3),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(opacity: animation, child: child);
},
child: ExtendedImage.asset(
key: ValueKey(globalState.backgroundImageAssetsPath),
width: double.infinity,
height: double.infinity,
globalState.backgroundImageAssetsPath,
fit: BoxFit.cover,
cacheWidth: null,
cacheHeight: null,
loadStateChanged: (state) {
if (state.extendedImageLoadState == LoadState.completed) {
return state.completedWidget;
}
// 加载中或失败时返回黑色背景,避免白屏闪烁
return Container(width: double.infinity, height: double.infinity, color: Colors.black);
},
),
),
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: BlurOvalWidget(
child: Container(
constraints: const BoxConstraints(maxWidth: 1440, maxHeight: 920),
child: _buildNavigationView(context, curIndex, globalState),
),
),
),
),
],
);
}
// Desktop 版本直接使用 NavigationView
return _buildNavigationView(context, curIndex, globalState);
}
Widget _buildNavigationView(BuildContext context, ValueNotifier<int> curIndex, AppGlobalState appState) {
return NavigationView(
appBar: NavigationAppBar(
automaticallyImplyLeading: false,
title: () {
return DragToMoveArea(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Row(
children: [
Image.asset(
"assets/app_logo_mini.png",
width: 20,
height: 20,
fit: BoxFit.cover,
),
const SizedBox(width: 12),
Text(S.current.app_index_version_info(ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev")),
],
),
automaticallyImplyLeading: false,
title: () {
if (kIsWeb) {
// Web 版本简化标题
return Align(
alignment: AlignmentDirectional.centerStart,
child: Row(
children: [
Image.asset("assets/app_logo_mini.png", width: 20, height: 20, fit: BoxFit.cover),
const SizedBox(width: 12),
Text("${S.current.app_index_version_info(ConstConf.appVersion, "WEB")} [PREVIEW]"),
],
),
);
}(),
actions: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Stack(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Icon(
FluentIcons.installation,
size: 22,
color: Colors.white.withValues(alpha: .6),
),
),
_makeAria2TaskNumWidget()
],
),
onPressed: () => _goDownloader(context),
// onPressed: model.goDownloader
}
return DragToMoveArea(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Row(
children: [
Image.asset("assets/app_logo_mini.png", width: 20, height: 20, fit: BoxFit.cover),
const SizedBox(width: 12),
Text(S.current.app_index_version_info(ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev")),
],
),
const SizedBox(width: 24),
const WindowButtons()
],
)),
),
);
}(),
actions: kIsWeb
? null
: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Stack(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Icon(FluentIcons.installation, size: 22, color: Colors.white.withValues(alpha: .6)),
),
_makeAria2TaskNumWidget(),
],
),
onPressed: () => _goDownloader(context),
),
const SizedBox(width: 24),
const WindowButtons(),
],
),
),
pane: NavigationPane(
key: Key("NavigationPane_${S.current.app_language_code}"),
selected: curIndex.value,
@@ -86,21 +144,14 @@ class IndexUI extends HookConsumerWidget {
}
Map<IconData, (String, Widget)> get pageMenus => {
FluentIcons.home: (
S.current.app_index_menu_home,
const HomeUI(),
),
FluentIcons.toolbox: (
S.current.app_index_menu_tools,
const ToolsUI(),
),
FluentIcons.power_apps: ((S.current.nav_title), const NavUI()),
FluentIcons.settings: (S.current.app_index_menu_settings, const SettingsUI()),
FluentIcons.info: (
S.current.app_index_menu_about,
const AboutUI(),
),
};
FluentIcons.home: (S.current.app_index_menu_home, const HomeUI()),
if (!kIsWeb) FluentIcons.toolbox: (S.current.app_index_menu_tools, const ToolsUI()),
if (kIsWeb && isYearlyReportPeriod())
FluentIcons.chart: (S.current.yearly_report_menu_title, const YearlyReportEntryUI()),
FluentIcons.power_apps: ((S.current.nav_title), const NavUI()),
FluentIcons.settings: (S.current.app_index_menu_settings, const SettingsUI()),
FluentIcons.info: (S.current.app_index_menu_about, const AboutUI()),
};
List<NavigationPaneItem> getNavigationPaneItems(ValueNotifier<int> curIndexState) {
// width = 64
@@ -116,10 +167,7 @@ class IndexUI extends HookConsumerWidget {
children: [
Icon(kv.key, size: 18),
const SizedBox(height: 3),
Text(
kv.value.$1,
style: const TextStyle(fontSize: 11),
)
Text(kv.value.$1, style: const TextStyle(fontSize: 11)),
],
),
),
@@ -144,27 +192,24 @@ class IndexUI extends HookConsumerWidget {
return const SizedBox();
}
return Positioned(
bottom: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.only(left: 6, right: 6, bottom: 1.5, top: 1.5),
child: Text(
"${aria2cState.aria2TotalTaskNum}",
style: const TextStyle(
fontSize: 8,
color: Colors.white,
),
),
));
bottom: 0,
right: 0,
child: Container(
decoration: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.only(left: 6, right: 6, bottom: 1.5, top: 1.5),
child: Text("${aria2cState.aria2TotalTaskNum}", style: const TextStyle(fontSize: 8, color: Colors.white)),
),
);
},
);
}
void _goDownloader(BuildContext context) {
context.push('/index/downloader');
if (kIsWeb) {
// Web 版本提示下载完整版
showToast(context, "此功能需要下载完整版 SCToolBox");
return;
}
context.push('/downloader');
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/app.dart';
@@ -14,86 +15,103 @@ class SettingsUI extends HookConsumerWidget {
final model = ref.read(settingsUIModelProvider.notifier);
final appGlobalState = ref.watch(appGlobalModelProvider);
final appGlobalModel = ref.read(appGlobalModelProvider.notifier);
return ListView(padding: const EdgeInsets.all(16), children: [
makeTitle(S.current.settings_title_general),
makeSettingsItem(
const Icon(FontAwesomeIcons.language, size: 20),
S.current.settings_app_language,
subTitle: S.current.settings_app_language_switch_info,
onTap: () {},
onComboChanged: appGlobalModel.changeLocale,
comboMenus: AppGlobalModel.appLocaleSupport,
selectedComboValue: appGlobalState.appLocale ?? const Locale("auto"),
showGoIcon: false,
),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.link, size: 20),
S.current.setting_action_create_settings_shortcut,
subTitle: S.current.setting_action_create_desktop_shortcut,
onTap: () => model.addShortCut(context)),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FontAwesomeIcons.networkWired, size: 20),
S.current.settings_item_dns,
subTitle: S.current.settings_item_dns_info,
switchStatus: sate.isUseInternalDNS,
onSwitch: model.onChangeUseInternalDNS,
onTap: () => model.onChangeUseInternalDNS(!sate.isUseInternalDNS)),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.delete, size: 20),
S.current.setting_action_clear_translation_file_cache,
subTitle: S.current.setting_action_info_cache_clearing_info(
(sate.locationCacheSize / 1024 / 1024).toStringAsFixed(2)),
onTap: () => model.cleanLocationCache(context)),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.speed_high, size: 20),
S.current.setting_action_tool_site_access_acceleration,
onTap: () =>
model.onChangeToolSiteMirror(!sate.isEnableToolSiteMirrors),
subTitle: S.current.setting_action_info_mirror_server_info,
onSwitch: model.onChangeToolSiteMirror,
switchStatus: sate.isEnableToolSiteMirrors),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.document_set, size: 20),
S.current.setting_action_view_log,
onTap: () => model.showLogs(),
subTitle: S.current.setting_action_info_view_log_file),
makeTitle(S.current.settings_title_game),
makeSettingsItem(const Icon(FontAwesomeIcons.microchip, size: 20),
S.current.setting_action_ignore_efficiency_cores_on_launch,
subTitle: S.current
.setting_action_set_core_count(sate.inputGameLaunchECore),
onTap: () => model.setGameLaunchECore(context)),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.folder_open, size: 20),
S.current.setting_action_set_launcher_file,
subTitle: sate.customLauncherPath != null
? "${sate.customLauncherPath}"
: S.current.setting_action_info_manual_launcher_location_setting,
onTap: () => model.setLauncherPath(context),
onDel: () {
model.delName("custom_launcher_path");
}),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.game, size: 20),
S.current.setting_action_set_game_file,
subTitle: sate.customGamePath != null
? "${sate.customGamePath}"
: S.current.setting_action_info_manual_game_location_setting,
onTap: () => model.setGamePath(context),
onDel: () {
model.delName("custom_game_path");
}),
const SizedBox(height: 12),
]);
return ListView(
padding: const EdgeInsets.all(16),
children: [
makeTitle(S.current.settings_title_general),
makeSettingsItem(
const Icon(FontAwesomeIcons.language, size: 20),
S.current.settings_app_language,
subTitle: S.current.settings_app_language_switch_info,
onTap: () {},
onComboChanged: appGlobalModel.changeLocale,
comboMenus: AppGlobalModel.appLocaleSupport,
selectedComboValue: appGlobalState.appLocale ?? const Locale("auto"),
showGoIcon: false,
),
if (!kIsWeb) ...[
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.link, size: 20),
S.current.setting_action_create_settings_shortcut,
subTitle: S.current.setting_action_create_desktop_shortcut,
onTap: () => model.addShortCut(context),
),
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FontAwesomeIcons.networkWired, size: 20),
S.current.settings_item_dns,
subTitle: S.current.settings_item_dns_info,
switchStatus: sate.isUseInternalDNS,
onSwitch: model.onChangeUseInternalDNS,
onTap: () => model.onChangeUseInternalDNS(!sate.isUseInternalDNS),
),
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.delete, size: 20),
S.current.setting_action_clear_translation_file_cache,
subTitle: S.current.setting_action_info_cache_clearing_info(
(sate.locationCacheSize / 1024 / 1024).toStringAsFixed(2),
),
onTap: () => model.cleanLocationCache(context),
),
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.speed_high, size: 20),
S.current.setting_action_tool_site_access_acceleration,
onTap: () => model.onChangeToolSiteMirror(!sate.isEnableToolSiteMirrors),
subTitle: S.current.setting_action_info_mirror_server_info,
onSwitch: model.onChangeToolSiteMirror,
switchStatus: sate.isEnableToolSiteMirrors,
),
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.document_set, size: 20),
S.current.setting_action_view_log,
onTap: () => model.showLogs(),
subTitle: S.current.setting_action_info_view_log_file,
),
makeTitle(S.current.settings_title_game),
makeSettingsItem(
const Icon(FontAwesomeIcons.microchip, size: 20),
S.current.setting_action_ignore_efficiency_cores_on_launch,
subTitle: S.current.setting_action_set_core_count(sate.inputGameLaunchECore),
onTap: () => model.setGameLaunchECore(context),
),
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.folder_open, size: 20),
S.current.setting_action_set_launcher_file,
subTitle: sate.customLauncherPath != null
? "${sate.customLauncherPath}"
: S.current.setting_action_info_manual_launcher_location_setting,
onTap: () => model.setLauncherPath(context),
onDel: () {
model.delName("custom_launcher_path");
},
),
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.game, size: 20),
S.current.setting_action_set_game_file,
subTitle: sate.customGamePath != null
? "${sate.customGamePath}"
: S.current.setting_action_info_manual_game_location_setting,
onTap: () => model.setGamePath(context),
onDel: () {
model.delName("custom_game_path");
},
),
const SizedBox(height: 12),
],
],
);
}
Widget makeTitle(String title) {
return Padding(
padding: const EdgeInsets.only(top: 12, bottom: 12),
child: Text(
title,
style: TextStyle(fontSize: 24),
),
child: Text(title, style: TextStyle(fontSize: 24)),
);
}
@@ -122,12 +140,7 @@ class SettingsUI extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(title),
const Spacer(),
],
),
Row(children: [Text(title), const Spacer()]),
if (subTitle != null) ...[
const SizedBox(height: 3),
Padding(
@@ -135,41 +148,29 @@ class SettingsUI extends HookConsumerWidget {
child: Text(
subTitle,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 12,
color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
),
),
]
],
],
),
),
if (onDel != null) ...[
Button(
onPressed: onDel,
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.delete),
)),
],
if (onSwitch != null) ...[
ToggleSwitch(checked: switchStatus, onChanged: onSwitch),
onPressed: onDel,
child: const Padding(padding: EdgeInsets.all(6), child: Icon(FluentIcons.delete)),
),
],
if (onSwitch != null) ...[ToggleSwitch(checked: switchStatus, onChanged: onSwitch)],
if (comboMenus.isNotEmpty) ...[
SizedBox(
height: 36,
child: ComboBox(
value: selectedComboValue,
items: [
for (final mkv in comboMenus.entries)
ComboBoxItem(
value: mkv.key,
child: Text(mkv.value),
)
],
items: [for (final mkv in comboMenus.entries) ComboBoxItem(value: mkv.key, child: Text(mkv.value))],
onChanged: onComboChanged,
),
)
),
],
const SizedBox(width: 12),
if (showGoIcon) const Icon(FluentIcons.chevron_right),

View File

@@ -41,7 +41,7 @@ final class SettingsUIModelProvider
}
}
String _$settingsUIModelHash() => r'5c08c56bf5464ef44bee8edb8c18c08d4217f135';
String _$settingsUIModelHash() => r'd19104d924f018a9230548d0372692fc344adacd';
abstract class _$SettingsUIModel extends $Notifier<SettingsUIState> {
SettingsUIState build();

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_ce/hive.dart';
@@ -9,7 +10,6 @@ import 'package:markdown_widget/widget/markdown.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/app.dart';
import 'package:starcitizen_doctor/common/conf/conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/provider/aria2c.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
@@ -30,55 +30,60 @@ class SplashUI extends HookConsumerWidget {
return null;
}, []);
return makeDefaultPage(context,
content: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset("assets/app_logo.png", width: 192, height: 192),
const SizedBox(height: 32),
const ProgressRing(),
const SizedBox(height: 32),
if (step == 0) Text(S.current.app_splash_checking_availability),
if (step == 1) Text(S.current.app_splash_checking_for_updates),
if (step == 2) Text(S.current.app_splash_almost_done),
],
),
return makeDefaultPage(
context,
content: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset("assets/app_logo.png", width: 192, height: 192),
const SizedBox(height: 32),
const ProgressRing(),
const SizedBox(height: 32),
if (step == 0) Text(S.current.app_splash_checking_availability),
if (step == 1) Text(S.current.app_splash_checking_for_updates),
if (step == 2) Text(S.current.app_splash_almost_done),
],
),
automaticallyImplyLeading: false,
titleRow: Align(
alignment: AlignmentDirectional.centerStart,
child: Row(
children: [
Image.asset(
"assets/app_logo_mini.png",
width: 20,
height: 20,
fit: BoxFit.cover,
),
const SizedBox(width: 12),
Text(S.current.app_index_version_info(
ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev"))
],
),
));
),
automaticallyImplyLeading: false,
titleRow: Align(
alignment: AlignmentDirectional.centerStart,
child: Row(
children: [
Image.asset("assets/app_logo_mini.png", width: 20, height: 20, fit: BoxFit.cover),
const SizedBox(width: 12),
Text(S.current.app_index_version_info(ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev")),
],
),
),
);
}
void _initApp(BuildContext context, AppGlobalModel appModel,
ValueNotifier<int> stepState, WidgetRef ref) async {
void _initApp(BuildContext context, AppGlobalModel appModel, ValueNotifier<int> stepState, WidgetRef ref) async {
await appModel.initApp();
final appConf = await Hive.openBox("app_conf");
final v = appConf.get("splash_alert_info_version", defaultValue: 0);
AnalyticsApi.touch("launch");
// 检查 Web 特殊路由
if (kIsWeb) {
final uri = Uri.base;
if (uri.path.contains('yearly_report') || uri.queryParameters.containsKey('yearly_report')) {
if (!context.mounted) return;
context.go("/tools/yearly_report");
return;
}
}
if (v < _alertInfoVersion) {
if (!context.mounted) return;
await _showAlert(context, appConf);
}
try {
await URLConf.checkHost();
} catch (e) {
dPrint("checkHost Error:$e");
}
// try {
// await URLConf.checkHost();
// } catch (e) {
// dPrint("checkHost Error:$e");
// }
stepState.value = 1;
if (!context.mounted) return;
dPrint("_initApp checkUpdate");
@@ -87,16 +92,16 @@ class SplashUI extends HookConsumerWidget {
dPrint("_initApp aria2cModelProvider");
ref.read(aria2cModelProvider);
if (!context.mounted) return;
context.go("/index");
context.go("/");
}
Future<void> _showAlert(BuildContext context, Box<dynamic> appConf) async {
final userOk = await showConfirmDialogs(
context,
S.current.app_splash_dialog_u_a_p_p,
MarkdownWidget(data: S.current.app_splash_dialog_u_a_p_p_content),
constraints:
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .5));
context,
S.current.app_splash_dialog_u_a_p_p,
MarkdownWidget(data: S.current.app_splash_dialog_u_a_p_p_content),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .5),
);
if (userOk) {
await appConf.put("splash_alert_info_version", _alertInfoVersion);
} else {

View File

@@ -13,7 +13,6 @@ import 'package:starcitizen_doctor/app.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/common/rust/api/asar_api.dart' as asar_api;
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/generated/no_l10n_strings.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
@@ -24,7 +23,7 @@ part 'rsi_launcher_enhance_dialog_ui.freezed.dart';
abstract class RSILauncherStateData with _$RSILauncherStateData {
const factory RSILauncherStateData({
required String version,
required asar_api.RsiLauncherAsarData data,
required dynamic data,
required String serverData,
@Default(false) bool isPatchInstalled,
String? enabledLocalization,
@@ -70,8 +69,11 @@ class RsiLauncherEnhanceDialogUI extends HookConsumerWidget {
workingText.value = S.current.tools_rsi_launcher_enhance_working_msg1;
if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) {
if (!context.mounted) return;
showToast(context, S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35));
showToast(
context,
S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35),
);
workingText.value = "";
return;
}
@@ -92,16 +94,16 @@ class RsiLauncherEnhanceDialogUI extends HookConsumerWidget {
return ContentDialog(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .48),
title: Row(children: [
IconButton(
icon: const Icon(
FluentIcons.back,
size: 22,
),
onPressed: workingText.value.isEmpty ? Navigator.of(context).pop : null),
const SizedBox(width: 12),
Text(S.current.tools_rsi_launcher_enhance_title),
]),
title: Row(
children: [
IconButton(
icon: const Icon(FluentIcons.back, size: 22),
onPressed: workingText.value.isEmpty ? Navigator.of(context).pop : null,
),
const SizedBox(width: 12),
Text(S.current.tools_rsi_launcher_enhance_title),
],
),
content: AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Column(
@@ -112,17 +114,16 @@ class RsiLauncherEnhanceDialogUI extends HookConsumerWidget {
InfoBar(
title: const SizedBox(),
content: Text(S.current.home_localization_action_rsi_launcher_no_game_path_msg),
style: InfoBarThemeData(decoration: (severity) {
return BoxDecoration(
color: Colors.orange,
);
}, iconColor: (severity) {
return Colors.white;
}),
),
const SizedBox(
height: 12,
style: InfoBarThemeData(
decoration: (severity) {
return BoxDecoration(color: Colors.orange);
},
iconColor: (severity) {
return Colors.white;
},
),
),
const SizedBox(height: 12),
],
if (workingText.value.isNotEmpty) ...[
Center(
@@ -144,19 +145,17 @@ class RsiLauncherEnhanceDialogUI extends HookConsumerWidget {
Expanded(
child: Text(
S.current.tools_rsi_launcher_enhance_msg_version(assarState.value?.version ?? ""),
style: TextStyle(
color: Colors.white.withValues(alpha: .6),
),
style: TextStyle(color: Colors.white.withValues(alpha: .6)),
),
),
Text(
S.current.tools_rsi_launcher_enhance_msg_patch_status((assarState.value?.isPatchInstalled ?? false)
? S.current.localization_info_installed
: S.current.tools_action_info_not_installed),
style: TextStyle(
color: Colors.white.withValues(alpha: .6),
S.current.tools_rsi_launcher_enhance_msg_patch_status(
(assarState.value?.isPatchInstalled ?? false)
? S.current.localization_info_installed
: S.current.tools_action_info_not_installed,
),
)
style: TextStyle(color: Colors.white.withValues(alpha: .6)),
),
],
),
if (assarState.value?.serverData.isEmpty ?? true) ...[
@@ -165,6 +164,84 @@ class RsiLauncherEnhanceDialogUI extends HookConsumerWidget {
const SizedBox(height: 24),
if (assarState.value?.enabledLocalization != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.current.tools_rsi_launcher_enhance_title_localization),
const SizedBox(height: 3),
Text(
S.current.tools_rsi_launcher_enhance_subtitle_localization,
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .6)),
),
],
),
),
ComboBox(
items: [
for (final key in supportLocalizationMap.keys)
ComboBoxItem(value: key, child: Text(supportLocalizationMap[key]!)),
],
value: assarState.value?.enabledLocalization,
onChanged: (v) {
assarState.value = assarState.value!.copyWith(enabledLocalization: v);
},
),
],
),
),
const SizedBox(height: 3),
if (assarState.value?.enableDownloaderBoost != null) ...[
IconButton(
icon: Padding(
padding: const EdgeInsets.only(top: 3, bottom: 3),
child: Row(
children: [
Expanded(
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(expandEnhance.value ? FluentIcons.chevron_up : FluentIcons.chevron_down),
const SizedBox(width: 12),
Text(
expandEnhance.value
? S.current.tools_rsi_launcher_enhance_action_fold
: S.current.tools_rsi_launcher_enhance_action_expand,
),
],
),
),
),
],
),
),
onPressed: () async {
if (!expandEnhance.value) {
final userOK = await showConfirmDialogs(
context,
S.current.tools_rsi_launcher_enhance_note_title,
Column(
mainAxisSize: MainAxisSize.min,
children: [Text(S.current.tools_rsi_launcher_enhance_note_msg)],
),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .55),
);
if (!userOK) return;
}
expandEnhance.value = !expandEnhance.value;
},
),
if (expandEnhance.value)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
@@ -174,111 +251,38 @@ class RsiLauncherEnhanceDialogUI extends HookConsumerWidget {
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.current.tools_rsi_launcher_enhance_title_localization),
const SizedBox(height: 3),
Text(
S.current.tools_rsi_launcher_enhance_subtitle_localization,
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: .6),
),
),
],
)),
ComboBox(
items: [
for (final key in supportLocalizationMap.keys)
ComboBoxItem(value: key, child: Text(supportLocalizationMap[key]!))
],
value: assarState.value?.enabledLocalization,
onChanged: (v) {
assarState.value = assarState.value!.copyWith(enabledLocalization: v);
},
),
],
)),
const SizedBox(height: 3),
if (assarState.value?.enableDownloaderBoost != null) ...[
IconButton(
icon: Padding(
padding: const EdgeInsets.only(top: 3, bottom: 3),
child: Row(
children: [
Expanded(
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(expandEnhance.value ? FluentIcons.chevron_up : FluentIcons.chevron_down),
const SizedBox(width: 12),
Text(expandEnhance.value
? S.current.tools_rsi_launcher_enhance_action_fold
: S.current.tools_rsi_launcher_enhance_action_expand),
],
))),
],
),
),
onPressed: () async {
if (!expandEnhance.value) {
final userOK = await showConfirmDialogs(
context,
S.current.tools_rsi_launcher_enhance_note_title,
Column(
mainAxisSize: MainAxisSize.min,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.current.tools_rsi_launcher_enhance_note_msg),
Text(S.current.tools_rsi_launcher_enhance_title_download_booster),
const SizedBox(height: 3),
Text(
S.current.tools_rsi_launcher_enhance_subtitle_download_booster,
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .6)),
),
],
),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .55));
if (!userOK) return;
}
expandEnhance.value = !expandEnhance.value;
},
),
if (expandEnhance.value)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.current.tools_rsi_launcher_enhance_title_download_booster),
const SizedBox(height: 3),
Text(
S.current.tools_rsi_launcher_enhance_subtitle_download_booster,
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: .6),
),
),
],
)),
),
ToggleSwitch(
onChanged: (value) {
assarState.value = assarState.value?.copyWith(enableDownloaderBoost: value);
},
checked: assarState.value?.enableDownloaderBoost ?? false,
)
])),
),
],
),
),
],
const SizedBox(height: 12),
Center(
child: FilledButton(
onPressed: doInstall,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
child: Text(S.current.tools_rsi_launcher_enhance_action_install),
))),
child: FilledButton(
onPressed: doInstall,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
child: Text(S.current.tools_rsi_launcher_enhance_action_install),
),
),
),
],
const SizedBox(height: 16),
Text(
@@ -303,37 +307,41 @@ class RsiLauncherEnhanceDialogUI extends HookConsumerWidget {
final dataPath = "${lPath}resources\\app.asar";
dPrint("[RsiLauncherEnhanceDialogUI] rsiLauncherDataPath ==== $dataPath");
try {
final data = await asar_api.getRsiLauncherAsarData(asarPath: dataPath);
dPrint("[RsiLauncherEnhanceDialogUI] rsiLauncherPath main.js path == ${data.mainJsPath}");
final version = RegExp(r"main\.(\w+)\.js").firstMatch(data.mainJsPath)?.group(1);
if (version == null) {
if (!context.mounted) return null;
showToast(context, S.current.tools_rsi_launcher_enhance_msg_error_get_launcher_info_error);
return null;
}
dPrint("[RsiLauncherEnhanceDialogUI] rsiLauncherPath main.js version == $version");
// final data = await asar_api.getRsiLauncherAsarData(asarPath: dataPath);
// dPrint("[RsiLauncherEnhanceDialogUI] rsiLauncherPath main.js path == ${data.mainJsPath}");
// final version = RegExp(r"main\.(\w+)\.js").firstMatch(data.mainJsPath)?.group(1);
// if (version == null) {
// if (!context.mounted) return null;
// showToast(context, S.current.tools_rsi_launcher_enhance_msg_error_get_launcher_info_error);
// return null;
// }
// dPrint("[RsiLauncherEnhanceDialogUI] rsiLauncherPath main.js version == $version");
final mainJsString = String.fromCharCodes(data.mainJsContent);
// final mainJsString = String.fromCharCodes(data.mainJsContent);
final (enabledLocalization, enableDownloaderBoost) = _readScriptState(mainJsString);
// final (enabledLocalization, enableDownloaderBoost) = _readScriptState(mainJsString);
return RSILauncherStateData(
version: version,
data: data,
serverData: "",
isPatchInstalled: mainJsString.contains("SC_TOOLBOX"),
enabledLocalization: enabledLocalization,
enableDownloaderBoost: enableDownloaderBoost,
);
// return RSILauncherStateData(
// version: version,
// data: data,
// serverData: "",
// isPatchInstalled: mainJsString.contains("SC_TOOLBOX"),
// enabledLocalization: enabledLocalization,
// enableDownloaderBoost: enableDownloaderBoost,
// );
} catch (e) {
if (!context.mounted) return null;
showToast(context, S.current.tools_rsi_launcher_enhance_msg_error_get_launcher_info_error_with_args(e));
return null;
}
return null;
}
Future<String> _loadEnhanceData(
BuildContext context, WidgetRef ref, ValueNotifier<RSILauncherStateData?> assarState) async {
BuildContext context,
WidgetRef ref,
ValueNotifier<RSILauncherStateData?> assarState,
) async {
final globalModel = ref.read(appGlobalModelProvider);
final enhancePath = "${globalModel.applicationSupportDir}/launcher_enhance_data";
@@ -418,11 +426,13 @@ class RsiLauncherEnhanceDialogUI extends HookConsumerWidget {
for (final line in serverScriptLines) {
final lineTrim = line.trim();
if (lineTrim.startsWith(SC_TOOLBOX_ENABLED_LOCALIZATION_SCRIPT_START)) {
scriptBuffer
.writeln("$SC_TOOLBOX_ENABLED_LOCALIZATION_SCRIPT_START\"${assarState.value!.enabledLocalization}\";");
scriptBuffer.writeln(
"$SC_TOOLBOX_ENABLED_LOCALIZATION_SCRIPT_START\"${assarState.value!.enabledLocalization}\";",
);
} else if (lineTrim.startsWith(SC_TOOLBOX_ENABLE_DOWNLOADER_BOOST_SCRIPT_START)) {
scriptBuffer
.writeln("$SC_TOOLBOX_ENABLE_DOWNLOADER_BOOST_SCRIPT_START${assarState.value!.enableDownloaderBoost};");
scriptBuffer.writeln(
"$SC_TOOLBOX_ENABLE_DOWNLOADER_BOOST_SCRIPT_START${assarState.value!.enableDownloaderBoost};",
);
} else {
scriptBuffer.writeln(line);
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RSILauncherStateData {
String get version; asar_api.RsiLauncherAsarData get data; String get serverData; bool get isPatchInstalled; String? get enabledLocalization; bool? get enableDownloaderBoost;
String get version; dynamic get data; String get serverData; bool get isPatchInstalled; String? get enabledLocalization; bool? get enableDownloaderBoost;
/// Create a copy of RSILauncherStateData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,12 +25,12 @@ $RSILauncherStateDataCopyWith<RSILauncherStateData> get copyWith => _$RSILaunche
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RSILauncherStateData&&(identical(other.version, version) || other.version == version)&&(identical(other.data, data) || other.data == data)&&(identical(other.serverData, serverData) || other.serverData == serverData)&&(identical(other.isPatchInstalled, isPatchInstalled) || other.isPatchInstalled == isPatchInstalled)&&(identical(other.enabledLocalization, enabledLocalization) || other.enabledLocalization == enabledLocalization)&&(identical(other.enableDownloaderBoost, enableDownloaderBoost) || other.enableDownloaderBoost == enableDownloaderBoost));
return identical(this, other) || (other.runtimeType == runtimeType&&other is RSILauncherStateData&&(identical(other.version, version) || other.version == version)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.serverData, serverData) || other.serverData == serverData)&&(identical(other.isPatchInstalled, isPatchInstalled) || other.isPatchInstalled == isPatchInstalled)&&(identical(other.enabledLocalization, enabledLocalization) || other.enabledLocalization == enabledLocalization)&&(identical(other.enableDownloaderBoost, enableDownloaderBoost) || other.enableDownloaderBoost == enableDownloaderBoost));
}
@override
int get hashCode => Object.hash(runtimeType,version,data,serverData,isPatchInstalled,enabledLocalization,enableDownloaderBoost);
int get hashCode => Object.hash(runtimeType,version,const DeepCollectionEquality().hash(data),serverData,isPatchInstalled,enabledLocalization,enableDownloaderBoost);
@override
String toString() {
@@ -45,7 +45,7 @@ abstract mixin class $RSILauncherStateDataCopyWith<$Res> {
factory $RSILauncherStateDataCopyWith(RSILauncherStateData value, $Res Function(RSILauncherStateData) _then) = _$RSILauncherStateDataCopyWithImpl;
@useResult
$Res call({
String version, asar_api.RsiLauncherAsarData data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost
String version, dynamic data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost
});
@@ -62,11 +62,11 @@ class _$RSILauncherStateDataCopyWithImpl<$Res>
/// Create a copy of RSILauncherStateData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? data = null,Object? serverData = null,Object? isPatchInstalled = null,Object? enabledLocalization = freezed,Object? enableDownloaderBoost = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? data = freezed,Object? serverData = null,Object? isPatchInstalled = null,Object? enabledLocalization = freezed,Object? enableDownloaderBoost = freezed,}) {
return _then(_self.copyWith(
version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
as String,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as asar_api.RsiLauncherAsarData,serverData: null == serverData ? _self.serverData : serverData // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as dynamic,serverData: null == serverData ? _self.serverData : serverData // ignore: cast_nullable_to_non_nullable
as String,isPatchInstalled: null == isPatchInstalled ? _self.isPatchInstalled : isPatchInstalled // ignore: cast_nullable_to_non_nullable
as bool,enabledLocalization: freezed == enabledLocalization ? _self.enabledLocalization : enabledLocalization // ignore: cast_nullable_to_non_nullable
as String?,enableDownloaderBoost: freezed == enableDownloaderBoost ? _self.enableDownloaderBoost : enableDownloaderBoost // ignore: cast_nullable_to_non_nullable
@@ -155,7 +155,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String version, asar_api.RsiLauncherAsarData data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String version, dynamic data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RSILauncherStateData() when $default != null:
return $default(_that.version,_that.data,_that.serverData,_that.isPatchInstalled,_that.enabledLocalization,_that.enableDownloaderBoost);case _:
@@ -176,7 +176,7 @@ return $default(_that.version,_that.data,_that.serverData,_that.isPatchInstalled
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String version, asar_api.RsiLauncherAsarData data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String version, dynamic data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost) $default,) {final _that = this;
switch (_that) {
case _RSILauncherStateData():
return $default(_that.version,_that.data,_that.serverData,_that.isPatchInstalled,_that.enabledLocalization,_that.enableDownloaderBoost);case _:
@@ -196,7 +196,7 @@ return $default(_that.version,_that.data,_that.serverData,_that.isPatchInstalled
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String version, asar_api.RsiLauncherAsarData data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String version, dynamic data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost)? $default,) {final _that = this;
switch (_that) {
case _RSILauncherStateData() when $default != null:
return $default(_that.version,_that.data,_that.serverData,_that.isPatchInstalled,_that.enabledLocalization,_that.enableDownloaderBoost);case _:
@@ -215,7 +215,7 @@ class _RSILauncherStateData implements RSILauncherStateData {
@override final String version;
@override final asar_api.RsiLauncherAsarData data;
@override final dynamic data;
@override final String serverData;
@override@JsonKey() final bool isPatchInstalled;
@override final String? enabledLocalization;
@@ -231,12 +231,12 @@ _$RSILauncherStateDataCopyWith<_RSILauncherStateData> get copyWith => __$RSILaun
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RSILauncherStateData&&(identical(other.version, version) || other.version == version)&&(identical(other.data, data) || other.data == data)&&(identical(other.serverData, serverData) || other.serverData == serverData)&&(identical(other.isPatchInstalled, isPatchInstalled) || other.isPatchInstalled == isPatchInstalled)&&(identical(other.enabledLocalization, enabledLocalization) || other.enabledLocalization == enabledLocalization)&&(identical(other.enableDownloaderBoost, enableDownloaderBoost) || other.enableDownloaderBoost == enableDownloaderBoost));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RSILauncherStateData&&(identical(other.version, version) || other.version == version)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.serverData, serverData) || other.serverData == serverData)&&(identical(other.isPatchInstalled, isPatchInstalled) || other.isPatchInstalled == isPatchInstalled)&&(identical(other.enabledLocalization, enabledLocalization) || other.enabledLocalization == enabledLocalization)&&(identical(other.enableDownloaderBoost, enableDownloaderBoost) || other.enableDownloaderBoost == enableDownloaderBoost));
}
@override
int get hashCode => Object.hash(runtimeType,version,data,serverData,isPatchInstalled,enabledLocalization,enableDownloaderBoost);
int get hashCode => Object.hash(runtimeType,version,const DeepCollectionEquality().hash(data),serverData,isPatchInstalled,enabledLocalization,enableDownloaderBoost);
@override
String toString() {
@@ -251,7 +251,7 @@ abstract mixin class _$RSILauncherStateDataCopyWith<$Res> implements $RSILaunche
factory _$RSILauncherStateDataCopyWith(_RSILauncherStateData value, $Res Function(_RSILauncherStateData) _then) = __$RSILauncherStateDataCopyWithImpl;
@override @useResult
$Res call({
String version, asar_api.RsiLauncherAsarData data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost
String version, dynamic data, String serverData, bool isPatchInstalled, String? enabledLocalization, bool? enableDownloaderBoost
});
@@ -268,11 +268,11 @@ class __$RSILauncherStateDataCopyWithImpl<$Res>
/// Create a copy of RSILauncherStateData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? data = null,Object? serverData = null,Object? isPatchInstalled = null,Object? enabledLocalization = freezed,Object? enableDownloaderBoost = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? data = freezed,Object? serverData = null,Object? isPatchInstalled = null,Object? enabledLocalization = freezed,Object? enableDownloaderBoost = freezed,}) {
return _then(_RSILauncherStateData(
version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
as String,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as asar_api.RsiLauncherAsarData,serverData: null == serverData ? _self.serverData : serverData // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as dynamic,serverData: null == serverData ? _self.serverData : serverData // ignore: cast_nullable_to_non_nullable
as String,isPatchInstalled: null == isPatchInstalled ? _self.isPatchInstalled : isPatchInstalled // ignore: cast_nullable_to_non_nullable
as bool,enabledLocalization: freezed == enabledLocalization ? _self.enabledLocalization : enabledLocalization // ignore: cast_nullable_to_non_nullable
as String?,enableDownloaderBoost: freezed == enableDownloaderBoost ? _self.enableDownloaderBoost : enableDownloaderBoost // ignore: cast_nullable_to_non_nullable

View File

@@ -67,30 +67,30 @@ class ToolsUIModel extends _$ToolsUIModel {
if (state.isItemLoading) return;
var items = <ToolsItemData>[];
state = state.copyWith(items: items, isItemLoading: true);
if (!skipPathScan) {
await reScanPath(context);
}
// if (!skipPathScan) {
// await reScanPath(context);
// }
try {
items = [
ToolsItemData(
"systemnfo",
S.current.tools_action_view_system_info,
S.current.tools_action_info_view_critical_system_info,
const Icon(FluentIcons.system, size: 24),
onTap: () => _showSystemInfo(context),
),
// ToolsItemData(
// "systemnfo",
// S.current.tools_action_view_system_info,
// S.current.tools_action_info_view_critical_system_info,
// const Icon(FluentIcons.system, size: 24),
// onTap: () => _showSystemInfo(context),
// ),
];
if (!context.mounted) return;
items.add(await _addP4kCard(context));
items.addAll([
ToolsItemData(
"hosts_booster",
S.current.tools_action_hosts_acceleration_experimental,
S.current.tools_action_info_hosts_acceleration_experimental_tip,
const Icon(FluentIcons.virtual_network, size: 24),
onTap: () => _doHostsBooster(context),
),
// ToolsItemData(
// "hosts_booster",
// S.current.tools_action_hosts_acceleration_experimental,
// S.current.tools_action_info_hosts_acceleration_experimental_tip,
// const Icon(FluentIcons.virtual_network, size: 24),
// onTap: () => _doHostsBooster(context),
// ),
ToolsItemData(
"log_analyze",
S.current.log_analyzer_title,
@@ -98,45 +98,45 @@ class ToolsUIModel extends _$ToolsUIModel {
Icon(FluentIcons.analytics_logo),
onTap: () => _showLogAnalyze(context),
),
ToolsItemData(
"rsilauncher_enhance_mod",
S.current.tools_rsi_launcher_enhance_title,
S.current.tools_action_rsi_launcher_enhance_info,
const Icon(FluentIcons.c_plus_plus, size: 24),
onTap: () => rsiEnhance(context),
),
ToolsItemData(
"reinstall_eac",
S.current.tools_action_reinstall_easyanticheat,
S.current.tools_action_info_reinstall_eac,
const Icon(FluentIcons.game, size: 24),
onTap: () => _reinstallEAC(context),
),
ToolsItemData(
"rsilauncher_admin_mode",
S.current.tools_action_rsi_launcher_admin_mode,
S.current.tools_action_info_run_rsi_as_admin,
const Icon(FluentIcons.admin, size: 24),
onTap: () => _adminRSILauncher(context),
),
ToolsItemData(
"unp4kc",
S.current.tools_action_unp4k,
S.current.tools_action_unp4k_info,
const Icon(FontAwesomeIcons.fileZipper, size: 24),
onTap: () => _unp4kc(context),
),
// ToolsItemData(
// "rsilauncher_enhance_mod",
// S.current.tools_rsi_launcher_enhance_title,
// S.current.tools_action_rsi_launcher_enhance_info,
// const Icon(FluentIcons.c_plus_plus, size: 24),
// onTap: () => rsiEnhance(context),
// ),
// ToolsItemData(
// "reinstall_eac",
// S.current.tools_action_reinstall_easyanticheat,
// S.current.tools_action_info_reinstall_eac,
// const Icon(FluentIcons.game, size: 24),
// onTap: () => _reinstallEAC(context),
// ),
// ToolsItemData(
// "rsilauncher_admin_mode",
// S.current.tools_action_rsi_launcher_admin_mode,
// S.current.tools_action_info_run_rsi_as_admin,
// const Icon(FluentIcons.admin, size: 24),
// onTap: () => _adminRSILauncher(context),
// ),
// ToolsItemData(
// "unp4kc",
// S.current.tools_action_unp4k,
// S.current.tools_action_unp4k_info,
// const Icon(FontAwesomeIcons.fileZipper, size: 24),
// onTap: () => _unp4kc(context),
// ),
]);
state = state.copyWith(items: items);
if (!context.mounted) return;
items.add(await _addShaderCard(context));
state = state.copyWith(items: items);
// items.add(await _addShaderCard(context));
// state = state.copyWith(items: items);
if (!context.mounted) return;
items.add(await _addPhotographyCard(context));
state = state.copyWith(items: items);
if (!context.mounted) return;
items.addAll(await _addNvmePatchCard(context));
// items.add(await _addPhotographyCard(context));
// state = state.copyWith(items: items);
// if (!context.mounted) return;
// items.addAll(await _addNvmePatchCard(context));
state = state.copyWith(items: items, isItemLoading: false);
} catch (e) {
if (!context.mounted) return;
@@ -176,7 +176,8 @@ class ToolsUIModel extends _$ToolsUIModel {
"remove_nvme_settings",
S.current.tools_action_remove_nvme_registry_patch,
S.current.tools_action_info_nvme_patch_issue(
nvmePatchStatus ? S.current.localization_info_installed : S.current.tools_action_info_not_installed),
nvmePatchStatus ? S.current.localization_info_installed : S.current.tools_action_info_not_installed,
),
const Icon(FluentIcons.hard_drive, size: 24),
onTap: nvmePatchStatus
? () async {
@@ -208,7 +209,7 @@ class ToolsUIModel extends _$ToolsUIModel {
state = state.copyWith(working: false);
loadToolsCard(context, skipPathScan: true);
},
)
),
];
}
@@ -266,8 +267,11 @@ class ToolsUIModel extends _$ToolsUIModel {
if (listData == null) {
return;
}
scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData,
checkExists: checkActive, withVersion: AppConf.gameChannels);
scInstallPaths = await SCLoggerHelper.getGameInstallPath(
listData,
checkExists: checkActive,
withVersion: AppConf.gameChannels,
);
if (scInstallPaths.isNotEmpty) {
scInstalledPath = scInstallPaths.first;
}
@@ -337,11 +341,12 @@ class ToolsUIModel extends _$ToolsUIModel {
Future<String> getSystemInfo() async {
return S.current.tools_action_info_system_info_content(
await SystemHelper.getSystemName(),
await SystemHelper.getCpuName(),
await SystemHelper.getSystemMemorySizeGB(),
await SystemHelper.getGpuInfo(),
await SystemHelper.getDiskInfo());
await SystemHelper.getSystemName(),
await SystemHelper.getCpuName(),
await SystemHelper.getSystemMemorySizeGB(),
await SystemHelper.getGpuInfo(),
await SystemHelper.getDiskInfo(),
);
}
/// 管理员模式运行 RSI 启动器
@@ -365,9 +370,7 @@ class ToolsUIModel extends _$ToolsUIModel {
builder: (context) => ContentDialog(
title: Text(S.current.tools_action_info_system_info_title),
content: Text(systemInfo),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .65,
),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .65),
actions: [
FilledButton(
child: Padding(
@@ -404,8 +407,11 @@ class ToolsUIModel extends _$ToolsUIModel {
if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) {
if (!context.mounted) return;
showToast(context, S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35));
showToast(
context,
S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35),
);
return;
}
@@ -440,8 +446,11 @@ class ToolsUIModel extends _$ToolsUIModel {
return;
}
final userSelect =
await FilePicker.platform.saveFile(initialDirectory: savePath, fileName: fileName, lockParentWindow: true);
final userSelect = await FilePicker.platform.saveFile(
initialDirectory: savePath,
fileName: fileName,
lockParentWindow: true,
);
if (userSelect == null) {
state = state.copyWith(working: false);
return;
@@ -468,7 +477,7 @@ class ToolsUIModel extends _$ToolsUIModel {
await aria2c.saveSession();
AnalyticsApi.touch("p4k_download");
if (!context.mounted) return;
context.push("/index/downloader");
context.push("/downloader");
} catch (e) {
state = state.copyWith(working: false);
if (!context.mounted) return;
@@ -550,16 +559,18 @@ class ToolsUIModel extends _$ToolsUIModel {
static Future<void> rsiEnhance(BuildContext context, {bool showNotGameInstallMsg = false}) async {
if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) {
if (!context.mounted) return;
showToast(context, S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35));
showToast(
context,
S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35),
);
return;
}
if (!context.mounted) return;
showDialog(
context: context,
builder: (BuildContext context) => RsiLauncherEnhanceDialogUI(
showNotGameInstallMsg: showNotGameInstallMsg,
));
context: context,
builder: (BuildContext context) => RsiLauncherEnhanceDialogUI(showNotGameInstallMsg: showNotGameInstallMsg),
);
}
Future<void> _showLogAnalyze(BuildContext context) async {

View File

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

Some files were not shown because too many files have changed in this diff Show More