From d740632c2769ebeb227f69d60cc5a86d4823308f Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Tue, 12 Dec 2023 21:40:18 +0800 Subject: [PATCH 01/15] code style --- .../Service/User/IUserCollectionService.cs | 2 + .../Service/User/UserCollectionService.cs | 2 +- .../Service/User/UserOptionResult.cs | 6 +- .../Snap.Hutao/Service/User/UserService.cs | 6 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 2 +- .../ViewModel/User/UserViewModel.cs | 118 +++++++++--------- 6 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs index 8db6973f..9d914db6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs @@ -20,6 +20,8 @@ internal interface IUserCollectionService UserGameRole? GetUserGameRoleByUid(string uid); ValueTask RemoveUserAsync(BindingUser user); + ValueTask> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea); + bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs index d0f689cb..8c8fb108 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs @@ -153,7 +153,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService if (newUser is null) { - return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed); + return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieRequestUserInfoFailed); } await GetUserCollectionAsync().ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserOptionResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserOptionResult.cs index 028c0b2d..ddf9f86f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserOptionResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserOptionResult.cs @@ -17,15 +17,15 @@ internal enum UserOptionResult /// /// Cookie不完整 /// - Incomplete, + CookieIncomplete, /// /// Cookie信息已经失效 /// - Invalid, + CookieInvalid, /// /// 用户的Cookie成功更新 /// - Updated, + CookieUpdated, } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index ae2dc00d..43b5e233 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -63,7 +63,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe if (string.IsNullOrEmpty(mid)) { - return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoMid); + return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoMid); } // 检查 mid 对应用户是否存在 @@ -74,7 +74,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe if (!cookie.TryGetSToken(isOversea, out Cookie? stoken)) { - return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken); + return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoSToken); } user.SToken = stoken; @@ -82,7 +82,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); - return new(UserOptionResult.Updated, mid); + return new(UserOptionResult.CookieUpdated, mid); } public async ValueTask RefreshCookieTokenAsync(Model.Entity.User user) diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 29bea2fe..615e07e0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -305,7 +305,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs index d02df2ad..129b7ff6 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs @@ -52,8 +52,11 @@ internal sealed partial class UserViewModel : ObservableObject get => selectedUser ??= userService.Current; set { - // Pre select the chosen role to avoid multiple UserChangedMessage - value?.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false); + if (value is { SelectedUserGameRole: null }) + { + // Pre select the chosen role to avoid multiple UserChangedMessage + value.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false); + } if (SetProperty(ref selectedUser, value)) { @@ -87,13 +90,13 @@ internal sealed partial class UserViewModel : ObservableObject infoBarService.Success(SH.FormatViewModelUserAdded(uid)); break; - case UserOptionResult.Incomplete: + case UserOptionResult.CookieIncomplete: infoBarService.Information(SH.ViewModelUserIncomplete); break; - case UserOptionResult.Invalid: + case UserOptionResult.CookieInvalid: infoBarService.Information(SH.ViewModelUserInvalid); break; - case UserOptionResult.Updated: + case UserOptionResult.CookieUpdated: infoBarService.Success(SH.FormatViewModelUserUpdated(uid)); break; default: @@ -118,16 +121,16 @@ internal sealed partial class UserViewModel : ObservableObject [Command("AddUserCommand")] private Task AddUserAsync() { - return AddUserCoreAsync(false).AsTask(); + return AddUserByManualInputCookieAsync(false).AsTask(); } [Command("AddOverseaUserCommand")] private Task AddOverseaUserAsync() { - return AddUserCoreAsync(true).AsTask(); + return AddUserByManualInputCookieAsync(true).AsTask(); } - private async ValueTask AddUserCoreAsync(bool isOversea) + private async ValueTask AddUserByManualInputCookieAsync(bool isOversea) { // ContentDialog must be created by main thread. await taskContext.SwitchToMainThreadAsync(); @@ -140,9 +143,7 @@ internal sealed partial class UserViewModel : ObservableObject if (result.TryGetValue(out string rawCookie)) { Cookie cookie = Cookie.Parse(rawCookie); - (UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie, isOversea).ConfigureAwait(false); - await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false); } } @@ -150,22 +151,21 @@ internal sealed partial class UserViewModel : ObservableObject [Command("LoginMihoyoUserCommand")] private void LoginMihoyoUser() { - if (runtimeOptions.IsWebView2Supported) - { - navigationService.Navigate(INavigationAwaiter.Default); - } - else - { - infoBarService.Warning(SH.CoreWebView2HelperVersionUndetected); - } + NavigateToLoginPage(); } [Command("LoginHoyoverseUserCommand")] private void LoginHoyoverseUser() + { + NavigateToLoginPage(); + } + + private void NavigateToLoginPage() + where TPage : Page { if (runtimeOptions.IsWebView2Supported) { - navigationService.Navigate(INavigationAwaiter.Default); + navigationService.Navigate(INavigationAwaiter.Default); } else { @@ -192,9 +192,7 @@ internal sealed partial class UserViewModel : ObservableObject if (sTokenResponse.IsOk()) { Cookie stokenV2 = Cookie.FromLoginResult(sTokenResponse.Data); - (UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false); - await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false); } } @@ -202,17 +200,19 @@ internal sealed partial class UserViewModel : ObservableObject [Command("RemoveUserCommand")] private async Task RemoveUserAsync(User? user) { - if (user is not null) + if (user is null) { - try - { - await userService.RemoveUserAsync(user).ConfigureAwait(false); - infoBarService.Success(SH.FormatViewModelUserRemoved(user.UserInfo?.Nickname)); - } - catch (UserdataCorruptedException ex) - { - infoBarService.Error(ex); - } + return; + } + + try + { + await userService.RemoveUserAsync(user).ConfigureAwait(false); + infoBarService.Success(SH.FormatViewModelUserRemoved(user.UserInfo?.Nickname)); + } + catch (UserdataCorruptedException ex) + { + infoBarService.Error(ex); } } @@ -243,44 +243,42 @@ internal sealed partial class UserViewModel : ObservableObject [Command("RefreshCookieTokenCommand")] private async Task RefreshCookieTokenAsync() { - if (SelectedUser is not null) + if (SelectedUser is null) { - if (await userService.RefreshCookieTokenAsync(SelectedUser).ConfigureAwait(false)) - { - infoBarService.Success(SH.ViewUserRefreshCookieTokenSuccess); - } - else - { - infoBarService.Warning(SH.ViewUserRefreshCookieTokenWarning); - } + return; + } + + if (await userService.RefreshCookieTokenAsync(SelectedUser).ConfigureAwait(false)) + { + infoBarService.Success(SH.ViewUserRefreshCookieTokenSuccess); + } + else + { + infoBarService.Warning(SH.ViewUserRefreshCookieTokenWarning); } } [Command("ClaimSignInRewardCommand")] private async Task ClaimSignInRewardAsync(AppBarButton? appBarButton) { - if (SelectedUser is not null) + if (!UserAndUid.TryFromUser(SelectedUser, out UserAndUid? userAndUid)) { - if (UserAndUid.TryFromUser(SelectedUser, out UserAndUid? userAndUid)) - { - (bool isOk, string message) = await signInService.ClaimRewardAsync(userAndUid).ConfigureAwait(false); - - if (isOk) - { - infoBarService.Success(message); - } - else - { - await taskContext.SwitchToMainThreadAsync(); - FlyoutBase.ShowAttachedFlyout(appBarButton); - infoBarService.Warning(message); - } - } - else - { - infoBarService.Warning(SH.MustSelectUserAndUid); - } + infoBarService.Warning(SH.MustSelectUserAndUid); + return; } + + (bool isOk, string message) = await signInService.ClaimRewardAsync(userAndUid).ConfigureAwait(false); + + if (isOk) + { + infoBarService.Success(message); + return; + } + + // Manual webview + await taskContext.SwitchToMainThreadAsync(); + FlyoutBase.ShowAttachedFlyout(appBarButton); + infoBarService.Warning(message); } [Command("OpenDocumentationCommand")] From 289b3219c9af96e114e03778e8c35e476af04ee9 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 13 Dec 2023 13:32:42 +0800 Subject: [PATCH 02/15] fix some image blank --- .../Snap.Hutao/Control/Theme/Uri.xaml | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Uri.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Uri.xaml index 3e50461f..4237308b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Uri.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Uri.xaml @@ -10,25 +10,25 @@ https://afdian.net/a/DismissedLight - https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png + https://api.snapgenshin.com/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png - https://static.snapgenshin.com/Bg/UI_Icon_Intee_Explore_1.png - https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png - https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png - https://static.snapgenshin.com/Bg/UI_MarkQuest_Events_Proce.png - https://static.snapgenshin.com/Bg/UI_MarkTower.png + https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png + https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png + https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png + https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png + https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png - https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_201.png - https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_204.png - https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_210.png - https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_220021.png + https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png + https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_204.png + https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_210.png + https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_220021.png - https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon25.png - https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png - https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png - https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png - https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png + https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png + https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png + https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png + https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png + https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png From de46d5f9bfbfa3967a33d03f730eb980c2cc9748 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 13 Dec 2023 17:24:44 +0800 Subject: [PATCH 03/15] Update KnownReturnCode.cs --- .../Web/Response/KnownReturnCode.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index 5725680a..e4f40b43 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -11,6 +11,7 @@ internal enum KnownReturnCode { /// /// 无效请求 + /// 因战绩功能服务优化升级,V2.10及以下版本将无法正常使用战绩功能,请更新米游社至最新版本再进行使用。 /// InvalidRequest = -10001, @@ -120,22 +121,32 @@ internal enum KnownReturnCode LoginStateInvalid = 1004, /// - /// 账号有风险 + /// 实时便笺 账号有风险 /// CODE1034 = 1034, /// - /// 请登录 + /// 实时便笺 当前账号存在风险,暂无数据 + /// + CODE5003 = 5003, + + /// + /// 请登录 登录后可查看战绩信息 /// PleaseLogin = 10001, + /// + /// 原神战绩 查看他人战绩次数过多,请休息一会儿再试 + /// + CODE10101 = 10101, + /// /// 数据未公开 /// DataIsNotPublicForTheUser = 10102, /// - /// 实时便笺 + /// 实时便笺 你的账号已被封禁,无法查看 /// CODE10103 = 10103, @@ -143,4 +154,4 @@ internal enum KnownReturnCode /// 实时便笺 /// CODE10104 = 10104, -} +} \ No newline at end of file From 8fe1b48fd47b82dd4b7b9ccd78bcc2943c3ef84a Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Wed, 13 Dec 2023 20:22:29 +0800 Subject: [PATCH 04/15] fix qrcode dialog --- src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml | 4 ++-- .../Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml index 62e88581..7dc77cd4 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml @@ -16,6 +16,6 @@ + Source="{x:Bind QRCodeSource, Mode=OneWay}"/> - + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs index dcbaa0fc..32be4c21 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs @@ -71,7 +71,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable private async ValueTask> GetUidGameTokenCoreAsync() { await taskContext.SwitchToMainThreadAsync(); - await ShowAsync(); + _ = ShowAsync(); while (!userManualCancellationTokenSource.IsCancellationRequested) { From 176baeb5c6666970d5beb5e17dfa05468dce0196 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Wed, 13 Dec 2023 22:06:43 +0800 Subject: [PATCH 05/15] shadow improvement --- .../Snap.Hutao/Control/Theme/Card.xaml | 20 +++++++++++++++---- .../View/Page/AnnouncementPage.xaml | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Card.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Card.xaml index d42eb68e..381d9610 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Card.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Card.xaml @@ -2,6 +2,22 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cwm="using:CommunityToolkit.WinUI.Media"> + + + + + + + + - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml index 1ef6d6b7..989ca3ff 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml @@ -183,7 +183,7 @@ - + From ac78df369cd632f280a1484b7d6ef1cde1f24dde Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Thu, 14 Dec 2023 14:48:56 +0800 Subject: [PATCH 06/15] impl #526 --- .../Model/Entity/Primitive/SchemeType.cs | 2 +- .../Model/Entity/SettingEntry.Constant.cs | 2 + .../Snap.Hutao/Resource/Localization/SH.resx | 3 + .../Snap.Hutao/Service/AppOptions.cs | 18 +- .../Snap.Hutao/Service/AppOptionsExtension.cs | 35 +- .../GameChannelOptionsService.cs | 6 +- .../Service/Game/GameServiceFacade.cs | 1 + .../Snap.Hutao/Service/Game/LaunchOptions.cs | 25 + .../Service/Game/LaunchOptionsExtension.cs | 87 +++ .../Game/Package/GamePackageService.cs | 6 +- .../Game/PathAbstraction/GamePathEntry.cs | 26 + .../Game/PathAbstraction/GamePathKind.cs | 13 + .../{ => PathAbstraction}/GamePathService.cs | 13 +- .../{ => PathAbstraction}/IGamePathService.cs | 2 +- .../Game/Process/GameProcessService.cs | 4 +- .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 527 +++++++++--------- .../Snap.Hutao/View/Page/SettingPage.xaml | 25 +- .../ViewModel/Game/LaunchGameViewModel.cs | 172 ++++-- .../ViewModel/Setting/SettingViewModel.cs | 27 +- 19 files changed, 587 insertions(+), 407 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => PathAbstraction}/GamePathService.cs (78%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => PathAbstraction}/IGamePathService.cs (79%) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs index 12971805..80696756 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs @@ -12,7 +12,7 @@ internal enum SchemeType /// /// 国际服 /// - Mihoyo, + Hoyoverse, /// /// 国服官服 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs index 97474eb7..28a54a49 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -13,6 +13,8 @@ internal sealed partial class SettingEntry /// public const string GamePath = "GamePath"; + public const string GamePathEntries = "GamePathEntries"; + /// /// PowerShell 路径 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index ab31fad0..1c7f44cb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2189,6 +2189,9 @@ 打开截图文件夹 + + 选择游戏路径 + 关于 胡桃 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs index e02352d9..8c9adcfa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs @@ -6,6 +6,8 @@ using Snap.Hutao.Core.Windowing; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; using System.Globalization; using System.IO; @@ -15,20 +17,12 @@ namespace Snap.Hutao.Service; [Injection(InjectAs.Singleton)] internal sealed partial class AppOptions : DbStoreOptions { - private string? gamePath; private string? powerShellPath; private bool? isEmptyHistoryWishVisible; private BackdropType? backdropType; private CultureInfo? currentCulture; - private bool? isAdvancedLaunchOptionsEnabled; private string? geetestCustomCompositeUrl; - public string GamePath - { - get => GetOption(ref gamePath, SettingEntry.GamePath); - set => SetOption(ref gamePath, SettingEntry.GamePath, value); - } - public string PowerShellPath { get @@ -80,14 +74,6 @@ internal sealed partial class AppOptions : DbStoreOptions set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name); } - public bool IsAdvancedLaunchOptionsEnabled - { - // DO NOT MOVE TO OTHER CLASS - // We use this property in SettingPage binding - get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled); - set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value); - } - public string GeetestCustomCompositeUrl { get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs index 0359ed1d..34c731f1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using Snap.Hutao.Model; +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; using System.Globalization; using System.IO; @@ -9,39 +11,6 @@ namespace Snap.Hutao.Service; internal static class AppOptionsExtension { - public static bool TryGetGameFolderAndFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName) - { - string gamePath = appOptions.GamePath; - - gameFolder = Path.GetDirectoryName(gamePath); - if (string.IsNullOrEmpty(gameFolder)) - { - gameFileName = default; - return false; - } - - gameFileName = Path.GetFileName(gamePath); - if (string.IsNullOrEmpty(gameFileName)) - { - return false; - } - - return true; - } - - public static bool TryGetGamePathAndGameFileName(this AppOptions appOptions, out string gamePath, [NotNullWhen(true)] out string? gameFileName) - { - gamePath = appOptions.GamePath; - - gameFileName = Path.GetFileName(gamePath); - if (string.IsNullOrEmpty(gameFileName)) - { - return false; - } - - return true; - } - public static NameValue? GetCurrentCultureForSelectionOrDefault(this AppOptions appOptions) { return appOptions.Cultures.SingleOrDefault(c => c.Value == appOptions.CurrentCulture); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs index 80fd68d3..2b63c89a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs @@ -13,11 +13,11 @@ namespace Snap.Hutao.Service.Game.Configuration; [Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))] internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService { - private readonly AppOptions appOptions; + private readonly LaunchOptions launchOptions; public ChannelOptions GetChannelOptions() { - string gamePath = appOptions.GamePath; + string gamePath = launchOptions.GamePath; string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase); @@ -38,7 +38,7 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer public bool SetChannelOptions(LaunchScheme scheme) { - string gamePath = appOptions.GamePath; + string gamePath = launchOptions.GamePath; string? directory = Path.GetDirectoryName(gamePath); ArgumentException.ThrowIfNullOrEmpty(directory); string configPath = Path.Combine(directory, ConfigFileName); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs index cb20776b..115d1ee7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -5,6 +5,7 @@ using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Game.Account; using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.PathAbstraction; using Snap.Hutao.Service.Game.Process; using Snap.Hutao.Service.Game.Scheme; using System.Collections.ObjectModel; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index 4241bba3..b4cbe11a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -6,6 +6,8 @@ using Microsoft.UI.Windowing; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; using System.Globalization; using Windows.Graphics; using Windows.Win32.Foundation; @@ -24,7 +26,10 @@ internal sealed class LaunchOptions : DbStoreOptions private readonly int primaryScreenHeight; private readonly int primaryScreenFps; + private string? gamePath; + private ImmutableList? gamePathEntries; private bool? isEnabled; + private bool? isAdvancedLaunchOptionsEnabled; private bool? isFullScreen; private bool? isBorderless; private bool? isExclusive; @@ -85,12 +90,32 @@ internal sealed class LaunchOptions : DbStoreOptions } } + public string GamePath + { + get => GetOption(ref gamePath, SettingEntry.GamePath); + set => SetOption(ref gamePath, SettingEntry.GamePath, value); + } + + public ImmutableList GamePathEntries + { + // Because DbStoreOptions can't detect collection change, We use + // ImmutableList to imply that the whole list needs to be replaced + get => GetOption(ref gamePathEntries, SettingEntry.GamePathEntries, raw => JsonSerializer.Deserialize>(raw), []); + set => SetOption(ref gamePathEntries, SettingEntry.GamePathEntries, value, value => JsonSerializer.Serialize(value)); + } + public bool IsEnabled { get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true); set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value); } + public bool IsAdvancedLaunchOptionsEnabled + { + get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled); + set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value); + } + public bool IsFullScreen { get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs new file mode 100644 index 00000000..5913e00e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs @@ -0,0 +1,87 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; +using System.IO; + +namespace Snap.Hutao.Service.Game; + +internal static class LaunchOptionsExtension +{ + public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName) + { + string gamePath = options.GamePath; + + gameFolder = Path.GetDirectoryName(gamePath); + if (string.IsNullOrEmpty(gameFolder)) + { + gameFileName = default; + return false; + } + + gameFileName = Path.GetFileName(gamePath); + if (string.IsNullOrEmpty(gameFileName)) + { + return false; + } + + return true; + } + + public static bool TryGetGamePathAndGameFileName(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameFileName) + { + gamePath = options.GamePath; + + gameFileName = Path.GetFileName(gamePath); + if (string.IsNullOrEmpty(gameFileName)) + { + return false; + } + + return true; + } + + public static ImmutableList GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry) + { + string gamePath = options.GamePath; + + if (string.IsNullOrEmpty(gamePath)) + { + entry = default; + return options.GamePathEntries; + } + + if (options.GamePathEntries.SingleOrDefault(entry => string.Equals(entry.Path, options.GamePath, StringComparison.OrdinalIgnoreCase)) is { } existed) + { + entry = existed; + return options.GamePathEntries; + } + + entry = GamePathEntry.Create(options.GamePath); + return [.. options.GamePathEntries, entry]; + } + + public static ImmutableList RemoveGamePathEntry(this LaunchOptions options, GamePathEntry? entry, out GamePathEntry? selected) + { + if (entry is not null) + { + if (string.Equals(options.GamePath, entry.Path, StringComparison.OrdinalIgnoreCase)) + { + options.GamePath = string.Empty; + } + + options.GamePathEntries = options.GamePathEntries.Remove(entry); + } + + return options.GetGamePathEntries(out selected); + } + + public static ImmutableList UpdateGamePathAndRefreshEntries(this LaunchOptions options, string gamePath) + { + options.GamePath = gamePath; + ImmutableList entries = options.GetGamePathEntries(out _); + options.GamePathEntries = entries; + return entries; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs index f230c984..09be982f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs @@ -16,12 +16,12 @@ internal sealed partial class GamePackageService : IGamePackageService { private readonly PackageConverter packageConverter; private readonly IServiceProvider serviceProvider; + private readonly LaunchOptions launchOptions; private readonly ITaskContext taskContext; - private readonly AppOptions appOptions; public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) { - if (!appOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName)) + if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName)) { return false; } @@ -58,7 +58,7 @@ internal sealed partial class GamePackageService : IGamePackageService string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; await taskContext.SwitchToMainThreadAsync(); - appOptions.GamePath = Path.Combine(gameFolder, exeName); + launchOptions.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, exeName)); } await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs new file mode 100644 index 00000000..8d8dfd2e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs @@ -0,0 +1,26 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.PathAbstraction; + +internal sealed class GamePathEntry +{ + [JsonPropertyName("Path")] + public string Path { get; set; } = default!; + + [JsonIgnore] + public GamePathKind Kind { get => GetKind(Path); } + + public static GamePathEntry Create(string path) + { + return new() + { + Path = path, + }; + } + + private static GamePathKind GetKind(string path) + { + return GamePathKind.None; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs new file mode 100644 index 00000000..3271c606 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.PathAbstraction; + +internal enum GamePathKind +{ + None, + ChineseClient, + OverseaClient, + ChineseCloud, + OverseaCloud, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs similarity index 78% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs index 7f773c77..c1c9eff4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs @@ -2,20 +2,21 @@ // Licensed under the MIT license. using Snap.Hutao.Service.Game.Locator; +using Snap.Hutao.Service.Game.PathAbstraction; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.PathAbstraction; [ConstructorGenerated] [Injection(InjectAs.Singleton, typeof(IGamePathService))] internal sealed partial class GamePathService : IGamePathService { private readonly IServiceProvider serviceProvider; - private readonly AppOptions appOptions; + private readonly LaunchOptions launchOptions; public async ValueTask> SilentGetGamePathAsync() { // Cannot find in setting - if (string.IsNullOrEmpty(appOptions.GamePath)) + if (string.IsNullOrEmpty(launchOptions.GamePath)) { IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService(); @@ -40,7 +41,7 @@ internal sealed partial class GamePathService : IGamePathService if (isOk) { // Save result. - appOptions.GamePath = path; + launchOptions.UpdateGamePathAndRefreshEntries(path); } else { @@ -48,9 +49,9 @@ internal sealed partial class GamePathService : IGamePathService } } - if (!string.IsNullOrEmpty(appOptions.GamePath)) + if (!string.IsNullOrEmpty(launchOptions.GamePath)) { - return new(true, appOptions.GamePath); + return new(true, launchOptions.GamePath); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs similarity index 79% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs index c0b09ccc..3c01c01c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.PathAbstraction; internal interface IGamePathService { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs index e9d6e28a..c55ee88e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -52,7 +52,7 @@ internal sealed partial class GameProcessService : IGameProcessService return; } - if (!appOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName)) + if (!launchOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName)) { ArgumentException.ThrowIfNullOrEmpty(gamePath); return; // null check passing, actually never reach. @@ -73,7 +73,7 @@ internal sealed partial class GameProcessService : IGameProcessService await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false); } - if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) + if (runtimeOptions.IsElevated && launchOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) { progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); try diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 9fd729b1..b9deafdf 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -38,7 +38,12 @@ - + + + + + + + Orientation="Horizontal">