From c4637a80633e5c03ec1d21c40251e4b81ec9b8ed Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Sun, 10 Mar 2024 18:00:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:riverpod=20=E8=BF=81=E7=A7=BB=20ToolsUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/utils/async.dart | 7 +- lib/ui/index_ui.dart | 3 + lib/ui/tools/tools_ui.dart | 261 +++++++++++ lib/ui/tools/tools_ui_model.dart | 552 +++++++++++++++++++++++ lib/ui/tools/tools_ui_model.freezed.dart | 300 ++++++++++++ lib/ui/tools/tools_ui_model.g.dart | 25 + 6 files changed, 1147 insertions(+), 1 deletion(-) create mode 100644 lib/ui/tools/tools_ui.dart create mode 100644 lib/ui/tools/tools_ui_model.dart create mode 100644 lib/ui/tools/tools_ui_model.freezed.dart create mode 100644 lib/ui/tools/tools_ui_model.g.dart diff --git a/lib/common/utils/async.dart b/lib/common/utils/async.dart index eccb0f6..b6a4bd3 100644 --- a/lib/common/utils/async.dart +++ b/lib/common/utils/async.dart @@ -1,11 +1,16 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:starcitizen_doctor/common/utils/base_utils.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; extension AsyncError on Future { - Future unwrap() async { + Future unwrap({BuildContext? context}) async { try { return await this; } catch (e) { dPrint("unwrap error:$e"); + if (context != null) { + showToast(context, "出现错误: $e"); + } return null; } } diff --git a/lib/ui/index_ui.dart b/lib/ui/index_ui.dart index 7b9f7c7..5a6f3c9 100644 --- a/lib/ui/index_ui.dart +++ b/lib/ui/index_ui.dart @@ -7,6 +7,7 @@ import 'package:starcitizen_doctor/provider/aria2c.dart'; import 'package:starcitizen_doctor/ui/home/home_ui.dart'; import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; import 'package:starcitizen_doctor/ui/party_room/party_room_ui.dart'; +import 'package:starcitizen_doctor/ui/tools/tools_ui.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:window_manager/window_manager.dart'; @@ -119,6 +120,8 @@ class IndexUI extends HookConsumerWidget { return const HomeUI(); case 1: return const PartyRoomUI(); + case 2: + return const ToolsUI(); default: return Center( child: Text("UnimplPage $value"), diff --git a/lib/ui/tools/tools_ui.dart b/lib/ui/tools/tools_ui.dart new file mode 100644 index 0000000..397bae6 --- /dev/null +++ b/lib/ui/tools/tools_ui.dart @@ -0,0 +1,261 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; + +class ToolsUI extends HookConsumerWidget { + const ToolsUI({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(toolsUIModelProvider); + final model = ref.read(toolsUIModelProvider.notifier); + + useEffect(() { + addPostFrameCallback(() { + model.loadToolsCard(context, skipPathScan: false); + }); + return null; + }, []); + + return Stack( + children: [ + Column( + children: [ + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.only(left: 22, right: 22), + child: Row( + children: [ + Expanded( + child: Column( + children: [ + makeGameLauncherPathSelect(context, model, state), + const SizedBox(height: 12), + makeGamePathSelect(context, model, state), + ], + ), + ), + const SizedBox(width: 12), + Button( + onPressed: state.working + ? null + : () => + model.loadToolsCard(context, skipPathScan: false), + child: const Padding( + padding: EdgeInsets.only( + top: 30, bottom: 30, left: 12, right: 12), + child: Icon(FluentIcons.refresh), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + if (state.items.isEmpty) + const Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ProgressRing(), + SizedBox(height: 12), + Text("正在扫描..."), + ], + ), + ), + ) + else + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: AlignedGridView.count( + crossAxisCount: 3, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemCount: (state.isItemLoading) + ? state.items.length + 1 + : state.items.length, + shrinkWrap: true, + itemBuilder: (context, index) { + if (index == state.items.length) { + return Container( + width: 300, + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: FluentTheme.of(context).cardColor, + ), + child: makeLoading(context)); + } + final item = state.items[index]; + return Container( + width: 300, + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: FluentTheme.of(context).cardColor, + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(.2), + borderRadius: + BorderRadius.circular(1000)), + child: Padding( + padding: const EdgeInsets.all(8), + child: item.icon, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + item.name, + style: const TextStyle(fontSize: 16), + )), + const SizedBox(width: 12), + ], + ), + const SizedBox(height: 12), + Expanded( + child: Text( + item.infoString, + style: TextStyle( + fontSize: 14, + color: Colors.white.withOpacity(.6)), + )), + Row( + children: [ + const Spacer(), + Button( + onPressed: state.working + ? null + : item.onTap == null + ? null + : () { + try { + item.onTap?.call(); + } catch (e) { + showToast( + context, "处理失败!:$e"); + } + }, + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(FluentIcons.play), + ), + ), + ], + ) + ], + ), + ), + ); + }, + ), + ), + ) + ], + ), + if (state.working) + Container( + decoration: BoxDecoration( + color: Colors.black.withAlpha(150), + ), + child: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ProgressRing(), + SizedBox(height: 12), + Text("正在处理..."), + ], + ), + ), + ) + ], + ); + } + + Widget makeGamePathSelect( + BuildContext context, ToolsUIModel model, ToolsUIState state) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("游戏安装位置: "), + const SizedBox(width: 6), + Expanded( + child: SizedBox( + height: 36, + child: ComboBox( + value: state.scInstalledPath, + items: [ + for (final path in state.scInstallPaths) + ComboBoxItem( + value: path, + child: Text(path), + ) + ], + onChanged: (v) { + model.loadToolsCard(context, skipPathScan: true); + model.onChangeGamePath(v!); + }, + ), + ), + ), + const SizedBox(width: 8), + Button( + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(FluentIcons.folder_open), + ), + onPressed: () => model.openDir(state.scInstalledPath)) + ], + ); + } + + Widget makeGameLauncherPathSelect( + BuildContext context, ToolsUIModel model, ToolsUIState state) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("RSI启动器位置:"), + const SizedBox(width: 6), + Expanded( + child: SizedBox( + height: 36, + child: ComboBox( + value: state.rsiLauncherInstalledPath, + items: [ + for (final path in state.rsiLauncherInstallPaths) + ComboBoxItem( + value: path, + child: Text(path), + ) + ], + onChanged: (v) { + model.loadToolsCard(context, skipPathScan: true); + model.onChangeLauncherPath(v!); + }, + ), + ), + ), + const SizedBox(width: 8), + Button( + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(FluentIcons.folder_open), + ), + onPressed: () => model.openDir(state.rsiLauncherInstalledPath)) + ], + ); + } +} diff --git a/lib/ui/tools/tools_ui_model.dart b/lib/ui/tools/tools_ui_model.dart new file mode 100644 index 0000000..780cbd4 --- /dev/null +++ b/lib/ui/tools/tools_ui_model.dart @@ -0,0 +1,552 @@ +// 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/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/provider.dart'; +import 'package:starcitizen_doctor/provider/aria2c.dart'; +import 'package:starcitizen_doctor/ui/home/downloader/home_downloader_ui_model.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:xml/xml.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 +class ToolsUIState with _$ToolsUIState { + const 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 = const ToolsUIState(); + return state; + } + + 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 = [ + ToolsItemData( + "systemnfo", + "查看系统信息", + "查看系统关键信息,用于快速问诊 \n\n耗时操作,请耐心等待。", + const Icon(FluentIcons.system, size: 28), + onTap: () => _showSystemInfo(context), + ), + ToolsItemData( + "p4k_downloader", + "P4K 分流下载 / 修复", + "使用星际公民中文百科提供的分流下载服务,可用于下载或修复 p4k。 \n资源有限,请勿滥用。", + const Icon(FontAwesomeIcons.download, size: 28), + onTap: () => _downloadP4k(context), + ), + ToolsItemData( + "reinstall_eac", + "重装 EasyAntiCheat 反作弊", + "若您遇到 EAC 错误,且自动修复无效,请尝试使用此功能重装 EAC。", + const Icon(FluentIcons.game, size: 28), + onTap: () => _reinstallEAC(context), + ), + ToolsItemData( + "rsilauncher_admin_mode", + "RSI Launcher 管理员模式", + "以管理员身份运行RSI启动器,可能会解决一些问题。\n\n若设置了能效核心屏蔽参数,也会在此应用。", + const Icon(FluentIcons.admin, size: 28), + onTap: () => _adminRSILauncher(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 (!context.mounted) return; + items.addAll(await _addLogCard(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; + showToast(context, "初始化失败,请截图报告给开发者。$e"); + } + } + + Future> _addLogCard(BuildContext context) async { + double logPathLen = 0; + try { + logPathLen = + (await File(await SCLoggerHelper.getLogFilePath() ?? "").length()) / + 1024 / + 1024; + } catch (_) {} + return [ + ToolsItemData( + "rsilauncher_log_fix", + "RSI Launcher Log 修复", + "在某些情况下 RSI启动器 的 log 文件会损坏,导致无法完成问题扫描,使用此工具清理损坏的 log 文件。\n\n当前日志文件大小:${(logPathLen.toStringAsFixed(4))} MB", + const Icon(FontAwesomeIcons.bookBible, size: 28), + onTap: () => _rsiLogFix(context), + ), + ]; + } + + Future> _addNvmePatchCard(BuildContext context) async { + final nvmePatchStatus = await SystemHelper.checkNvmePatchStatus(); + return [ + if (nvmePatchStatus) + ToolsItemData( + "remove_nvme_settings", + "移除 nvme 注册表补丁", + "若您使用 nvme 补丁出现问题,请运行此工具。(可能导致游戏 安装/更新 不可用。)\n\n当前补丁状态:${(nvmePatchStatus) ? "已安装" : "未安装"}", + const Icon(FluentIcons.hard_drive, size: 28), + onTap: nvmePatchStatus + ? () async { + state = state.copyWith(working: true); + await SystemHelper.doRemoveNvmePath(); + state = state.copyWith(working: false); + if (!context.mounted) return; + showToast(context, "已移除,重启电脑生效!"); + loadToolsCard(context, skipPathScan: true); + } + : null, + ), + if (!nvmePatchStatus) + ToolsItemData( + "add_nvme_settings", + "写入 nvme 注册表补丁", + "手动写入NVM补丁,该功能仅在您知道自己在作什么的情况下使用", + const Icon(FontAwesomeIcons.cashRegister, size: 28), + onTap: () async { + state = state.copyWith(working: true); + final r = await SystemHelper.addNvmePatch(); + if (r == "") { + if (!context.mounted) return; + showToast(context, + "修复成功,请尝试重启电脑后继续安装游戏! 若注册表修改操作导致其他软件出现兼容问题,请使用 工具 中的 NVME 注册表清理。"); + } else { + if (!context.mounted) return; + showToast(context, "修复失败,$r"); + } + state = state.copyWith(working: false); + loadToolsCard(context, skipPathScan: true); + }, + ) + ]; + } + + Future _addShaderCard(BuildContext context) async { + final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath(); + return ToolsItemData( + "clean_shaders", + "清理着色器缓存", + "若游戏画面出现异常或版本更新后可使用本工具清理过期的着色器(当大于500M时,建议清理) \n\n缓存大小:${((await SystemHelper.getDirLen(gameShaderCachePath ?? "", skipPath: [ + "$gameShaderCachePath\\Crashes" + ])) / 1024 / 1024).toStringAsFixed(4)} MB", + const Icon(FontAwesomeIcons.shapes, size: 28), + onTap: () => _cleanShaderCache(context), + ); + } + + Future _addPhotographyCard(BuildContext context) async { + // 获取配置文件状态 + final isEnable = await _checkPhotographyStatus(context); + + return ToolsItemData( + "photography_mode", + isEnable ? "关闭摄影模式" : "开启摄影模式", + isEnable + ? "还原镜头摇晃效果。\n\n@拉邦那 Lapernum 提供参数信息。" + : "一键关闭游戏内镜头晃动以便于摄影操作。\n\n @拉邦那 Lapernum 提供参数信息。", + const Icon(FontAwesomeIcons.camera, size: 28), + onTap: () => _onChangePhotographyMode(context, isEnable), + ); + } + + /// ---------------------------- func ------------------------------------------------------- + /// ----------------------------------------------------------------------------------------- + /// ----------------------------------------------------------------------------------------- + /// ----------------------------------------------------------------------------------------- + + Future reScanPath(BuildContext context) 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: false, withVersion: ["LIVE", "PTU", "EPTU"]); + 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, "解析 log 文件失败!\n请尝试使用 RSI Launcher log 修复 工具!"); + } + + if (rsiLauncherInstalledPath == "") { + if (!context.mounted) return; + showToast(context, "未找到 RSI 启动器,请尝试重新安装,或在设置中手动添加。"); + } + if (scInstalledPath == "") { + if (!context.mounted) return; + showToast(context, "未找到星际公民游戏安装位置,请至少完成一次游戏启动操作 或在设置中手动添加。"); + } + } + + /// 重装EAC + Future _reinstallEAC(BuildContext context) async { + if (state.scInstalledPath.isEmpty) { + showToast(context, "该功能需要一个有效的游戏安装目录"); + 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, + "已为您移除 EAC 文件,接下来将为您打开 RSI 启动器,请您前往 SETTINGS -> VERIFY 重装 EAC。"); + _adminRSILauncher(context); + } catch (e) { + showToast(context, "出现错误:$e"); + } + state = state.copyWith(working: false); + loadToolsCard(context, skipPathScan: true); + } + + Future getSystemInfo() async { + return "系统:${await SystemHelper.getSystemName()}\n\n" + "处理器:${await SystemHelper.getCpuName()}\n\n" + "内存大小:${await SystemHelper.getSystemMemorySizeGB()}GB\n\n" + "显卡信息:\n${await SystemHelper.getGpuInfo()}\n\n" + "硬盘信息:\n${await SystemHelper.getDiskInfo()}\n\n"; + } + + /// 管理员模式运行 RSI 启动器 + Future _adminRSILauncher(BuildContext context) async { + if (state.rsiLauncherInstalledPath == "") { + showToast(context, "未找到 RSI 启动器目录,请您尝试手动操作。"); + } + SystemHelper.checkAndLaunchRSILauncher(state.rsiLauncherInstalledPath); + } + + Future _rsiLogFix(BuildContext context) async { + state = state.copyWith(working: true); + final path = await SCLoggerHelper.getLogFilePath(); + if (!await File(path!).exists()) { + if (!context.mounted) return; + showToast( + context, "日志文件不存在,请尝试进行一次游戏启动或游戏安装,并退出启动器,若无法解决问题,请尝试将启动器更新至最新版本!"); + return; + } + try { + SystemHelper.killRSILauncher(); + await File(path).delete(recursive: true); + if (!context.mounted) return; + showToast(context, "清理完毕,请完成一次安装 / 游戏启动 操作。"); + SystemHelper.checkAndLaunchRSILauncher(state.rsiLauncherInstalledPath); + } catch (_) { + if (!context.mounted) return; + showToast(context, "清理失败,请手动移除,文件位置:$path"); + } + + state = state.copyWith(working: false); + } + + openDir(path) async { + await Process.run( + SystemHelper.powershellPath, ["explorer.exe", "/select,\"$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: const Text('系统信息'), + content: Text(systemInfo), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * .65, + ), + actions: [ + FilledButton( + child: const Padding( + padding: EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8), + child: Text('关闭'), + ), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ); + state = state.copyWith(working: false); + } + + Future _cleanShaderCache(BuildContext context) async { + state = state.copyWith(working: true); + final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath(); + final l = + await Directory(gameShaderCachePath!).list(recursive: false).toList(); + for (var value in l) { + if (value is Directory) { + if (!value.absolute.path.contains("Crashes")) { + await value.delete(recursive: true); + } + } + } + if (!context.mounted) return; + loadToolsCard(context, skipPathScan: true); + state = state.copyWith(working: false); + } + + Future _downloadP4k(BuildContext context) async { + String savePath = state.scInstalledPath; + String fileName = "Data.p4k"; + + if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) { + if (!context.mounted) return; + showToast(context, "RSI启动器正在运行!请先关闭启动器再使用此功能!", + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * .35)); + return; + } + + if (!context.mounted) return; + await showToast( + context, + "P4k 是星际公民的核心游戏文件,高达 100GB+,盒子提供的离线下载是为了帮助一些p4k文件下载超级慢的用户 或用于修复官方启动器无法修复的 p4k 文件。" + "\n\n接下来会弹窗询问您保存位置(可以选择星际公民文件夹也可以选择别处),下载完成后请确保 P4K 文件夹位于 LIVE 文件夹内,之后使用星际公民启动器校验更新即可。"); + + try { + state = state.copyWith(working: true); + final aria2cManager = ref.read(aria2cModelProvider.notifier); + await aria2cManager + .launchDaemon(appGlobalState.applicationBinaryModuleDir!); + final aria2c = ref.read(aria2cModelProvider).aria2c!; + + // check download task list + for (var value in [ + ...await aria2c.tellActive(), + ...await aria2c.tellWaiting(0, 100000) + ]) { + final t = HomeDownloaderUIModel.getTaskTypeAndName(value); + if (t.key == "torrent" && t.value.contains("Data.p4k")) { + if (!context.mounted) return; + showToast(context, "已经有一个p4k下载任务正在进行中,请前往下载管理器查看!"); + state = state.copyWith(working: false); + return; + } + } + + var torrentUrl = ""; + final l = await Api.getAppTorrentDataList(); + for (var torrent in l) { + if (torrent.name == "Data.p4k") { + torrentUrl = torrent.url!; + } + } + if (torrentUrl == "") { + state = state.copyWith(working: false); + if (!context.mounted) return; + showToast(context, "功能维护中,请稍后重试!"); + 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 b64Str = base64Encode(btData.data!); + + final gid = + await aria2c.addTorrent(b64Str, extraParams: {"dir": savePath}); + state = state.copyWith(working: false); + dPrint("Aria2cManager.aria2c.addUri resp === $gid"); + await aria2c.saveSession(); + 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, "初始化失败!: $e"); + } + await Future.delayed(const Duration(seconds: 3)); + launchUrlString( + "https://citizenwiki.cn/SC%E6%B1%89%E5%8C%96%E7%9B%92%E5%AD%90#%E5%88%86%E6%B5%81%E4%B8%8B%E8%BD%BD%E6%95%99%E7%A8%8B"); + } + + Future _checkPhotographyStatus(BuildContext context, + {bool? setMode}) async { + final scInstalledPath = state.scInstalledPath; + const 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, "配置文件不存在,请尝试运行一次游戏"); + 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; + } + + _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); + } +} diff --git a/lib/ui/tools/tools_ui_model.freezed.dart b/lib/ui/tools/tools_ui_model.freezed.dart new file mode 100644 index 0000000..9cff683 --- /dev/null +++ b/lib/ui/tools/tools_ui_model.freezed.dart @@ -0,0 +1,300 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'tools_ui_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ToolsUIState { + bool get working => throw _privateConstructorUsedError; + String get scInstalledPath => throw _privateConstructorUsedError; + String get rsiLauncherInstalledPath => throw _privateConstructorUsedError; + List get scInstallPaths => throw _privateConstructorUsedError; + List get rsiLauncherInstallPaths => + throw _privateConstructorUsedError; + List get items => throw _privateConstructorUsedError; + bool get isItemLoading => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ToolsUIStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ToolsUIStateCopyWith<$Res> { + factory $ToolsUIStateCopyWith( + ToolsUIState value, $Res Function(ToolsUIState) then) = + _$ToolsUIStateCopyWithImpl<$Res, ToolsUIState>; + @useResult + $Res call( + {bool working, + String scInstalledPath, + String rsiLauncherInstalledPath, + List scInstallPaths, + List rsiLauncherInstallPaths, + List items, + bool isItemLoading}); +} + +/// @nodoc +class _$ToolsUIStateCopyWithImpl<$Res, $Val extends ToolsUIState> + implements $ToolsUIStateCopyWith<$Res> { + _$ToolsUIStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? working = null, + Object? scInstalledPath = null, + Object? rsiLauncherInstalledPath = null, + Object? scInstallPaths = null, + Object? rsiLauncherInstallPaths = null, + Object? items = null, + Object? isItemLoading = null, + }) { + return _then(_value.copyWith( + working: null == working + ? _value.working + : working // ignore: cast_nullable_to_non_nullable + as bool, + scInstalledPath: null == scInstalledPath + ? _value.scInstalledPath + : scInstalledPath // ignore: cast_nullable_to_non_nullable + as String, + rsiLauncherInstalledPath: null == rsiLauncherInstalledPath + ? _value.rsiLauncherInstalledPath + : rsiLauncherInstalledPath // ignore: cast_nullable_to_non_nullable + as String, + scInstallPaths: null == scInstallPaths + ? _value.scInstallPaths + : scInstallPaths // ignore: cast_nullable_to_non_nullable + as List, + rsiLauncherInstallPaths: null == rsiLauncherInstallPaths + ? _value.rsiLauncherInstallPaths + : rsiLauncherInstallPaths // ignore: cast_nullable_to_non_nullable + as List, + items: null == items + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + isItemLoading: null == isItemLoading + ? _value.isItemLoading + : isItemLoading // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ToolsUIStateImplCopyWith<$Res> + implements $ToolsUIStateCopyWith<$Res> { + factory _$$ToolsUIStateImplCopyWith( + _$ToolsUIStateImpl value, $Res Function(_$ToolsUIStateImpl) then) = + __$$ToolsUIStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool working, + String scInstalledPath, + String rsiLauncherInstalledPath, + List scInstallPaths, + List rsiLauncherInstallPaths, + List items, + bool isItemLoading}); +} + +/// @nodoc +class __$$ToolsUIStateImplCopyWithImpl<$Res> + extends _$ToolsUIStateCopyWithImpl<$Res, _$ToolsUIStateImpl> + implements _$$ToolsUIStateImplCopyWith<$Res> { + __$$ToolsUIStateImplCopyWithImpl( + _$ToolsUIStateImpl _value, $Res Function(_$ToolsUIStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? working = null, + Object? scInstalledPath = null, + Object? rsiLauncherInstalledPath = null, + Object? scInstallPaths = null, + Object? rsiLauncherInstallPaths = null, + Object? items = null, + Object? isItemLoading = null, + }) { + return _then(_$ToolsUIStateImpl( + working: null == working + ? _value.working + : working // ignore: cast_nullable_to_non_nullable + as bool, + scInstalledPath: null == scInstalledPath + ? _value.scInstalledPath + : scInstalledPath // ignore: cast_nullable_to_non_nullable + as String, + rsiLauncherInstalledPath: null == rsiLauncherInstalledPath + ? _value.rsiLauncherInstalledPath + : rsiLauncherInstalledPath // ignore: cast_nullable_to_non_nullable + as String, + scInstallPaths: null == scInstallPaths + ? _value._scInstallPaths + : scInstallPaths // ignore: cast_nullable_to_non_nullable + as List, + rsiLauncherInstallPaths: null == rsiLauncherInstallPaths + ? _value._rsiLauncherInstallPaths + : rsiLauncherInstallPaths // ignore: cast_nullable_to_non_nullable + as List, + items: null == items + ? _value._items + : items // ignore: cast_nullable_to_non_nullable + as List, + isItemLoading: null == isItemLoading + ? _value.isItemLoading + : isItemLoading // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$ToolsUIStateImpl implements _ToolsUIState { + const _$ToolsUIStateImpl( + {this.working = false, + this.scInstalledPath = "", + this.rsiLauncherInstalledPath = "", + final List scInstallPaths = const [], + final List rsiLauncherInstallPaths = const [], + final List items = const [], + this.isItemLoading = false}) + : _scInstallPaths = scInstallPaths, + _rsiLauncherInstallPaths = rsiLauncherInstallPaths, + _items = items; + + @override + @JsonKey() + final bool working; + @override + @JsonKey() + final String scInstalledPath; + @override + @JsonKey() + final String rsiLauncherInstalledPath; + final List _scInstallPaths; + @override + @JsonKey() + List get scInstallPaths { + if (_scInstallPaths is EqualUnmodifiableListView) return _scInstallPaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_scInstallPaths); + } + + final List _rsiLauncherInstallPaths; + @override + @JsonKey() + List get rsiLauncherInstallPaths { + if (_rsiLauncherInstallPaths is EqualUnmodifiableListView) + return _rsiLauncherInstallPaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_rsiLauncherInstallPaths); + } + + final List _items; + @override + @JsonKey() + List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } + + @override + @JsonKey() + final bool isItemLoading; + + @override + String toString() { + return 'ToolsUIState(working: $working, scInstalledPath: $scInstalledPath, rsiLauncherInstalledPath: $rsiLauncherInstalledPath, scInstallPaths: $scInstallPaths, rsiLauncherInstallPaths: $rsiLauncherInstallPaths, items: $items, isItemLoading: $isItemLoading)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ToolsUIStateImpl && + (identical(other.working, working) || other.working == working) && + (identical(other.scInstalledPath, scInstalledPath) || + other.scInstalledPath == scInstalledPath) && + (identical( + other.rsiLauncherInstalledPath, rsiLauncherInstalledPath) || + other.rsiLauncherInstalledPath == rsiLauncherInstalledPath) && + const DeepCollectionEquality() + .equals(other._scInstallPaths, _scInstallPaths) && + const DeepCollectionEquality().equals( + other._rsiLauncherInstallPaths, _rsiLauncherInstallPaths) && + const DeepCollectionEquality().equals(other._items, _items) && + (identical(other.isItemLoading, isItemLoading) || + other.isItemLoading == isItemLoading)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + working, + scInstalledPath, + rsiLauncherInstalledPath, + const DeepCollectionEquality().hash(_scInstallPaths), + const DeepCollectionEquality().hash(_rsiLauncherInstallPaths), + const DeepCollectionEquality().hash(_items), + isItemLoading); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ToolsUIStateImplCopyWith<_$ToolsUIStateImpl> get copyWith => + __$$ToolsUIStateImplCopyWithImpl<_$ToolsUIStateImpl>(this, _$identity); +} + +abstract class _ToolsUIState implements ToolsUIState { + const factory _ToolsUIState( + {final bool working, + final String scInstalledPath, + final String rsiLauncherInstalledPath, + final List scInstallPaths, + final List rsiLauncherInstallPaths, + final List items, + final bool isItemLoading}) = _$ToolsUIStateImpl; + + @override + bool get working; + @override + String get scInstalledPath; + @override + String get rsiLauncherInstalledPath; + @override + List get scInstallPaths; + @override + List get rsiLauncherInstallPaths; + @override + List get items; + @override + bool get isItemLoading; + @override + @JsonKey(ignore: true) + _$$ToolsUIStateImplCopyWith<_$ToolsUIStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/tools/tools_ui_model.g.dart b/lib/ui/tools/tools_ui_model.g.dart new file mode 100644 index 0000000..04f1e67 --- /dev/null +++ b/lib/ui/tools/tools_ui_model.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tools_ui_model.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$toolsUIModelHash() => r'4fb78bfe350d792cfdadd3314f4763097ea1b279'; + +/// See also [ToolsUIModel]. +@ProviderFor(ToolsUIModel) +final toolsUIModelProvider = + AutoDisposeNotifierProvider.internal( + ToolsUIModel.new, + name: r'toolsUIModelProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$toolsUIModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ToolsUIModel = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member