// ignore_for_file: avoid_build_context_in_providers import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:starcitizen_doctor/api/analytics.dart'; import 'package:starcitizen_doctor/api/api.dart'; import 'package:starcitizen_doctor/common/conf/conf.dart'; import 'package:starcitizen_doctor/common/helper/log_helper.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; import 'package:starcitizen_doctor/common/io/rs_http.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/multi_window_manager.dart'; import 'package:starcitizen_doctor/common/utils/provider.dart'; import 'package:starcitizen_doctor/provider/download_manager.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:xml/xml.dart'; import 'dialogs/hosts_booster_dialog_ui.dart'; import 'dialogs/rsi_launcher_enhance_dialog_ui.dart'; import 'yearly_report_ui/yearly_report_ui.dart'; part 'tools_ui_model.g.dart'; part 'tools_ui_model.freezed.dart'; class ToolsItemData { String key; ToolsItemData(this.key, this.name, this.infoString, this.icon, {this.onTap}); String name; String infoString; Widget icon; AsyncCallback? onTap; } @freezed abstract class ToolsUIState with _$ToolsUIState { factory ToolsUIState({ @Default(false) bool working, @Default("") String scInstalledPath, @Default("") String rsiLauncherInstalledPath, @Default([]) List scInstallPaths, @Default([]) List rsiLauncherInstallPaths, @Default([]) List items, @Default(false) bool isItemLoading, }) = _ToolsUIState; } @riverpod class ToolsUIModel extends _$ToolsUIModel { @override ToolsUIState build() { state = ToolsUIState(); return state; } Future loadToolsCard(BuildContext context, {bool skipPathScan = false}) async { if (state.isItemLoading) return; var items = []; state = state.copyWith(items: items, isItemLoading: true); if (!skipPathScan) { await reScanPath(context); } try { items = [ if (Platform.isWindows) 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), ), ]; // 年度报告入口 logic final now = DateTime.now(); int? reportYear; if (now.month == 12 && now.day >= 10) { reportYear = now.year; } else if (now.month == 1 && now.day <= 20) { reportYear = now.year - 1; } if (reportYear != null) { items.insert( 0, ToolsItemData( "yearly_report", S.current.yearly_report_card_title(reportYear.toString()), S.current.yearly_report_card_desc(reportYear.toString()), const Icon(FontAwesomeIcons.star, size: 22), onTap: () async { _openYearlyReport(context, reportYear!); }, ), ); } if (!context.mounted) return; items.add(await _addP4kCard(context)); items.addAll([ if (Platform.isWindows) 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, S.current.log_analyzer_description, 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), ), if (Platform.isWindows) 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), ), if (Platform.isWindows) 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( "dcb_viewer", S.current.tools_action_dcb_viewer, S.current.tools_action_dcb_viewer_info, const Icon(FluentIcons.database, size: 24), onTap: () => _dcbViewer(context), ), ]); state = state.copyWith(items: items); if (!context.mounted) return; items.add(await _addGraphicsRendererCard(context)); state = state.copyWith(items: items); if (!context.mounted) return; 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 (Platform.isWindows) { if (!context.mounted) return; items.addAll(await _addNvmePatchCard(context)); } state = state.copyWith(items: items, isItemLoading: false); } catch (e) { if (!context.mounted) return; showToast(context, S.current.tools_action_info_init_failed(e)); } } Future _addP4kCard(BuildContext context) async { var torrentUrl = ""; var versionInfo = ""; try { final l = await Api.getAppTorrentDataList(); for (var torrent in l) { if (torrent.name == "Data.p4k") { torrentUrl = torrent.url ?? ""; versionInfo = torrent.info ?? ""; } } } catch (e) { dPrint("get torrent url failed: $e"); } return ToolsItemData( "p4k_downloader", S.current.tools_action_p4k_download_repair, S.current.tools_action_info_p4k_download_repair_tip(versionInfo), const Icon(FontAwesomeIcons.download, size: 24), onTap: () => _downloadP4k(context, torrentUrl), ); } Future> _addNvmePatchCard(BuildContext context) async { final nvmePatchStatus = await SystemHelper.checkNvmePatchStatus(); return [ if (nvmePatchStatus) ToolsItemData( "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, ), const Icon(FluentIcons.hard_drive, size: 24), onTap: nvmePatchStatus ? () async { state = state.copyWith(working: true); await SystemHelper.doRemoveNvmePath(); state = state.copyWith(working: false); if (!context.mounted) return; showToast(context, S.current.tools_action_info_removed_restart_effective); loadToolsCard(context, skipPathScan: true); } : null, ), if (!nvmePatchStatus) ToolsItemData( "add_nvme_settings", S.current.tools_action_write_nvme_registry_patch, S.current.tools_action_info_manual_nvme_patch, const Icon(FontAwesomeIcons.cashRegister, size: 24), onTap: () async { state = state.copyWith(working: true); final r = await SystemHelper.addNvmePatch(); if (r == "") { if (!context.mounted) return; showToast(context, S.current.tools_action_info_fix_success_restart); } else { if (!context.mounted) return; showToast(context, S.current.doctor_action_result_fix_fail(r)); } state = state.copyWith(working: false); loadToolsCard(context, skipPathScan: true); }, ), ]; } Future _addShaderCard(BuildContext context) async { final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath(); final shaderSize = ((await SystemHelper.getDirLen( gameShaderCachePath ?? "", skipPath: ["$gameShaderCachePath\\Crashes".platformPath], )) / 1024 / 1024) .toStringAsFixed(4); return ToolsItemData( "clean_shaders", S.current.tools_action_clear_shader_cache, S.current.tools_action_info_shader_cache_issue(shaderSize), const Icon(FontAwesomeIcons.shapes, size: 24), onTap: () => _cleanShaderCache(context), ); } /// 获取所有可用的版本号 Future> _getAvailableGraphicsVersions() async { final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath(); if (gameShaderCachePath == null) return []; final dir = Directory(gameShaderCachePath); if (!await dir.exists()) return []; final versions = []; await for (var entity in dir.list()) { if (entity is Directory) { final name = entity.path.split(Platform.pathSeparator).last; if (name.startsWith("starcitizen_")) { // 提取版本号,例如 starcitizen_1234567 -> 1234567 final version = name.replaceFirst("starcitizen_", ""); if (version.isNotEmpty) { versions.add(version); } } } } // 按版本号降序排序(最新的在前面) versions.sort((a, b) => b.compareTo(a)); return versions; } /// 获取当前渲染器设置 Future<(int, String?)> _getCurrentGraphicsRenderer() async { final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath(); if (gameShaderCachePath == null) return (-1, null); final versions = await _getAvailableGraphicsVersions(); if (versions.isEmpty) return (-1, null); // 使用最新版本 final latestVersion = versions.first; final settingsPath = "$gameShaderCachePath\\starcitizen_$latestVersion\\GraphicsSettings\\GraphicsSettings.json".platformPath; final file = File(settingsPath); if (!await file.exists()) return (-1, latestVersion); try { final content = await file.readAsString(); final json = jsonDecode(content) as Map; final graphicsSettings = json["GraphicsSettings"] as Map?; final renderer = graphicsSettings?["GraphicsRenderer"] as int? ?? 0; return (renderer, latestVersion); } catch (e) { dPrint("_getCurrentGraphicsRenderer error: $e"); return (-1, latestVersion); } } /// 保存渲染器设置 Future _saveGraphicsRenderer(String version, int renderer) async { final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath(); if (gameShaderCachePath == null) throw "Shader cache path not found"; final settingsDir = "$gameShaderCachePath\\starcitizen_$version\\GraphicsSettings"; final settingsPath = "$settingsDir\\GraphicsSettings.json"; // 确保目录存在 await Directory(settingsDir).create(recursive: true); final json = { "GraphicsSettings": {"SettingsVersion": 1, "GraphicsRenderer": renderer}, }; await File(settingsPath).writeAsString(const JsonEncoder.withIndent(' ').convert(json)); } /// 获取渲染器名称 String _getRendererName(int renderer) { switch (renderer) { case 0: return S.current.tools_graphics_renderer_dx11; case 1: return S.current.tools_graphics_renderer_vulkan; default: return S.current.tools_graphics_renderer_unknown; } } Future _addGraphicsRendererCard(BuildContext context) async { final (renderer, _) = await _getCurrentGraphicsRenderer(); final rendererName = _getRendererName(renderer); return ToolsItemData( "graphics_renderer", S.current.tools_action_switch_graphics_renderer, S.current.tools_action_switch_graphics_renderer_info(rendererName), const Icon(FluentIcons.video, size: 24), onTap: () => _showGraphicsRendererDialog(context), ); } Future _showGraphicsRendererDialog(BuildContext context) async { final versions = await _getAvailableGraphicsVersions(); final (currentRenderer, latestVersion) = await _getCurrentGraphicsRenderer(); if (!context.mounted) return; showDialog( context: context, builder: (dialogContext) => _GraphicsRendererDialog( versions: versions, initialVersion: latestVersion ?? "", initialRenderer: currentRenderer >= 0 ? currentRenderer : 0, onSave: (version, renderer) async { try { await _saveGraphicsRenderer(version, renderer); if (!context.mounted) return; showToast(context, S.current.tools_graphics_renderer_dialog_save_success); loadToolsCard(context, skipPathScan: true); Navigator.of(dialogContext).pop(); } catch (e) { if (!context.mounted) return; showToast(context, S.current.tools_graphics_renderer_dialog_save_failed(e)); } }, ), ); } Future _addPhotographyCard(BuildContext context) async { // 获取配置文件状态 final isEnable = await _checkPhotographyStatus(context); return ToolsItemData( "photography_mode", isEnable ? S.current.tools_action_close_photography_mode : S.current.tools_action_open_photography_mode, isEnable ? S.current.tools_action_info_restore_lens_shake : S.current.tools_action_info_one_key_close_lens_shake, const Icon(FontAwesomeIcons.camera, size: 24), onTap: () => _onChangePhotographyMode(context, isEnable), ); } /// ---------------------------- func ------------------------------------------------------- /// ----------------------------------------------------------------------------------------- /// ----------------------------------------------------------------------------------------- /// ----------------------------------------------------------------------------------------- Future reScanPath(BuildContext context, {bool checkActive = false, bool skipToast = false}) async { var scInstallPaths = []; var rsiLauncherInstallPaths = []; var scInstalledPath = ""; var rsiLauncherInstalledPath = ""; state = state.copyWith( scInstalledPath: scInstalledPath, rsiLauncherInstalledPath: rsiLauncherInstalledPath, scInstallPaths: scInstallPaths, rsiLauncherInstallPaths: rsiLauncherInstallPaths, ); try { rsiLauncherInstalledPath = await SystemHelper.getRSILauncherPath(); rsiLauncherInstallPaths.add(rsiLauncherInstalledPath); final listData = await SCLoggerHelper.getLauncherLogList(); if (listData == null) { return; } scInstallPaths = await SCLoggerHelper.getGameInstallPath( listData, checkExists: checkActive, withVersion: AppConf.gameChannels, ); if (scInstallPaths.isNotEmpty) { scInstalledPath = scInstallPaths.first; } state = state.copyWith( scInstalledPath: scInstalledPath, rsiLauncherInstalledPath: rsiLauncherInstalledPath, scInstallPaths: scInstallPaths, rsiLauncherInstallPaths: rsiLauncherInstallPaths, ); } catch (e) { dPrint(e); if (!context.mounted) return; showToast(context, S.current.tools_action_info_log_file_parse_failed); } if (!skipToast) { if (rsiLauncherInstalledPath == "") { if (!context.mounted) return; showToast(context, S.current.tools_action_info_rsi_launcher_not_found); } if (scInstalledPath == "") { if (!context.mounted) return; showToast(context, S.current.tools_action_info_star_citizen_not_found); } } } /// 重装EAC Future _reinstallEAC(BuildContext context) async { if (state.scInstalledPath.isEmpty) { showToast(context, S.current.tools_action_info_valid_game_directory_needed); return; } state = state.copyWith(working: true); try { final eacPath = "${state.scInstalledPath}\\EasyAntiCheat"; final eacJsonPath = "$eacPath\\Settings.json"; if (await File(eacJsonPath).exists()) { Map envVars = Platform.environment; final eacJsonData = await File(eacJsonPath).readAsString(); final Map eacJson = json.decode(eacJsonData); final eacID = eacJson["productid"]; if (eacID != null) { final eacCacheDir = Directory("${envVars["appdata"]}\\EasyAntiCheat\\$eacID"); if (await eacCacheDir.exists()) { await eacCacheDir.delete(recursive: true); } } } final dir = Directory(eacPath); if (await dir.exists()) { await dir.delete(recursive: true); } final eacLauncher = File("${state.scInstalledPath}\\StarCitizen_Launcher.exe"); if (await eacLauncher.exists()) { await eacLauncher.delete(recursive: true); } if (!context.mounted) return; showToast(context, S.current.tools_action_info_eac_file_removed); _adminRSILauncher(context); } catch (e) { showToast(context, S.current.tools_action_info_error_occurred(e)); } state = state.copyWith(working: false); loadToolsCard(context, skipPathScan: true); } Future 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(), ); } /// 管理员模式运行 RSI 启动器 Future _adminRSILauncher(BuildContext context) async { if (state.rsiLauncherInstalledPath == "") { showToast(context, S.current.tools_action_info_rsi_launcher_directory_not_found); } SystemHelper.checkAndLaunchRSILauncher(state.rsiLauncherInstalledPath); } Future openDir(dynamic path) async { SystemHelper.openDir(path); } Future _showSystemInfo(BuildContext context) async { state = state.copyWith(working: true); final systemInfo = await getSystemInfo(); if (!context.mounted) return; showDialog( context: context, 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), actions: [ FilledButton( child: Padding( padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8), child: Text(S.current.action_close), ), onPressed: () => Navigator.pop(context), ), ], ), ); state = state.copyWith(working: false); } /// 清理着色器缓存(全部清理模式),保留最新两个文件夹的 GraphicsSettings static Future cleanShaderCache() async { await _cleanShaderCacheWithMode(keepLatest: false); } /// 清理着色器缓存(保留最新模式) /// 保留最新文件夹的所有内容,保留第二新文件夹的 GraphicsSettings,清理其他 static Future cleanShaderCacheKeepLatest() async { await _cleanShaderCacheWithMode(keepLatest: true); } /// 内部清理方法 /// [keepLatest] true: 保留最新模式,false: 全部清理模式 static Future _cleanShaderCacheWithMode({required bool keepLatest}) async { final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath(); if (gameShaderCachePath == null) return; final dir = Directory(gameShaderCachePath); if (!await dir.exists()) return; // 获取所有 starcitizen_* 目录并按创建时间排序 final scDirs = []; final otherDirs = []; await for (var entity in dir.list(recursive: false)) { if (entity is Directory) { final dirName = entity.path.split(Platform.pathSeparator).last; if (dirName == "Crashes") continue; if (dirName.startsWith("starcitizen_")) { scDirs.add(entity); } else { otherDirs.add(entity); } } } // 按目录名(版本号)降序排序,最新的在前面 scDirs.sort((a, b) { final aName = a.path.split(Platform.pathSeparator).last; final bName = b.path.split(Platform.pathSeparator).last; return bName.compareTo(aName); }); // 清理非 starcitizen_* 目录 for (var d in otherDirs) { try { await d.delete(recursive: true); } catch (e) { dPrint("_cleanShaderCacheWithMode delete other dir error: $e"); } } // 根据模式清理 starcitizen_* 目录 for (var i = 0; i < scDirs.length; i++) { final scDir = scDirs[i]; if (keepLatest) { // 保留最新模式: // 第一个(最新):完全保留 // 第二个:仅保留 GraphicsSettings // 其他:仅保留 GraphicsSettings if (i == 0) { // 最新的文件夹完全保留,不做任何操作 continue; } else { // 其他文件夹:清理内容,保留 GraphicsSettings await _cleanShaderCacheDirectory(scDir); } } else { // 全部清理模式: // 前两个:仅保留 GraphicsSettings // 其他:仅保留 GraphicsSettings await _cleanShaderCacheDirectory(scDir); } } } Future _cleanShaderCache(BuildContext context) async { // 显示对话框让用户选择清理模式 final result = await showDialog( context: context, builder: (dialogContext) => ContentDialog( constraints: BoxConstraints(maxWidth: 380), title: Text(S.current.tools_shader_clean_dialog_title), content: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 12), SizedBox( width: double.infinity, child: FilledButton( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text(S.current.tools_shader_clean_keep_latest), ), onPressed: () => Navigator.pop(dialogContext, "keep_latest"), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: Button( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text(S.current.tools_shader_clean_all), ), onPressed: () => Navigator.pop(dialogContext, "clean_all"), ), ), ], ), actions: [Button(child: Text(S.current.app_common_tip_cancel), onPressed: () => Navigator.pop(dialogContext))], ), ); if (result == null || !context.mounted) return; state = state.copyWith(working: true); if (result == "keep_latest") { await cleanShaderCacheKeepLatest(); } else { await cleanShaderCache(); } if (!context.mounted) return; loadToolsCard(context, skipPathScan: true); state = state.copyWith(working: false); } /// 清理着色器缓存目录,保留 GraphicsSettings 文件夹 static Future _cleanShaderCacheDirectory(Directory dir) async { try { final contents = await dir.list(recursive: false).toList(); for (var entity in contents) { final name = entity.path.split(Platform.pathSeparator).last; // 保留 GraphicsSettings 文件夹 if (name == "GraphicsSettings") continue; if (entity is Directory) { await entity.delete(recursive: true); } else if (entity is File) { await entity.delete(); } } } catch (e) { dPrint("_cleanShaderCacheDirectory error: $e"); } } Future _downloadP4k(BuildContext context, String torrentUrl) async { String savePath = state.scInstalledPath; String fileName = "Data.p4k"; 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), ); return; } if (!context.mounted) return; final ok = await showConfirmDialogs( context, S.current.tools_action_p4k_download_repair, Text(S.current.tools_action_info_p4k_file_description), ); if (!ok) return; try { state = state.copyWith(working: true); final downloadManager = ref.read(downloadManagerProvider.notifier); await downloadManager.initDownloader(); // check download task list if (await downloadManager.isNameInTask("Data.p4k")) { if (!context.mounted) return; showToast(context, S.current.tools_action_info_p4k_download_in_progress); state = state.copyWith(working: false); return; } if (torrentUrl == "") { state = state.copyWith(working: false); if (!context.mounted) return; showToast(context, S.current.tools_action_info_function_under_maintenance); return; } final userSelect = await FilePicker.platform.saveFile( initialDirectory: savePath, fileName: fileName, lockParentWindow: true, ); if (userSelect == null) { state = state.copyWith(working: false); return; } savePath = userSelect; dPrint(savePath); if (savePath.endsWith("\\$fileName")) { savePath = savePath.substring(0, savePath.length - fileName.length - 1); } if (!context.mounted) return; final btData = await RSHttp.get(torrentUrl).unwrap(context: context); if (btData == null || btData.data == null) { state = state.copyWith(working: false); return; } final taskId = await downloadManager.addTorrent(btData.data!, outputFolder: savePath); state = state.copyWith(working: false); dPrint("DownloadManager.addTorrent resp === $taskId"); AnalyticsApi.touch("p4k_download"); if (!context.mounted) return; context.push("/index/downloader"); } catch (e) { state = state.copyWith(working: false); if (!context.mounted) return; showToast(context, S.current.app_init_failed_with_reason(e)); rethrow; } if (!context.mounted) return; launchUrlString("https://support.citizenwiki.cn/d/8"); } Future _checkPhotographyStatus(BuildContext context, {bool? setMode}) async { final scInstalledPath = state.scInstalledPath; final keys = ["AudioShakeStrength", "CameraSpringMovement", "ShakeScale"]; final attributesFile = File("$scInstalledPath\\USER\\Client\\0\\Profiles\\default\\attributes.xml"); if (setMode == null) { bool isEnable = false; if (scInstalledPath.isNotEmpty) { if (await attributesFile.exists()) { final xmlFile = XmlDocument.parse(await attributesFile.readAsString()); isEnable = true; for (var k in keys) { if (!isEnable) break; final e = xmlFile.rootElement.children.where((element) => element.getAttribute("name") == k).firstOrNull; if (e != null && e.getAttribute("value") == "0") { } else { isEnable = false; } } } } return isEnable; } else { if (!await attributesFile.exists()) { if (!context.mounted) return false; showToast(context, S.current.tools_action_info_config_file_not_exist); return false; } final xmlFile = XmlDocument.parse(await attributesFile.readAsString()); // clear all xmlFile.rootElement.children.removeWhere((element) => keys.contains(element.getAttribute("name"))); if (setMode) { for (var element in keys) { XmlElement newNode = XmlElement(XmlName('Attr'), [ XmlAttribute(XmlName('name'), element), XmlAttribute(XmlName('value'), '0'), ]); xmlFile.rootElement.children.add(newNode); } } dPrint(xmlFile); await attributesFile.delete(); await attributesFile.writeAsString(xmlFile.toXmlString(pretty: true)); } return true; } Future _onChangePhotographyMode(BuildContext context, bool isEnable) async { _checkPhotographyStatus(context, setMode: !isEnable).unwrap(context: context); loadToolsCard(context, skipPathScan: true); } void onChangeGamePath(String v) { state = state.copyWith(scInstalledPath: v); } void onChangeLauncherPath(String s) { state = state.copyWith(rsiLauncherInstalledPath: s); } Future _doHostsBooster(BuildContext context) async { showDialog(context: context, builder: (BuildContext context) => const HostsBoosterDialogUI()); } Future _unp4kc(BuildContext context) async { context.push("/tools/unp4kc"); } Future _dcbViewer(BuildContext context) async { context.push("/tools/dcb_viewer"); } static Future 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), ); return; } if (!context.mounted) return; showDialog( context: context, builder: (BuildContext context) => RsiLauncherEnhanceDialogUI(showNotGameInstallMsg: showNotGameInstallMsg), ); } Future _showLogAnalyze(BuildContext context) async { if (state.scInstalledPath.isEmpty) { showToast(context, S.current.tools_action_info_valid_game_directory_needed); return; } if (!context.mounted) return; await MultiWindowManager.launchSubWindow( WindowTypes.logAnalyze, S.current.log_analyzer_window_title, appGlobalState, ); } void _openYearlyReport(BuildContext context, int year) { if (state.scInstallPaths.isEmpty) { showToast(context, S.current.tools_action_info_valid_game_directory_needed); return; } Navigator.of(context).push( FluentPageRoute( builder: (context) => YearlyReportUI(gameInstallPaths: state.scInstallPaths, year: year), ), ); } } /// 图形渲染器切换对话框 class _GraphicsRendererDialog extends StatefulWidget { final List versions; final String initialVersion; final int initialRenderer; final Future Function(String version, int renderer) onSave; const _GraphicsRendererDialog({ required this.versions, required this.initialVersion, required this.initialRenderer, required this.onSave, }); @override State<_GraphicsRendererDialog> createState() => _GraphicsRendererDialogState(); } class _GraphicsRendererDialogState extends State<_GraphicsRendererDialog> { late TextEditingController _versionController; late int _selectedRenderer; bool _isSaving = false; @override void initState() { super.initState(); _versionController = TextEditingController(text: widget.initialVersion); _selectedRenderer = widget.initialRenderer; } @override void dispose() { _versionController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ContentDialog( title: Text(S.current.tools_graphics_renderer_dialog_title), constraints: const BoxConstraints(maxWidth: 460), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.versions.isEmpty) Padding( padding: EdgeInsets.only(bottom: 12), child: InfoBar( title: Text(S.current.tools_graphics_renderer_dialog_no_version), severity: InfoBarSeverity.warning, ), ), // 版本选择 Text(S.current.tools_graphics_renderer_dialog_version), const SizedBox(height: 8), AutoSuggestBox( controller: _versionController, placeholder: S.current.tools_graphics_renderer_dialog_version_hint, items: widget.versions.map((v) => AutoSuggestBoxItem(value: v, label: v)).toList(), onSelected: (item) { setState(() { _versionController.text = item.value ?? ""; }); }, ), const SizedBox(height: 16), // 渲染器选择 Text(S.current.tools_graphics_renderer_dialog_renderer), const SizedBox(height: 8), ComboBox( value: _selectedRenderer, items: [ ComboBoxItem(value: 0, child: Text(S.current.tools_graphics_renderer_dx11)), ComboBoxItem(value: 1, child: Text(S.current.tools_graphics_renderer_vulkan)), ], onChanged: (value) { if (value != null) { setState(() { _selectedRenderer = value; }); } }, ), ], ), actions: [ Button(onPressed: _isSaving ? null : () => Navigator.of(context).pop(), child: Text(S.current.action_close)), FilledButton( onPressed: _isSaving || _versionController.text.isEmpty ? null : () async { setState(() { _isSaving = true; }); try { await widget.onSave(_versionController.text, _selectedRenderer); if (context.mounted) { Navigator.of(context).pop(); } } finally { if (mounted) { setState(() { _isSaving = false; }); } } }, child: _isSaving ? const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2)) : Text(S.current.tools_graphics_renderer_dialog_save), ), ], ); } }