diff --git a/lib/app.dart b/lib/app.dart index 52af49f..d6b6146 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -23,6 +23,7 @@ import 'common/helper/system_helper.dart'; import 'common/io/rs_http.dart'; import 'common/rust/frb_generated.dart'; import 'data/app_version_data.dart'; +import 'ui/home/game_doctor/game_doctor_ui.dart'; import 'ui/index_ui.dart'; import 'ui/settings/upgrade_dialog.dart'; @@ -43,6 +44,13 @@ GoRouter router(RouterRef ref) { path: '/index', pageBuilder: (context, state) => myPageBuilder(context, state, const IndexUI()), + routes: [ + GoRoute( + path: 'game_doctor', + pageBuilder: (context, state) => + myPageBuilder(context, state, const HomeGameDoctorUI()), + ), + ], ), ], ); diff --git a/lib/app.g.dart b/lib/app.g.dart index 6d87e98..9aebe88 100644 --- a/lib/app.g.dart +++ b/lib/app.g.dart @@ -6,7 +6,7 @@ part of 'app.dart'; // RiverpodGenerator // ************************************************************************** -String _$routerHash() => r'6e1ddfd4fb004ca0c2262ab17a4b0882f12bcc3b'; +String _$routerHash() => r'd8cf08526e81cdcda0203c32fc89140f2ffb0058'; /// See also [router]. @ProviderFor(router) diff --git a/lib/ui/home/game_doctor/game_doctor_ui.dart b/lib/ui/home/game_doctor/game_doctor_ui.dart new file mode 100644 index 0000000..0fefbf2 --- /dev/null +++ b/lib/ui/home/game_doctor/game_doctor_ui.dart @@ -0,0 +1,294 @@ +import 'package:fluent_ui/fluent_ui.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:starcitizen_doctor/common/helper/log_helper.dart'; +import 'package:starcitizen_doctor/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import 'game_doctor_ui_model.dart'; + +class HomeGameDoctorUI extends HookConsumerWidget { + const HomeGameDoctorUI({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(homeGameDoctorUIModelProvider); + final homeState = ref.watch(homeUIModelProvider); + final model = ref.read(homeGameDoctorUIModelProvider.notifier); + + useEffect(() { + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + dPrint("HomeGameDoctorUI useEffect doCheck timeStamp === $timeStamp"); + model.doCheck(context); + }); + return null; + }, const []); + + return makeDefaultPage(context, + title: "一键诊断", + useBodyContainer: true, + content: Stack( + children: [ + Column( + children: [ + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + for (final item in const { + "rsi_log": "RSI启动器log", + "game_log": "游戏运行log", + }.entries) + 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), + ], + ), + ), + onPressed: () => + _onTapButton(context, item.key, homeState)), + ), + ], + ), + if (state.isChecking) + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ProgressRing(), + const SizedBox(height: 12), + Text(state.lastScreenInfo) + ], + ), + )) + else if (state.checkResult == null || + state.checkResult!.isEmpty) ...[ + const Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 12), + Text("扫描完毕,没有找到问题!", maxLines: 1), + 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 + : "正在处理..."), + ], + ), + ), + ), + Positioned( + bottom: 20, + right: 20, + child: makeRescueBanner(context), + ) + ], + )); + } + + Widget makeRescueBanner(BuildContext context) { + return GestureDetector( + onTap: () async { + await showToast(context, + "您即将前往由 深空治疗中心(QQ群号:536454632 ) 提供的游戏异常救援服务,主要解决游戏安装失败与频繁闪退,如游戏玩法问题,请勿加群。"); + launchUrlString( + "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, + ), + 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), + const Text("需要帮助? 点击加群寻求免费人工支援!"), + ], + ), + )), + ), + ); + } + + List makeResult(BuildContext context, HomeGameDoctorState state, + HomeGameDoctorUIModel model) { + return [ + const SizedBox(height: 24), + Text(state.lastScreenInfo, maxLines: 1), + const SizedBox(height: 12), + Text( + "注意:本工具检测结果仅供参考,若您不理解以下操作,请提供截图给有经验的玩家!", + style: TextStyle(color: Colors.red, fontSize: 16), + ), + const SizedBox(height: 24), + ListView.builder( + itemCount: state.checkResult!.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + final item = state.checkResult![index]; + return makeResultItem(context, item, state, model); + }, + ), + const SizedBox(height: 64), + ]; + } + + Widget makeResultItem(BuildContext context, MapEntry item, + HomeGameDoctorState state, HomeGameDoctorUIModel model) { + final errorNames = { + "unSupport_system": + MapEntry("不支持的操作系统,游戏可能无法运行", "请升级您的系统 (${item.value})"), + "no_live_path": MapEntry("安装目录缺少LIVE文件夹,可能导致安装失败", + "点击修复为您创建 LIVE 文件夹,完成后重试安装。(${item.value})"), + "nvme_PhysicalBytes": MapEntry("新型 NVME 设备,与 RSI 启动器暂不兼容,可能导致安装失败", + "为注册表项添加 ForcedPhysicalSectorSizeInBytes 值 模拟旧设备。硬盘分区(${item.value})"), + "eac_file_miss": const MapEntry("EasyAntiCheat 文件丢失", + "未在 LIVE 文件夹找到 EasyAntiCheat 文件 或 文件不完整,请使用 RSI 启动器校验文件"), + "eac_not_install": const MapEntry("EasyAntiCheat 未安装 或 未正常退出", + "EasyAntiCheat 未安装,请点击修复为您一键安装。(在游戏正常启动并结束前,该问题会一直出现,若您因为其他原因游戏闪退,可忽略此条目)"), + "cn_user_name": + const MapEntry("中文用户名!", "中文用户名可能会导致游戏启动/安装错误! 点击修复按钮查看修改教程!"), + "cn_install_path": MapEntry("中文安装路径!", + "中文安装路径!这可能会导致游戏 启动/安装 错误!(${item.value}),请在RSI启动器更换安装路径。"), + "low_ram": MapEntry( + "物理内存过低", "您至少需要 16GB 的物理内存(Memory)才可运行此游戏。(当前大小:${item.value})"), + }; + bool isCheckedError = errorNames.containsKey(item.key); + + if (isCheckedError) { + return Container( + 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), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4, bottom: 4), + child: Column( + children: [ + const SizedBox(height: 4), + Text( + "修复建议: ${errorNames[item.key]?.value ?? "暂无解决方法,请截图反馈"}", + style: TextStyle( + fontSize: 14, color: Colors.white.withOpacity(.7)), + ), + ], + ), + ), + trailing: Button( + onPressed: (errorNames[item.key]?.value == null || state.isFixing) + ? null + : () async { + await model.doFix(context, item); + }, + child: const Padding( + padding: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), + child: Text("修复"), + ), + ), + ), + ); + } + + final isSubTitleUrl = item.value.startsWith("https://"); + + return Container( + decoration: BoxDecoration( + color: FluentTheme.of(context).cardColor, + ), + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + 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.withOpacity(.7)), + ), + ], + ), + trailing: isSubTitleUrl + ? Button( + onPressed: () { + launchUrlString(item.value); + }, + child: const Padding( + padding: + EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), + child: Text("查看解决方案"), + ), + ) + : null, + ), + ); + } + + _onTapButton( + BuildContext context, String key, HomeUIModelState homeState) async { + switch (key) { + case "rsi_log": + final path = await SCLoggerHelper.getLogFilePath(); + if (path == null) return; + SystemHelper.openDir(path); + return; + case "game_log": + if (homeState.scInstalledPath == "not_install" || + homeState.scInstalledPath == null) { + showToast(context, "请在首页选择游戏安装目录。"); + return; + } + SystemHelper.openDir("${homeState.scInstalledPath}\\Game.log"); + return; + } + } +} diff --git a/lib/ui/home/game_doctor/game_doctor_ui_model.dart b/lib/ui/home/game_doctor/game_doctor_ui_model.dart new file mode 100644 index 0000000..3f5fe21 --- /dev/null +++ b/lib/ui/home/game_doctor/game_doctor_ui_model.dart @@ -0,0 +1,273 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:starcitizen_doctor/common/helper/log_helper.dart'; +import 'package:starcitizen_doctor/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/common/utils/base_utils.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +part 'game_doctor_ui_model.g.dart'; + +part 'game_doctor_ui_model.freezed.dart'; + +@freezed +class HomeGameDoctorState with _$HomeGameDoctorState { + const factory HomeGameDoctorState({ + @Default(false) bool isChecking, + @Default(false) bool isFixing, + @Default("") String lastScreenInfo, + @Default("") String isFixingString, + List>? checkResult, + }) = _HomeGameDoctorState; +} + +@riverpod +class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel { + @override + HomeGameDoctorState build() { + state = const HomeGameDoctorState(); + return state; + } + + Future doFix( + // ignore: avoid_build_context_in_providers + BuildContext context, + MapEntry item) async { + final checkResult = + List>.from(state.checkResult ?? []); + state = state.copyWith(isFixing: true, isFixingString: ""); + switch (item.key) { + case "unSupport_system": + showToast(context, "若您的硬件达标,请尝试安装最新的 Windows 系统。"); + break; + case "no_live_path": + try { + await Directory(item.value).create(recursive: true); + if (!context.mounted) break; + showToast(context, "创建文件夹成功,请尝试继续下载游戏!"); + checkResult.remove(item); + state = state.copyWith(checkResult: checkResult); + } catch (e) { + showToast(context, "创建文件夹失败,请尝试手动创建。\n目录:${item.value} \n错误:$e"); + } + break; + case "nvme_PhysicalBytes": + final r = await SystemHelper.addNvmePatch(); + if (r == "") { + if (!context.mounted) break; + showToast(context, + "修复成功,请尝试重启后继续安装游戏! 若注册表修改操作导致其他软件出现兼容问题,请使用 工具 中的 NVME 注册表清理。"); + checkResult.remove(item); + state = state.copyWith(checkResult: checkResult); + } else { + if (!context.mounted) break; + showToast(context, "修复失败,$r"); + } + break; + case "eac_file_miss": + showToast( + context, "未在 LIVE 文件夹找到 EasyAntiCheat 文件 或 文件不完整,请使用 RSI 启动器校验文件"); + break; + case "eac_not_install": + final eacJsonPath = "${item.value}\\Settings.json"; + final eacJsonData = await File(eacJsonPath).readAsBytes(); + 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]); + dPrint("${item.value}\\EasyAntiCheat_EOS_Setup.exe install $eacID"); + if (result.stderr == "") { + if (!context.mounted) break; + showToast(context, "修复成功,请尝试启动游戏。(若问题无法解决,请使用工具箱的 《重装 EAC》)"); + checkResult.remove(item); + state = state.copyWith(checkResult: checkResult); + } else { + if (!context.mounted) break; + showToast(context, "修复失败,${result.stderr}"); + } + } catch (e) { + if (!context.mounted) break; + showToast(context, "修复失败,$e"); + } + break; + case "cn_user_name": + showToast(context, "即将跳转,教程来自互联网,请谨慎操作..."); + await Future.delayed(const Duration(milliseconds: 300)); + launchUrlString( + "https://btfy.eu.org/?q=5L+u5pS5d2luZG93c+eUqOaIt+WQjeS7juS4reaWh+WIsOiLseaWhw=="); + break; + default: + showToast(context, "该问题暂不支持自动处理,请提供截图寻求帮助"); + break; + } + state = state.copyWith(isFixing: false, isFixingString: ""); + } + + // ignore: avoid_build_context_in_providers + doCheck(BuildContext context) async { + if (state.isChecking) return; + state = state.copyWith(isChecking: true, lastScreenInfo: "正在分析..."); + dPrint("-------- start docker check -----"); + if (!context.mounted) return; + await _statCheck(context); + state = state.copyWith(isChecking: false); + } + + // ignore: avoid_build_context_in_providers + _statCheck(BuildContext context) async { + final homeState = ref.read(homeUIModelProvider); + final scInstalledPath = homeState.scInstalledPath!; + + final checkResult = >[]; + // TODO for debug + // checkResult?.add(const MapEntry("unSupport_system", "android")); + // checkResult?.add(const MapEntry("nvme_PhysicalBytes", "C")); + // checkResult?.add(const MapEntry("no_live_path", "")); + + 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) { + const lastScreenInfo = "分析完毕,没有发现问题"; + state = state.copyWith(checkResult: null, lastScreenInfo: lastScreenInfo); + } else { + final lastScreenInfo = "分析完毕,发现 ${checkResult.length} 个问题"; + state = state.copyWith( + checkResult: checkResult, lastScreenInfo: lastScreenInfo); + } + + if (scInstalledPath == "not_install" && (checkResult.isEmpty)) { + if (!context.mounted) return; + showToast(context, "扫描完毕,没有发现问题,若仍然安装失败,请尝试使用工具箱中的 RSI启动器管理员模式。"); + } + } + + // ignore: avoid_build_context_in_providers + Future _checkGameRunningLog(BuildContext context, String scInstalledPath, + List> checkResult) async { + if (scInstalledPath == "not_install") return; + const lastScreenInfo = "正在检查:Game.log"; + state = state.copyWith(lastScreenInfo: lastScreenInfo); + final logs = await SCLoggerHelper.getGameRunningLogs(scInstalledPath); + if (logs == null) return; + final info = SCLoggerHelper.getGameRunningLogInfo(logs); + if (info != null) { + if (info.key != "_") { + checkResult.add(MapEntry("游戏异常退出:${info.key}", info.value)); + } else { + checkResult + .add(MapEntry("游戏异常退出:未知异常", "info:${info.value},请点击右下角加群反馈。")); + } + } + } + + // ignore: avoid_build_context_in_providers + Future _checkEAC(BuildContext context, String scInstalledPath, + List> checkResult) async { + if (scInstalledPath == "not_install") return; + const lastScreenInfo = "正在检查:EAC"; + state = state.copyWith(lastScreenInfo: lastScreenInfo); + + final eacPath = "$scInstalledPath\\EasyAntiCheat"; + final eacJsonPath = "$eacPath\\Settings.json"; + if (!await Directory(eacPath).exists() || + !await File(eacJsonPath).exists()) { + checkResult.add(const MapEntry("eac_file_miss", "")); + return; + } + final eacJsonData = await File(eacJsonPath).readAsBytes(); + final Map eacJson = json.decode(utf8.decode(eacJsonData)); + final eacID = eacJson["productid"]; + final eacDeploymentId = eacJson["deploymentid"]; + if (eacID == null || eacDeploymentId == null) { + checkResult.add(const MapEntry("eac_file_miss", "")); + return; + } + final eacFilePath = + "${Platform.environment["appdata"]}\\EasyAntiCheat\\$eacID\\$eacDeploymentId\\anticheatlauncher.log"; + if (!await File(eacFilePath).exists()) { + checkResult.add(MapEntry("eac_not_install", eacPath)); + return; + } + } + + final cnExp = RegExp(r"[^\x00-\xff]"); + + // ignore: avoid_build_context_in_providers + Future _checkPreInstall(BuildContext context, String scInstalledPath, + List> checkResult) async { + const lastScreenInfo = "正在检查:运行环境"; + 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 = "不支持的操作系统:${Platform.operatingSystemVersion}"; + state = state.copyWith(lastScreenInfo: lastScreenInfo); + await showToast(context, lastScreenInfo); + } + + if (cnExp.hasMatch(await SCLoggerHelper.getLogFilePath() ?? "")) { + checkResult.add(const MapEntry("cn_user_name", "")); + } + + // 检查 RAM + final ramSize = await SystemHelper.getSystemMemorySizeGB(); + if (ramSize < 16) { + checkResult.add(MapEntry("low_ram", "$ramSize")); + } + state = state.copyWith(lastScreenInfo: "正在检查:安装信息"); + // 检查安装分区 + try { + final listData = await SCLoggerHelper.getGameInstallPath( + await SCLoggerHelper.getLauncherLogList() ?? []); + final p = []; + final checkedPath = []; + for (var installPath in listData) { + if (!checkedPath.contains(installPath)) { + if (cnExp.hasMatch(installPath)) { + checkResult.add(MapEntry("cn_install_path", installPath)); + } + if (scInstalledPath == "not_install") { + checkedPath.add(installPath); + if (!await Directory(installPath).exists()) { + checkResult.add(MapEntry("no_live_path", installPath)); + } + } + final tp = installPath.split(":")[0]; + if (!p.contains(tp)) { + p.add(tp); + } + } + } + + // call check + for (var element in p) { + var result = await Process.run('powershell', [ + "(fsutil fsinfo sectorinfo $element: | Select-String 'PhysicalBytesPerSectorForPerformance').ToString().Split(':')[1].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); + if (physicalBytesPerSectorForPerformance > 4096) { + checkResult.add(MapEntry("nvme_PhysicalBytes", element)); + } + } + } + } catch (e) { + dPrint(e); + } + } +} diff --git a/lib/ui/home/game_doctor/game_doctor_ui_model.freezed.dart b/lib/ui/home/game_doctor/game_doctor_ui_model.freezed.dart new file mode 100644 index 0000000..8289532 --- /dev/null +++ b/lib/ui/home/game_doctor/game_doctor_ui_model.freezed.dart @@ -0,0 +1,242 @@ +// 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 'game_doctor_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 _$HomeGameDoctorState { + bool get isChecking => throw _privateConstructorUsedError; + bool get isFixing => throw _privateConstructorUsedError; + String get lastScreenInfo => throw _privateConstructorUsedError; + String get isFixingString => throw _privateConstructorUsedError; + List>? get checkResult => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HomeGameDoctorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HomeGameDoctorStateCopyWith<$Res> { + factory $HomeGameDoctorStateCopyWith( + HomeGameDoctorState value, $Res Function(HomeGameDoctorState) then) = + _$HomeGameDoctorStateCopyWithImpl<$Res, HomeGameDoctorState>; + @useResult + $Res call( + {bool isChecking, + bool isFixing, + String lastScreenInfo, + String isFixingString, + List>? checkResult}); +} + +/// @nodoc +class _$HomeGameDoctorStateCopyWithImpl<$Res, $Val extends HomeGameDoctorState> + implements $HomeGameDoctorStateCopyWith<$Res> { + _$HomeGameDoctorStateCopyWithImpl(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? isChecking = null, + Object? isFixing = null, + Object? lastScreenInfo = null, + Object? isFixingString = null, + Object? checkResult = freezed, + }) { + return _then(_value.copyWith( + isChecking: null == isChecking + ? _value.isChecking + : isChecking // ignore: cast_nullable_to_non_nullable + as bool, + isFixing: null == isFixing + ? _value.isFixing + : isFixing // ignore: cast_nullable_to_non_nullable + as bool, + lastScreenInfo: null == lastScreenInfo + ? _value.lastScreenInfo + : lastScreenInfo // ignore: cast_nullable_to_non_nullable + as String, + isFixingString: null == isFixingString + ? _value.isFixingString + : isFixingString // ignore: cast_nullable_to_non_nullable + as String, + checkResult: freezed == checkResult + ? _value.checkResult + : checkResult // ignore: cast_nullable_to_non_nullable + as List>?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HomeGameDoctorStateImplCopyWith<$Res> + implements $HomeGameDoctorStateCopyWith<$Res> { + factory _$$HomeGameDoctorStateImplCopyWith(_$HomeGameDoctorStateImpl value, + $Res Function(_$HomeGameDoctorStateImpl) then) = + __$$HomeGameDoctorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool isChecking, + bool isFixing, + String lastScreenInfo, + String isFixingString, + List>? checkResult}); +} + +/// @nodoc +class __$$HomeGameDoctorStateImplCopyWithImpl<$Res> + extends _$HomeGameDoctorStateCopyWithImpl<$Res, _$HomeGameDoctorStateImpl> + implements _$$HomeGameDoctorStateImplCopyWith<$Res> { + __$$HomeGameDoctorStateImplCopyWithImpl(_$HomeGameDoctorStateImpl _value, + $Res Function(_$HomeGameDoctorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isChecking = null, + Object? isFixing = null, + Object? lastScreenInfo = null, + Object? isFixingString = null, + Object? checkResult = freezed, + }) { + return _then(_$HomeGameDoctorStateImpl( + isChecking: null == isChecking + ? _value.isChecking + : isChecking // ignore: cast_nullable_to_non_nullable + as bool, + isFixing: null == isFixing + ? _value.isFixing + : isFixing // ignore: cast_nullable_to_non_nullable + as bool, + lastScreenInfo: null == lastScreenInfo + ? _value.lastScreenInfo + : lastScreenInfo // ignore: cast_nullable_to_non_nullable + as String, + isFixingString: null == isFixingString + ? _value.isFixingString + : isFixingString // ignore: cast_nullable_to_non_nullable + as String, + checkResult: freezed == checkResult + ? _value._checkResult + : checkResult // ignore: cast_nullable_to_non_nullable + as List>?, + )); + } +} + +/// @nodoc + +class _$HomeGameDoctorStateImpl implements _HomeGameDoctorState { + const _$HomeGameDoctorStateImpl( + {this.isChecking = false, + this.isFixing = false, + this.lastScreenInfo = "", + this.isFixingString = "", + final List>? checkResult}) + : _checkResult = checkResult; + + @override + @JsonKey() + final bool isChecking; + @override + @JsonKey() + final bool isFixing; + @override + @JsonKey() + final String lastScreenInfo; + @override + @JsonKey() + final String isFixingString; + final List>? _checkResult; + @override + List>? get checkResult { + final value = _checkResult; + if (value == null) return null; + if (_checkResult is EqualUnmodifiableListView) return _checkResult; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'HomeGameDoctorState(isChecking: $isChecking, isFixing: $isFixing, lastScreenInfo: $lastScreenInfo, isFixingString: $isFixingString, checkResult: $checkResult)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HomeGameDoctorStateImpl && + (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)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + isChecking, + isFixing, + lastScreenInfo, + isFixingString, + const DeepCollectionEquality().hash(_checkResult)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HomeGameDoctorStateImplCopyWith<_$HomeGameDoctorStateImpl> get copyWith => + __$$HomeGameDoctorStateImplCopyWithImpl<_$HomeGameDoctorStateImpl>( + this, _$identity); +} + +abstract class _HomeGameDoctorState implements HomeGameDoctorState { + const factory _HomeGameDoctorState( + {final bool isChecking, + final bool isFixing, + final String lastScreenInfo, + final String isFixingString, + final List>? checkResult}) = + _$HomeGameDoctorStateImpl; + + @override + bool get isChecking; + @override + bool get isFixing; + @override + String get lastScreenInfo; + @override + String get isFixingString; + @override + List>? get checkResult; + @override + @JsonKey(ignore: true) + _$$HomeGameDoctorStateImplCopyWith<_$HomeGameDoctorStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/home/game_doctor/game_doctor_ui_model.g.dart b/lib/ui/home/game_doctor/game_doctor_ui_model.g.dart new file mode 100644 index 0000000..7bec1d3 --- /dev/null +++ b/lib/ui/home/game_doctor/game_doctor_ui_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'game_doctor_ui_model.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$homeGameDoctorUIModelHash() => + r'59cbd6f866bacbc24eb0c0eb0ad88fe7f2dac4a7'; + +/// See also [HomeGameDoctorUIModel]. +@ProviderFor(HomeGameDoctorUIModel) +final homeGameDoctorUIModelProvider = AutoDisposeNotifierProvider< + HomeGameDoctorUIModel, HomeGameDoctorState>.internal( + HomeGameDoctorUIModel.new, + name: r'homeGameDoctorUIModelProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$homeGameDoctorUIModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$HomeGameDoctorUIModel = 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 diff --git a/lib/ui/home/home_ui.dart b/lib/ui/home/home_ui.dart index a727474..3a46111 100644 --- a/lib/ui/home/home_ui.dart +++ b/lib/ui/home/home_ui.dart @@ -4,6 +4,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_tilt/flutter_tilt.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:starcitizen_doctor/api/analytics.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; @@ -427,7 +428,7 @@ class HomeUI extends HookConsumerWidget { Widget makeIndexActionLists( BuildContext context, HomeUIModel model, HomeUIModelState homeState) { final items = [ - _HomeItemData("auto_check", "一键诊断", "一键诊断星际公民常见问题", + _HomeItemData("game_doctor", "一键诊断", "一键诊断星际公民常见问题", FluentIcons.auto_deploy_settings), _HomeItemData( "localization", "汉化管理", "快捷安装汉化资源", FluentIcons.locale_language), @@ -445,7 +446,7 @@ class HomeUI extends HookConsumerWidget { itemBuilder: (context, index) { final item = items.elementAt(index); return HoverButton( - onPressed: () => model.onMenuTap(item.key), + onPressed: () => _onMenuTap(context, item.key), builder: (BuildContext context, Set states) { return Container( width: 300, @@ -746,6 +747,10 @@ class HomeUI extends HookConsumerWidget { showDialog( context: context, builder: (context) => const HomeCountdownDialogUI()); } + + _onMenuTap(BuildContext context, String key) { + context.push("/index/$key"); + } } class _HomeItemData { diff --git a/lib/ui/home/home_ui_model.dart b/lib/ui/home/home_ui_model.dart index e8c6fdc..105aceb 100644 --- a/lib/ui/home/home_ui_model.dart +++ b/lib/ui/home/home_ui_model.dart @@ -121,8 +121,6 @@ class HomeUIModel extends _$HomeUIModel { return title; } - onMenuTap(String key) {} - // ignore: avoid_build_context_in_providers Future goWebView(BuildContext context, String title, String url, {bool useLocalization = false, diff --git a/lib/ui/home/home_ui_model.g.dart b/lib/ui/home/home_ui_model.g.dart index 64bcc71..ecc8ad2 100644 --- a/lib/ui/home/home_ui_model.g.dart +++ b/lib/ui/home/home_ui_model.g.dart @@ -6,7 +6,7 @@ part of 'home_ui_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$homeUIModelHash() => r'3094d9ab828a578670e11f3eaffa57bdb95a004b'; +String _$homeUIModelHash() => r'4ef5c2bdf2d9a506b2e139d321c52c97cf4bc30d'; /// See also [HomeUIModel]. @ProviderFor(HomeUIModel) diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 85f6ef7..31e0455 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -32,7 +32,8 @@ Widget makeDefaultPage(BuildContext context, List? actions, Widget? content, bool automaticallyImplyLeading = true, - String title = ""}) { + String title = "", + bool useBodyContainer = false}) { return NavigationView( appBar: NavigationAppBar( automaticallyImplyLeading: automaticallyImplyLeading, @@ -54,7 +55,15 @@ Widget makeDefaultPage(BuildContext context, mainAxisAlignment: MainAxisAlignment.end, children: [...?actions, const WindowButtons()], )), - content: content, + content: useBodyContainer + ? Container( + decoration: BoxDecoration( + color: FluentTheme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(9), + ), + child: content, + ) + : content, ); } @@ -128,18 +137,19 @@ CustomTransitionPage myPageBuilder( BuildContext context, GoRouterState state, Widget child) { return CustomTransitionPage( child: child, + transitionDuration: const Duration(milliseconds: 150), + reverseTransitionDuration: const Duration(milliseconds: 150), transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { - return Semantics( - scopesRoute: true, - explicitChildNodes: true, - child: EntrancePageTransition( - animation: CurvedAnimation( - parent: animation, - curve: FluentTheme.of(context).animationCurve, - ), - child: child, - ), + return SlideTransition( + position: Tween( + begin: const Offset(0.0, 1.0), + end: const Offset(0.0, 0.0), + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, + )), + child: child, ); }); }