From 4dd6765e35c485adda5315fa055ad97957e056cb Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Tue, 28 Nov 2023 16:18:24 +0800 Subject: [PATCH 01/47] show ip --- .../Snap.Hutao/Resource/Localization/SH.resx | 6 +++++ .../Service/Hutao/HutaoUserOptions.cs | 11 +++++++++ .../Service/Hutao/HutaoUserService.cs | 1 + .../Snap.Hutao/View/Page/SettingPage.xaml | 1 + .../Web/Hutao/HomaPassportClient.cs | 11 +++++++++ src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs | 24 +++++++++++++++++++ .../Snap.Hutao/Web/HutaoEndpoints.cs | 3 +++ 7 files changed, 57 insertions(+) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 1f7e8c14..f6d73cbe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2249,6 +2249,12 @@ 设备 ID + + IP:{0},归属:{1} + + + 设备 IP + 在祈愿记录页面显示或隐藏无记录的历史祈愿活动 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs index 87074a42..efdd065f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs @@ -19,6 +19,7 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions public string? ActualUserName { get => IsLoggedIn ? UserName : null; } + /// + /// 设备当前IP + /// + public IPInfo IPInfo { get => ipInfo; set => ipInfo = value; } + /// /// 是否已登录 /// @@ -131,6 +137,11 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions + diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs index 6e93e96a..4c35a2a3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs @@ -193,6 +193,17 @@ internal sealed partial class HomaPassportClient return HutaoResponse.DefaultIfNull(resp); } + public async ValueTask GetIPInfoAsync(CancellationToken token = default) + { + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(HutaoEndpoints.Ip) + .Get(); + + IPInfo? resp = await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false); + + return resp ?? new(); + } + private static string Encrypt(string text) { byte[] plaintextBytes = Encoding.UTF8.GetBytes(text); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs new file mode 100644 index 00000000..3a080ea4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs @@ -0,0 +1,24 @@ +namespace Snap.Hutao.Web.Hutao; + +internal sealed class IPInfo +{ + public IPInfo() + { + } + + [JsonConstructor] + public IPInfo(string ip, string division) + { + Ip = ip; + Division = division; + } + + public string Ip { get; set; } = "Unknown"; + + public string Division { get; set; } = "Unknown"; + + public override string ToString() + { + return SH.FormatViewPageSettingDeviceIpDescription(Ip, Division); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs index 1ee466fb..dbf33add 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs @@ -11,6 +11,7 @@ namespace Snap.Hutao.Web; /// [HighQuality] [SuppressMessage("", "SA1201")] +[SuppressMessage("", "SA1203")] [SuppressMessage("", "SA1124")] internal static class HutaoEndpoints { @@ -22,6 +23,8 @@ internal static class HutaoEndpoints public const string AnnouncementUpload = $"{HomaSnapGenshinApi}/Service/Announcement/Upload"; + public const string Ip = $"{ApiSnapGenshin}/ip"; + public static string GachaLogCompensation(int days) { return $"{HomaSnapGenshinApi}/Service/GachaLog/Compensation?days={days}"; From 045c127fb2b1322110a4f2bbc5d9d141c8d5366d Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 6 Dec 2023 13:53:16 +0800 Subject: [PATCH 02/47] code style --- .../ExceptionService/ExceptionRecorder.cs | 2 +- .../Service/Hutao/HutaoSpiralAbyssService.cs | 2 +- .../Service/Hutao/HutaoUserOptions.cs | 13 +------- .../Service/Hutao/HutaoUserService.cs | 3 +- .../HutaoPassportRegisterDialog.xaml.cs | 4 +-- .../HutaoPassportResetPasswordDialog.xaml.cs | 4 +-- .../HutaoPassportUnregisterDialog.xaml.cs | 4 +-- .../Snap.Hutao/View/Page/SettingPage.xaml | 9 +++++- .../Setting/HutaoPassportViewModel.cs | 2 +- .../ViewModel/Setting/SettingViewModel.cs | 15 +++++++++ .../SpiralAbyss/SpiralAbyssRecordViewModel.cs | 2 +- .../Web/Hutao/HutaoInfrastructureClient.cs | 31 +++++++++++++++++++ ...ssportClient.cs => HutaoPassportClient.cs} | 17 ++-------- ...portHttpRequestMessageBuilderExtension.cs} | 2 +- src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs | 24 -------------- .../Snap.Hutao/Web/Hutao/IPInformation.cs | 24 ++++++++++++++ ...ploadClient.cs => HutaoLogUploadClient.cs} | 4 +-- ...yssClient.cs => HutaoSpiralAbyssClient.cs} | 4 +-- .../Snap.Hutao/Web/HutaoEndpoints.cs | 5 ++- 19 files changed, 100 insertions(+), 71 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hutao/{HomaPassportClient.cs => HutaoPassportClient.cs} (93%) rename src/Snap.Hutao/Snap.Hutao/Web/Hutao/{HomaPassportHttpRequestMessageBuilderExtension.cs => HutaoPassportHttpRequestMessageBuilderExtension.cs} (88%) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/{HomaLogUploadClient.cs => HutaoLogUploadClient.cs} (93%) rename src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/{HomaSpiralAbyssClient.cs => HutaoSpiralAbyssClient.cs} (98%) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs index fec24cb5..87514817 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs @@ -31,7 +31,7 @@ internal sealed partial class ExceptionRecorder private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { ValueTask task = serviceProvider - .GetRequiredService() + .GetRequiredService() .UploadLogAsync(e.Exception); if (!task.IsCompleted) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs index 0566c9a0..f8fb9434 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs @@ -19,7 +19,7 @@ internal sealed partial class HutaoSpiralAbyssService : IHutaoSpiralAbyssService private readonly TimeSpan cacheExpireTime = TimeSpan.FromHours(4); private readonly IObjectCacheDbService objectCacheDbService; - private readonly HomaSpiralAbyssClient homaClient; + private readonly HutaoSpiralAbyssClient homaClient; private readonly JsonSerializerOptions options; private readonly IMemoryCache memoryCache; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs index efdd065f..71988df7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs @@ -19,7 +19,6 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions public string? ActualUserName { get => IsLoggedIn ? UserName : null; } - /// - /// 设备当前IP - /// - public IPInfo IPInfo { get => ipInfo; set => ipInfo = value; } - /// /// 是否已登录 /// @@ -69,7 +63,7 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions public HutaoUserOptions Value { get => this; } - public async ValueTask PostLoginSucceedAsync(HomaPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token) + public async ValueTask PostLoginSucceedAsync(HutaoPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token) { LocalSetting.Set(SettingKeys.PassportUserName, username); LocalSetting.Set(SettingKeys.PassportPassword, password); @@ -137,11 +131,6 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions(); - homaPassportClient = serviceProvider.GetRequiredService(); + homaPassportClient = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs index 480a71ce..7d4a9406 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs @@ -13,7 +13,7 @@ namespace Snap.Hutao.View.Dialog; [DependencyProperty("VerifyCode", typeof(string))] internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog { - private readonly HomaPassportClient homaPassportClient; + private readonly HutaoPassportClient homaPassportClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; @@ -22,7 +22,7 @@ internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - homaPassportClient = serviceProvider.GetRequiredService(); + homaPassportClient = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs index b3c8b380..1707c24b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs @@ -13,7 +13,7 @@ namespace Snap.Hutao.View.Dialog; [DependencyProperty("VerifyCode", typeof(string))] internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog { - private readonly HomaPassportClient homaPassportClient; + private readonly HutaoPassportClient homaPassportClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; @@ -22,7 +22,7 @@ internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - homaPassportClient = serviceProvider.GetRequiredService(); + homaPassportClient = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index 644e3adb..55b61e25 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -6,7 +6,9 @@ xmlns:cwc="using:CommunityToolkit.WinUI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:mxi="using:Microsoft.Xaml.Interactivity" xmlns:shc="using:Snap.Hutao.Control" + xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shch="using:Snap.Hutao.Control.Helper" xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shvc="using:Snap.Hutao.View.Control" @@ -14,6 +16,11 @@ d:DataContext="{d:DesignInstance shvs:SettingViewModel}" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" mc:Ignorable="d"> + + + + + @@ -103,7 +110,7 @@ Description="{Binding HutaoOptions.DeviceId}" Header="{shcm:ResourceString Name=ViewPageSettingDeviceIdHeader}" IsClickEnabled="True"/> - + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs index e889afc7..22ce7a2b 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs @@ -19,7 +19,7 @@ namespace Snap.Hutao.ViewModel.Setting; internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel { private readonly IContentDialogFactory contentDialogFactory; - private readonly HomaPassportClient homaPassportClient; + private readonly HutaoPassportClient homaPassportClient; private readonly HutaoUserOptions hutaoUserOptions; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index 26fc68e6..24d3ba74 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -23,6 +23,7 @@ using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.User; using Snap.Hutao.View.Dialog; using Snap.Hutao.ViewModel.Guide; +using Snap.Hutao.Web.Hutao; using System.Globalization; using System.IO; using System.Runtime.InteropServices; @@ -41,6 +42,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel private readonly HomeCardOptions homeCardOptions = new(); private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; + private readonly HutaoInfrastructureClient hutaoInfrastructureClient; private readonly HutaoPassportViewModel hutaoPassportViewModel; private readonly IContentDialogFactory contentDialogFactory; private readonly IGameLocatorFactory gameLocatorFactory; @@ -57,6 +59,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel private NameValue? selectedBackdropType; private NameValue? selectedCulture; + private IPInformation? ipInformation; /// /// 应用程序设置 @@ -98,6 +101,18 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel } } + public IPInformation? IPInformation { get => ipInformation; private set => SetProperty(ref ipInformation, value); } + + protected override async ValueTask InitializeUIAsync() + { + IPInformation? information = await hutaoInfrastructureClient.GetIPInformationAsync().ConfigureAwait(false); + + await taskContext.SwitchToMainThreadAsync(); + IPInformation = information; + + return true; + } + [Command("ResetStaticResourceCommand")] private static void ResetStaticResource() { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs index 301024dc..8a1fd9d9 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs @@ -24,7 +24,7 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss; internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient { private readonly ISpiralAbyssRecordService spiralAbyssRecordService; - private readonly HomaSpiralAbyssClient spiralAbyssClient; + private readonly HutaoSpiralAbyssClient spiralAbyssClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; private readonly IUserService userService; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs new file mode 100644 index 00000000..e3a5073a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Service.Hutao; +using Snap.Hutao.Web.Request.Builder; +using Snap.Hutao.Web.Request.Builder.Abstraction; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; + +namespace Snap.Hutao.Web.Hutao; + +[HttpClient(HttpClientConfiguration.Default)] +[ConstructorGenerated(ResolveHttpClient = true)] +internal sealed partial class HutaoInfrastructureClient +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + public async ValueTask GetIPInformationAsync(CancellationToken token = default) + { + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(HutaoEndpoints.Ip) + .Get(); + + IPInformation? resp = await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false); + return resp ?? IPInformation.Default; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs index 4c35a2a3..80fb41b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs @@ -17,7 +17,7 @@ namespace Snap.Hutao.Web.Hutao; [HighQuality] [HttpClient(HttpClientConfiguration.Default)] [ConstructorGenerated(ResolveHttpClient = true)] -internal sealed partial class HomaPassportClient +internal sealed partial class HutaoPassportClient { /// /// 通行证请求公钥 @@ -35,7 +35,7 @@ internal sealed partial class HomaPassportClient """; private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; + private readonly ILogger logger; private readonly HutaoUserOptions hutaoUserOptions; private readonly HttpClient httpClient; @@ -193,17 +193,6 @@ internal sealed partial class HomaPassportClient return HutaoResponse.DefaultIfNull(resp); } - public async ValueTask GetIPInfoAsync(CancellationToken token = default) - { - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(HutaoEndpoints.Ip) - .Get(); - - IPInfo? resp = await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false); - - return resp ?? new(); - } - private static string Encrypt(string text) { byte[] plaintextBytes = Encoding.UTF8.GetBytes(text); @@ -214,4 +203,4 @@ internal sealed partial class HomaPassportClient return Convert.ToBase64String(encryptedBytes); } } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportHttpRequestMessageBuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportHttpRequestMessageBuilderExtension.cs similarity index 88% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportHttpRequestMessageBuilderExtension.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportHttpRequestMessageBuilderExtension.cs index a99499b3..91d9edeb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportHttpRequestMessageBuilderExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportHttpRequestMessageBuilderExtension.cs @@ -6,7 +6,7 @@ using Snap.Hutao.Web.Request.Builder; namespace Snap.Hutao.Web.Hutao; -internal static class HomaPassportHttpRequestMessageBuilderExtension +internal static class HutaoPassportHttpRequestMessageBuilderExtension { public static async ValueTask TrySetTokenAsync(this HttpRequestMessageBuilder builder, HutaoUserOptions hutaoUserOptions) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs deleted file mode 100644 index 3a080ea4..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Snap.Hutao.Web.Hutao; - -internal sealed class IPInfo -{ - public IPInfo() - { - } - - [JsonConstructor] - public IPInfo(string ip, string division) - { - Ip = ip; - Division = division; - } - - public string Ip { get; set; } = "Unknown"; - - public string Division { get; set; } = "Unknown"; - - public override string ToString() - { - return SH.FormatViewPageSettingDeviceIpDescription(Ip, Division); - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs new file mode 100644 index 00000000..1ce1379d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hutao; + +internal sealed class IPInformation +{ + public static IPInformation Default { get; } = new() + { + Ip = "Unknown", + Division = "Unknown" + }; + + [JsonPropertyName("ip")] + public string Ip { get; set; } = default!; + + [JsonPropertyName("division")] + public string Division { get; set; } = default!; + + public override string ToString() + { + return SH.FormatViewPageSettingDeviceIpDescription(Ip, Division); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HomaLogUploadClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HomaLogUploadClient.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs index 30d110e4..6f198105 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HomaLogUploadClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs @@ -16,10 +16,10 @@ namespace Snap.Hutao.Web.Hutao.Log; [HighQuality] [ConstructorGenerated(ResolveHttpClient = true)] [HttpClient(HttpClientConfiguration.Default)] -internal sealed partial class HomaLogUploadClient +internal sealed partial class HutaoLogUploadClient { private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; + private readonly ILogger logger; private readonly RuntimeOptions runtimeOptions; private readonly HttpClient httpClient; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HomaSpiralAbyssClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs similarity index 98% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HomaSpiralAbyssClient.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs index d4841cd1..5283549c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HomaSpiralAbyssClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs @@ -22,10 +22,10 @@ namespace Snap.Hutao.Web.Hutao.SpiralAbyss; [HighQuality] [ConstructorGenerated(ResolveHttpClient = true)] [HttpClient(HttpClientConfiguration.Default)] -internal sealed partial class HomaSpiralAbyssClient +internal sealed partial class HutaoSpiralAbyssClient { private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; + private readonly ILogger logger; private readonly IServiceProvider serviceProvider; private readonly HttpClient httpClient; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs index dbf33add..faf4cd2d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs @@ -11,7 +11,6 @@ namespace Snap.Hutao.Web; /// [HighQuality] [SuppressMessage("", "SA1201")] -[SuppressMessage("", "SA1203")] [SuppressMessage("", "SA1124")] internal static class HutaoEndpoints { @@ -23,8 +22,6 @@ internal static class HutaoEndpoints public const string AnnouncementUpload = $"{HomaSnapGenshinApi}/Service/Announcement/Upload"; - public const string Ip = $"{ApiSnapGenshin}/ip"; - public static string GachaLogCompensation(int days) { return $"{HomaSnapGenshinApi}/Service/GachaLog/Compensation?days={days}"; @@ -251,6 +248,8 @@ internal static class HutaoEndpoints } #endregion + public const string Ip = $"{ApiSnapGenshin}/ip"; + public static string Website(string path) { return $"{HomaSnapGenshinApi}/{path}"; From a97aa26d795918ff4d2ee7ebb60c1c234c62c0bb Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 6 Dec 2023 15:41:13 +0800 Subject: [PATCH 03/47] refactor options --- .../Snap.Hutao/Core/RuntimeOptions.cs | 175 +++++++----------- .../Extension/DateTimeOffsetExtension.cs | 44 ++--- .../Extension/EnumerableExtension.cs | 13 -- .../Snap.Hutao/Extension/NullableExtension.cs | 18 ++ .../Snap.Hutao/Extension/SpanExtension.cs | 4 +- .../Model/InterChange/Achievement/UIAFInfo.cs | 2 +- .../Model/InterChange/GachaLog/UIGFInfo.cs | 2 +- .../Model/InterChange/Inventory/UIIFInfo.cs | 2 +- .../Snap.Hutao/Resource/Localization/SH.resx | 2 +- .../Snap.Hutao/Service/AppOptions.cs | 106 +++-------- .../GachaLogQueryManualInputProvider.cs | 2 +- .../GachaLogQueryWebCacheProvider.cs | 2 +- .../Service/GachaLog/UIGFImportService.cs | 2 +- .../Snap.Hutao/Service/Game/LaunchOptions.cs | 95 ++++------ .../Service/Hutao/HutaoUserOptions.cs | 121 +----------- .../Hutao/HutaoUserOptionsExtension.cs | 88 +++++++++ .../Service/Hutao/HutaoUserService.cs | 4 +- .../Service/Metadata/MetadataOptions.cs | 80 +------- .../Metadata/MetadataOptionsExtension.cs | 47 +++++ .../Snap.Hutao/Service/SupportedCultures.cs | 29 +++ .../Setting/HutaoPassportViewModel.cs | 4 +- .../Snap.Hutao/Web/Hutao/IPInformation.cs | 2 +- .../SpiralAbyss/HutaoSpiralAbyssClient.cs | 2 +- 23 files changed, 360 insertions(+), 486 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptionsExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptionsExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs index 49c50cab..9dd3079a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.Extensions.Options; using Microsoft.Web.WebView2.Core; using Microsoft.Win32; using Snap.Hutao.Core.Setting; @@ -12,28 +11,16 @@ using Windows.Storage; namespace Snap.Hutao.Core; -/// -/// 存储环境相关的选项 -/// 运行时运算得到的选项,无数据库交互 -/// [Injection(InjectAs.Singleton)] -internal sealed class RuntimeOptions : IOptions +internal sealed class RuntimeOptions { - private readonly ILogger logger; - private readonly bool isWebView2Supported; private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected; private bool? isElevated; - /// - /// 构造一个新的胡桃选项 - /// - /// 日志器 public RuntimeOptions(ILogger logger) { - this.logger = logger; - AppLaunchTime = DateTimeOffset.UtcNow; DataFolder = GetDataFolderPath(); @@ -45,117 +32,95 @@ internal sealed class RuntimeOptions : IOptions UserAgent = $"Snap Hutao/{Version}"; DeviceId = GetUniqueUserId(); - DetectWebView2Environment(ref webView2Version, ref isWebView2Supported); + DetectWebView2Environment(logger, out webView2Version, out isWebView2Supported); + + static string GetDataFolderPath() + { + string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty); + + if (!string.IsNullOrEmpty(preferredPath)) + { + Directory.CreateDirectory(preferredPath); + return preferredPath; + } + + // Fallback to MyDocuments + string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + +#if RELEASE + // 将测试版与正式版的文件目录分离 + string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao"; +#else + // 使得迁移能正常生成 + string folderName = "Hutao"; +#endif + string path = Path.GetFullPath(Path.Combine(myDocuments, folderName)); + Directory.CreateDirectory(path); + return path; + } + + static string GetUniqueUserId() + { + string userName = Environment.UserName; + object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName); + return Convert.ToMd5HexString($"{userName}{machineGuid}"); + } + + static void DetectWebView2Environment(ILogger logger, out string webView2Version, out bool isWebView2Supported) + { + try + { + webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString(); + isWebView2Supported = true; + } + catch (FileNotFoundException ex) + { + webView2Version = SH.CoreWebView2HelperVersionUndetected; + isWebView2Supported = false; + logger.LogError(ex, "WebView2 Runtime not installed."); + } + } } - /// - /// 当前版本 - /// public Version Version { get; } - /// - /// 标准UA - /// public string UserAgent { get; } - /// - /// 安装位置 - /// public string InstalledLocation { get; } - /// - /// 数据文件夹路径 - /// public string DataFolder { get; } - /// - /// 本地缓存 - /// public string LocalCache { get; } - /// - /// 包家族名称 - /// public string FamilyName { get; } - /// - /// 设备Id - /// public string DeviceId { get; } - /// - /// WebView2 版本 - /// public string WebView2Version { get => webView2Version; } - /// - /// 是否支持 WebView2 - /// public bool IsWebView2Supported { get => isWebView2Supported; } - /// - /// 是否为提升的权限 - /// - public bool IsElevated { get => isElevated ??= GetElevated(); } + public bool IsElevated + { + get + { + return isElevated ??= GetElevated(); + + static bool GetElevated() + { + if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false)) + { + return true; + } + + using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) + { + WindowsPrincipal principal = new(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } + } + } public DateTimeOffset AppLaunchTime { get; } - - /// - public RuntimeOptions Value { get => this; } - - private static string GetDataFolderPath() - { - string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty); - - if (!string.IsNullOrEmpty(preferredPath) && Directory.Exists(preferredPath)) - { - return preferredPath; - } - - string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); -#if RELEASE - // 将测试版与正式版的文件目录分离 - string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao"; -#else - // 使得迁移能正常生成 - string folderName = "Hutao"; -#endif - string path = Path.GetFullPath(Path.Combine(myDocument, folderName)); - Directory.CreateDirectory(path); - return path; - } - - private static string GetUniqueUserId() - { - string userName = Environment.UserName; - object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName); - return Convert.ToMd5HexString($"{userName}{machineGuid}"); - } - - private static bool GetElevated() - { - if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false)) - { - return true; - } - - using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) - { - WindowsPrincipal principal = new(identity); - return principal.IsInRole(WindowsBuiltInRole.Administrator); - } - } - - private void DetectWebView2Environment(ref string webView2Version, ref bool isWebView2Supported) - { - try - { - webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString(); - isWebView2Supported = true; - } - catch (FileNotFoundException ex) - { - logger.LogError(ex, "WebView2 Runtime not installed."); - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtension.cs index 867ef104..f31d6af8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtension.cs @@ -11,35 +11,27 @@ internal static class DateTimeOffsetExtension { public static readonly DateTimeOffset DatebaseDefaultTime = new(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)); - /// - /// 从Unix时间戳转换 - /// - /// 时间戳 - /// 默认值 - /// 转换的时间 - public static DateTimeOffset FromUnixTime(long? timestamp, in DateTimeOffset defaultValue) + public static DateTimeOffset UnsafeRelaxedFromUnixTime(long? timestamp, in DateTimeOffset defaultValue) { - if (timestamp is { } value) - { - try - { - return DateTimeOffset.FromUnixTimeSeconds(value); - } - catch (ArgumentOutOfRangeException) - { - try - { - return DateTimeOffset.FromUnixTimeMilliseconds(value); - } - catch (ArgumentOutOfRangeException) - { - return defaultValue; - } - } - } - else + if (timestamp is not { } value) { return defaultValue; } + + try + { + return DateTimeOffset.FromUnixTimeSeconds(value); + } + catch (ArgumentOutOfRangeException) + { + try + { + return DateTimeOffset.FromUnixTimeMilliseconds(value); + } + catch (ArgumentOutOfRangeException) + { + return defaultValue; + } + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs index eaa5e998..3ff7c070 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs @@ -17,19 +17,6 @@ internal static partial class EnumerableExtension return source.ElementAtOrDefault(index) ?? source.LastOrDefault(); } - /// - /// 如果传入集合不为空则原路返回, - /// 如果传入集合为空返回一个集合的空集 - /// - /// 源类型 - /// 源 - /// 源集合或空集 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable EmptyIfNull(this IEnumerable? source) - { - return source ?? Enumerable.Empty(); - } - /// /// 寻找枚举中唯一的值,找不到时 /// 回退到首个或默认值 diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/NullableExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/NullableExtension.cs index 85f31b64..7c25a349 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/NullableExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/NullableExtension.cs @@ -17,4 +17,22 @@ internal static class NullableExtension value = default; return false; } + + public static string ToStringOrEmpty(this in T? nullable) + where T : struct + { + string? result = default; + + if (nullable.HasValue) + { + result = nullable.Value.ToString(); + } + + if (string.IsNullOrEmpty(result)) + { + result = string.Empty; + } + + return result; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs index dee1a93c..67435eb5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs @@ -18,9 +18,9 @@ internal static class SpanExtension /// Span /// 最大值的下标 public static int IndexOfMax(this in ReadOnlySpan span) - where T : INumber + where T : INumber, IMinMaxValue { - T max = T.Zero; + T max = T.MinValue; int maxIndex = 0; for (int i = 0; i < span.Length; i++) { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs index e15c6147..0155e807 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs @@ -30,7 +30,7 @@ internal sealed class UIAFInfo : IMappingFrom [JsonIgnore] public DateTimeOffset ExportDateTime { - get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); + get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs index b5302513..f0eeae6e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs @@ -38,7 +38,7 @@ internal sealed class UIGFInfo : IMappingFrom DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); + get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs index a17ddfad..1ef79580 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs @@ -35,7 +35,7 @@ internal sealed class UIIFInfo [JsonIgnore] public DateTimeOffset ExportDateTime { - get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); + get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index f6d73cbe..e79abdea 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2250,7 +2250,7 @@ 设备 ID - IP:{0},归属:{1} + IP:{0} 归属服务器:{1} 设备 IP diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs index 4b0f32d8..e02352d9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs @@ -11,25 +11,10 @@ using System.IO; namespace Snap.Hutao.Service; -/// -/// 应用程序选项 -/// 存储服务相关的选项 -/// [ConstructorGenerated(CallBaseConstructor = true)] [Injection(InjectAs.Singleton)] internal sealed partial class AppOptions : DbStoreOptions { - private readonly List> supportedBackdropTypesInner = CollectionsNameValue.FromEnum(); - - private readonly List> supportedCulturesInner = - [ - ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")), - ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")), - ToNameValue(CultureInfo.GetCultureInfo("en")), - ToNameValue(CultureInfo.GetCultureInfo("ko")), - ToNameValue(CultureInfo.GetCultureInfo("ja")), - ]; - private string? gamePath; private string? powerShellPath; private bool? isEmptyHistoryWishVisible; @@ -38,75 +23,67 @@ internal sealed partial class AppOptions : DbStoreOptions private bool? isAdvancedLaunchOptionsEnabled; private string? geetestCustomCompositeUrl; - /// - /// 游戏路径 - /// public string GamePath { get => GetOption(ref gamePath, SettingEntry.GamePath); set => SetOption(ref gamePath, SettingEntry.GamePath, value); } - /// - /// PowerShell 路径 - /// public string PowerShellPath { - get => GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetPowerShellLocationOrEmpty); + get + { + return GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetDefaultPowerShellLocationOrEmpty); + + static string GetDefaultPowerShellLocationOrEmpty() + { + string? paths = Environment.GetEnvironmentVariable("Path"); + if (!string.IsNullOrEmpty(paths)) + { + foreach (StringSegment path in new StringTokenizer(paths, [';'])) + { + if (path is { HasValue: true, Length: > 0 }) + { + if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase)) + { + return Path.Combine(path.Value, "powershell.exe"); + } + } + } + } + + return string.Empty; + } + } set => SetOption(ref powerShellPath, SettingEntry.PowerShellPath, value); } - /// - /// 游戏路径 - /// public bool IsEmptyHistoryWishVisible { get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible); set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value); } - /// - /// 所有支持的背景样式 - /// - public List> BackdropTypes { get => supportedBackdropTypesInner; } + public List> BackdropTypes { get; } = CollectionsNameValue.FromEnum(); - /// - /// 背景类型 默认 Mica - /// public BackdropType BackdropType { get => GetOption(ref backdropType, SettingEntry.SystemBackdropType, v => Enum.Parse(v), BackdropType.Mica).Value; - set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToString()!); + set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToStringOrEmpty()); } - /// - /// 所有支持的语言 - /// - public List> Cultures { get => supportedCulturesInner; } + public List> Cultures { get; } = SupportedCultures.Get(); - /// - /// 初始化前的语言 - /// 通过设置与获取此属性,就可以获取到与系统同步的语言 - /// - public CultureInfo PreviousCulture { get; set; } = default!; - - /// - /// 当前语言 - /// 默认为系统语言 - /// public CultureInfo CurrentCulture { get => GetOption(ref currentCulture, SettingEntry.Culture, CultureInfo.GetCultureInfo, CultureInfo.CurrentCulture); set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name); } - /// - /// 是否启用高级功能 - /// DO NOT MOVE TO OTHER CLASS - /// We are binding this property in SettingPage - /// 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); } @@ -117,28 +94,5 @@ internal sealed partial class AppOptions : DbStoreOptions set => SetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl, value); } - private static NameValue ToNameValue(CultureInfo info) - { - return new(info.NativeName, info); - } - - private static string GetPowerShellLocationOrEmpty() - { - string? paths = Environment.GetEnvironmentVariable("Path"); - if (!string.IsNullOrEmpty(paths)) - { - foreach (StringSegment path in new StringTokenizer(paths, [';'])) - { - if (path is { HasValue: true, Length: > 0 }) - { - if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase)) - { - return Path.Combine(path.Value, "powershell.exe"); - } - } - } - } - - return string.Empty; - } + internal CultureInfo PreviousCulture { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs index a146c2d3..9e7b98fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -33,7 +33,7 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP if (query.TryGetValue("auth_appid", out string? appId) && appId is "webview_gacha") { string? queryLanguageCode = query["lang"]; - if (metadataOptions.IsCurrentLocale(queryLanguageCode)) + if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode)) { return new(true, new(queryString)); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs index b36ce4a4..247fe149 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs @@ -90,7 +90,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv NameValueCollection query = HttpUtility.ParseQueryString(result.TrimEnd("#/log")); string? queryLanguageCode = query["lang"]; - if (metadataOptions.IsCurrentLocale(queryLanguageCode)) + if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode)) { return new(true, new(result)); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs index ed63e016..6f82560e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs @@ -37,7 +37,7 @@ internal sealed partial class UIGFImportService : IUIGFImportService // v2.1 only support CHS if (version is UIGFVersion.Major2Minor2OrLower) { - if (!metadataOptions.IsCurrentLocale(uigf.Info.Language)) + if (!metadataOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language)) { string message = SH.FormatServiceGachaUIGFImportLanguageNotMatch(uigf.Info.Language, metadataOptions.LanguageCode); ThrowHelper.InvalidOperation(message); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index 5876b870..4241bba3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -40,10 +40,6 @@ internal sealed class LaunchOptions : DbStoreOptions private bool? useStarwardPlayTimeStatistics; private bool? setDiscordActivityWhenPlaying; - /// - /// 构造一个新的启动游戏选项 - /// - /// 服务提供器 public LaunchOptions(IServiceProvider serviceProvider) : base(serviceProvider) { @@ -53,47 +49,66 @@ internal sealed class LaunchOptions : DbStoreOptions InitializeMonitors(Monitors); InitializeScreenFps(out primaryScreenFps); + + static void InitializeMonitors(List> monitors) + { + try + { + // This list can't use foreach + // https://github.com/microsoft/CsWinRT/issues/747 + IReadOnlyList displayAreas = DisplayArea.FindAll(); + for (int i = 0; i < displayAreas.Count; i++) + { + DisplayArea displayArea = displayAreas[i]; + int index = i + 1; + monitors.Add(new($"{displayArea.DisplayId.Value:X8}:{index}", index)); + } + } + catch + { + monitors.Clear(); + } + } + + static void InitializeScreenFps(out int fps) + { + HDC hDC = default; + try + { + hDC = GetDC(HWND.Null); + fps = GetDeviceCaps(hDC, GET_DEVICE_CAPS_INDEX.VREFRESH); + } + finally + { + _ = ReleaseDC(HWND.Null, hDC); + } + } } - /// - /// 是否启用启动参数 - /// public bool IsEnabled { get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true); set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value); } - /// - /// 是否全屏 - /// public bool IsFullScreen { get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen); set => SetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, value); } - /// - /// 是否无边框 - /// public bool IsBorderless { get => GetOption(ref isBorderless, SettingEntry.LaunchIsBorderless); set => SetOption(ref isBorderless, SettingEntry.LaunchIsBorderless, value); } - /// - /// 是否独占全屏 - /// public bool IsExclusive { get => GetOption(ref isExclusive, SettingEntry.LaunchIsExclusive); set => SetOption(ref isExclusive, SettingEntry.LaunchIsExclusive, value); } - /// - /// 屏幕宽度 - /// public int ScreenWidth { get => GetOption(ref screenWidth, SettingEntry.LaunchScreenWidth, primaryScreenWidth); @@ -106,9 +121,6 @@ internal sealed class LaunchOptions : DbStoreOptions set => SetOption(ref isScreenWidthEnabled, SettingEntry.LaunchIsScreenWidthEnabled, value); } - /// - /// 屏幕高度 - /// public int ScreenHeight { get => GetOption(ref screenHeight, SettingEntry.LaunchScreenHeight, primaryScreenHeight); @@ -121,32 +133,20 @@ internal sealed class LaunchOptions : DbStoreOptions set => SetOption(ref isScreenHeightEnabled, SettingEntry.LaunchIsScreenHeightEnabled, value); } - /// - /// 是否全屏 - /// public bool UnlockFps { get => GetOption(ref unlockFps, SettingEntry.LaunchUnlockFps); set => SetOption(ref unlockFps, SettingEntry.LaunchUnlockFps, value); } - /// - /// 目标帧率 - /// public int TargetFps { get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps); set => SetOption(ref targetFps, SettingEntry.LaunchTargetFps, value); } - /// - /// 所有监视器 - /// public List> Monitors { get; } = []; - /// - /// 目标帧率 - /// [AllowNull] public NameValue Monitor { @@ -196,31 +196,4 @@ internal sealed class LaunchOptions : DbStoreOptions get => GetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, true); set => SetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, value); } - - private static void InitializeMonitors(List> monitors) - { - // This list can't use foreach - // https://github.com/microsoft/CsWinRT/issues/747 - IReadOnlyList displayAreas = DisplayArea.FindAll(); - for (int i = 0; i < displayAreas.Count; i++) - { - DisplayArea displayArea = displayAreas[i]; - int index = i + 1; - monitors.Add(new($"{displayArea.DisplayId.Value:X8}:{index}", index)); - } - } - - private static void InitializeScreenFps(out int fps) - { - HDC hDC = default; - try - { - hDC = GetDC(HWND.Null); - fps = GetDeviceCaps(hDC, GET_DEVICE_CAPS_INDEX.VREFRESH); - } - finally - { - _ = ReleaseDC(HWND.Null, hDC); - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs index 71988df7..71fabc3c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs @@ -2,141 +2,38 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.Extensions.Options; -using Snap.Hutao.Core.Setting; -using Snap.Hutao.Web.Hutao; -using System.Globalization; -using System.Text.RegularExpressions; namespace Snap.Hutao.Service.Hutao; -/// -/// 胡桃用户选项 -/// [Injection(InjectAs.Singleton)] -internal sealed class HutaoUserOptions : ObservableObject, IOptions +internal sealed class HutaoUserOptions : ObservableObject { - private readonly TaskCompletionSource initializedTaskCompletionSource = new(); + private readonly TaskCompletionSource initialization = new(); + private string? userName = SH.ViewServiceHutaoUserLoginOrRegisterHint; - private string? token; private bool isLoggedIn; - private bool isHutaoCloudServiceAllowed; + private bool isCloudServiceAllowed; private bool isLicensedDeveloper; + private bool isMaintainer; private string? gachaLogExpireAt; private string? gachaLogExpireAtSlim; - private bool isMaintainer; + private string? token; - /// - /// 用户名 - /// public string? UserName { get => userName; set => SetProperty(ref userName, value); } - /// - /// 真正的用户名 - /// - public string? ActualUserName { get => IsLoggedIn ? UserName : null; } - - /// - /// 是否已登录 - /// public bool IsLoggedIn { get => isLoggedIn; set => SetProperty(ref isLoggedIn, value); } - /// - /// 胡桃云服务是否可用 - /// - public bool IsCloudServiceAllowed { get => isHutaoCloudServiceAllowed; set => SetProperty(ref isHutaoCloudServiceAllowed, value); } + public bool IsCloudServiceAllowed { get => isCloudServiceAllowed; set => SetProperty(ref isCloudServiceAllowed, value); } - /// - /// 是否为开发者 - /// public bool IsLicensedDeveloper { get => isLicensedDeveloper; set => SetProperty(ref isLicensedDeveloper, value); } public bool IsMaintainer { get => isMaintainer; set => SetProperty(ref isMaintainer, value); } - /// - /// 祈愿记录服务到期时间 - /// public string? GachaLogExpireAt { get => gachaLogExpireAt; set => SetProperty(ref gachaLogExpireAt, value); } public string? GachaLogExpireAtSlim { get => gachaLogExpireAtSlim; set => SetProperty(ref gachaLogExpireAtSlim, value); } - /// - public HutaoUserOptions Value { get => this; } + internal string? Token { get => token; set => token = value; } - public async ValueTask PostLoginSucceedAsync(HutaoPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token) - { - LocalSetting.Set(SettingKeys.PassportUserName, username); - LocalSetting.Set(SettingKeys.PassportPassword, password); - - await taskContext.SwitchToMainThreadAsync(); - UserName = username; - this.token = token; - IsLoggedIn = true; - initializedTaskCompletionSource.TrySetResult(); - - await taskContext.SwitchToBackgroundAsync(); - Web.Response.Response userInfoResponse = await passportClient.GetUserInfoAsync(default).ConfigureAwait(false); - if (userInfoResponse.IsOk()) - { - await taskContext.SwitchToMainThreadAsync(); - UpdateUserInfo(userInfoResponse.Data); - return true; - } - - return false; - } - - public void LogoutOrUnregister() - { - LocalSetting.Set(SettingKeys.PassportUserName, string.Empty); - LocalSetting.Set(SettingKeys.PassportPassword, string.Empty); - - UserName = null; - token = null; - IsLoggedIn = false; - ClearUserInfo(); - } - - /// - /// 登录失败 - /// - public void LoginFailed() - { - UserName = SH.ViewServiceHutaoUserLoginFailHint; - initializedTaskCompletionSource.TrySetResult(); - } - - public void SkipLogin() - { - initializedTaskCompletionSource.TrySetResult(); - } - - /// - /// 刷新用户信息 - /// - /// 用户信息 - public void UpdateUserInfo(UserInfo userInfo) - { - IsLicensedDeveloper = userInfo.IsLicensedDeveloper; - IsMaintainer = userInfo.IsMaintainer; - string unescaped = Regex.Unescape(SH.ServiceHutaoUserGachaLogExpiredAt); - GachaLogExpireAt = string.Format(CultureInfo.CurrentCulture, unescaped, userInfo.GachaLogExpireAt); - GachaLogExpireAtSlim = $"{userInfo.GachaLogExpireAt:yyyy.MM.dd HH:mm:ss}"; - IsCloudServiceAllowed = IsLicensedDeveloper || userInfo.GachaLogExpireAt > DateTimeOffset.UtcNow; - } - - public async ValueTask GetTokenAsync() - { - await initializedTaskCompletionSource.Task.ConfigureAwait(false); - return token; - } - - private void ClearUserInfo() - { - IsLicensedDeveloper = false; - IsMaintainer = false; - GachaLogExpireAt = null; - GachaLogExpireAtSlim = null; - IsCloudServiceAllowed = false; - } + internal TaskCompletionSource Initialization { get => initialization; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptionsExtension.cs new file mode 100644 index 00000000..d7bf4d84 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptionsExtension.cs @@ -0,0 +1,88 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Setting; +using Snap.Hutao.Web.Hutao; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Snap.Hutao.Service.Hutao; + +internal static class HutaoUserOptionsExtension +{ + public static string? GetActualUserName(this HutaoUserOptions options) + { + return options.IsLoggedIn ? options.UserName : null; + } + + public static async ValueTask GetTokenAsync(this HutaoUserOptions options) + { + await options.Initialization.Task.ConfigureAwait(false); + return options.Token; + } + + public static async ValueTask PostLoginSucceedAsync(this HutaoUserOptions options, HutaoPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token) + { + LocalSetting.Set(SettingKeys.PassportUserName, username); + LocalSetting.Set(SettingKeys.PassportPassword, password); + + await taskContext.SwitchToMainThreadAsync(); + options.UserName = username; + options.Token = token; + options.IsLoggedIn = true; + options.Initialization.TrySetResult(); + + await taskContext.SwitchToBackgroundAsync(); + Web.Response.Response userInfoResponse = await passportClient.GetUserInfoAsync(default).ConfigureAwait(false); + if (userInfoResponse.IsOk()) + { + await taskContext.SwitchToMainThreadAsync(); + UpdateUserInfo(options, userInfoResponse.Data); + return true; + } + + return false; + + static void UpdateUserInfo(HutaoUserOptions options, UserInfo userInfo) + { + options.IsLicensedDeveloper = userInfo.IsLicensedDeveloper; + options.IsMaintainer = userInfo.IsMaintainer; + + options.IsCloudServiceAllowed = options.IsLicensedDeveloper || userInfo.GachaLogExpireAt > DateTimeOffset.UtcNow; + string unescaped = Regex.Unescape(SH.ServiceHutaoUserGachaLogExpiredAt); + options.GachaLogExpireAt = string.Format(CultureInfo.CurrentCulture, unescaped, userInfo.GachaLogExpireAt); + options.GachaLogExpireAtSlim = $"{userInfo.GachaLogExpireAt:yyyy.MM.dd HH:mm:ss}"; + } + } + + public static void PostLogoutOrUnregister(this HutaoUserOptions options) + { + LocalSetting.Set(SettingKeys.PassportUserName, string.Empty); + LocalSetting.Set(SettingKeys.PassportPassword, string.Empty); + + options.UserName = null; + options.Token = null; + options.IsLoggedIn = false; + ClearUserInfo(options); + + static void ClearUserInfo(HutaoUserOptions options) + { + options.IsLicensedDeveloper = false; + options.IsMaintainer = false; + options.GachaLogExpireAt = null; + options.GachaLogExpireAtSlim = null; + options.IsCloudServiceAllowed = false; + } + } + + public static void PostLoginFailed(this HutaoUserOptions options) + { + options.UserName = SH.ViewServiceHutaoUserLoginFailHint; + options.Initialization.TrySetResult(); + } + + public static void PostLoginSkipped(this HutaoUserOptions options) + { + options.Initialization.TrySetResult(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs index 1156614b..34527a60 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs @@ -36,7 +36,7 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password)) { - options.SkipLogin(); + options.PostLoginSkipped(); } else { @@ -52,7 +52,7 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe else { await taskContext.SwitchToMainThreadAsync(); - options.LoginFailed(); + options.PostLoginFailed(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptions.cs index 5fe5a13b..3bb1b91c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptions.cs @@ -1,19 +1,14 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.Extensions.Options; using Snap.Hutao.Core; -using System.Globalization; using System.IO; namespace Snap.Hutao.Service.Metadata; -/// -/// 元数据选项 -/// [ConstructorGenerated] [Injection(InjectAs.Singleton)] -internal sealed partial class MetadataOptions : IOptions +internal sealed partial class MetadataOptions { private readonly AppOptions appOptions; private readonly RuntimeOptions hutaoOptions; @@ -22,9 +17,6 @@ internal sealed partial class MetadataOptions : IOptions private string? fallbackDataFolder; private string? localizedDataFolder; - /// - /// 中文数据文件夹 - /// public string FallbackDataFolder { get @@ -39,9 +31,6 @@ internal sealed partial class MetadataOptions : IOptions } } - /// - /// 本地化数据文件夹 - /// public string LocalizedDataFolder { get @@ -56,17 +45,11 @@ internal sealed partial class MetadataOptions : IOptions } } - /// - /// 当前使用的元数据本地化名称 - /// public string LocaleName { - get => localeName ??= GetLocaleName(appOptions.CurrentCulture); + get => localeName ??= MetadataOptionsExtension.GetLocaleName(appOptions.CurrentCulture); } - /// - /// 当前语言代码 - /// public string LanguageCode { get @@ -79,63 +62,4 @@ internal sealed partial class MetadataOptions : IOptions throw new KeyNotFoundException($"Invalid localeName: '{LocaleName}'"); } } - - /// - public MetadataOptions Value { get => this; } - - /// - /// 获取语言名称 - /// - /// 语言信息 - /// 元数据语言名称 - public static string GetLocaleName(CultureInfo cultureInfo) - { - while (true) - { - if (LocaleNames.TryGetLocaleNameFromLanguageName(cultureInfo.Name, out string? localeName)) - { - return localeName; - } - else - { - cultureInfo = cultureInfo.Parent; - } - } - } - - /// - /// 检查是否为当前语言名称 - /// - /// 语言代码 - /// 是否为当前语言名称 - public bool IsCurrentLocale(string? languageCode) - { - if (string.IsNullOrEmpty(languageCode)) - { - return false; - } - - CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageCode); - return GetLocaleName(cultureInfo) == LocaleName; - } - - /// - /// 获取本地的本地化元数据文件 - /// - /// 文件名 - /// 本地的本地化元数据文件 - public string GetLocalizedLocalFile(string fileNameWithExtension) - { - return Path.Combine(LocalizedDataFolder, fileNameWithExtension); - } - - /// - /// 获取服务器上的本地化元数据文件 - /// - /// 文件名 - /// 服务器上的本地化元数据文件 - public string GetLocalizedRemoteFile(string fileNameWithExtension) - { - return Web.HutaoEndpoints.Metadata(LocaleName, fileNameWithExtension); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptionsExtension.cs new file mode 100644 index 00000000..59c17e93 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptionsExtension.cs @@ -0,0 +1,47 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Globalization; +using System.IO; + +namespace Snap.Hutao.Service.Metadata; + +internal static class MetadataOptionsExtension +{ + public static string GetLocalizedLocalFile(this MetadataOptions options, string fileNameWithExtension) + { + return Path.Combine(options.LocalizedDataFolder, fileNameWithExtension); + } + + public static string GetLocalizedRemoteFile(this MetadataOptions options, string fileNameWithExtension) + { + return Web.HutaoEndpoints.Metadata(options.LocaleName, fileNameWithExtension); + } + + public static bool LanguageCodeFitsCurrentLocale(this MetadataOptions options, string? languageCode) + { + if (string.IsNullOrEmpty(languageCode)) + { + return false; + } + + // We want to make sure code fits in 1 of 15 metadata locales + CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageCode); + return GetLocaleName(cultureInfo) == options.LocaleName; + } + + internal static string GetLocaleName(CultureInfo cultureInfo) + { + while (true) + { + if (LocaleNames.TryGetLocaleNameFromLanguageName(cultureInfo.Name, out string? localeName)) + { + return localeName; + } + else + { + cultureInfo = cultureInfo.Parent; + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs b/src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs new file mode 100644 index 00000000..8e352ef8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs @@ -0,0 +1,29 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model; +using System.Globalization; + +namespace Snap.Hutao.Service; + +internal static class SupportedCultures +{ + private static readonly List> Cultures = + [ + ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")), + ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")), + ToNameValue(CultureInfo.GetCultureInfo("en")), + ToNameValue(CultureInfo.GetCultureInfo("ko")), + ToNameValue(CultureInfo.GetCultureInfo("ja")), + ]; + + public static List> Get() + { + return Cultures; + } + + private static NameValue ToNameValue(CultureInfo info) + { + return new(info.NativeName, info); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs index 22ce7a2b..e06e0bc7 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs @@ -77,7 +77,7 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel infoBarService.Information(response.GetLocalizationMessageOrMessage()); await taskContext.SwitchToMainThreadAsync(); - hutaoUserOptions.LogoutOrUnregister(); + hutaoUserOptions.PostLogoutOrUnregister(); } } } @@ -110,7 +110,7 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel [Command("LogoutCommand")] private void LogoutAsync() { - hutaoUserOptions.LogoutOrUnregister(); + hutaoUserOptions.PostLogoutOrUnregister(); } [Command("ResetPasswordCommand")] diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs index 1ce1379d..8546fc7c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs @@ -8,7 +8,7 @@ internal sealed class IPInformation public static IPInformation Default { get; } = new() { Ip = "Unknown", - Division = "Unknown" + Division = "Unknown", }; [JsonPropertyName("ip")] diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs index 5283549c..d4aa1546 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs @@ -233,7 +233,7 @@ internal sealed partial class HutaoSpiralAbyssClient if (spiralAbyssResponse.IsOk()) { HutaoUserOptions options = serviceProvider.GetRequiredService(); - return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data, options.ActualUserName); + return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data, options.GetActualUserName()); } } } From 97842559d776da0ecb0ef36786d9e17a5ba8d6a7 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 6 Dec 2023 15:45:30 +0800 Subject: [PATCH 04/47] apply api changes --- .../Snap.Hutao/ViewModel/Setting/SettingViewModel.cs | 10 +++++++--- .../Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs | 7 ++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index 24d3ba74..5f2fb09c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -24,6 +24,7 @@ using Snap.Hutao.Service.User; using Snap.Hutao.View.Dialog; using Snap.Hutao.ViewModel.Guide; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Response; using System.Globalization; using System.IO; using System.Runtime.InteropServices; @@ -105,10 +106,13 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel protected override async ValueTask InitializeUIAsync() { - IPInformation? information = await hutaoInfrastructureClient.GetIPInformationAsync().ConfigureAwait(false); + Response resp = await hutaoInfrastructureClient.GetIPInformationAsync().ConfigureAwait(false); - await taskContext.SwitchToMainThreadAsync(); - IPInformation = information; + if (resp.IsOk()) + { + await taskContext.SwitchToMainThreadAsync(); + IPInformation = resp.Data; + } return true; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs index e3a5073a..c995e4cf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs @@ -5,6 +5,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Service.Hutao; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; +using Snap.Hutao.Web.Response; using System.Net.Http; using System.Security.Cryptography; using System.Text; @@ -19,13 +20,13 @@ internal sealed partial class HutaoInfrastructureClient private readonly ILogger logger; private readonly HttpClient httpClient; - public async ValueTask GetIPInformationAsync(CancellationToken token = default) + public async ValueTask> GetIPInformationAsync(CancellationToken token = default) { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(HutaoEndpoints.Ip) .Get(); - IPInformation? resp = await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false); - return resp ?? IPInformation.Default; + Response? resp = await builder.TryCatchSendAsync>(httpClient, logger, token).ConfigureAwait(false); + return Response.Response.DefaultIfNull(resp); } } \ No newline at end of file From e6e6e22b9cfc76d07f37dc3a9982527bb95842f8 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 6 Dec 2023 16:39:48 +0800 Subject: [PATCH 05/47] apply hutao api changes --- .../Snap.Hutao/Resource/Localization/SH.resx | 3 +++ .../ViewModel/Setting/SettingViewModel.cs | 11 +++++++++-- .../Snap.Hutao/Web/Hutao/IPInformation.cs | 11 +++++++++-- .../HttpRequestMessageBuilderExtension.cs | 19 +++++++++++++++++++ .../Builder/HttpTryCatchSendStrategy.cs | 10 ++++++++++ 5 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index e79abdea..8977675b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2828,6 +2828,9 @@ 无效的 UID + + 胡桃服务维护中 + 验证失败,请手动验证或前往「米游社-我的角色」页面查看 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index 5f2fb09c..b3d438ba 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -107,12 +107,19 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel protected override async ValueTask InitializeUIAsync() { Response resp = await hutaoInfrastructureClient.GetIPInformationAsync().ConfigureAwait(false); + IPInformation info; if (resp.IsOk()) { - await taskContext.SwitchToMainThreadAsync(); - IPInformation = resp.Data; + info = resp.Data; } + else + { + info = IPInformation.Default; + } + + await taskContext.SwitchToMainThreadAsync(); + IPInformation = info; return true; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs index 8546fc7c..f7ee777d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs @@ -5,10 +5,12 @@ namespace Snap.Hutao.Web.Hutao; internal sealed class IPInformation { + private const string Unknown = "Unknown"; + public static IPInformation Default { get; } = new() { - Ip = "Unknown", - Division = "Unknown", + Ip = Unknown, + Division = Unknown, }; [JsonPropertyName("ip")] @@ -19,6 +21,11 @@ internal sealed class IPInformation public override string ToString() { + if (Ip is Unknown && Division is Unknown) + { + return SH.WebHutaoServiceUnAvailable; + } + return SH.FormatViewPageSettingDeviceIpDescription(Ip, Division); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs index 469b5e72..5fe0b0e3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs @@ -1,7 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Web.Hutao; using System.IO; +using System.Net; using System.Net.Http; using System.Net.Sockets; @@ -12,6 +14,7 @@ internal static class HttpRequestMessageBuilderExtension private const string RequestErrorMessage = "请求异常已忽略"; internal static async ValueTask TryCatchSendAsync(this HttpRequestMessageBuilder builder, HttpClient httpClient, ILogger logger, CancellationToken token) + where TResult : class { try { @@ -26,6 +29,22 @@ internal static class HttpRequestMessageBuilderExtension catch (HttpRequestException ex) { logger.LogWarning(ex, RequestErrorMessage); + + if (ex.StatusCode is HttpStatusCode.BadGateway) + { + Type resultType = typeof(TResult); + + if (resultType == typeof(HutaoResponse)) + { + return Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default) as TResult; + } + + if (resultType.IsConstructedGenericType && resultType.GetGenericTypeDefinition() == typeof(HutaoResponse<>)) + { + return Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default, default) as TResult; + } + } + return default; } catch (IOException ex) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs new file mode 100644 index 00000000..53eb0ea1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs @@ -0,0 +1,10 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Request.Builder; + +internal enum HttpTryCatchSendStrategy +{ + Default, + HutaoApi, +} \ No newline at end of file From 3ba3ba55cb6523d074dde228495a66288ddd62cc Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 6 Dec 2023 17:16:23 +0800 Subject: [PATCH 06/47] adjust propertynames --- .../View/Page/AnnouncementPage.xaml | 2 +- .../Snap.Hutao/View/Page/DailyNotePage.xaml | 12 ++--- .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 50 +++++++++---------- .../Snap.Hutao/View/Page/SettingPage.xaml | 18 +++---- .../ViewModel/DailyNote/DailyNoteViewModel.cs | 14 ++---- .../ViewModel/Game/LaunchGameViewModel.cs | 13 +---- .../ViewModel/Home/AnnouncementViewModel.cs | 2 +- .../ViewModel/Setting/SettingViewModel.cs | 19 +++---- 8 files changed, 57 insertions(+), 73 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml index 440c769b..1ef6d6b7 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml @@ -199,7 +199,7 @@ Margin="16,16,16,0" Style="{StaticResource TitleTextBlockStyle}" Text="{Binding GreetingText}"/> - + - + + IsEnabled="{Binding DailyNoteOptions.IsAutoRefreshEnabled}" + ItemsSource="{Binding DailyNoteOptions.RefreshTimes}" + SelectedItem="{Binding DailyNoteOptions.SelectedRefreshTime, Mode=TwoWay}"> @@ -493,13 +493,13 @@ Description="{shcm:ResourceString Name=ViewPageDailyNoteSlientModeDescription}" Header="{shcm:ResourceString Name=ViewPageDailyNoteSlientModeHeader}" HeaderIcon="{shcm:FontIcon Glyph=}"> - + - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index c01951f2..9fd729b1 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -159,7 +159,7 @@ + IsEnabled="{Binding RuntimeOptions.IsElevated}"> @@ -170,7 +170,7 @@ - + - + - + - + - + + SelectedItem="{Binding LaunchOptions.SelectedAspectRatio, Mode=TwoWay}"/> @@ -231,9 +231,9 @@ Width="156" Padding="12,6,0,0" VerticalAlignment="Center" - IsEnabled="{Binding Options.IsScreenWidthEnabled}" - Value="{Binding Options.ScreenWidth, Mode=TwoWay}"/> - + IsEnabled="{Binding LaunchOptions.IsScreenWidthEnabled}" + Value="{Binding LaunchOptions.ScreenWidth, Mode=TwoWay}"/> + @@ -242,9 +242,9 @@ Width="156" Padding="12,6,0,0" VerticalAlignment="Center" - IsEnabled="{Binding Options.IsScreenHeightEnabled}" - Value="{Binding Options.ScreenHeight, Mode=TwoWay}"/> - + IsEnabled="{Binding LaunchOptions.IsScreenHeightEnabled}" + Value="{Binding LaunchOptions.ScreenHeight, Mode=TwoWay}"/> + @@ -252,11 +252,11 @@ + IsEnabled="{Binding LaunchOptions.IsMonitorEnabled}" + ItemsSource="{Binding LaunchOptions.Monitors}" + SelectedItem="{Binding LaunchOptions.Monitor, Mode=TwoWay}"/> - + @@ -265,20 +265,20 @@ Description="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsHeader}" HeaderIcon="{shcm:FontIcon Glyph=}" - IsEnabled="{Binding HutaoOptions.IsElevated}" + IsEnabled="{Binding RuntimeOptions.IsElevated}" Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"> - + + Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/> @@ -290,13 +290,13 @@ Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}" HeaderIcon="{shcm:FontIcon Glyph=}"> - + - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index 55b61e25..780d6e5f 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -203,7 +203,7 @@ @@ -214,7 +214,7 @@ @@ -312,7 +312,7 @@ - + @@ -326,7 +326,7 @@ - + @@ -345,7 +345,7 @@ Header="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleHeader}" HeaderIcon="{shcm:FontIcon Glyph=}"> @@ -397,13 +397,13 @@ - + Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/> + - + + Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/> diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs index 9ab591a2..0a08086e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs @@ -26,8 +26,8 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel { private readonly IContentDialogFactory contentDialogFactory; private readonly IDailyNoteService dailyNoteService; + private readonly DailyNoteOptions dailyNoteOptions; private readonly IInfoBarService infoBarService; - private readonly DailyNoteOptions options; private readonly RuntimeOptions runtimeOptions; private readonly ITaskContext taskContext; private readonly IUserService userService; @@ -35,15 +35,11 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel private ObservableCollection? userAndUids; private ObservableCollection? dailyNoteEntries; - /// - /// 选项 - /// - public DailyNoteOptions Options { get => options; } + public DailyNoteOptions DailyNoteOptions { get => dailyNoteOptions; } public RuntimeOptions RuntimeOptions { get => runtimeOptions; } - [SuppressMessage("", "CA1822")] - public IWebViewerSource VerifyUrlSource { get => new DailyNoteWebViewerSource(); } + public IWebViewerSource VerifyUrlSource { get; } = new DailyNoteWebViewerSource(); /// /// 用户与角色集合 @@ -128,13 +124,13 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel private async Task ConfigDailyNoteWebhookUrlAsync() { DailyNoteWebhookDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(true); - dialog.Text = options.WebhookUrl; + dialog.Text = dailyNoteOptions.WebhookUrl; (bool isOk, string url) = await dialog.GetInputUrlAsync().ConfigureAwait(false); if (isOk) { await taskContext.SwitchToMainThreadAsync(); - options.WebhookUrl = url; + dailyNoteOptions.WebhookUrl = url; infoBarService.Information(SH.ViewModelDailyNoteConfigWebhookUrlComplete); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index a33cf0f8..437e333c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -88,21 +88,12 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel /// public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); } - /// - /// 启动选项 - /// - public LaunchOptions Options { get => launchOptions; } + public LaunchOptions LaunchOptions { get => launchOptions; } public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; } - /// - /// 胡桃选项 - /// - public RuntimeOptions HutaoOptions { get => runtimeOptions; } + public RuntimeOptions RuntimeOptions { get => runtimeOptions; } - /// - /// 应用选项 - /// public AppOptions AppOptions { get => appOptions; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs index adf63276..36553850 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs @@ -39,7 +39,7 @@ internal sealed partial class AnnouncementViewModel : Abstraction.ViewModel /// /// 用户选项 /// - public HutaoUserOptions UserOptions { get => hutaoUserOptions; } + public HutaoUserOptions HutaoUserOptions { get => hutaoUserOptions; } /// /// 欢迎语 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index b3d438ba..a602938f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -62,10 +62,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel private NameValue? selectedCulture; private IPInformation? ipInformation; - /// - /// 应用程序设置 - /// - public AppOptions Options { get => appOptions; } + public AppOptions AppOptions { get => appOptions; } public RuntimeOptions HutaoOptions { get => runtimeOptions; } @@ -79,24 +76,24 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel public NameValue? SelectedBackdropType { - get => selectedBackdropType ??= Options.BackdropTypes.Single(t => t.Value == Options.BackdropType); + get => selectedBackdropType ??= AppOptions.BackdropTypes.Single(t => t.Value == AppOptions.BackdropType); set { if (SetProperty(ref selectedBackdropType, value) && value is not null) { - Options.BackdropType = value.Value; + AppOptions.BackdropType = value.Value; } } } public NameValue? SelectedCulture { - get => selectedCulture ??= Options.GetCurrentCultureForSelectionOrDefault(); + get => selectedCulture ??= AppOptions.GetCurrentCultureForSelectionOrDefault(); set { if (SetProperty(ref selectedCulture, value) && value is not null) { - Options.CurrentCulture = value.Value; + AppOptions.CurrentCulture = value.Value; AppInstance.Restart(string.Empty); } } @@ -149,7 +146,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel await taskContext.SwitchToMainThreadAsync(); try { - Options.GamePath = path; + AppOptions.GamePath = path; } catch (SqliteException ex) { @@ -167,14 +164,14 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel if (isOk && Path.GetFileNameWithoutExtension(file).EqualsAny(["POWERSHELL", "PWSH"], StringComparison.OrdinalIgnoreCase)) { await taskContext.SwitchToMainThreadAsync(); - Options.PowerShellPath = file; + AppOptions.PowerShellPath = file; } } [Command("DeleteGameWebCacheCommand")] private void DeleteGameWebCache() { - string gamePath = Options.GamePath; + string gamePath = AppOptions.GamePath; if (!string.IsNullOrEmpty(gamePath)) { From 82ccd59451c0d2e3a752fedfe8543cfa398927e7 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Thu, 7 Dec 2023 09:16:00 +0800 Subject: [PATCH 07/47] sign in website url --- .../Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs index d656bece..88bd31c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs @@ -21,6 +21,6 @@ internal sealed class SignInWebViewerSouce : DependencyObject, IWebViewerSource { return userAndUid.User.IsOversea ? "https://act.hoyolab.com/ys/event/signin-sea-v3/index.html?act_id=e202102251931481" - : "https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?act_id=e202009291139501"; + : "https://act.mihoyo.com/bbs/event/signin/hk4e/index.html?act_id=e202311201442471"; } } \ No newline at end of file From 8d8ec8b05dc09ba34ffa0744c619021f881c1c13 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Thu, 7 Dec 2023 09:34:05 +0800 Subject: [PATCH 08/47] code style --- .../Snap.Hutao/Web/Enka/EnkaClient.cs | 50 ++++++++++--------- .../Snap.Hutao/Web/HutaoEndpoints.cs | 4 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs index 0faf5d10..7290ebc3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs @@ -59,33 +59,35 @@ internal sealed partial class EnkaClient .SetRequestUri(url) .Get(); - HttpResponseMessage response = await httpClient.SendAsync(builder.HttpRequestMessage, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false); - if (response.IsSuccessStatusCode) + using (HttpResponseMessage response = await httpClient.SendAsync(builder.HttpRequestMessage, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false)) { - return await response.Content.ReadFromJsonAsync(options, token).ConfigureAwait(false); - } - else - { - // https://github.com/yoimiya-kokomi/miao-plugin/pull/441 - // Additionally, HTTP codes for UID requests: - // 400 = wrong UID format - // 404 = player does not exist(MHY server told that) - // 429 = rate - limit - // 424 = game maintenance / everything is broken after the update - // 500 = general server error - // 503 = I screwed up massively - string message = response.StatusCode switch + if (response.IsSuccessStatusCode) { - HttpStatusCode.BadRequest => SH.WebEnkaResponseStatusCode400, - HttpStatusCode.NotFound => SH.WebEnkaResponseStatusCode404, - HttpStatusCode.FailedDependency => SH.WebEnkaResponseStatusCode424, - HttpStatusCode.TooManyRequests => SH.WebEnkaResponseStatusCode429, - HttpStatusCode.InternalServerError => SH.WebEnkaResponseStatusCode500, - HttpStatusCode.ServiceUnavailable => SH.WebEnkaResponseStatusCode503, - _ => SH.WebEnkaResponseStatusCodeUnknown, - }; + return await response.Content.ReadFromJsonAsync(options, token).ConfigureAwait(false); + } + else + { + // https://github.com/yoimiya-kokomi/miao-plugin/pull/441 + // Additionally, HTTP codes for UID requests: + // 400 = wrong UID format + // 404 = player does not exist(MHY server told that) + // 429 = rate - limit + // 424 = game maintenance / everything is broken after the update + // 500 = general server error + // 503 = I screwed up massively + string message = response.StatusCode switch + { + HttpStatusCode.BadRequest => SH.WebEnkaResponseStatusCode400, + HttpStatusCode.NotFound => SH.WebEnkaResponseStatusCode404, + HttpStatusCode.FailedDependency => SH.WebEnkaResponseStatusCode424, + HttpStatusCode.TooManyRequests => SH.WebEnkaResponseStatusCode429, + HttpStatusCode.InternalServerError => SH.WebEnkaResponseStatusCode500, + HttpStatusCode.ServiceUnavailable => SH.WebEnkaResponseStatusCode503, + _ => SH.WebEnkaResponseStatusCodeUnknown, + }; - return new() { Message = message, }; + return new() { Message = message, }; + } } } catch (HttpRequestException) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs index faf4cd2d..c6867f9e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs @@ -11,6 +11,7 @@ namespace Snap.Hutao.Web; /// [HighQuality] [SuppressMessage("", "SA1201")] +[SuppressMessage("", "SA1203")] [SuppressMessage("", "SA1124")] internal static class HutaoEndpoints { @@ -248,8 +249,6 @@ internal static class HutaoEndpoints } #endregion - public const string Ip = $"{ApiSnapGenshin}/ip"; - public static string Website(string path) { return $"{HomaSnapGenshinApi}/{path}"; @@ -260,6 +259,7 @@ internal static class HutaoEndpoints return $"{ApiSnapGenshinEnka}/{uid}"; } + public const string Ip = $"{ApiSnapGenshin}/ip"; private const string ApiSnapGenshin = "https://api.snapgenshin.com"; private const string ApiSnapGenshinMetadata = $"{ApiSnapGenshin}/metadata"; private const string ApiSnapGenshinStaticRaw = $"{ApiSnapGenshin}/static/raw"; From e5d67a80dd2879586393fd8101bf13b5fb581553 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Thu, 7 Dec 2023 10:37:15 +0800 Subject: [PATCH 09/47] move files --- .../Control/SizeRestrictedContentControl.cs | 1 - .../Core/DependencyInjection/DependencyInjection.cs | 8 ++------ .../Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs | 2 -- .../Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs | 1 - .../Snap.Hutao/Service/Abstraction/DbStoreOptions.cs | 11 ++++++++++- .../Factory/TypedWishSummaryBuilderContext.cs | 2 +- .../Service/GachaLog/GachaLogHutaoCloudService.cs | 2 +- .../Service/GachaLog/IGachaLogHutaoCloudService.cs | 2 +- .../Service/Game/Process/GameProcessService.cs | 1 - .../Service/Hutao/HutaoSpiralAbyssService.cs | 2 +- .../Snap.Hutao/Service/Metadata/LocaleNames.cs | 4 ++-- .../View/Dialog/HutaoPassportRegisterDialog.xaml.cs | 1 + .../Dialog/HutaoPassportResetPasswordDialog.xaml.cs | 1 + .../View/Dialog/HutaoPassportUnregisterDialog.xaml.cs | 1 + .../ViewModel/Setting/HutaoPassportViewModel.cs | 1 + .../SpiralAbyss/SpiralAbyssRecordViewModel.cs | 2 +- .../Web/Hutao/GachaLog/HomaGachaLogClient.cs | 1 + .../Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs | 2 +- .../Hutao/HutaoAsAService/HutaoAsAServiceClient.cs | 1 + .../Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs | 5 +---- .../Snap.Hutao/Web/Hutao/HutaoPassportClient.cs | 1 + .../Web/Hutao/{ => Response}/HutaoResponse.cs | 6 +++--- .../Web/Hutao/{ => Response}/ILocalizableResponse.cs | 2 +- .../{ => Response}/LocalizableResponseExtension.cs | 4 ++-- .../Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs | 1 + .../Builder/HttpRequestMessageBuilderExtension.cs | 2 +- 26 files changed, 36 insertions(+), 31 deletions(-) rename src/Snap.Hutao/Snap.Hutao/Web/Hutao/{ => Response}/HutaoResponse.cs (89%) rename src/Snap.Hutao/Snap.Hutao/Web/Hutao/{ => Response}/ILocalizableResponse.cs (81%) rename src/Snap.Hutao/Snap.Hutao/Web/Hutao/{ => Response}/LocalizableResponseExtension.cs (90%) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs b/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs index 986de656..587070b6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs @@ -3,7 +3,6 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Snap.Hutao.Core; using Windows.Foundation; namespace Snap.Hutao.Control; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs index d6bf777d..50c41966 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs @@ -52,14 +52,10 @@ internal static class DependencyInjection CultureInfo cultureInfo = appOptions.CurrentCulture; + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; CultureInfo.CurrentCulture = cultureInfo; CultureInfo.CurrentUICulture = cultureInfo; - - CultureInfo.DefaultThreadCurrentCulture = cultureInfo; - CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; - - CultureInfo.DefaultThreadCurrentCulture = cultureInfo; - CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs index 83dc3e2a..dfc1c56a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs @@ -11,8 +11,6 @@ namespace Snap.Hutao.Core.Diagnostics; /// internal readonly struct ValueStopwatch { - private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - private readonly long startTimestamp; private ValueStopwatch(long startTimestamp) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs index b17463bc..8a5a9225 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Intrinsic; -using System.Runtime.InteropServices; namespace Snap.Hutao.Model.Metadata.Avatar; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs index 3ea09669..1baf293c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs @@ -128,7 +128,16 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions(); string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value; - storage = value is null ? defaultValue : deserializer(value)!; + if (value is null) + { + storage = defaultValue; + } + else + { + T targetValue = deserializer(value); + ArgumentNullException.ThrowIfNull(targetValue); + storage = targetValue; + } } return storage; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs index 32d141c3..5c964feb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; -using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao.GachaLog; +using Snap.Hutao.Web.Hutao.Response; namespace Snap.Hutao.Service.GachaLog.Factory; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs index c5d95cc4..9998565e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs @@ -10,8 +10,8 @@ using Snap.Hutao.Service.GachaLog.Factory; using Snap.Hutao.Service.Metadata; using Snap.Hutao.ViewModel.GachaLog; using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; -using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao.GachaLog; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Response; namespace Snap.Hutao.Service.GachaLog; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogHutaoCloudService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogHutaoCloudService.cs index fd87d389..18fc51ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogHutaoCloudService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogHutaoCloudService.cs @@ -3,8 +3,8 @@ using Snap.Hutao.Model.Entity; using Snap.Hutao.ViewModel.GachaLog; -using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao.GachaLog; +using Snap.Hutao.Web.Hutao.Response; namespace Snap.Hutao.Service.GachaLog; 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 8053bc09..d1f8f00e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Snap.Hutao.Core; -using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Service.Discord; using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Game.Unlocker; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs index f8fb9434..c250c8a4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Microsoft.Extensions.Caching.Memory; -using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Hutao.SpiralAbyss; using Snap.Hutao.Web.Response; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs index c760a0df..86b29ebe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs @@ -8,6 +8,8 @@ namespace Snap.Hutao.Service.Metadata; /// internal static class LocaleNames { + public const string CHS = "CHS"; // Chinese (Simplified) + public const string CHT = "CHT"; // Chinese (Traditional) public const string DE = "DE"; // German public const string EN = "EN"; // English public const string ES = "ES"; // Spanish @@ -21,8 +23,6 @@ internal static class LocaleNames public const string TH = "TH"; // Thai public const string TR = "TR"; // Turkish public const string VI = "VI"; // Vietnamese - public const string CHS = "CHS"; // Chinese (Simplified) - public const string CHT = "CHT"; // Chinese (Traditional) public static bool TryGetLocaleNameFromLanguageName(string languageName, [NotNullWhen(true)] out string? localeName) { diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportRegisterDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportRegisterDialog.xaml.cs index b3cffe51..307c4353 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportRegisterDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportRegisterDialog.xaml.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Common; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Service.Notification; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; namespace Snap.Hutao.View.Dialog; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs index 7d4a9406..0224ce4d 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Common; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Service.Notification; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; namespace Snap.Hutao.View.Dialog; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs index 1707c24b..993196a1 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Common; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Service.Notification; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; namespace Snap.Hutao.View.Dialog; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs index e06e0bc7..8dcf373a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs @@ -7,6 +7,7 @@ using Snap.Hutao.Service.Notification; using Snap.Hutao.View.Dialog; using Snap.Hutao.Web; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Windows.System; namespace Snap.Hutao.ViewModel.Setting; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs index 8a1fd9d9..aeb7db5b 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs @@ -8,7 +8,7 @@ using Snap.Hutao.Service.SpiralAbyss; using Snap.Hutao.Service.User; using Snap.Hutao.ViewModel.Complex; using Snap.Hutao.ViewModel.User; -using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Hutao.SpiralAbyss; using Snap.Hutao.Web.Hutao.SpiralAbyss.Post; using System.Collections.ObjectModel; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/HomaGachaLogClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/HomaGachaLogClient.cs index 6634724a..a85f2acc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/HomaGachaLogClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/HomaGachaLogClient.cs @@ -3,6 +3,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Service.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using System.Net.Http; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs index 87eda9bb..995b1ce9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs @@ -5,7 +5,7 @@ namespace Snap.Hutao.Web.Hutao.Geetest; internal sealed class GeetestResponse { - public static GeetestResponse InternalFailure { get; } = new() { Code = Response.Response.InternalFailure }; + public static GeetestResponse InternalFailure { get; } = new() { Code = Web.Response.Response.InternalFailure }; [JsonPropertyName("code")] public int Code { get; set; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoAsAService/HutaoAsAServiceClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoAsAService/HutaoAsAServiceClient.cs index c5d83080..c1290fb4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoAsAService/HutaoAsAServiceClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoAsAService/HutaoAsAServiceClient.cs @@ -4,6 +4,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Service.Hutao; using Snap.Hutao.Service.Metadata; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using System.Net.Http; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs index c995e4cf..7a1c3e55 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs @@ -2,13 +2,10 @@ // Licensed under the MIT license. using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; -using Snap.Hutao.Service.Hutao; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using Snap.Hutao.Web.Response; using System.Net.Http; -using System.Security.Cryptography; -using System.Text; namespace Snap.Hutao.Web.Hutao; @@ -27,6 +24,6 @@ internal sealed partial class HutaoInfrastructureClient .Get(); Response? resp = await builder.TryCatchSendAsync>(httpClient, logger, token).ConfigureAwait(false); - return Response.Response.DefaultIfNull(resp); + return Web.Response.Response.DefaultIfNull(resp); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs index 80fb41b2..f3be6276 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs @@ -3,6 +3,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Service.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using System.Net.Http; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/HutaoResponse.cs similarity index 89% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoResponse.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/HutaoResponse.cs index e73529e0..906a7845 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/HutaoResponse.cs @@ -3,9 +3,9 @@ using System.Runtime.CompilerServices; -namespace Snap.Hutao.Web.Hutao; +namespace Snap.Hutao.Web.Hutao.Response; -internal sealed class HutaoResponse : Response.Response, ILocalizableResponse +internal sealed class HutaoResponse : Web.Response.Response, ILocalizableResponse { [JsonConstructor] public HutaoResponse(int returnCode, string message, string? localizationKey) @@ -38,7 +38,7 @@ internal sealed class HutaoResponse : Response.Response, ILocalizableResponse } [SuppressMessage("", "SA1402")] -internal sealed class HutaoResponse : Response.Response, ILocalizableResponse +internal sealed class HutaoResponse : Web.Response.Response, ILocalizableResponse { [JsonConstructor] public HutaoResponse(int returnCode, string message, TData? data, string? localizationKey) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/ILocalizableResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/ILocalizableResponse.cs similarity index 81% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/ILocalizableResponse.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/ILocalizableResponse.cs index 49eaf8c5..1ee0c724 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/ILocalizableResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/ILocalizableResponse.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hutao; +namespace Snap.Hutao.Web.Hutao.Response; internal interface ILocalizableResponse { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/LocalizableResponseExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/LocalizableResponseExtension.cs similarity index 90% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/LocalizableResponseExtension.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/LocalizableResponseExtension.cs index 8bffc613..d28440fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/LocalizableResponseExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/LocalizableResponseExtension.cs @@ -3,7 +3,7 @@ using System.Globalization; -namespace Snap.Hutao.Web.Hutao; +namespace Snap.Hutao.Web.Hutao.Response; internal static class LocalizableResponseExtension { @@ -19,7 +19,7 @@ internal static class LocalizableResponseExtension } public static string GetLocalizationMessageOrMessage(this TResponse localizableResponse) - where TResponse : Response.Response, ILocalizableResponse + where TResponse : Web.Response.Response, ILocalizableResponse { return localizableResponse.GetLocalizationMessageOrDefault() ?? localizableResponse.Message; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs index d4aa1546..427575a2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs @@ -8,6 +8,7 @@ using Snap.Hutao.ViewModel.User; using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Hutao.SpiralAbyss.Post; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs index 5fe0b0e3..f25bb246 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; using System.IO; using System.Net; using System.Net.Http; From bd344e50abb03621c4242f5c4e4bfb2ccee1864d Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Thu, 7 Dec 2023 10:57:16 +0800 Subject: [PATCH 10/47] minor game process optimization --- .../Service/Game/Process/GameProcessService.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 d1f8f00e..e9d6e28a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -32,8 +32,17 @@ internal sealed partial class GameProcessService : IGameProcessService return true; } - return System.Diagnostics.Process.GetProcessesByName(YuanShenProcessName).Length > 0 - || System.Diagnostics.Process.GetProcessesByName(GenshinImpactProcessName).Length > 0; + // Original two GetProcessesByName is O(2n) + // GetProcesses once and manually loop is O(n) + foreach (ref System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan()) + { + if (process.ProcessName is YuanShenProcessName or GenshinImpactProcessName) + { + return true; + } + } + + return false; } public async ValueTask LaunchAsync(IProgress progress) From 559ae250bd34f17039c7e02c45eb583107ceca90 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Thu, 7 Dec 2023 17:25:48 +0800 Subject: [PATCH 11/47] cultivation wip [skip ci] --- .../BaseClassLibrary/LinqTest.cs | 34 + .../Extension/EnumerableExtension.List.cs | 2 +- .../Snap.Hutao/Extension/SpanExtension.cs | 6 - ...CultivateEntryLevelInformation.Designer.cs | 612 ++++++++++++++++++ ...85530_AddCultivateEntryLevelInformation.cs | 56 ++ .../Migrations/AppDbContextModelSnapshot.cs | 61 +- .../Entity/CultivateEntryLevelInformation.cs | 60 ++ .../Snap.Hutao/Model/Entity/CultivateItem.cs | 4 +- .../Model/Entity/Database/AppDbContext.cs | 53 +- .../Cultivation/CultivationDbService.cs | 18 + .../Cultivation/CultivationMetadataContext.cs | 18 + .../Service/Cultivation/CultivationService.cs | 32 +- .../Cultivation/ICultivationDbService.cs | 2 + .../ICultivationMetadataContext.cs | 14 + .../Cultivation/ICultivationService.cs | 43 +- .../Service/Cultivation/LevelInformation.cs | 59 ++ .../ContextAbstraction/IMetadataContext.cs | 6 + .../IMetadataDictionaryIdAvatarSource.cs | 11 + .../IMetadataDictionaryIdMaterialSource.cs | 12 + .../IMetadataDictionaryIdWeaponSource.cs | 11 + .../IMetadataListMaterialSource.cs | 11 + .../MetadataServiceContextExtension.cs | 68 ++ ...CultivatePromotionDeltaBatchDialog.xaml.cs | 27 +- .../AvatarProperty/AvatarPropertyViewModel.cs | 94 +-- .../Cultivation/CultivationViewModel.cs | 99 ++- .../Cultivation/StatisticsCultivateItem.cs | 2 +- .../Snap.Hutao/ViewModel/TestViewModel.cs | 18 +- .../ViewModel/Wiki/WikiAvatarViewModel.cs | 5 +- .../ViewModel/Wiki/WikiWeaponViewModel.cs | 3 +- .../Event/Calculate/AvatarPromotionDelta.cs | 38 ++ .../Takumi/Event/Calculate/CalculateClient.cs | 4 +- .../Hoyolab/Takumi/Event/Calculate/Item.cs | 4 +- .../Takumi/Event/Calculate/ItemHelper.cs | 18 +- 33 files changed, 1252 insertions(+), 253 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntryLevelInformation.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationMetadataContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationMetadataContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Cultivation/LevelInformation.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdAvatarSource.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdMaterialSource.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdWeaponSource.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataListMaterialSource.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/MetadataServiceContextExtension.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs new file mode 100644 index 00000000..e5f4f20b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public sealed class LinqTest +{ + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void LinqOrderByWithWrapperStruct() + { + List list = [1, 5, 2, 6, 3, 7, 4, 8]; + string result = string.Join(", ", list.OrderBy(i => i).Select(i => i.Value)); + + Console.WriteLine(result); + } + + private readonly struct MyUInt32 + { + public readonly uint Value; + + public MyUInt32(uint value) + { + Value = value; + } + + public static implicit operator MyUInt32(uint value) + { + return new(value); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs index 68b066d5..d1519d60 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs @@ -207,4 +207,4 @@ internal static partial class EnumerableExtension list.Sort((left, right) => keySelector(right).CompareTo(keySelector(left))); return list; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs index 67435eb5..9f60a0fd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System.Numerics; -using System.Runtime.InteropServices; namespace Snap.Hutao.Extension; @@ -75,9 +74,4 @@ internal static class SpanExtension return unchecked((byte)(sum / count)); } - - public static Span AsSpan(this List list) - { - return CollectionsMarshal.AsSpan(list); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs new file mode 100644 index 00000000..c3e5e810 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs @@ -0,0 +1,612 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Model.Entity.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20231207085530_AddCultivateEntryLevelInformation")] + partial class AddCultivateEntryLevelInformation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CalculatorRefreshTime") + .HasColumnType("TEXT"); + + b.Property("GameRecordRefreshTime") + .HasColumnType("TEXT"); + + b.Property("Info") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShowcaseRefreshTime") + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("avatar_infos"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("cultivate_entries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("AvatarLevelTo") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("SkillALevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillALevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelTo") + .HasColumnType("INTEGER"); + + b.Property("WeaponLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("WeaponLevelTo") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("EntryId"); + + b.ToTable("cultivate_entry_level_informations"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("IsFinished") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("EntryId"); + + b.ToTable("cultivate_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AttachedUid") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("cultivate_projects"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DailyNote") + .HasColumnType("TEXT"); + + b.Property("DailyTaskNotify") + .HasColumnType("INTEGER"); + + b.Property("DailyTaskNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotify") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("RefreshTime") + .HasColumnType("TEXT"); + + b.Property("ResinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ResinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotify") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("UserId"); + + b.ToTable("daily_notes"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("gacha_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("GachaType") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("QueryType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("gacha_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AttachUid") + .HasColumnType("TEXT"); + + b.Property("MihoyoSDK") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("game_accounts"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AppendPropIdList") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("MainPropId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_reliquaries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("PromoteLevel") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_weapons"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ExpireTime") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("object_cache"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SpiralAbyss") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("spiral_abysses"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Aid") + .HasColumnType("TEXT"); + + b.Property("CookieToken") + .HasColumnType("TEXT"); + + b.Property("CookieTokenLastUpdateTime") + .HasColumnType("TEXT"); + + b.Property("Fingerprint") + .HasColumnType("TEXT"); + + b.Property("FingerprintLastUpdateTime") + .HasColumnType("TEXT"); + + b.Property("IsOversea") + .HasColumnType("INTEGER"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LToken") + .HasColumnType("TEXT") + .HasColumnName("Ltoken"); + + b.Property("Mid") + .HasColumnType("TEXT"); + + b.Property("SToken") + .HasColumnType("TEXT") + .HasColumnName("Stoken"); + + b.HasKey("InnerId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => + { + b.HasOne("Snap.Hutao.Model.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.cs new file mode 100644 index 00000000..37a9b8f8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.cs @@ -0,0 +1,56 @@ +// +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + /// + public partial class AddCultivateEntryLevelInformation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "cultivate_entry_level_informations", + columns: table => new + { + InnerId = table.Column(type: "TEXT", nullable: false), + EntryId = table.Column(type: "TEXT", nullable: false), + AvatarLevelFrom = table.Column(type: "INTEGER", nullable: false), + AvatarLevelTo = table.Column(type: "INTEGER", nullable: false), + SkillALevelFrom = table.Column(type: "INTEGER", nullable: false), + SkillALevelTo = table.Column(type: "INTEGER", nullable: false), + SkillELevelFrom = table.Column(type: "INTEGER", nullable: false), + SkillELevelTo = table.Column(type: "INTEGER", nullable: false), + SkillQLevelFrom = table.Column(type: "INTEGER", nullable: false), + SkillQLevelTo = table.Column(type: "INTEGER", nullable: false), + WeaponLevelFrom = table.Column(type: "INTEGER", nullable: false), + WeaponLevelTo = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cultivate_entry_level_informations", x => x.InnerId); + table.ForeignKey( + name: "FK_cultivate_entry_level_informations_cultivate_entries_EntryId", + column: x => x.EntryId, + principalTable: "cultivate_entries", + principalColumn: "InnerId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_cultivate_entry_level_informations_EntryId", + table: "cultivate_entry_level_informations", + column: "EntryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "cultivate_entry_level_informations"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index 6c6a556f..9129b6e7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs @@ -113,13 +113,59 @@ namespace Snap.Hutao.Migrations b.ToTable("cultivate_entries"); }); + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("AvatarLevelTo") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("SkillALevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillALevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelTo") + .HasColumnType("INTEGER"); + + b.Property("WeaponLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("WeaponLevelTo") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("EntryId"); + + b.ToTable("cultivate_entry_level_informations"); + }); + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => { b.Property("InnerId") .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("Count") + b.Property("Count") .HasColumnType("INTEGER"); b.Property("EntryId") @@ -128,7 +174,7 @@ namespace Snap.Hutao.Migrations b.Property("IsFinished") .HasColumnType("INTEGER"); - b.Property("ItemId") + b.Property("ItemId") .HasColumnType("INTEGER"); b.HasKey("InnerId"); @@ -481,6 +527,17 @@ namespace Snap.Hutao.Migrations b.Navigation("Project"); }); + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => { b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry") diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntryLevelInformation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntryLevelInformation.cs new file mode 100644 index 00000000..6e71d625 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntryLevelInformation.cs @@ -0,0 +1,60 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Abstraction; +using Snap.Hutao.Service.Cultivation; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Snap.Hutao.Model.Entity; + +[Table("cultivate_entry_level_informations")] +internal sealed class CultivateEntryLevelInformation : IMappingFrom +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid InnerId { get; set; } + + public Guid EntryId { get; set; } + + [ForeignKey(nameof(EntryId))] + public CultivateEntry? Entry { get; set; } + + public uint AvatarLevelFrom { get; set; } + + public uint AvatarLevelTo { get; set; } + + public uint SkillALevelFrom { get; set; } + + public uint SkillALevelTo { get; set; } + + public uint SkillELevelFrom { get; set; } + + public uint SkillELevelTo { get; set; } + + public uint SkillQLevelFrom { get; set; } + + public uint SkillQLevelTo { get; set; } + + public uint WeaponLevelFrom { get; set; } + + public uint WeaponLevelTo { get; set; } + + public static CultivateEntryLevelInformation From(Guid entryId, LevelInformation source) + { + return new() + { + EntryId = entryId, + AvatarLevelFrom = source.AvatarLevelFrom, + AvatarLevelTo = source.AvatarLevelTo, + SkillALevelFrom = source.SkillALevelFrom, + SkillALevelTo = source.SkillALevelTo, + SkillELevelFrom = source.SkillELevelFrom, + SkillELevelTo = source.SkillELevelTo, + SkillQLevelFrom = source.SkillQLevelFrom, + SkillQLevelTo = source.SkillQLevelTo, + WeaponLevelFrom = source.WeaponLevelFrom, + WeaponLevelTo = source.WeaponLevelTo, + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateItem.cs index af0ffec5..eef1e917 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateItem.cs @@ -35,12 +35,12 @@ internal sealed class CultivateItem : IDbMappingForeignKeyFrom /// 物品 Id /// - public int ItemId { get; set; } + public uint ItemId { get; set; } /// /// 物品个数 /// - public int Count { get; set; } + public uint Count { get; set; } /// /// 是否完成此项 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs index 8d3b18e3..fc1146be 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs @@ -37,89 +37,40 @@ internal sealed class AppDbContext : DbContext logger.LogInformation("{Name}[{Id}] created", nameof(AppDbContext), ContextId); } - /// - /// 设置 - /// public DbSet Settings { get; set; } = default!; - /// - /// 用户 - /// public DbSet Users { get; set; } = default!; - /// - /// 成就 - /// public DbSet Achievements { get; set; } = default!; - /// - /// 成就存档 - /// public DbSet AchievementArchives { get; set; } = default!; - /// - /// 卡池数据 - /// public DbSet GachaItems { get; set; } = default!; - /// - /// 卡池存档 - /// public DbSet GachaArchives { get; set; } = default!; - /// - /// 角色信息 - /// public DbSet AvatarInfos { get; set; } = default!; - /// - /// 游戏内账号 - /// public DbSet GameAccounts { get; set; } = default!; - /// - /// 实时便笺 - /// public DbSet DailyNotes { get; set; } = default!; - /// - /// 对象缓存 - /// public DbSet ObjectCache { get; set; } = default!; - /// - /// 培养计划 - /// public DbSet CultivateProjects { get; set; } = default!; - /// - /// 培养入口点 - /// public DbSet CultivateEntries { get; set; } = default!; - /// - /// 培养消耗物品 - /// + public DbSet LevelInformations { get; set; } = default!; + public DbSet CultivateItems { get; set; } = default!; - /// - /// 背包内物品 - /// public DbSet InventoryItems { get; set; } = default!; - /// - /// 背包内武器 - /// public DbSet InventoryWeapons { get; set; } = default!; - /// - /// 背包内圣遗物 - /// public DbSet InventoryReliquaries { get; set; } = default!; - /// - /// 深渊记录 - /// public DbSet SpiralAbysses { get; set; } = default!; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs index cba52ed6..77e4516d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs @@ -161,4 +161,22 @@ internal sealed partial class CultivationDbService : ICultivationDbService return appDbContext.CultivateProjects.AsNoTracking().ToObservableCollection(); } } + + public async ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.LevelInformations.ExecuteDeleteWhereAsync(l => l.EntryId == entryId).ConfigureAwait(false); + } + } + + public async ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.LevelInformations.AddAndSaveAsync(levelInformation).ConfigureAwait(false); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationMetadataContext.cs new file mode 100644 index 00000000..35996d60 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationMetadataContext.cs @@ -0,0 +1,18 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Metadata.Item; +using Snap.Hutao.Model.Primitive; + +namespace Snap.Hutao.Service.Cultivation; + +internal sealed class CultivationMetadataContext : ICultivationMetadataContext +{ + public List Materials { get; set; } = default!; + + public Dictionary IdMaterialMap { get; set; } = default!; + + public Dictionary IdAvatarMap { get; set; } = default!; + + public Dictionary IdWeaponMap { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 67b1ecb8..5f429153 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -7,8 +7,8 @@ using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Model.Metadata.Item; -using Snap.Hutao.Model.Primitive; using Snap.Hutao.Service.Inventroy; +using Snap.Hutao.Service.Metadata.ContextAbstraction; using Snap.Hutao.ViewModel.Cultivation; using System.Collections.ObjectModel; @@ -29,7 +29,7 @@ internal sealed partial class CultivationService : ICultivationService private readonly ITaskContext taskContext; /// - public List GetInventoryItemViews(CultivateProject cultivateProject, List metadata, ICommand saveCommand) + public List GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand) { using (IServiceScope scope = serviceProvider.CreateScope()) { @@ -39,7 +39,7 @@ internal sealed partial class CultivationService : ICultivationService List entities = cultivationDbService.GetInventoryItemListByProjectId(projectId); List results = []; - foreach (Material meta in metadata.Where(m => m.IsInventoryItem()).OrderBy(m => m.Id.Value)) + foreach (Material meta in context.EnumerateInventroyMaterial()) { InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.From(projectId, meta.Id); results.Add(new(entity, meta, saveCommand)); @@ -50,11 +50,7 @@ internal sealed partial class CultivationService : ICultivationService } /// - public async ValueTask> GetCultivateEntriesAsync( - CultivateProject cultivateProject, - List materials, - Dictionary idAvatarMap, - Dictionary idWeaponMap) + public async ValueTask> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context) { await taskContext.SwitchToBackgroundAsync(); List entries = await cultivationDbService @@ -67,13 +63,13 @@ internal sealed partial class CultivationService : ICultivationService List entryItems = []; foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false)) { - entryItems.Add(new(item, materials.Single(m => m.Id == item.ItemId))); + entryItems.Add(new(item, context.GetMaterial(item.ItemId))); } Item itemBase = entry.Type switch { - CultivateType.AvatarAndSkill => idAvatarMap[entry.Id].ToItem(), - CultivateType.Weapon => idWeaponMap[entry.Id].ToItem(), + CultivateType.AvatarAndSkill => context.GetAvatar(entry.Id).ToItem(), + CultivateType.Weapon => context.GetWeapon(entry.Id).ToItem(), // TODO: support furniture calc _ => default!, @@ -89,9 +85,7 @@ internal sealed partial class CultivationService : ICultivationService /// public async ValueTask> GetStatisticsCultivateItemCollectionAsync( - CultivateProject cultivateProject, - List materials, - CancellationToken token) + CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token) { await taskContext.SwitchToBackgroundAsync(); List resultItems = []; @@ -115,7 +109,7 @@ internal sealed partial class CultivationService : ICultivationService } else { - resultItems.Add(new(materials.Single(m => m.Id == item.ItemId), item)); + resultItems.Add(new(context.GetMaterial(item.ItemId), item)); } } } @@ -158,7 +152,7 @@ internal sealed partial class CultivationService : ICultivationService } /// - public async ValueTask SaveConsumptionAsync(CultivateType type, uint itemId, List items) + public async ValueTask SaveConsumptionAsync(CultivateType type, uint itemId, List items, LevelInformation levelInformation) { if (items.Count == 0) { @@ -188,8 +182,12 @@ internal sealed partial class CultivationService : ICultivationService } Guid entryId = entry.InnerId; - await cultivationDbService.RemoveCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false); + await cultivationDbService.RemoveLevelInformationByEntryIdAsync(entryId).ConfigureAwait(false); + CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, levelInformation); + await cultivationDbService.AddLevelInformationAsync(entryLevelInformation).ConfigureAwait(false); + + await cultivationDbService.RemoveCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false); IEnumerable toAdd = items.Select(item => CultivateItem.From(entryId, item)); await cultivationDbService.AddCultivateItemRangeAsync(toAdd).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs index ffb2ed1f..5c1fca8c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs @@ -35,4 +35,6 @@ internal interface ICultivationDbService void UpdateCultivateItem(CultivateItem item); ValueTask UpdateCultivateItemAsync(CultivateItem item); + ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId); + ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationMetadataContext.cs new file mode 100644 index 00000000..43c05dfb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationMetadataContext.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Metadata.ContextAbstraction; + +namespace Snap.Hutao.Service.Cultivation; + +internal interface ICultivationMetadataContext : IMetadataContext, + IMetadataListMaterialSource, + IMetadataDictionaryIdMaterialSource, + IMetadataDictionaryIdAvatarSource, + IMetadataDictionaryIdWeaponSource +{ +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs index 2b690bb0..2beb88ab 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs @@ -3,8 +3,6 @@ using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Primitive; -using Snap.Hutao.Model.Metadata.Item; -using Snap.Hutao.Model.Primitive; using Snap.Hutao.ViewModel.Cultivation; using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; using System.Collections.ObjectModel; @@ -27,38 +25,12 @@ internal interface ICultivationService /// ObservableCollection ProjectCollection { get; } - /// - /// 获取绑定用的养成列表 - /// - /// 养成计划 - /// 材料 - /// Id 角色映射 - /// Id 武器映射 - /// 绑定用的养成列表 - ValueTask> GetCultivateEntriesAsync( - CultivateProject cultivateProject, - List materials, - Dictionary idAvatarMap, - Dictionary idWeaponMap); + ValueTask> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context); - /// - /// 获取物品列表 - /// - /// 养成计划 - /// 元数据 - /// 保存命令 - /// 物品列表 - List GetInventoryItemViews(CultivateProject cultivateProject, List metadata, ICommand saveCommand); + List GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand); - /// - /// 异步获取统计物品列表 - /// - /// 养成计划 - /// 材料 - /// 取消令牌 - /// 统计物品列表 ValueTask> GetStatisticsCultivateItemCollectionAsync( - CultivateProject cultivateProject, List materials, CancellationToken token); + CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token); /// /// 删除养成清单 @@ -74,14 +46,7 @@ internal interface ICultivationService /// 任务 ValueTask RemoveProjectAsync(CultivateProject project); - /// - /// 异步保存养成物品 - /// - /// 类型 - /// 主Id - /// 待存物品 - /// 是否保存成功 - ValueTask SaveConsumptionAsync(CultivateType type, uint itemId, List items); + ValueTask SaveConsumptionAsync(CultivateType type, uint itemId, List items, LevelInformation levelInformation); /// /// 保存养成物品状态 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/LevelInformation.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/LevelInformation.cs new file mode 100644 index 00000000..13a5f527 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/LevelInformation.cs @@ -0,0 +1,59 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Abstraction; +using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; + +namespace Snap.Hutao.Service.Cultivation; + +internal sealed class LevelInformation : IMappingFrom +{ + public uint AvatarLevelFrom { get; private set; } + + public uint AvatarLevelTo { get; private set; } + + public uint SkillALevelFrom { get; private set; } + + public uint SkillALevelTo { get; private set; } + + public uint SkillELevelFrom { get; private set; } + + public uint SkillELevelTo { get; private set; } + + public uint SkillQLevelFrom { get; private set; } + + public uint SkillQLevelTo { get; private set; } + + public uint WeaponLevelFrom { get; private set; } + + public uint WeaponLevelTo { get; private set; } + + public static LevelInformation From(AvatarPromotionDelta delta) + { + LevelInformation levelInformation = new(); + + if (delta.AvatarId != 0U) + { + levelInformation.AvatarLevelFrom = delta.AvatarLevelCurrent; + levelInformation.AvatarLevelTo = delta.AvatarLevelTarget; + } + + if (delta.SkillList is [PromotionDelta skillA, PromotionDelta skillE, PromotionDelta skillQ, ..]) + { + levelInformation.SkillALevelFrom = skillA.LevelCurrent; + levelInformation.SkillALevelTo = skillA.LevelTarget; + levelInformation.SkillELevelFrom = skillE.LevelCurrent; + levelInformation.SkillELevelTo = skillE.LevelTarget; + levelInformation.SkillQLevelFrom = skillQ.LevelCurrent; + levelInformation.SkillQLevelTo = skillQ.LevelTarget; + } + + if (delta.Weapon is { } weapon) + { + levelInformation.WeaponLevelFrom = weapon.LevelCurrent; + levelInformation.WeaponLevelTo = weapon.LevelTarget; + } + + return levelInformation; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataContext.cs new file mode 100644 index 00000000..ca35233e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataContext.cs @@ -0,0 +1,6 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Metadata.ContextAbstraction; + +internal interface IMetadataContext; \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdAvatarSource.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdAvatarSource.cs new file mode 100644 index 00000000..d8cb7428 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdAvatarSource.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Primitive; + +namespace Snap.Hutao.Service.Metadata.ContextAbstraction; + +internal interface IMetadataDictionaryIdAvatarSource +{ + public Dictionary IdAvatarMap { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdMaterialSource.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdMaterialSource.cs new file mode 100644 index 00000000..dff1b88e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdMaterialSource.cs @@ -0,0 +1,12 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Metadata.Item; +using Snap.Hutao.Model.Primitive; + +namespace Snap.Hutao.Service.Metadata.ContextAbstraction; + +internal interface IMetadataDictionaryIdMaterialSource +{ + public Dictionary IdMaterialMap { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdWeaponSource.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdWeaponSource.cs new file mode 100644 index 00000000..d664ecd5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdWeaponSource.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Primitive; + +namespace Snap.Hutao.Service.Metadata.ContextAbstraction; + +internal interface IMetadataDictionaryIdWeaponSource +{ + public Dictionary IdWeaponMap { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataListMaterialSource.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataListMaterialSource.cs new file mode 100644 index 00000000..7a54f01d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataListMaterialSource.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Metadata.Item; + +namespace Snap.Hutao.Service.Metadata.ContextAbstraction; + +internal interface IMetadataListMaterialSource +{ + public List Materials { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/MetadataServiceContextExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/MetadataServiceContextExtension.cs new file mode 100644 index 00000000..114b56f5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/MetadataServiceContextExtension.cs @@ -0,0 +1,68 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Item; +using Snap.Hutao.Model.Metadata.Weapon; +using Snap.Hutao.Model.Primitive; + +namespace Snap.Hutao.Service.Metadata.ContextAbstraction; + +internal static class MetadataServiceContextExtension +{ + public static async ValueTask GetContextAsync(this IMetadataService metadataService, CancellationToken token = default) + where TContext : IMetadataContext, new() + { + TContext context = new(); + + // List + { + if (context is IMetadataListMaterialSource listMaterialSource) + { + listMaterialSource.Materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false); + } + } + + // Dictionary + { + if (context is IMetadataDictionaryIdAvatarSource dictionaryAvatarSource) + { + dictionaryAvatarSource.IdAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false); + } + + if (context is IMetadataDictionaryIdMaterialSource dictionaryMaterialSource) + { + dictionaryMaterialSource.IdMaterialMap = await metadataService.GetIdToMaterialMapAsync(token).ConfigureAwait(false); + } + + if (context is IMetadataDictionaryIdWeaponSource dictionaryWeaponSource) + { + dictionaryWeaponSource.IdWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false); + } + } + + return context; + } + +#pragma warning disable SH002 + public static IEnumerable EnumerateInventroyMaterial(this IMetadataListMaterialSource context) + { + return context.Materials.Where(m => m.IsInventoryItem()).OrderBy(m => m.Id.Value); + } + + public static Avatar GetAvatar(this IMetadataDictionaryIdAvatarSource context, AvatarId id) + { + return context.IdAvatarMap[id]; + } + + public static Material GetMaterial(this IMetadataDictionaryIdMaterialSource context, MaterialId id) + { + return context.IdMaterialMap[id]; + } + + public static Weapon GetWeapon(this IMetadataDictionaryIdWeaponSource context, WeaponId id) + { + return context.IdWeaponMap[id]; + } +#pragma warning restore SH002 +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml.cs index eeeb6742..758ca5de 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Core.Setting; using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; namespace Snap.Hutao.View.Dialog; @@ -25,6 +26,30 @@ internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); - return new(result == ContentDialogResult.Primary, PromotionDelta); + if (result is not ContentDialogResult.Primary) + { + return new(false, default!); + } + + LocalSetting.Set(SettingKeys.CultivationAvatarLevelCurrent, PromotionDelta.AvatarLevelCurrent); + LocalSetting.Set(SettingKeys.CultivationAvatarLevelTarget, PromotionDelta.AvatarLevelTarget); + + if (PromotionDelta.SkillList is [PromotionDelta skillA, PromotionDelta skillE, PromotionDelta skillQ, ..]) + { + LocalSetting.Set(SettingKeys.CultivationAvatarSkillACurrent, skillA.LevelCurrent); + LocalSetting.Set(SettingKeys.CultivationAvatarSkillATarget, skillA.LevelTarget); + LocalSetting.Set(SettingKeys.CultivationAvatarSkillECurrent, skillE.LevelCurrent); + LocalSetting.Set(SettingKeys.CultivationAvatarSkillETarget, skillE.LevelTarget); + LocalSetting.Set(SettingKeys.CultivationAvatarSkillQCurrent, skillQ.LevelCurrent); + LocalSetting.Set(SettingKeys.CultivationAvatarSkillQTarget, skillQ.LevelTarget); + } + + if (PromotionDelta.Weapon is { } weapon) + { + LocalSetting.Set(SettingKeys.CultivationWeapon90LevelCurrent, weapon.LevelCurrent); + LocalSetting.Set(SettingKeys.CultivationWeapon90LevelTarget, weapon.LevelTarget); + } + + return new(true, PromotionDelta); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index cf249276..e68a7dbf 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -169,61 +169,61 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I if (userService.Current is null) { infoBarService.Warning(SH.MustSelectUserAndUid); + return; } - else + + if (avatar.Weapon is null) { - if (avatar.Weapon is null) - { - infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull); - return; - } + infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull); + return; + } - CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); - CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); - (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); + CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); + CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); + (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); - if (!isOk) - { - return; - } + if (!isOk) + { + return; + } - Response consumptionResponse = await calculatorClient - .ComputeAsync(userService.Current.Entity, delta) + Response consumptionResponse = await calculatorClient + .ComputeAsync(userService.Current.Entity, delta) + .ConfigureAwait(false); + + if (!consumptionResponse.IsOk()) + { + return; + } + + CalculatorConsumption consumption = consumptionResponse.Data; + LevelInformation levelInformation = LevelInformation.From(delta); + + List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); + bool avatarSaved = await cultivationService + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation) + .ConfigureAwait(false); + + try + { + // Take a hot path if avatar is not saved. + bool avatarAndWeaponSaved = avatarSaved && await cultivationService + .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation) .ConfigureAwait(false); - if (!consumptionResponse.IsOk()) + if (avatarAndWeaponSaved) { - return; + infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); } - - CalculatorConsumption consumption = consumptionResponse.Data; - - List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); - bool avatarSaved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) - .ConfigureAwait(false); - - try + else { - // take a hot path if avatar is not saved. - bool avatarAndWeaponSaved = avatarSaved && await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) - .ConfigureAwait(false); - - if (avatarAndWeaponSaved) - { - infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); - } - else - { - infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); - } - } - catch (Core.ExceptionService.UserdataCorruptedException ex) - { - infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); + infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); } } + catch (Core.ExceptionService.UserdataCorruptedException ex) + { + infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); + } } [Command("BatchCultivateCommand")] @@ -274,8 +274,11 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I continue; } + // TODO: handle this correctly. + // We should always create a new baseline each time. baseline.Weapon.Id = avatar.Weapon.Id; baseline.Weapon.LevelCurrent = Math.Min(avatar.Weapon.LevelNumber, baseline.Weapon.LevelTarget); + baseline.Weapon.LevelTarget = Math.Min(avatar.Weapon.MaxLevel, baseline.Weapon.LevelTarget); Response consumptionResponse = await calculatorClient .ComputeAsync(userService.Current.Entity, baseline) @@ -289,17 +292,18 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I else { CalculatorConsumption consumption = consumptionResponse.Data; + LevelInformation levelInformation = LevelInformation.From(baseline); List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); bool avatarSaved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation) .ConfigureAwait(false); try { // take a hot path if avatar is not saved. bool avatarAndWeaponSaved = avatarSaved && await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) + .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation) .ConfigureAwait(false); if (avatarAndWeaponSaved) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs index 19b51d44..fa7424cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs @@ -3,10 +3,9 @@ using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Entity; -using Snap.Hutao.Model.Metadata.Item; -using Snap.Hutao.Model.Primitive; using Snap.Hutao.Service.Cultivation; using Snap.Hutao.Service.Metadata; +using Snap.Hutao.Service.Metadata.ContextAbstraction; using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Notification; using Snap.Hutao.View.Dialog; @@ -38,14 +37,8 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel private ObservableCollection? cultivateEntries; private ObservableCollection? statisticsItems; - /// - /// 项目 - /// public ObservableCollection? Projects { get => projects; set => SetProperty(ref projects, value); } - /// - /// 当前选中的计划 - /// public CultivateProject? SelectedProject { get => selectedProject; set @@ -58,19 +51,10 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel } } - /// - /// 物品列表 - /// public List? InventoryItems { get => inventoryItems; set => SetProperty(ref inventoryItems, value); } - /// - /// 养成列表 - /// public ObservableCollection? CultivateEntries { get => cultivateEntries; set => SetProperty(ref cultivateEntries, value); } - /// - /// 统计列表 - /// public ObservableCollection? StatisticsItems { get => statisticsItems; set => SetProperty(ref statisticsItems, value); } protected override async ValueTask InitializeUIAsync() @@ -95,60 +79,61 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel CultivateProjectDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); (bool isOk, CultivateProject project) = await dialog.CreateProjectAsync().ConfigureAwait(false); - if (isOk) + if (!isOk) { - ProjectAddResult result = await cultivationService.TryAddProjectAsync(project).ConfigureAwait(false); + return; + } - switch (result) - { - case ProjectAddResult.Added: - infoBarService.Success(SH.ViewModelCultivationProjectAdded); - await taskContext.SwitchToMainThreadAsync(); - SelectedProject = project; - break; - case ProjectAddResult.InvalidName: - infoBarService.Information(SH.ViewModelCultivationProjectInvalidName); - break; - case ProjectAddResult.AlreadyExists: - infoBarService.Information(SH.ViewModelCultivationProjectAlreadyExists); - break; - default: - throw Must.NeverHappen(); - } + switch (await cultivationService.TryAddProjectAsync(project).ConfigureAwait(false)) + { + case ProjectAddResult.Added: + infoBarService.Success(SH.ViewModelCultivationProjectAdded); + await taskContext.SwitchToMainThreadAsync(); + SelectedProject = project; + break; + case ProjectAddResult.InvalidName: + infoBarService.Information(SH.ViewModelCultivationProjectInvalidName); + break; + case ProjectAddResult.AlreadyExists: + infoBarService.Information(SH.ViewModelCultivationProjectAlreadyExists); + break; + default: + throw Must.NeverHappen(); } } [Command("RemoveProjectCommand")] private async Task RemoveProjectAsync(CultivateProject? project) { - if (project is not null) + if (project is null) { - await cultivationService.RemoveProjectAsync(project).ConfigureAwait(false); - - await taskContext.SwitchToMainThreadAsync(); - ArgumentNullException.ThrowIfNull(Projects); - SelectedProject = Projects.FirstOrDefault(); + return; } + + await cultivationService.RemoveProjectAsync(project).ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + ArgumentNullException.ThrowIfNull(Projects); + SelectedProject = Projects.FirstOrDefault(); } private async ValueTask UpdateEntryCollectionAsync(CultivateProject? project) { - if (project is not null) + if (project is null) { - List materials = await metadataService.GetMaterialListAsync().ConfigureAwait(false); - Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); - Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false); - - ObservableCollection entries = await cultivationService - .GetCultivateEntriesAsync(project, materials, idAvatarMap, idWeaponMap) - .ConfigureAwait(false); - - await taskContext.SwitchToMainThreadAsync(); - CultivateEntries = entries; - InventoryItems = cultivationService.GetInventoryItemViews(project, materials, SaveInventoryItemCommand); - - await UpdateStatisticsItemsAsync().ConfigureAwait(false); + return; } + + CultivationMetadataContext context = await metadataService.GetContextAsync().ConfigureAwait(false); + + ObservableCollection entries = await cultivationService + .GetCultivateEntriesAsync(project, context) + .ConfigureAwait(false); + + await taskContext.SwitchToMainThreadAsync(); + CultivateEntries = entries; + InventoryItems = cultivationService.GetInventoryItemViews(project, context, SaveInventoryItemCommand); + + await UpdateStatisticsItemsAsync().ConfigureAwait(false); } [Command("RemoveEntryCommand")] @@ -194,8 +179,8 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel ObservableCollection statistics; try { - List materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false); - statistics = await cultivationService.GetStatisticsCultivateItemCollectionAsync(SelectedProject, materials, token).ConfigureAwait(false); + CultivationMetadataContext context = await metadataService.GetContextAsync().ConfigureAwait(false); + statistics = await cultivationService.GetStatisticsCultivateItemCollectionAsync(SelectedProject, context, token).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs index ea004849..359cb342 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs @@ -30,7 +30,7 @@ internal sealed class StatisticsCultivateItem /// /// 对应背包物品的个数 /// - public int Count { get; set; } + public uint Count { get; set; } /// /// 对应背包物品的个数 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs index e894f15b..ddd8383f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs @@ -16,10 +16,10 @@ namespace Snap.Hutao.ViewModel; [Injection(InjectAs.Scoped)] internal sealed partial class TestViewModel : Abstraction.ViewModel { - private readonly MainWindow mainWindow; + private readonly HutaoAsAServiceClient homaAsAServiceClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; - private readonly HutaoAsAServiceClient homaAsAServiceClient; + private readonly MainWindow mainWindow; private UploadAnnouncement announcement = new(); @@ -39,15 +39,10 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel set => LocalSetting.Set(SettingKeys.OverrideElevationRequirement, value); } - protected override ValueTask InitializeUIAsync() - { - return ValueTask.FromResult(true); - } - [Command("ResetGuideStateCommand")] private static void ResetGuideState() { - LocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, (uint)GuideState.Language); + UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language); } [Command("ExceptionCommand")] @@ -59,11 +54,12 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel [Command("ResetMainWindowSizeCommand")] private void ResetMainWindowSize() { - mainWindow.AppWindow.Resize(new(1280, 720)); + double scale = mainWindow.WindowOptions.GetRasterizationScale(); + mainWindow.AppWindow.Resize(new Windows.Graphics.SizeInt32(1280, 720).Scale(scale)); } [Command("UploadAnnouncementCommand")] - private async void UploadAnnouncementAsync() + private async Task UploadAnnouncementAsync() { Web.Response.Response response = await homaAsAServiceClient.UploadAnnouncementAsync(Announcement).ConfigureAwait(false); if (response.IsOk()) @@ -75,7 +71,7 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel } [Command("CompensationGachaLogServiceTimeCommand")] - private async void CompensationGachaLogServiceTimeAsync() + private async Task CompensationGachaLogServiceTimeAsync() { Web.Response.Response response = await homaAsAServiceClient.GachaLogCompensationAsync(15).ConfigureAwait(false); if (response.IsOk()) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs index 68d0116d..e0b4bad6 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs @@ -135,8 +135,6 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel return; } - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); CalculableOptions options = new(avatar.ToCalculable(), null); CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); (bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); @@ -156,11 +154,12 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel } CalculateConsumption consumption = consumptionResponse.Data; + LevelInformation levelInformation = LevelInformation.From(delta); List items = CalculateItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); try { bool saved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation) .ConfigureAwait(false); if (saved) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs index 17544deb..d64196c8 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs @@ -148,10 +148,11 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel } CalculateConsumption consumption = consumptionResponse.Data; + LevelInformation levelInformation = LevelInformation.From(delta); try { bool saved = await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, weapon.Id, consumption.WeaponConsume.EmptyIfNull()) + .SaveConsumptionAsync(CultivateType.Weapon, weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation) .ConfigureAwait(false); if (saved) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs index 39578208..fd886db4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs @@ -3,6 +3,7 @@ using Snap.Hutao.Core.Setting; using Snap.Hutao.Model.Primitive; +using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; @@ -62,4 +63,41 @@ internal sealed class AvatarPromotionDelta Weapon = new() { LevelTarget = LocalSetting.Get(SettingKeys.CultivationWeapon90LevelTarget, 90U), }, }; } + + public static bool TryGetNonErrorCopy(AvatarPromotionDelta source, AvatarView avatar, out AvatarPromotionDelta? copy) + { + copy = new() + { + AvatarId = avatar.Id, + AvatarLevelCurrent = Math.Min(avatar.LevelNumber, source.AvatarLevelTarget), + }; + + if (avatar.Skills is [SkillView skillA, SkillView skillE, SkillView skillQ, ..]) + { + copy.SkillList = new(3); + copy.SkillList.Add(new() + { + Id = skillA.GroupId, + LevelCurrent = Math.Min(skillA.LevelNumber, skillA.LevelTarget), + }); + copy.SkillList[0].Id = skillA.GroupId; + copy.SkillList[0].LevelCurrent = Math.Min(skillA.LevelNumber, copy.SkillList[0].LevelTarget); + } + else + { + return false; + } + + target = new() + { + AvatarId = source.AvatarId, + AvatarLevelCurrent = source.AvatarLevelCurrent, + AvatarLevelTarget = source.AvatarLevelTarget, + ElementAttrId = source.ElementAttrId, + SkillList = source.SkillList?.Select(i => i.Clone()).ToList(), + Weapon = source.Weapon?.Clone(), + }; + + return true; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs index da9af651..8d08d3be 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs @@ -187,9 +187,9 @@ internal sealed partial class CalculateClient private class IdCount { [JsonPropertyName("cnt")] - public int Count { get; set; } + public uint Count { get; set; } [JsonPropertyName("id")] - public int Id { get; set; } + public uint Id { get; set; } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/Item.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/Item.cs index 2ef8573d..ba897985 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/Item.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/Item.cs @@ -15,7 +15,7 @@ internal sealed class Item /// Id /// [JsonPropertyName("id")] - public int Id { get; set; } + public uint Id { get; set; } /// /// 名称 @@ -33,7 +33,7 @@ internal sealed class Item /// 数量 /// [JsonPropertyName("num")] - public int Num { get; set; } + public uint Num { get; set; } /// /// 物品星级 仅有家具为有效值 diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/ItemHelper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/ItemHelper.cs index cb79da8c..a27f98b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/ItemHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/ItemHelper.cs @@ -1,25 +1,18 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using System.Runtime.InteropServices; + namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; -/// -/// 物品帮助类 -/// [HighQuality] internal static class ItemHelper { - /// - /// 合并两个物品列表 - /// - /// 左列表 - /// 右列表 - /// 合并且排序好的列表 public static List Merge(List? left, List? right) { if (left.IsNullOrEmpty() && right.IsNullOrEmpty()) { - return new(0); + return []; } if (left.IsNullOrEmpty() && !right.IsNullOrEmpty()) @@ -38,9 +31,10 @@ internal static class ItemHelper List result = new(left.Count + right.Count); result.AddRange(left); - foreach (Item item in right) + foreach (ref readonly Item item in CollectionsMarshal.AsSpan(right)) { - if (result.SingleOrDefault(i => i.Id == item.Id) is { } existed) + uint id = item.Id; + if (result.SingleOrDefault(i => i.Id == id) is { } existed) { existed.Num += item.Num; } From a5bfdbaa4b56cbca0fc5802ea4f8ae4e89d072eb Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Thu, 7 Dec 2023 22:38:21 +0800 Subject: [PATCH 12/47] impl #1016 --- .../Extension/StringBuilderExtension.cs | 16 +- .../Migrations/AppDbContextModelSnapshot.cs | 36 +-- .../Snap.Hutao/Model/Entity/CultivateEntry.cs | 8 + .../Entity/CultivateEntryLevelInformation.cs | 37 ++- .../Snap.Hutao/Resource/Localization/SH.resx | 3 + .../Cultivation/CultivationDbService.cs | 15 ++ .../Service/Cultivation/CultivationService.cs | 12 +- .../Cultivation/ICultivationDbService.cs | 4 + .../Dialog/CultivatePromotionDeltaDialog.xaml | 8 +- .../Snap.Hutao/View/Page/CultivationPage.xaml | 12 +- .../AvatarProperty/AvatarPropertyViewModel.cs | 244 ++++++++---------- .../Cultivation/CultivateEntryView.cs | 92 +++++-- .../Event/Calculate/AvatarPromotionDelta.cs | 38 --- .../AvatarPromotionDeltaExtension.cs | 61 +++++ 14 files changed, 343 insertions(+), 243 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDeltaExtension.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs index 515666c7..f71001ec 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs @@ -38,9 +38,23 @@ internal static class StringBuilderExtension return condition ? sb.Append(value) : sb; } + public static string ToStringTrimEnd(this StringBuilder builder) + { + if (builder.Length > 1 && char.IsWhiteSpace(builder[^1])) + { + return builder.ToString(0, builder.Length - 1); + } + + return builder.ToString(); + } + public static string ToStringTrimEndReturn(this StringBuilder builder) { - Must.Argument(builder.Length >= 1, "StringBuilder 的长度必须大于 0"); + if (builder.Length < 1) + { + return string.Empty; + } + int remove = 0; if (builder[^1] is '\n') { diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index 9129b6e7..81c6a057 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs @@ -42,7 +42,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("ArchiveId"); - b.ToTable("achievements"); + b.ToTable("achievements", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => @@ -60,7 +60,7 @@ namespace Snap.Hutao.Migrations b.HasKey("InnerId"); - b.ToTable("achievement_archives"); + b.ToTable("achievement_archives", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b => @@ -88,7 +88,7 @@ namespace Snap.Hutao.Migrations b.HasKey("InnerId"); - b.ToTable("avatar_infos"); + b.ToTable("avatar_infos", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => @@ -110,7 +110,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("ProjectId"); - b.ToTable("cultivate_entries"); + b.ToTable("cultivate_entries", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => @@ -156,7 +156,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("EntryId"); - b.ToTable("cultivate_entry_level_informations"); + b.ToTable("cultivate_entry_level_informations", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => @@ -181,7 +181,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("EntryId"); - b.ToTable("cultivate_items"); + b.ToTable("cultivate_items", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b => @@ -202,7 +202,7 @@ namespace Snap.Hutao.Migrations b.HasKey("InnerId"); - b.ToTable("cultivate_projects"); + b.ToTable("cultivate_projects", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => @@ -258,7 +258,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("UserId"); - b.ToTable("daily_notes"); + b.ToTable("daily_notes", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => @@ -276,7 +276,7 @@ namespace Snap.Hutao.Migrations b.HasKey("InnerId"); - b.ToTable("gacha_archives"); + b.ToTable("gacha_archives", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => @@ -307,7 +307,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("ArchiveId"); - b.ToTable("gacha_items"); + b.ToTable("gacha_items", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b => @@ -332,7 +332,7 @@ namespace Snap.Hutao.Migrations b.HasKey("InnerId"); - b.ToTable("game_accounts"); + b.ToTable("game_accounts", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => @@ -354,7 +354,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("ProjectId"); - b.ToTable("inventory_items"); + b.ToTable("inventory_items", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b => @@ -383,7 +383,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("ProjectId"); - b.ToTable("inventory_reliquaries"); + b.ToTable("inventory_reliquaries", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b => @@ -408,7 +408,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("ProjectId"); - b.ToTable("inventory_weapons"); + b.ToTable("inventory_weapons", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b => @@ -424,7 +424,7 @@ namespace Snap.Hutao.Migrations b.HasKey("Key"); - b.ToTable("object_cache"); + b.ToTable("object_cache", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => @@ -437,7 +437,7 @@ namespace Snap.Hutao.Migrations b.HasKey("Key"); - b.ToTable("settings"); + b.ToTable("settings", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b => @@ -459,7 +459,7 @@ namespace Snap.Hutao.Migrations b.HasKey("InnerId"); - b.ToTable("spiral_abysses"); + b.ToTable("spiral_abysses", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => @@ -502,7 +502,7 @@ namespace Snap.Hutao.Migrations b.HasKey("InnerId"); - b.ToTable("users"); + b.ToTable("users", (string)null); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs index cd892254..348ffdd4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs @@ -33,6 +33,8 @@ internal sealed class CultivateEntry : IDbMappingForeignKeyFrom /// 养成类型 /// @@ -59,4 +61,10 @@ internal sealed class CultivateEntry : IDbMappingForeignKeyFrom +internal sealed class CultivateEntryLevelInformation : IMappingFrom { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -40,21 +41,29 @@ internal sealed class CultivateEntryLevelInformation : IMappingFrom new() + { + EntryId = entryId, + AvatarLevelFrom = source.AvatarLevelFrom, + AvatarLevelTo = source.AvatarLevelTo, + SkillALevelFrom = source.SkillALevelFrom, + SkillALevelTo = source.SkillALevelTo, + SkillELevelFrom = source.SkillELevelFrom, + SkillELevelTo = source.SkillELevelTo, + SkillQLevelFrom = source.SkillQLevelFrom, + SkillQLevelTo = source.SkillQLevelTo, + }, + CultivateType.Weapon => new() + { + EntryId = entryId, + WeaponLevelFrom = source.WeaponLevelFrom, + WeaponLevelTo = source.WeaponLevelTo, + }, + _ => throw Must.NeverHappen($"不支持的养成类型{type}"), }; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 8977675b..d97bc0c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1406,6 +1406,9 @@ 请先前往养成计划页面创建计划并选中 + + 重新添加物品以查看养成描述 + 添加成功 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs index 77e4516d..8e9f18a5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Snap.Hutao.Core.Database; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; @@ -51,6 +52,20 @@ internal sealed partial class CultivationDbService : ICultivationDbService } } + public async ValueTask> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + return await appDbContext.CultivateEntries + .AsNoTracking() + .Where(e => e.ProjectId == projectId) + .Include(e => e.LevelInformation) + .ToListAsync() + .ConfigureAwait(false); + } + } + public async ValueTask> GetCultivateItemListByEntryIdAsync(Guid entryId) { using (IServiceScope scope = serviceProvider.CreateScope()) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 5f429153..7491b43d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -54,19 +54,19 @@ internal sealed partial class CultivationService : ICultivationService { await taskContext.SwitchToBackgroundAsync(); List entries = await cultivationDbService - .GetCultivateEntryListByProjectIdAsync(cultivateProject.InnerId) + .GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(cultivateProject.InnerId) .ConfigureAwait(false); List resultEntries = new(entries.Count); foreach (CultivateEntry entry in entries) { List entryItems = []; - foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false)) + foreach (CultivateItem cultivateItem in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false)) { - entryItems.Add(new(item, context.GetMaterial(item.ItemId))); + entryItems.Add(new(cultivateItem, context.GetMaterial(cultivateItem.ItemId))); } - Item itemBase = entry.Type switch + Item item = entry.Type switch { CultivateType.AvatarAndSkill => context.GetAvatar(entry.Id).ToItem(), CultivateType.Weapon => context.GetWeapon(entry.Id).ToItem(), @@ -75,7 +75,7 @@ internal sealed partial class CultivationService : ICultivationService _ => default!, }; - resultEntries.Add(new(entry, itemBase, entryItems)); + resultEntries.Add(new(entry, item, entryItems)); } return resultEntries @@ -184,7 +184,7 @@ internal sealed partial class CultivationService : ICultivationService Guid entryId = entry.InnerId; await cultivationDbService.RemoveLevelInformationByEntryIdAsync(entryId).ConfigureAwait(false); - CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, levelInformation); + CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, type, levelInformation); await cultivationDbService.AddLevelInformationAsync(entryLevelInformation).ConfigureAwait(false); await cultivationDbService.RemoveCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs index 5c1fca8c..45c90413 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs @@ -35,6 +35,10 @@ internal interface ICultivationDbService void UpdateCultivateItem(CultivateItem item); ValueTask UpdateCultivateItemAsync(CultivateItem item); + ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId); + ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation); + + ValueTask> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml index 71055f38..09808809 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml @@ -74,7 +74,7 @@ Margin="0,8,0,0" Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}"> @@ -124,10 +124,10 @@ @@ -172,6 +172,6 @@ Value="{Binding LevelTarget, Mode=TwoWay}"/> - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml index e275304f..2b0c2c38 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml @@ -130,11 +130,17 @@ Height="48" Icon="{Binding Icon}" Quality="{Binding Quality}"/> - + VerticalAlignment="Center"> + + + + @@ -187,153 +195,86 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I return; } - Response consumptionResponse = await calculatorClient - .ComputeAsync(userService.Current.Entity, delta) - .ConfigureAwait(false); + CultivateCoreResult result = await CultivateCoreAsync(userService.Current.Entity, delta, avatar).ConfigureAwait(false); - if (!consumptionResponse.IsOk()) + switch (result) { - return; - } - - CalculatorConsumption consumption = consumptionResponse.Data; - LevelInformation levelInformation = LevelInformation.From(delta); - - List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); - bool avatarSaved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation) - .ConfigureAwait(false); - - try - { - // Take a hot path if avatar is not saved. - bool avatarAndWeaponSaved = avatarSaved && await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation) - .ConfigureAwait(false); - - if (avatarAndWeaponSaved) - { + case CultivateCoreResult.Ok: infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); - } - else - { + break; + case CultivateCoreResult.SaveConsumptionFailed: infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); - } - } - catch (Core.ExceptionService.UserdataCorruptedException ex) - { - infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); + break; } } [Command("BatchCultivateCommand")] private async Task BatchCultivateAsync() { - if (summary is { Avatars: { } avatars }) + if (summary is not { Avatars: { } avatars }) { - if (userService.Current is null) + return; + } + + if (userService.Current is null) + { + infoBarService.Warning(SH.MustSelectUserAndUid); + return; + } + + CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + (bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false); + + if (!isOk) + { + return; + } + + ArgumentNullException.ThrowIfNull(baseline.SkillList); + ArgumentNullException.ThrowIfNull(baseline.Weapon); + + ContentDialog progressDialog = await contentDialogFactory + .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle) + .ConfigureAwait(false); + using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false)) + { + BatchCultivateResult result = default; + foreach (AvatarView avatar in avatars) { - infoBarService.Warning(SH.MustSelectUserAndUid); + if (!baseline.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy)) + { + ++result.SkippedCount; + continue; + } + + CultivateCoreResult coreResult = await CultivateCoreAsync(userService.Current.Entity, copy, avatar).ConfigureAwait(false); + + switch (coreResult) + { + case CultivateCoreResult.Ok: + ++result.SucceedCount; + break; + case CultivateCoreResult.ComputeConsumptionFailed: + result.Interrupted = true; + break; + case CultivateCoreResult.SaveConsumptionFailed: + result.Interrupted = true; + break; + } + + if (result.Interrupted) + { + break; + } + } + + if (result.Interrupted) + { + infoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount)); } else { - CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - (bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false); - - if (isOk) - { - ArgumentNullException.ThrowIfNull(baseline.SkillList); - ArgumentNullException.ThrowIfNull(baseline.Weapon); - - ContentDialog progressDialog = await contentDialogFactory - .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle) - .ConfigureAwait(false); - using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false)) - { - BatchCultivateResult result = default; - foreach (AvatarView avatar in avatars) - { - baseline.AvatarId = avatar.Id; - baseline.AvatarLevelCurrent = Math.Min(avatar.LevelNumber, baseline.AvatarLevelTarget); - - if (avatar.Skills.Count < 3) - { - continue; - } - - baseline.SkillList[0].Id = avatar.Skills[0].GroupId; - baseline.SkillList[0].LevelCurrent = Math.Min(avatar.Skills[0].LevelNumber, baseline.SkillList[0].LevelTarget); - baseline.SkillList[1].Id = avatar.Skills[1].GroupId; - baseline.SkillList[1].LevelCurrent = Math.Min(avatar.Skills[1].LevelNumber, baseline.SkillList[1].LevelTarget); - baseline.SkillList[2].Id = avatar.Skills[2].GroupId; - baseline.SkillList[2].LevelCurrent = Math.Min(avatar.Skills[2].LevelNumber, baseline.SkillList[2].LevelTarget); - - if (avatar.Weapon is null) - { - result.SkippedCount++; - continue; - } - - // TODO: handle this correctly. - // We should always create a new baseline each time. - baseline.Weapon.Id = avatar.Weapon.Id; - baseline.Weapon.LevelCurrent = Math.Min(avatar.Weapon.LevelNumber, baseline.Weapon.LevelTarget); - baseline.Weapon.LevelTarget = Math.Min(avatar.Weapon.MaxLevel, baseline.Weapon.LevelTarget); - - Response consumptionResponse = await calculatorClient - .ComputeAsync(userService.Current.Entity, baseline) - .ConfigureAwait(false); - - if (!consumptionResponse.IsOk()) - { - result.Interrupted = true; - break; - } - else - { - CalculatorConsumption consumption = consumptionResponse.Data; - LevelInformation levelInformation = LevelInformation.From(baseline); - - List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); - bool avatarSaved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation) - .ConfigureAwait(false); - - try - { - // take a hot path if avatar is not saved. - bool avatarAndWeaponSaved = avatarSaved && await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation) - .ConfigureAwait(false); - - if (avatarAndWeaponSaved) - { - result.SucceedCount++; - } - else - { - result.Interrupted = true; - break; - } - } - catch (Core.ExceptionService.UserdataCorruptedException ex) - { - infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); - } - } - } - - if (result.Interrupted) - { - infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); - infoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount)); - } - else - { - infoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount)); - } - } - } + infoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount)); } } } @@ -372,4 +313,43 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I } } } + + private async ValueTask CultivateCoreAsync(Model.Entity.User user, CalculatorAvatarPromotionDelta delta, AvatarView avatar) + { + Response consumptionResponse = await calculatorClient.ComputeAsync(user, delta).ConfigureAwait(false); + + if (!consumptionResponse.IsOk()) + { + return CultivateCoreResult.ComputeConsumptionFailed; + } + + CalculatorConsumption consumption = consumptionResponse.Data; + LevelInformation levelInformation = LevelInformation.From(delta); + + List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); + bool avatarSaved = await cultivationService + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation) + .ConfigureAwait(false); + + try + { + ArgumentNullException.ThrowIfNull(avatar.Weapon); + + // Take a hot path if avatar is not saved. + bool avatarAndWeaponSaved = avatarSaved && await cultivationService + .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation) + .ConfigureAwait(false); + + if (!avatarAndWeaponSaved) + { + return CultivateCoreResult.SaveConsumptionFailed; + } + } + catch (Core.ExceptionService.UserdataCorruptedException ex) + { + infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); + } + + return CultivateCoreResult.Ok; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs index 071018e9..a8960008 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using Snap.Hutao.Model; +using Snap.Hutao.Model.Entity; +using System.Text; namespace Snap.Hutao.ViewModel.Cultivation; @@ -11,13 +13,7 @@ namespace Snap.Hutao.ViewModel.Cultivation; [HighQuality] internal sealed class CultivateEntryView : Item { - /// - /// 构造一个新的养成清单入口 - /// - /// 实体入口 - /// 对应物品 - /// 物品列表 - public CultivateEntryView(Model.Entity.CultivateEntry entry, Item item, List items) + public CultivateEntryView(CultivateEntry entry, Item item, List items) { Id = entry.Id; EntryId = entry.InnerId; @@ -26,33 +22,75 @@ internal sealed class CultivateEntryView : Item Badge = item.Badge; Quality = item.Quality; Items = items; + + Description = ParseDescription(entry); + IsToday = items.Any(i => i.IsToday); + DaysOfWeek = items.FirstOrDefault(i => i.DaysOfWeek != DaysOfWeek.Any)?.DaysOfWeek ?? DaysOfWeek.Any; + + static string ParseDescription(CultivateEntry entry) + { + if (entry.LevelInformation is null) + { + return SH.ViewModelCultivationEntryViewDescriptionDefault; + } + + CultivateEntryLevelInformation info = entry.LevelInformation; + + switch (entry.Type) + { + case Model.Entity.Primitive.CultivateType.AvatarAndSkill: + { + StringBuilder stringBuilder = new(); + + if (info.AvatarLevelFrom != info.AvatarLevelTo) + { + stringBuilder.Append("Lv.").Append(info.AvatarLevelFrom).Append(" → Lv.").Append(info.AvatarLevelTo).Append(' '); + } + + if (info.SkillALevelFrom != info.SkillALevelTo) + { + stringBuilder.Append("A: ").Append(info.SkillALevelFrom).Append(" → ").Append(info.SkillALevelTo).Append(' '); + } + + if (info.SkillELevelFrom != info.SkillELevelTo) + { + stringBuilder.Append("E: ").Append(info.SkillELevelFrom).Append(" → ").Append(info.SkillELevelTo).Append(' '); + } + + if (info.SkillQLevelFrom != info.SkillQLevelTo) + { + stringBuilder.Append("Q: ").Append(info.SkillQLevelFrom).Append(" → ").Append(info.SkillQLevelTo).Append(' '); + } + + return stringBuilder.ToStringTrimEnd(); + } + + case Model.Entity.Primitive.CultivateType.Weapon: + { + StringBuilder stringBuilder = new(); + + if (info.WeaponLevelFrom != info.WeaponLevelTo) + { + stringBuilder.Append("Lv.").Append(info.WeaponLevelFrom).Append(" → Lv.").Append(info.WeaponLevelTo); + } + + return stringBuilder.ToString(); + } + } + + return string.Empty; + } } - /// - /// 实体 - /// public List Items { get; set; } = default!; - /// - /// 是否为今日的材料 - /// - public bool IsToday { get => Items.Any(i => i.IsToday); } + public bool IsToday { get; } - /// - /// 星期中的日期 - /// - public DaysOfWeek DaysOfWeek - { - get => Items.FirstOrDefault(i => i.DaysOfWeek != DaysOfWeek.Any)?.DaysOfWeek ?? DaysOfWeek.Any; - } + public DaysOfWeek DaysOfWeek { get; } + + public string Description { get; } - /// - /// Id - /// internal uint Id { get; set; } - /// - /// 入口Id - /// internal Guid EntryId { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs index fd886db4..39578208 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs @@ -3,7 +3,6 @@ using Snap.Hutao.Core.Setting; using Snap.Hutao.Model.Primitive; -using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; @@ -63,41 +62,4 @@ internal sealed class AvatarPromotionDelta Weapon = new() { LevelTarget = LocalSetting.Get(SettingKeys.CultivationWeapon90LevelTarget, 90U), }, }; } - - public static bool TryGetNonErrorCopy(AvatarPromotionDelta source, AvatarView avatar, out AvatarPromotionDelta? copy) - { - copy = new() - { - AvatarId = avatar.Id, - AvatarLevelCurrent = Math.Min(avatar.LevelNumber, source.AvatarLevelTarget), - }; - - if (avatar.Skills is [SkillView skillA, SkillView skillE, SkillView skillQ, ..]) - { - copy.SkillList = new(3); - copy.SkillList.Add(new() - { - Id = skillA.GroupId, - LevelCurrent = Math.Min(skillA.LevelNumber, skillA.LevelTarget), - }); - copy.SkillList[0].Id = skillA.GroupId; - copy.SkillList[0].LevelCurrent = Math.Min(skillA.LevelNumber, copy.SkillList[0].LevelTarget); - } - else - { - return false; - } - - target = new() - { - AvatarId = source.AvatarId, - AvatarLevelCurrent = source.AvatarLevelCurrent, - AvatarLevelTarget = source.AvatarLevelTarget, - ElementAttrId = source.ElementAttrId, - SkillList = source.SkillList?.Select(i => i.Clone()).ToList(), - Weapon = source.Weapon?.Clone(), - }; - - return true; - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDeltaExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDeltaExtension.cs new file mode 100644 index 00000000..1c5452d2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDeltaExtension.cs @@ -0,0 +1,61 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.ViewModel.AvatarProperty; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; + +internal static class AvatarPromotionDeltaExtension +{ + public static bool TryGetNonErrorCopy(this AvatarPromotionDelta source, AvatarView avatar, [NotNullWhen(true)] out AvatarPromotionDelta? copy) + { + copy = new() + { + AvatarId = avatar.Id, + AvatarLevelCurrent = Math.Min(avatar.LevelNumber, source.AvatarLevelTarget), + AvatarLevelTarget = source.AvatarLevelTarget, + }; + + if (avatar.Skills is not ([SkillView skillViewA, SkillView skillViewE, SkillView skillViewQ, ..]) || + source.SkillList is not ([PromotionDelta deltaA, PromotionDelta deltaE, PromotionDelta deltaQ, ..])) + { + return false; + } + + copy.SkillList = new(3) + { + new() + { + Id = skillViewA.GroupId, + LevelCurrent = Math.Min(skillViewA.LevelNumber, deltaA.LevelTarget), + LevelTarget = deltaA.LevelTarget, + }, + new() + { + Id = skillViewE.GroupId, + LevelCurrent = Math.Min(skillViewE.LevelNumber, deltaE.LevelTarget), + LevelTarget = deltaE.LevelTarget, + }, + new() + { + Id = skillViewQ.GroupId, + LevelCurrent = Math.Min(skillViewQ.LevelNumber, deltaQ.LevelTarget), + LevelTarget = deltaQ.LevelTarget, + }, + }; + + if (avatar.Weapon is not WeaponView weaponView || source.Weapon is not { } deltaWeapon) + { + return false; + } + + copy.Weapon = new() + { + Id = weaponView.Id, + LevelCurrent = Math.Min(weaponView.LevelNumber, deltaWeapon.LevelTarget), + LevelTarget = Math.Min(weaponView.MaxLevel, deltaWeapon.LevelTarget), + }; + + return true; + } +} \ No newline at end of file From 24b66de082ab13d55e4b782d33c7974d15096866 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Thu, 7 Dec 2023 22:55:32 +0800 Subject: [PATCH 13/47] code style --- .../Sharding/HttpShardCopyWorkerOptions.cs | 2 +- .../Snap.Hutao/Extension/WinRTExtension.cs | 2 +- .../Service/Abstraction/DbStoreOptions.cs | 2 +- .../Cultivation/CultivationDbService.cs | 1 - .../Service/GachaLog/GachaLogDbService.cs | 2 +- .../AvatarProperty/AvatarPropertyViewModel.cs | 70 +++++++++---------- .../Request/NameValueCollectionExtension.cs | 22 +++--- 7 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorkerOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorkerOptions.cs index afa5ab83..21df6075 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorkerOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorkerOptions.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. using Microsoft.Win32.SafeHandles; +using Snap.Hutao.Web.Request.Builder; using System.IO; using System.Net.Http; -using Snap.Hutao.Web.Request.Builder; namespace Snap.Hutao.Core.IO.Http.Sharding; diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs index 608eb0a4..f07285eb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs @@ -19,7 +19,7 @@ internal static class WinRTExtension } // protected bool disposed; - [UnsafeAccessor(UnsafeAccessorKind.Field, Name ="disposed")] + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "disposed")] private static extern ref bool GetProtectedDisposed(IObjectReference objRef); // private object _disposedLock diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs index 1baf293c..56167c1b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs @@ -116,7 +116,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions反序列化器 /// 默认值 /// - [return:NotNull] + [return: NotNull] protected T GetOption(ref T? storage, string key, Func deserializer, [DisallowNull] T defaultValue) { if (storage is not null) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs index 8e9f18a5..164971a4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Internal; using Snap.Hutao.Core.Database; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs index cfa27516..4d6b8abb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs @@ -287,7 +287,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService Time = i.Time, Id = i.Id, }); - return [..result]; + return [.. result]; } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index 2e8b94c8..c758b59f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -279,41 +279,6 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I } } - [Command("ExportAsImageCommand")] - private async Task ExportAsImageAsync(FrameworkElement? element) - { - if (element is { IsLoaded: true }) - { - RenderTargetBitmap bitmap = new(); - await bitmap.RenderAsync(element); - - IBuffer buffer = await bitmap.GetPixelsAsync(); - bool clipboardOpened = false; - using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight)) - { - Bgra32 tint = appResourceProvider.GetResource("CompatBackgroundColor"); - softwareBitmap.NormalBlend(tint); - using (InMemoryRandomAccessStream memory = new()) - { - BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, memory); - encoder.SetSoftwareBitmap(softwareBitmap); - await encoder.FlushAsync(); - - clipboardOpened = clipboardInterop.SetBitmap(memory); - } - } - - if (clipboardOpened) - { - infoBarService.Success(SH.ViewModelAvatarPropertyExportImageSuccess); - } - else - { - infoBarService.Warning(SH.ViewModelAvatarPropertyOpenClipboardFail); - } - } - } - private async ValueTask CultivateCoreAsync(Model.Entity.User user, CalculatorAvatarPromotionDelta delta, AvatarView avatar) { Response consumptionResponse = await calculatorClient.ComputeAsync(user, delta).ConfigureAwait(false); @@ -352,4 +317,39 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I return CultivateCoreResult.Ok; } + + [Command("ExportAsImageCommand")] + private async Task ExportAsImageAsync(FrameworkElement? element) + { + if (element is { IsLoaded: true }) + { + RenderTargetBitmap bitmap = new(); + await bitmap.RenderAsync(element); + + IBuffer buffer = await bitmap.GetPixelsAsync(); + bool clipboardOpened = false; + using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight)) + { + Bgra32 tint = appResourceProvider.GetResource("CompatBackgroundColor"); + softwareBitmap.NormalBlend(tint); + using (InMemoryRandomAccessStream memory = new()) + { + BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, memory); + encoder.SetSoftwareBitmap(softwareBitmap); + await encoder.FlushAsync(); + + clipboardOpened = clipboardInterop.SetBitmap(memory); + } + } + + if (clipboardOpened) + { + infoBarService.Success(SH.ViewModelAvatarPropertyExportImageSuccess); + } + else + { + infoBarService.Warning(SH.ViewModelAvatarPropertyOpenClipboardFail); + } + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/NameValueCollectionExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/NameValueCollectionExtension.cs index 8375b0ef..1ac71767 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/NameValueCollectionExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/NameValueCollectionExtension.cs @@ -18,21 +18,21 @@ internal static class NameValueCollectionExtension } StringBuilder sb = new(); - string?[] keys = collection.AllKeys; - for (int i = 0; i < count; i++) + foreach (string? key in collection.AllKeys) { - string? key = keys[i]; - if (collection.GetValues(key) is { } values) + if (collection.GetValues(key) is not { } values) { - foreach (ref readonly string value in values.AsSpan()) - { - if (!string.IsNullOrEmpty(key)) - { - sb.Append(key).Append('='); - } + continue; + } - sb.Append(HttpUtility.UrlEncode(value)).Append('&'); + foreach (ref readonly string value in values.AsSpan()) + { + if (!string.IsNullOrEmpty(key)) + { + sb.Append(key).Append('='); } + + sb.Append(HttpUtility.UrlEncode(value)).Append('&'); } } From f16769969e6d533f25f42fc2462a970b429acccc Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Thu, 7 Dec 2023 23:00:40 +0800 Subject: [PATCH 14/47] fix #1163 --- src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index d97bc0c7..5dfa4703 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -504,7 +504,7 @@ 精炼 {0} 阶 - 必须先选择一个用户与角色 + 必须登录 米游社/HoYoLAB 并选择一个用户与角色 删除了 Uid:{0} 的 {1} 条祈愿记录 From c68fbe9d96358a77e2ad5dd1b0f7f47838e29bab Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Thu, 7 Dec 2023 14:36:21 +0800 Subject: [PATCH 15/47] Auto sync appxmanifest --- .github/workflows/PublishDistribution.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/PublishDistribution.yml b/.github/workflows/PublishDistribution.yml index e052ffed..f192bd4d 100644 --- a/.github/workflows/PublishDistribution.yml +++ b/.github/workflows/PublishDistribution.yml @@ -18,3 +18,11 @@ jobs: PURGE_URL: ${{ secrets.PURGE_URL }} run: | curl -X PATCH $PURGE_URL + + - name: Sync development appxmanifest + env: + VERSION: ${{ github.ref }} + shell: pwsh + run: | + $ver = ' Version="'+$Env:VERSION+'.0"' + $content = (Get-Content src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest -Encoding UTF8) -replace ' Version="([0-9\\.]+)"', $ver From 7ef2834b422dba889de00179b9775a01fb98bc9c Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Thu, 7 Dec 2023 19:16:57 +0800 Subject: [PATCH 16/47] Hello cake --- .config/dotnet-tools.json | 12 ++++ azure-pipelines.yml | 96 +++------------------------- build.cake | 128 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 87 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 build.cake diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..3f287339 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "cake.tool": { + "version": "4.0.0", + "commands": [ + "dotnet-cake" + ] + } + } +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 55f9c894..6d2b3c33 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -30,7 +30,7 @@ pr: - .github/ISSUE_TEMPLATE/*.yml - .github/workflows/*.yml - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx - + pool: name: Default @@ -58,93 +58,10 @@ steps: version: '8.x' includePreviewVersions: true -- task: NuGetToolInstaller@1 - name: 'NuGetToolInstaller' - displayName: 'NuGet Installer' - -- task: NuGetCommand@2 - displayName: NuGet restore - inputs: - command: 'restore' - restoreSolution: '$(solution)' - feedsToUse: 'config' - nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.Config' - -- task: MsixPackaging@1 - displayName: Build binary package - inputs: - outputPath: '$(Build.ArtifactStagingDirectory)/' - solution: '$(solution)' - clean: false - generateBundle: false - buildConfiguration: 'Release' - buildPlatform: 'x64' - updateAppVersion: false - appPackageDistributionMode: 'SideloadOnly' - msbuildLocationMethod: 'location' - msbuildLocation: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Msbuild\Current\Bin\MSBuild.exe' - -- task: MagicChunks@2 - inputs: - sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\AppxManifest.xml' - fileType: 'Xml' - targetPathType: 'source' - transformationType: 'json' - transformations: | - { - "Package/Identity/@Name": "7f0db578-026f-4e0b-a75b-d5d06bb0a74c", - "Package/Identity/@Publisher": "CN=DGP Studio CI", - "Package/Identity/@Version": "$(build_date).$(rev_number)", - "Package/Properties/DisplayName": "胡桃 Alpha", - "Package/Properties/PublisherDisplayName":"DGP Studio CI", - "Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha" - } - - task: CmdLine@2 - displayName: Create resources folder + displayName: dotnet cake inputs: - script: | - mkdir Assets - - mkdir Resource - workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64' - - -- task: CopyFiles@2 - displayName: Copy Assets Folder - inputs: - SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Assets' - Contents: '**' - TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\Assets' - -- task: CopyFiles@2 - displayName: Copy Resource Folder - inputs: - SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Resource' - Contents: '**' - TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\Resource' - -- task: CmdLine@2 - displayName: Build MSIX - inputs: - script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix' - -- task: MsixSigning@1 - name: signMsix - displayName: Sign MSIX package - inputs: - package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix' - certificate: 'DGP_Studio_CI.pfx' - passwordVariable: 'pw' - condition: succeeded() - - -#- task: PublishPipelineArtifact@1 -# displayName: 'Upload Output' -# inputs: -# targetPath: '$(Build.ArtifactStagingDirectory)/' -# artifact: 'Output' -# publishLocation: 'pipeline' + script: dotnet tool restore && dotnet cake --Version=$(build_date).$(rev_number) --pw=$(pw) - task: DownloadSecureFile@1 name: cerFile @@ -152,10 +69,15 @@ steps: inputs: secureFile: 'Snap.Hutao.CI.cer' +- task: DownloadSecureFile@1 + displayName: Download PFX + inputs: + secureFile: 'DGP_Studio_CI.pfx' + - task: GitHubRelease@1 inputs: gitHubConnection: 'github.com_Masterain' - repositoryName: 'DGP-Studio/Snap.Hutao' + repositoryName: 'DGP-Studio/Snap.Hutao.CI.Test' action: 'create' target: '$(Build.SourceVersion)' tagSource: 'userSpecifiedTag' diff --git a/build.cake b/build.cake new file mode 100644 index 00000000..27344afc --- /dev/null +++ b/build.cake @@ -0,0 +1,128 @@ +#tool "nuget:?package=nuget.commandline&version=6.5.0" +var target = Argument("target", "Build"); +var configuration = Argument("configuration", "Release"); + +var version = HasArgument("Version") ? Argument("Version") : throw new Exception("Empty Version"); +var pw = HasArgument("pw") ? Argument("pw") : throw new Exception("Empty pw"); + +var solution = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao.sln"); +var project = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "Snap.Hutao.csproj"); +var binPath = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "bin", "x64", "Release", "net8.0-windows10.0.22621.0", "win-x64"); +var outputPath = AzurePipelines.Environment.Build.ArtifactStagingDirectory.FullPath; + +Task("Build") + .IsDependentOn("Build binary package") + .IsDependentOn("Copy files") + .IsDependentOn("Build MSIX") + .IsDependentOn("Sign MSIX"); + +Task("NuGet Restore") + .Does(() => +{ + Information("Restoring packages..."); + DotNetRestore(project, new DotNetRestoreSettings + { + Verbosity = DotNetVerbosity.Detailed, + Interactive = false, + ConfigFile = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "NuGet.Config") + }); +}); + +Task("Generate AppxManifest") + .Does(() => +{ + Information("Generating AppxManifest..."); + + var manifestPath = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest"); + var manifest = System.IO.File.ReadAllText(manifestPath); + + manifest = manifest + .Replace("Snap Hutao", "Snap Hutao Alpha") + .Replace("胡桃", "胡桃 Alpha") + .Replace("DGP Studio", "DGP Studio CI"); + manifest = System.Text.RegularExpressions.Regex.Replace(manifest, " Name=\"([^\"]*)\"", " Name=\"7f0db578-026f-4e0b-a75b-d5d06bb0a74c\""); + manifest = System.Text.RegularExpressions.Regex.Replace(manifest, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=DGP Studio CI\""); + manifest = System.Text.RegularExpressions.Regex.Replace(manifest, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\""); + + System.IO.File.WriteAllText(manifestPath, manifest); + + Information("Generated."); +}); + +Task("Build binary package") + .IsDependentOn("NuGet Restore") + .IsDependentOn("Generate AppxManifest") + .Does(() => +{ + Information("Building binary package..."); + + var settings = new DotNetBuildSettings + { + Configuration = configuration + }; + + settings.MSBuildSettings = new DotNetMSBuildSettings + { + ArgumentCustomization = args => args.Append("/p:Platform=x64") + .Append("/p:UapAppxPackageBuildMode=SideloadOnly") + .Append("/p:AppxPackageSigningEnabled=false") + .Append("/p:AppxBundle=Never") + .Append("/p:AppxPackageOutput=" + outputPath) + }; + + DotNetBuild(project, settings); +}); + +Task("Copy files") + .IsDependentOn("Build binary package") + .Does(() => +{ + Information("Copying assets..."); + CopyDirectory( + System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "Assets"), + System.IO.Path.Combine(binPath, "Assets") + ); + + Information("Copying resource..."); + CopyDirectory( + System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "Resource"), + System.IO.Path.Combine(binPath, "Resource") + ); +}); + +Task("Build MSIX") + .IsDependentOn("Build binary package") + .IsDependentOn("Copy files") + .Does(() => +{ + var p = StartProcess( + "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.22621.0\\x64\\makeappx.exe", + new ProcessSettings + { + Arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix") + } + ); + if (p != 0) + { + throw new InvalidOperationException("Build failed with exit code " + p); + } +}); + +Task("Sign MSIX") + .IsDependentOn("Build MSIX") + .Does(() => +{ + var p = StartProcess( + "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.22621.0\\x64\\signtool.exe", + new ProcessSettings + { + Arguments = "sign /debug /v /a /fd SHA256 /f " + System.IO.Path.Combine(AzurePipelines.Environment.Agent.HomeDirectory.FullPath, "_work", "_temp", "DGP_Studio_CI.pfx") + " /p " + pw + " " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix") + } + ); + if (p != 0) + { + throw new InvalidOperationException("Build failed with exit code " + p); + } +}); + +RunTarget(target); From e090d7e04b0fa1e277901e71e21d7c513072160b Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Thu, 7 Dec 2023 19:18:46 +0800 Subject: [PATCH 17/47] wrong repo --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6d2b3c33..31d95af2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -77,7 +77,7 @@ steps: - task: GitHubRelease@1 inputs: gitHubConnection: 'github.com_Masterain' - repositoryName: 'DGP-Studio/Snap.Hutao.CI.Test' + repositoryName: 'DGP-Studio/Snap.Hutao' action: 'create' target: '$(Build.SourceVersion)' tagSource: 'userSpecifiedTag' From bde51220605d408a0fa81759006e7b03871e1cc2 Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Fri, 8 Dec 2023 10:01:34 +0800 Subject: [PATCH 18/47] change target repo and avoid abs path --- azure-pipelines.yml | 2 +- build.cake | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 31d95af2..a4ca8a38 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -77,7 +77,7 @@ steps: - task: GitHubRelease@1 inputs: gitHubConnection: 'github.com_Masterain' - repositoryName: 'DGP-Studio/Snap.Hutao' + repositoryName: 'DGP-Automation/Hutao-Auto-Release' action: 'create' target: '$(Build.SourceVersion)' tagSource: 'userSpecifiedTag' diff --git a/build.cake b/build.cake index 27344afc..31a5f671 100644 --- a/build.cake +++ b/build.cake @@ -96,7 +96,7 @@ Task("Build MSIX") .Does(() => { var p = StartProcess( - "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.22621.0\\x64\\makeappx.exe", + "makeappx.exe", new ProcessSettings { Arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix") @@ -113,7 +113,7 @@ Task("Sign MSIX") .Does(() => { var p = StartProcess( - "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.22621.0\\x64\\signtool.exe", + "signtool.exe", new ProcessSettings { Arguments = "sign /debug /v /a /fd SHA256 /f " + System.IO.Path.Combine(AzurePipelines.Environment.Agent.HomeDirectory.FullPath, "_work", "_temp", "DGP_Studio_CI.pfx") + " /p " + pw + " " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix") From 1ab1d182af95c74679fa2b3ec82c463137ed3760 Mon Sep 17 00:00:00 2001 From: qhy040404 <45379733+qhy040404@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:30:07 +0800 Subject: [PATCH 19/47] Update PublishDistribution.yml --- .github/workflows/PublishDistribution.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/PublishDistribution.yml b/.github/workflows/PublishDistribution.yml index f192bd4d..e052ffed 100644 --- a/.github/workflows/PublishDistribution.yml +++ b/.github/workflows/PublishDistribution.yml @@ -18,11 +18,3 @@ jobs: PURGE_URL: ${{ secrets.PURGE_URL }} run: | curl -X PATCH $PURGE_URL - - - name: Sync development appxmanifest - env: - VERSION: ${{ github.ref }} - shell: pwsh - run: | - $ver = ' Version="'+$Env:VERSION+'.0"' - $content = (Get-Content src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest -Encoding UTF8) -replace ' Version="([0-9\\.]+)"', $ver From 859492e580632a20221dee8070b197482543c4de Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Fri, 8 Dec 2023 11:16:55 +0800 Subject: [PATCH 20/47] infobarservice refactor --- .../Extension/ContentDialogHideToken.cs | 2 +- .../Snap.Hutao/Core/Threading/ITaskContext.cs | 18 +---- .../Snap.Hutao/Core/Threading/TaskContext.cs | 6 +- .../Factory/Progress/ProgressFactory.cs | 2 +- .../Service/Notification/IInfoBarService.cs | 81 +------------------ .../Service/Notification/InfoBarService.cs | 77 ++---------------- .../Notification/InfoBarServiceExtension.cs | 59 ++++++++++++++ .../ViewModel/Guide/GuideViewModel.cs | 28 +++---- 8 files changed, 84 insertions(+), 189 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarServiceExtension.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogHideToken.cs b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogHideToken.cs index c0178f3f..2b5f14af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogHideToken.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogHideToken.cs @@ -24,7 +24,7 @@ internal struct ContentDialogHideToken : IDisposable, IAsyncDisposable if (!disposed && !disposing) { disposing = true; - taskContext.InvokeOnMainThread(contentDialog.Hide); // Hide() must be called on main thread. + taskContext.InvokeOnMainThread(contentDialog.Hide); disposing = false; disposed = true; } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs index 10cf42bb..1a52c409 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs @@ -8,25 +8,13 @@ namespace Snap.Hutao.Core.Threading; /// internal interface ITaskContext { - SynchronizationContext GetSynchronizationContext(); + SynchronizationContext SynchronizationContext { get; } + + void BeginInvokeOnMainThread(Action action); - /// - /// 在主线程上同步等待执行操作 - /// - /// 操作 void InvokeOnMainThread(Action action); - /// - /// 异步切换到 后台线程 - /// - /// 使用 异步切换到 主线程 - /// 等待体 ThreadPoolSwitchOperation SwitchToBackgroundAsync(); - /// - /// 异步切换到 主线程 - /// - /// 使用 异步切换到 后台线程 - /// 等待体 DispatcherQueueSwitchOperation SwitchToMainThreadAsync(); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs index c0d5e145..32b133ed 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs @@ -24,6 +24,8 @@ internal sealed class TaskContext : ITaskContext SynchronizationContext.SetSynchronizationContext(synchronizationContext); } + public SynchronizationContext SynchronizationContext { get => synchronizationContext; } + /// public ThreadPoolSwitchOperation SwitchToBackgroundAsync() { @@ -42,8 +44,8 @@ internal sealed class TaskContext : ITaskContext dispatcherQueue.Invoke(action); } - public SynchronizationContext GetSynchronizationContext() + public void BeginInvokeOnMainThread(Action action) { - return synchronizationContext; + dispatcherQueue.TryEnqueue(() => action()); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs index fa35afd5..e655fe85 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs @@ -11,6 +11,6 @@ internal sealed partial class ProgressFactory : IProgressFactory public IProgress CreateForMainThread(Action handler) { - return new DispatcherQueueProgress(handler, taskContext.GetSynchronizationContext()); + return new DispatcherQueueProgress(handler, taskContext.SynchronizationContext); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/IInfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/IInfoBarService.cs index 432c943b..2cc22e7e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Notification/IInfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/IInfoBarService.cs @@ -6,89 +6,10 @@ using System.Collections.ObjectModel; namespace Snap.Hutao.Service.Notification; -/// -/// 消息条服务 -/// [HighQuality] internal interface IInfoBarService { - /// - /// 获取操作的集合 - /// ObservableCollection Collection { get; } - /// - /// 显示错误消息 - /// - /// 消息 - /// 关闭延迟 - void Error(string message, int delay = 30000); - - /// - /// 显示错误消息 - /// - /// 标题 - /// 消息 - /// 关闭延迟 - void Error(string title, string message, int delay = 30000); - - /// - /// 显示错误消息 - /// - /// 异常 - /// 关闭延迟 - void Error(Exception exception, int delay = 30000); - - /// - /// 显示错误消息 - /// - /// 异常 - /// 标题 - /// 关闭延迟 - void Error(Exception exception, string title, int delay = 30000); - - /// - /// 显示提示信息 - /// - /// 消息 - /// 关闭延迟 - void Information(string message, int delay = 5000); - - /// - /// 显示提示信息 - /// - /// 标题 - /// 消息 - /// 关闭延迟 - void Information(string title, string message, int delay = 5000); - - /// - /// 显示成功信息 - /// - /// 消息 - /// 关闭延迟 - void Success(string message, int delay = 5000); - - /// - /// 显示成功信息 - /// - /// 标题 - /// 消息 - /// 关闭延迟 - void Success(string title, string message, int delay = 5000); - - /// - /// 显示警告信息 - /// - /// 消息 - /// 关闭延迟 - void Warning(string message, int delay = 15000); - - /// - /// 显示警告信息 - /// - /// 标题 - /// 消息 - /// 关闭延迟 - void Warning(string title, string message, int delay = 15000); + void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs index f5ebe8f9..e7c3daf2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs @@ -34,84 +34,17 @@ internal sealed class InfoBarService : IInfoBarService get => collection ??= []; } - /// - public void Information(string message, int delay = 5000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Informational, null, message, delay); - } - - /// - public void Information(string title, string message, int delay = 5000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Informational, title, message, delay); - } - - /// - public void Success(string message, int delay = 5000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Success, null, message, delay); - } - - /// - public void Success(string title, string message, int delay = 5000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Success, title, message, delay); - } - - /// - public void Warning(string message, int delay = 30000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Warning, null, message, delay); - } - - /// - public void Warning(string title, string message, int delay = 30000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Warning, title, message, delay); - } - - /// - public void Error(string message, int delay = 0) - { - PrepareInfoBarAndShow(InfoBarSeverity.Error, null, message, delay); - } - - /// - public void Error(string title, string message, int delay = 0) - { - PrepareInfoBarAndShow(InfoBarSeverity.Error, title, message, delay); - } - - /// - public void Error(Exception ex, int delay = 0) - { - PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, ex.Message, delay); - } - - /// - public void Error(Exception ex, string title, int delay = 0) - { - PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay); - } - - private void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay) + public void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay) { if (collection is null) { return; } - PrepareInfoBarAndShowInternalAsync(severity, title, message, delay).SafeForget(logger); + PrepareInfoBarAndShowCoreAsync(severity, title, message, delay).SafeForget(logger); } - /// - /// 准备信息条并显示 - /// - /// 严重程度 - /// 标题 - /// 消息 - /// 关闭延迟 - private async ValueTask PrepareInfoBarAndShowInternalAsync(InfoBarSeverity severity, string? title, string? message, int delay) + private async ValueTask PrepareInfoBarAndShowCoreAsync(InfoBarSeverity severity, string? title, string? message, int delay) { await taskContext.SwitchToMainThreadAsync(); @@ -139,7 +72,7 @@ internal sealed class InfoBarService : IInfoBarService private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args) { ArgumentNullException.ThrowIfNull(collection); - taskContext.InvokeOnMainThread(() => collection.Remove(sender)); + taskContext.BeginInvokeOnMainThread(() => collection.Remove(sender)); sender.Closed -= infobarClosedEventHandler; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarServiceExtension.cs new file mode 100644 index 00000000..2a966d5f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarServiceExtension.cs @@ -0,0 +1,59 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; + +namespace Snap.Hutao.Service.Notification; + +internal static class InfoBarServiceExtension +{ + public static void Information(this IInfoBarService infoBarService, string message, int delay = 5000) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Informational, null, message, delay); + } + + public static void Information(this IInfoBarService infoBarService, string title, string message, int delay = 5000) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Informational, title, message, delay); + } + + public static void Success(this IInfoBarService infoBarService, string message, int delay = 5000) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Success, null, message, delay); + } + + public static void Success(this IInfoBarService infoBarService, string title, string message, int delay = 5000) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Success, title, message, delay); + } + + public static void Warning(this IInfoBarService infoBarService, string message, int delay = 30000) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Warning, null, message, delay); + } + + public static void Warning(this IInfoBarService infoBarService, string title, string message, int delay = 30000) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Warning, title, message, delay); + } + + public static void Error(this IInfoBarService infoBarService, string message, int delay = 0) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Error, null, message, delay); + } + + public static void Error(this IInfoBarService infoBarService, string title, string message, int delay = 0) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Error, title, message, delay); + } + + public static void Error(this IInfoBarService infoBarService, Exception ex, int delay = 0) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, ex.Message, delay); + } + + public static void Error(this IInfoBarService infoBarService, Exception ex, string title, int delay = 0) + { + infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/GuideViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/GuideViewModel.cs index d87da8a9..4ac4a17c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/GuideViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/GuideViewModel.cs @@ -140,11 +140,10 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel /// /// 下载信息 /// - public ObservableCollection? DownloadSummaries { get => downloadSummaries; set => SetProperty(ref downloadSummaries, value); } - - protected override ValueTask InitializeUIAsync() + public ObservableCollection? DownloadSummaries { - return ValueTask.FromResult(true); + get => downloadSummaries; + set => SetProperty(ref downloadSummaries, value); } [Command("NextOrCompleteCommand")] @@ -160,28 +159,21 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel private async ValueTask DownloadStaticResourceAsync() { - HashSet downloadSummaries = []; + DownloadSummaries = StaticResource + .GetUnfulfilledCategorySet() + .Select(category => new DownloadSummary(serviceProvider, category)) + .ToObservableCollection(); - HashSet categories = StaticResource.GetUnfulfilledCategorySet(); - - foreach (string category in categories) - { - downloadSummaries.Add(new(serviceProvider, category)); - } - - DownloadSummaries = downloadSummaries.ToObservableCollection(); - - await Parallel.ForEachAsync(downloadSummaries, async (summary, token) => + await Parallel.ForEachAsync(DownloadSummaries, async (summary, token) => { if (await summary.DownloadAndExtractAsync().ConfigureAwait(false)) { - taskContext.InvokeOnMainThread(() => DownloadSummaries.Remove(summary)); + taskContext.BeginInvokeOnMainThread(() => DownloadSummaries.Remove(summary)); } }).ConfigureAwait(false); StaticResource.FulfillAll(); - - LocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, (uint)GuideState.Completed); + UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Completed); AppInstance.Restart(string.Empty); } } \ No newline at end of file From 50c0fa2061e54ca089d6d7134cea859e2a65cd8f Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Fri, 8 Dec 2023 11:40:28 +0800 Subject: [PATCH 21/47] abstract --- build.cake | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/build.cake b/build.cake index 31a5f671..aa282fe7 100644 --- a/build.cake +++ b/build.cake @@ -5,11 +5,17 @@ var configuration = Argument("configuration", "Release"); var version = HasArgument("Version") ? Argument("Version") : throw new Exception("Empty Version"); var pw = HasArgument("pw") ? Argument("pw") : throw new Exception("Empty pw"); -var solution = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao.sln"); -var project = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "Snap.Hutao.csproj"); -var binPath = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "bin", "x64", "Release", "net8.0-windows10.0.22621.0", "win-x64"); +// Default Azure Pipelines + +var repoDir = AzurePipelines.Environment.Build.SourcesDirectory.FullPath; var outputPath = AzurePipelines.Environment.Build.ArtifactStagingDirectory.FullPath; +var solution = System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln"); +var project = System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Snap.Hutao.csproj"); +var binPath = System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "bin", "x64", "Release", "net8.0-windows10.0.22621.0", "win-x64"); + +var pfxFile = System.IO.Path.Combine(AzurePipelines.Environment.Agent.HomeDirectory.FullPath, "_work", "_temp", "DGP_Studio_CI.pfx"); + Task("Build") .IsDependentOn("Build binary package") .IsDependentOn("Copy files") @@ -20,11 +26,13 @@ Task("NuGet Restore") .Does(() => { Information("Restoring packages..."); + + var nugetConfig = System.IO.Path.Combine(repoDir, "NuGet.Config"); DotNetRestore(project, new DotNetRestoreSettings { Verbosity = DotNetVerbosity.Detailed, Interactive = false, - ConfigFile = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "NuGet.Config") + ConfigFile = nugetConfig }); }); @@ -33,18 +41,18 @@ Task("Generate AppxManifest") { Information("Generating AppxManifest..."); - var manifestPath = System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest"); - var manifest = System.IO.File.ReadAllText(manifestPath); + var manifest = System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest"); + var content = System.IO.File.ReadAllText(manifest); - manifest = manifest + content = content .Replace("Snap Hutao", "Snap Hutao Alpha") .Replace("胡桃", "胡桃 Alpha") .Replace("DGP Studio", "DGP Studio CI"); - manifest = System.Text.RegularExpressions.Regex.Replace(manifest, " Name=\"([^\"]*)\"", " Name=\"7f0db578-026f-4e0b-a75b-d5d06bb0a74c\""); - manifest = System.Text.RegularExpressions.Regex.Replace(manifest, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=DGP Studio CI\""); - manifest = System.Text.RegularExpressions.Regex.Replace(manifest, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\""); + content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"7f0db578-026f-4e0b-a75b-d5d06bb0a74c\""); + content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=DGP Studio CI\""); + content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\""); - System.IO.File.WriteAllText(manifestPath, manifest); + System.IO.File.WriteAllText(manifest, content); Information("Generated."); }); @@ -79,13 +87,13 @@ Task("Copy files") { Information("Copying assets..."); CopyDirectory( - System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "Assets"), + System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Assets"), System.IO.Path.Combine(binPath, "Assets") ); Information("Copying resource..."); CopyDirectory( - System.IO.Path.Combine(AzurePipelines.Environment.Build.SourcesDirectory.FullPath, "src", "Snap.Hutao", "Snap.Hutao", "Resource"), + System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Resource"), System.IO.Path.Combine(binPath, "Resource") ); }); @@ -116,7 +124,7 @@ Task("Sign MSIX") "signtool.exe", new ProcessSettings { - Arguments = "sign /debug /v /a /fd SHA256 /f " + System.IO.Path.Combine(AzurePipelines.Environment.Agent.HomeDirectory.FullPath, "_work", "_temp", "DGP_Studio_CI.pfx") + " /p " + pw + " " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix") + Arguments = "sign /debug /v /a /fd SHA256 /f " + pfxFile + " /p " + pw + " " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix") } ); if (p != 0) From 3cc17375f0fcb503b0cc404e74ad7a14575e3395 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Fri, 8 Dec 2023 16:19:38 +0800 Subject: [PATCH 22/47] Settings folder size display --- .../UnsafeRuntimeBehaviorTest.cs | 22 ++++++ .../Snap.Hutao/Control/Theme/Color.xaml | 2 +- .../Snap.Hutao/Core/Caching/ImageCache.cs | 25 ++---- .../Model/Metadata/Item/Material.cs | 2 + .../Snap.Hutao/Resource/Localization/SH.resx | 10 ++- .../Game/Locator/UnityLogGameLocator.cs | 2 +- .../View/Control/StatisticsCard.xaml | 2 +- .../Snap.Hutao/View/Page/SettingPage.xaml | 76 +++++++++++-------- .../ViewModel/Setting/FolderViewModel.cs | 45 +++++++++++ .../ViewModel/Setting/SettingViewModel.cs | 46 +++++------ 10 files changed, 153 insertions(+), 79 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/FolderViewModel.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs index 36601cfa..86de485c 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs @@ -23,6 +23,18 @@ public sealed class UnsafeRuntimeBehaviorTest } } + [TestMethod] + public unsafe void UInt32LayoutIsLittleEndian() + { + ulong testValue = 0x1234567887654321; + ref BuildVersion version = ref Unsafe.As(ref testValue); + + Assert.AreEqual(0x1234, version.Major); + Assert.AreEqual(0x5678, version.Minor); + Assert.AreEqual(0x8765, version.Patch); + Assert.AreEqual(0x4321, version.Build); + } + [TestMethod] public unsafe void ReadOnlyStructCanBeModifiedInCtor() { @@ -34,6 +46,8 @@ public sealed class UnsafeRuntimeBehaviorTest Assert.AreEqual(1212, testStruct.Value4); } + + private readonly struct TestStruct { public readonly int Value1; @@ -46,4 +60,12 @@ public sealed class UnsafeRuntimeBehaviorTest CollectionsMarshal.AsSpan(list).CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As(ref this), 4)); } } + + private readonly struct BuildVersion + { + public readonly ushort Build; + public readonly ushort Patch; + public readonly ushort Minor; + public readonly ushort Major; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml index 8557c744..5ea53fe5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml @@ -23,7 +23,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs index eaaca065..debe0818 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs @@ -18,41 +18,30 @@ namespace Snap.Hutao.Core.Caching; /// The class's name will become the cache folder's name /// [HighQuality] +[ConstructorGenerated] [Injection(InjectAs.Singleton, typeof(IImageCache))] [HttpClient(HttpClientConfiguration.Default)] [PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)] -internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation +internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation { private const string CacheFolderName = nameof(ImageCache); - private static readonly FrozenDictionary RetryCountToDelay = new Dictionary() + private readonly FrozenDictionary retryCountToDelay = new Dictionary() { [0] = TimeSpan.FromSeconds(4), [1] = TimeSpan.FromSeconds(16), [2] = TimeSpan.FromSeconds(64), }.ToFrozenDictionary(); + private readonly ConcurrentDictionary concurrentTasks = new(); + private readonly IHttpClientFactory httpClientFactory; private readonly IServiceProvider serviceProvider; private readonly ILogger logger; - private readonly ConcurrentDictionary concurrentTasks = new(); - private string? baseFolder; private string? cacheFolder; - /// - /// Initializes a new instance of the class. - /// - /// 服务提供器 - public ImageCache(IServiceProvider serviceProvider) - { - logger = serviceProvider.GetRequiredService>(); - httpClientFactory = serviceProvider.GetRequiredService(); - - this.serviceProvider = serviceProvider; - } - /// public void RemoveInvalid() { @@ -62,7 +51,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation /// public void Remove(Uri uriForCachedItem) { - Remove(new ReadOnlySpan(ref uriForCachedItem)); + Remove([uriForCachedItem]); } /// @@ -191,7 +180,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation case HttpStatusCode.TooManyRequests: { retryCount++; - TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount]; + TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount]; logger.LogInformation("Retry {Uri} after {Delay}.", uri, delay); await Task.Delay(delay).ConfigureAwait(false); break; diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/Material.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/Material.cs index 34e5908b..fc4cf24a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/Material.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/Material.cs @@ -34,6 +34,8 @@ internal sealed class Material : DisplayItem /// 是否为物品栏物品 public bool IsInventoryItem() { + // TODO: Add a pre-filtered metadata set to check if it's an inventory item + // 原质 if (Id == 112001U) { diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 5dfa4703..1be8d96c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1565,6 +1565,9 @@ 创建桌面快捷方式失败 + + 已使用磁盘空间:{0} + 无感验证复合 Url 配置成功 @@ -2205,7 +2208,7 @@ 图片缓存 在此处存放 - 打开 缓存 文件夹 + 缓存 文件夹 复制 @@ -2232,7 +2235,7 @@ 用户数据/元数据 在此处存放 - 打开 数据 文件夹 + 数据 文件夹 删除 @@ -2525,6 +2528,9 @@ 立即登录或注册 + + 打开文件夹 + 角色出场率 = 本层上阵该角色次数(层内重复出现只记一次)/ 深渊记录总数 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs index 38866257..ca41a9cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs @@ -29,7 +29,7 @@ internal sealed partial class UnityLogGameLocator : IGameLocator // Fallback to the CN server. string logFilePathFinal = File.Exists(logFilePathOversea) ? logFilePathOversea : logFilePathChinese; - if (TempFile.CopyFrom(logFilePathFinal) is TempFile file) + if (TempFile.CopyFrom(logFilePathFinal) is { } file) { using (file) { diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml index 17aaaa0a..b2be538d 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml @@ -59,7 +59,7 @@ + Visibility="{Binding UserOptions.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}"> + internal static class MemoryCacheExtension { - /// - /// 尝试从 IMemoryCache 中移除并返回具有指定键的值 - /// - /// 缓存 - /// 键 - /// 值 - /// 是否移除成功 public static bool TryRemove(this IMemoryCache memoryCache, string key, out object? value) { if (!memoryCache.TryGetValue(key, out value)) @@ -27,4 +20,16 @@ internal static class MemoryCacheExtension memoryCache.Remove(key); return true; } + + public static bool TryGetRequiredValue(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out T? value) + where T : class + { + if (!memoryCache.TryGetValue(key, out value)) + { + return false; + } + + ArgumentNullException.ThrowIfNull(value); + return true; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs index 45a51287..df39f93b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs @@ -28,11 +28,9 @@ internal sealed partial class AnnouncementService : IAnnouncementService public async ValueTask GetAnnouncementWrapperAsync(CancellationToken cancellationToken = default) { // 缓存中存在记录,直接返回 - if (memoryCache.TryGetValue(CacheKey, out object? cache)) + if (memoryCache.TryGetRequiredValue(CacheKey, out AnnouncementWrapper? cache)) { - AnnouncementWrapper? wrapper = (AnnouncementWrapper?)cache; - ArgumentNullException.ThrowIfNull(wrapper); - return wrapper; + return cache; } await taskContext.SwitchToBackgroundAsync(); @@ -40,30 +38,32 @@ internal sealed partial class AnnouncementService : IAnnouncementService .GetAnnouncementsAsync(cancellationToken) .ConfigureAwait(false); - if (announcementWrapperResponse.IsOk()) + if (!announcementWrapperResponse.IsOk()) { - AnnouncementWrapper wrapper = announcementWrapperResponse.Data; - Response> announcementContentResponse = await announcementClient - .GetAnnouncementContentsAsync(cancellationToken) - .ConfigureAwait(false); - - if (announcementContentResponse.IsOk()) - { - List contents = announcementContentResponse.Data.List; - - Dictionary contentMap = contents - .ToDictionary(id => id.AnnId, content => content.Content); - - // 将活动公告置于前方 - wrapper.List.Reverse(); - - PreprocessAnnouncements(contentMap, wrapper.List); - - return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30)); - } + return default!; } - return default!; + AnnouncementWrapper wrapper = announcementWrapperResponse.Data; + Response> announcementContentResponse = await announcementClient + .GetAnnouncementContentsAsync(cancellationToken) + .ConfigureAwait(false); + + if (!announcementContentResponse.IsOk()) + { + return default!; + } + + List contents = announcementContentResponse.Data.List; + + Dictionary contentMap = contents + .ToDictionary(id => id.AnnId, content => content.Content); + + // 将活动公告置于前方 + wrapper.List.Reverse(); + + PreprocessAnnouncements(contentMap, wrapper.List); + + return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30)); } private static void PreprocessAnnouncements(Dictionary contentMap, List announcementListWrappers) @@ -103,56 +103,60 @@ internal sealed partial class AnnouncementService : IAnnouncementService .List .Single(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title)); - if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is { Success: true } match) + if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } match) { - DateTimeOffset versionUpdateTime = DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture); - _ = 1; - foreach (ref readonly Announcement announcement in CollectionsMarshal.AsSpan(activities)) + return; + } + + DateTimeOffset versionUpdateTime = DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture); + + foreach (ref readonly Announcement announcement in CollectionsMarshal.AsSpan(activities)) + { + if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent) { - if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent) + announcement.StartTime = versionUpdateTime; + continue; + } + + if (AnnouncementRegex.PersistentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } persistent) + { + announcement.StartTime = versionUpdateTime; + announcement.EndTime = versionUpdateTime + TimeSpan.FromDays(42); + continue; + } + + if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient) + { + announcement.StartTime = versionUpdateTime; + announcement.EndTime = DateTimeOffset.Parse(transient.Groups[2].ValueSpan, CultureInfo.InvariantCulture); + continue; + } + + MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex.Matches(announcement.Content); + if (matches.Count < 2) + { + continue; + } + + List dateTimes = matches.Select(match => DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture)).ToList(); + DateTimeOffset min = DateTimeOffset.MaxValue; + DateTimeOffset max = DateTimeOffset.MinValue; + + foreach (DateTimeOffset time in dateTimes) + { + if (time < min) { - announcement.StartTime = versionUpdateTime; - continue; + min = time; } - if (AnnouncementRegex.PersistentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } persistent) + if (time > max) { - announcement.StartTime = versionUpdateTime; - announcement.EndTime = versionUpdateTime + TimeSpan.FromDays(42); - continue; - } - - if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient) - { - announcement.StartTime = versionUpdateTime; - announcement.EndTime = DateTimeOffset.Parse(transient.Groups[2].ValueSpan, CultureInfo.InvariantCulture); - continue; - } - - MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex.Matches(announcement.Content); - if (matches.Count >= 2) - { - List dateTimes = matches.Select(match => DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture)).ToList(); - DateTimeOffset min = DateTimeOffset.MaxValue; - DateTimeOffset max = DateTimeOffset.MinValue; - - foreach (DateTimeOffset time in dateTimes) - { - if (time < min) - { - min = time; - } - - if (time > max) - { - max = time; - } - } - - announcement.StartTime = min; - announcement.EndTime = max; + max = time; } } + + announcement.StartTime = min; + announcement.EndTime = max; } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs index 57bfc0af..7a4678cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs @@ -21,41 +21,42 @@ internal static class AchievementFinishPercent int totalFinished = 0; int totalCount = 0; - if (viewModel.Achievements is { } achievements) + if (viewModel.Achievements is not { } achievements) { - if (viewModel.AchievementGoals is { } achievementGoals) + return; + } + + if (viewModel.AchievementGoals is not { } achievementGoals) + { + return; + } + + if (achievements.SourceCollection is not List list) + { + // Fast path + throw Must.NeverHappen("AchievementViewModel.Achievements.SourceCollection 应为 List"); + } + + Dictionary counter = achievementGoals.ToDictionary(x => x.Id, AchievementGoalStatistics.From); + + foreach (ref readonly AchievementView achievement in CollectionsMarshal.AsSpan(list)) + { + ref AchievementGoalStatistics goalStat = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal); + + goalStat.TotalCount += 1; + totalCount += 1; + if (achievement.IsChecked) { - Dictionary counter = achievementGoals.ToDictionary(x => x.Id, AchievementGoalStatistics.From); - - // Fast path - if (achievements.SourceCollection is List list) - { - foreach (ref readonly AchievementView achievement in CollectionsMarshal.AsSpan(list)) - { - // Make the state update as fast as possible - ref AchievementGoalStatistics stat = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal); - - stat.TotalCount += 1; - totalCount += 1; - if (achievement.IsChecked) - { - stat.Finished += 1; - totalFinished += 1; - } - } - } - else - { - Must.NeverHappen("AchievementViewModel.Achievements.SourceCollection 应为 List"); - } - - foreach (AchievementGoalStatistics statistics in counter.Values) - { - statistics.AchievementGoal.UpdateFinishDescriptionAndPercent(statistics); - } - - viewModel.FinishDescription = AchievementStatistics.Format(totalFinished, totalCount, out _); + goalStat.Finished += 1; + totalFinished += 1; } } + + foreach (AchievementGoalStatistics statistics in counter.Values) + { + statistics.AchievementGoal.UpdateFinishDescriptionAndPercent(statistics); + } + + viewModel.FinishDescription = AchievementStatistics.Format(totalFinished, totalCount, out _); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs index bd5e5c48..c89d7b26 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs @@ -72,17 +72,7 @@ internal sealed class AchievementGoalView : ObservableObject, INameIcon, IMappin /// 统计 public void UpdateFinishDescriptionAndPercent(AchievementGoalStatistics statistics) { - UpdateFinishDescriptionAndPercent(statistics.Finished, statistics.TotalCount); - } - - /// - /// 更新进度 - /// - /// 完成项 - /// 总项 - private void UpdateFinishDescriptionAndPercent(int finished, int count) - { - FinishDescription = AchievementStatistics.Format(finished, count, out double finishPercent); + FinishDescription = AchievementStatistics.Format(statistics.Finished, statistics.TotalCount, out double finishPercent); FinishPercent = finishPercent; } } \ No newline at end of file From e8762d658fb75bd9eae946035a8e0500ce9baa5f Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Mon, 11 Dec 2023 11:44:03 +0800 Subject: [PATCH 35/47] add console window --- .../Snap.Hutao.Win32/NativeMethods.txt | 2 + .../DependencyInjection.cs | 8 ++- .../Core/Logging/ConsoleWindowLifeTime.cs | 28 ++++++++ .../Snap.Hutao/Core/Logging/DebugLogger.cs | 70 ------------------- .../Logging/DebugLoggerFactoryExtensions.cs | 24 ------- .../Core/Logging/DebugLoggerProvider.cs | 22 ------ .../Core/Logging/LoggerFactoryExtensions.cs | 15 ++++ .../Snap.Hutao/Core/Logging/NullScope.cs | 24 ------- .../Snap.Hutao/Core/Setting/SettingKeys.cs | 35 +--------- .../Snap.Hutao/Resource/Localization/SH.resx | 6 ++ src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 1 + .../Snap.Hutao/View/Page/SettingPage.xaml | 7 ++ .../ViewModel/Abstraction/ViewModel.cs | 46 ++++++------ .../ViewModel/Game/LaunchGameViewModel.cs | 54 +++++--------- .../ViewModel/Setting/SettingViewModel.cs | 7 ++ 15 files changed, 117 insertions(+), 232 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Logging/ConsoleWindowLifeTime.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Logging/LoggerFactoryExtensions.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt index 80fd8e62..3a8308d4 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt +++ b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt @@ -10,9 +10,11 @@ DwmSetWindowAttribute GetDeviceCaps // KERNEL32 +AllocConsole CloseHandle CreateEventW CreateRemoteThread +FreeConsole GetModuleHandleW GetProcAddress K32EnumProcessModules diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs index 50c41966..1f515abd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs @@ -24,7 +24,7 @@ internal static class DependencyInjection ServiceProvider serviceProvider = new ServiceCollection() // Microsoft extension - .AddLogging(builder => builder.AddUnconditionalDebug()) + .AddLogging(builder => builder.AddConsoleWindow()) .AddMemoryCache() // Hutao extensions @@ -39,6 +39,7 @@ internal static class DependencyInjection Ioc.Default.ConfigureServices(serviceProvider); + serviceProvider.InitializeConsoleWindow(); serviceProvider.InitializeCulture(); return serviceProvider; @@ -61,4 +62,9 @@ internal static class DependencyInjection SH.Culture = cultureInfo; } + + private static void InitializeConsoleWindow(this IServiceProvider serviceProvider) + { + _ = serviceProvider.GetRequiredService(); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/ConsoleWindowLifeTime.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/ConsoleWindowLifeTime.cs new file mode 100644 index 00000000..66f4ceaf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/ConsoleWindowLifeTime.cs @@ -0,0 +1,28 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Setting; +using static Windows.Win32.PInvoke; + +namespace Snap.Hutao.Core.Logging; + +internal sealed class ConsoleWindowLifeTime : IDisposable +{ + private readonly bool consoleWindowAllocated; + + public ConsoleWindowLifeTime() + { + if (LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false)) + { + consoleWindowAllocated = AllocConsole(); + } + } + + public void Dispose() + { + if (consoleWindowAllocated) + { + FreeConsole(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs deleted file mode 100644 index b90e42ab..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.Diagnostics; - -namespace Snap.Hutao.Core.Logging; - -/// -/// A logger that writes messages in the debug output window only when a debugger is attached. -/// -internal sealed class DebugLogger : ILogger -{ - private readonly string name; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the logger. - public DebugLogger(string name) - { - this.name = name; - } - - /// - public IDisposable BeginScope(TState state) - where TState : notnull - { - return NullScope.Instance; - } - - /// - public bool IsEnabled(LogLevel logLevel) - { - // If the filter is null, everything is enabled - return logLevel != LogLevel.None; - } - - /// - [SuppressMessage("", "SH002")] - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - if (!IsEnabled(logLevel)) - { - return; - } - - ArgumentNullException.ThrowIfNull(formatter); - - string message = formatter(state, exception); - - if (string.IsNullOrEmpty(message)) - { - return; - } - - message = $"{logLevel}: {message}"; - - if (exception is not null) - { - message += Environment.NewLine + Environment.NewLine + exception; - } - - DebugWriteLine(message, name); - } - - private static void DebugWriteLine(string message, string name) - { - Debug.WriteLine(message, category: name); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs deleted file mode 100644 index 852a90c9..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Snap.Hutao.Core.Logging; - -/// -/// Extension methods for the class. -/// -internal static class DebugLoggerFactoryExtensions -{ - /// - /// Adds a debug logger named 'Debug' to the factory. - /// - /// The extension method argument. - /// builder - public static ILoggingBuilder AddUnconditionalDebug(this ILoggingBuilder builder) - { - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - return builder; - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs deleted file mode 100644 index 2cf5dd5e..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Core.Logging; - -/// -/// The provider for the . -/// -[ProviderAlias("Debug")] -internal sealed class DebugLoggerProvider : ILoggerProvider -{ - /// - public ILogger CreateLogger(string name) - { - return new DebugLogger(name); - } - - /// - public void Dispose() - { - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/LoggerFactoryExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LoggerFactoryExtensions.cs new file mode 100644 index 00000000..1dd99ff7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LoggerFactoryExtensions.cs @@ -0,0 +1,15 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Logging; + +internal static class LoggerFactoryExtensions +{ + public static ILoggingBuilder AddConsoleWindow(this ILoggingBuilder builder) + { + builder.Services.AddSingleton(); + + builder.AddSimpleConsole(); + return builder; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs deleted file mode 100644 index 65db7224..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Core.Logging; - -/// -/// An empty scope without any logic -/// -internal sealed class NullScope : IDisposable -{ - private NullScope() - { - } - - /// - /// 实例 - /// - public static NullScope Instance { get; } = new NullScope(); - - /// - public void Dispose() - { - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs index d67cc641..3565689c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs @@ -9,59 +9,26 @@ namespace Snap.Hutao.Core.Setting; [HighQuality] internal static class SettingKeys { - /// - /// 窗体矩形 - /// public const string WindowRect = "WindowRect"; - /// - /// 导航侧栏是否展开 - /// public const string IsNavPaneOpen = "IsNavPaneOpen"; - /// - /// 启动次数 - /// public const string LaunchTimes = "LaunchTimes"; - /// - /// 数据文件夹 - /// public const string DataFolderPath = "DataFolderPath"; - /// - /// 通行证用户名(邮箱) - /// public const string PassportUserName = "PassportUserName"; - /// - /// 通行证密码 - /// public const string PassportPassword = "PassportPassword"; - /// - /// 消息是否显示 - /// public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked"; - /// - /// 1.7.0 版本指引状态 - /// public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState"; - /// - /// 排除的系统公告 - /// public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds"; - /// - /// 禁用元数据更新检查 - /// public const string SuppressMetadataInitialization = "SuppressMetadataInitialization"; - /// - /// 覆盖管理员权限执行命令 - /// public const string OverrideElevationRequirement = "OverrideElevationRequirement"; public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent"; @@ -83,4 +50,6 @@ internal static class SettingKeys public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented"; public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever"; + + public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled"; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 2ba84fb3..5ee7f8a2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2528,6 +2528,12 @@ 立即登录或注册 + + 控制胡桃启动时是否开启控制台,重启后生效 + + + 调试控制台 + 打开文件夹 diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index a1ed520d..96133dfe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -297,6 +297,7 @@ + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index 4a25e5aa..3a65b2f0 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -402,6 +402,13 @@ Foreground="{ThemeResource SystemFillColorCriticalBrush}" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingDangerousHeader}"/> + + + + - /// 是否初始化完成 - /// public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); } - /// public CancellationToken CancellationToken { get; set; } - /// public SemaphoreSlim DisposeLock { get; set; } = new(1); - /// public bool IsViewDisposed { get; set; } protected TaskCompletionSource Initialization { get; } = new(); - /// - /// 异步初始化UI - /// - /// 任务 [Command("OpenUICommand")] protected virtual async Task OpenUIAsync() { @@ -42,20 +33,11 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel Initialization.TrySetResult(IsInitialized); } - /// - /// 异步初始化界面数据 - /// - /// 初始化是否成功 protected virtual ValueTask InitializeUIAsync() { return ValueTask.FromResult(true); } - /// - /// 保证 using scope 内的代码运行完成 - /// 防止 视图资源被回收 - /// - /// 解除执行限制 protected async ValueTask EnterCriticalExecutionAsync() { ThrowIfViewDisposed(); @@ -64,10 +46,28 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel return disposable; } - /// - /// 当页面被释放后抛出异常 - /// - /// 操作被用户取消 + protected bool SetProperty(ref T storage, T value, Action changedCallback, [CallerMemberName] string? propertyName = null) + { + if (SetProperty(ref storage, value, propertyName)) + { + changedCallback(value); + return true; + } + + return false; + } + + protected bool SetProperty(ref T storage, T value, Func changedAsyncCallback, [CallerMemberName] string? propertyName = null) + { + if (SetProperty(ref storage, value, propertyName)) + { + changedAsyncCallback(value).SafeForget(); + return true; + } + + return false; + } + private void ThrowIfViewDisposed() { if (IsViewDisposed) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 437e333c..01bb8cdd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -55,37 +55,38 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel private GameAccount? selectedGameAccount; private GameResource? gameResource; - /// - /// 已知的服务器方案 - /// - [SuppressMessage("", "CA1822")] - public List KnownSchemes { get => KnownLaunchSchemes.Get(); } + public List KnownSchemes { get; } = KnownLaunchSchemes.Get(); - /// - /// 当前选择的服务器方案 - /// public LaunchScheme? SelectedScheme { - get => selectedScheme; set + get => selectedScheme; + set { - if (SetProperty(ref selectedScheme, value)) + SetProperty(ref selectedScheme, value, UpdateGameResourceAsync); + + async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme) { - if (value is not null) + if (scheme is null) { - UpdateGameResourceAsync(value).SafeForget(); + return; + } + + await taskContext.SwitchToBackgroundAsync(); + Web.Response.Response response = await resourceClient + .GetResourceAsync(scheme) + .ConfigureAwait(false); + + if (response.IsOk()) + { + await taskContext.SwitchToMainThreadAsync(); + GameResource = response.Data; } } } } - /// - /// 游戏账号集合 - /// public ObservableCollection? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); } - /// - /// 选中的账号 - /// public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); } public LaunchOptions LaunchOptions { get => launchOptions; } @@ -96,9 +97,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel public AppOptions AppOptions { get => appOptions; } - /// - /// 游戏资源 - /// public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); } protected override async ValueTask InitializeUIAsync() @@ -165,20 +163,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel return true; } - private async ValueTask UpdateGameResourceAsync(LaunchScheme scheme) - { - await taskContext.SwitchToBackgroundAsync(); - Web.Response.Response response = await resourceClient - .GetResourceAsync(scheme) - .ConfigureAwait(false); - - if (response.IsOk()) - { - await taskContext.SwitchToMainThreadAsync(); - GameResource = response.Data; - } - } - [Command("LaunchCommand")] private async Task LaunchAsync() { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index cae48b9e..6b09771c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -107,6 +107,13 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel public IPInformation? IPInformation { get => ipInformation; private set => SetProperty(ref ipInformation, value); } + [SuppressMessage("", "CA1822")] + public bool IsAllocConsoleDebugModeEnabled + { + get => LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false); + set => LocalSetting.Set(SettingKeys.IsAllocConsoleDebugModeEnabled, value); + } + protected override async ValueTask InitializeUIAsync() { CacheFolderView = new(taskContext, runtimeOptions.LocalCache); From 73c62a63eaf5eb92586e2aa4402e17eb0d6c1fa9 Mon Sep 17 00:00:00 2001 From: Masterain Date: Sun, 10 Dec 2023 20:44:05 -0800 Subject: [PATCH 36/47] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 46 +++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 37136b2a..c8c874d6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -73,28 +73,34 @@ steps: inputs: secureFile: 'Snap.Hutao.CI.cer' -- task: GitHubRelease@1 +- task: PublishBuildArtifacts@1 inputs: - gitHubConnection: 'github.com_Masterain' - repositoryName: 'DGP-Automation/Hutao-Auto-Release' - action: 'create' - target: '$(Build.SourceVersion)' - tagSource: 'userSpecifiedTag' - tag: '$(version)' - title: '$(version)' - releaseNotesSource: 'inline' - releaseNotesInline: | - ## 普通用户请勿下载 - 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用** + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: '*' + publishLocation: 'Container' - 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本 - - assets: | - $(Build.ArtifactStagingDirectory)/* - $(cerFile.secureFilePath) - isPreRelease: true - changeLogCompareToRelease: 'lastFullRelease' - changeLogType: 'commitBased' +#- task: GitHubRelease@1 +# inputs: +# gitHubConnection: 'github.com_Masterain' +# repositoryName: 'DGP-Automation/Hutao-Auto-Release' +# action: 'create' +# target: '$(Build.SourceVersion)' +# tagSource: 'userSpecifiedTag' +# tag: '$(version)' +# title: '$(version)' +# releaseNotesSource: 'inline' +# releaseNotesInline: | +# ## 普通用户请勿下载 +# 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用** +# +# 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本 +# +# assets: | +# $(Build.ArtifactStagingDirectory)/* +# $(cerFile.secureFilePath) +# isPreRelease: true +# changeLogCompareToRelease: 'lastFullRelease' +# changeLogType: 'commitBased' - task: rclone@1 From 9c106b24fb255fa52ffc164476d621434e83b997 Mon Sep 17 00:00:00 2001 From: Masterain Date: Sun, 10 Dec 2023 21:02:19 -0800 Subject: [PATCH 37/47] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c8c874d6..c85582a4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -73,11 +73,11 @@ steps: inputs: secureFile: 'Snap.Hutao.CI.cer' -- task: PublishBuildArtifacts@1 +- task: PublishPipelineArtifact@1 inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: '*' - publishLocation: 'Container' + targetPath: '$(Build.ArtifactStagingDirectory)' + artifact: '*' + publishLocation: 'pipeline' #- task: GitHubRelease@1 # inputs: From c39a198c57a70d40ffe013e57ae5be2031135fd9 Mon Sep 17 00:00:00 2001 From: Masterain Date: Sun, 10 Dec 2023 21:07:17 -0800 Subject: [PATCH 38/47] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c85582a4..22beb308 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -76,7 +76,7 @@ steps: - task: PublishPipelineArtifact@1 inputs: targetPath: '$(Build.ArtifactStagingDirectory)' - artifact: '*' + artifact: 'Snap.Hutao.Alpha-$(version).msix' publishLocation: 'pipeline' #- task: GitHubRelease@1 From a8d4dc84a1b50f01e2245281c655a9c2080480ef Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Mon, 11 Dec 2023 14:31:34 +0800 Subject: [PATCH 39/47] impl #870 --- .../Snap.Hutao/Control/Theme/Glyph.xaml | 1 + src/Snap.Hutao/Snap.Hutao/Core/Random.cs | 5 + .../Factory/QrCode/IQrCodeFactory.cs | 9 ++ .../Factory/QrCode/QrCodeFactory.cs | 25 ++++ .../Snap.Hutao/Resource/Localization/SH.resx | 6 + src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 7 ++ .../Snap.Hutao/View/Dialog/QrCodeDialog.xaml | 21 ++++ .../View/Dialog/QrCodeDialog.xaml.cs | 107 ++++++++++++++++++ src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 7 ++ .../ViewModel/User/UserViewModel.cs | 28 +++++ src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs | 23 +++- .../Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs | 14 +++ .../Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs | 63 +++++++++++ .../Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs | 11 ++ .../Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs | 20 ++++ .../Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs | 14 +++ .../Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs | 24 ++++ .../QrCode/QrCodeQueryPayload.Constant.cs | 13 +++ .../Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs | 17 +++ .../Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs | 14 +++ .../Takumi/Account/GameTokenWrapper.cs | 13 +++ .../Takumi/Account/SessionAppClient.cs | 48 ++++++++ .../Web/Response/KnownReturnCode.cs | 5 + 23 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml index 555f142e..d658fc29 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml @@ -17,4 +17,5 @@ + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Random.cs b/src/Snap.Hutao/Snap.Hutao/Core/Random.cs index 74afc366..f1427404 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Random.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Random.cs @@ -19,4 +19,9 @@ internal static class Random { return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyz".AsSpan(), length)); } + + public static string GetLetterAndNumberString(int length) + { + return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".AsSpan(), length)); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs new file mode 100644 index 00000000..3381dc01 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs @@ -0,0 +1,9 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Factory.QrCode; + +internal interface IQrCodeFactory +{ + byte[] CreateByteArr(string source); +} diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs new file mode 100644 index 00000000..b16d0d29 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using QRCoder; + +namespace Snap.Hutao.Factory.QrCode; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IQrCodeFactory))] +internal class QrCodeFactory : IQrCodeFactory +{ + public byte[] CreateByteArr(string source) + { + using (QRCodeGenerator generator = new()) + { + using (QRCodeData data = generator.CreateQrCode(source, QRCodeGenerator.ECCLevel.Q)) + { + using (BitmapByteQRCode code = new(data)) + { + return code.GetGraphic(10); + } + } + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 2ba84fb3..42907257 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1265,6 +1265,9 @@ 正在转换客户端 + + 使用米游社扫描二维码 + 该操作是不可逆的,所有用户登录状态会丢失 @@ -2618,6 +2621,9 @@ 网页登录 + + 扫码登录 + 手动输入 diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index a1ed520d..7f248f3e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -173,6 +173,7 @@ + @@ -301,6 +302,7 @@ + all @@ -329,6 +331,11 @@ + + + MSBuild:Compile + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml new file mode 100644 index 00000000..bc8e44cb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs new file mode 100644 index 00000000..e14bf962 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs @@ -0,0 +1,107 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using Snap.Hutao.Factory.QrCode; +using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +using Snap.Hutao.Web.Response; +using System.IO; +using System.Text.RegularExpressions; + +namespace Snap.Hutao.View.Dialog; + +/// +/// 扫描二维码对话框 +/// +[HighQuality] +internal sealed partial class QrCodeDialog : ContentDialog +{ + private readonly ITaskContext taskContext; + private readonly QrCodeClient qrCodeClient; + private readonly IQrCodeFactory qrCodeFactory; + + private QrCodeAccount? account; + + /// + /// 构造一个新的扫描二维码对话框 + /// + /// 服务提供器 + public QrCodeDialog(IServiceProvider serviceProvider) + { + InitializeComponent(); + + taskContext = serviceProvider.GetRequiredService(); + qrCodeClient = serviceProvider.GetRequiredService(); + qrCodeFactory = serviceProvider.GetRequiredService(); + + Initialize().SafeForget(); + } + + /// + /// 获取登录的用户 + /// + /// QrCodeAccount + public async ValueTask> GetAccountAsync() + { + await taskContext.SwitchToMainThreadAsync(); + ContentDialogResult result = await ShowAsync(); + return new(account is not null, account!); + } + + private async ValueTask Initialize() + { + Response fetch = await qrCodeClient.PostQrCodeFetchAsync().ConfigureAwait(false); + if (fetch.IsOk()) + { + string url = Regex.Unescape(fetch.Data.Url); + string ticket = url.ToUri().Query.Split('&').Last().Split('=').Last(); + + await taskContext.SwitchToMainThreadAsync(); + BitmapImage bitmap = new(); + await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.CreateByteArr(url)).AsRandomAccessStream()); + + ImageView.Source = bitmap; + if (bitmap is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) + { + VisualStateManager.GoToState(this, "Loaded", true); + } + + await taskContext.SwitchToBackgroundAsync(); + + using (PeriodicTimer timer = new(TimeSpan.FromSeconds(3))) + { + while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) + { + Response query = await qrCodeClient.PostQrCodeQueryAsync(ticket).ConfigureAwait(false); + if (query.IsOk(false)) + { + switch (query.Data.Stat) + { + case QrCodeQueryStatus.INIT: + case QrCodeQueryStatus.SCANNED: + break; // @switch + case QrCodeQueryStatus.CONFIRMED: + if (query.Data.Payload.Proto == QrCodeQueryPayload.ACCOUNT) + { + account = JsonSerializer.Deserialize(query.Data.Payload.Raw); + await taskContext.SwitchToMainThreadAsync(); + Hide(); + return; + } + + break; // @switch + } + } + else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) + { + Initialize().SafeForget(); + break; // @while + } + } + } + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index 0fbae96e..fe59a7b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -154,6 +154,13 @@ Command="{Binding LoginMihoyoUserCommand}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentWebsite}}" Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/> + ? users; @@ -173,6 +178,29 @@ internal sealed partial class UserViewModel : ObservableObject } } + [Command("LoginQrCodeCommand")] + private async Task LoginQrCode() + { + // ContentDialog must be created by main thread. + await taskContext.SwitchToMainThreadAsync(); + + QrCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + ValueResult result = await dialog.GetAccountAsync().ConfigureAwait(false); + + if (result.TryGetValue(out QrCodeAccount account)) + { + Response gameTokenResp = await sessionAppClient.PostSTokenByGameTokenAsync(account).ConfigureAwait(false); + + if (gameTokenResp.IsOk()) + { + Cookie stokenV2 = Cookie.FromLoginResult(gameTokenResp.Data); + (UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false); + + await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false); + } + } + } + [Command("RemoveUserCommand")] private async Task RemoveUserAsync(User? user) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index 64ffdee8..e19db0fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -16,6 +16,15 @@ namespace Snap.Hutao.Web; [SuppressMessage("", "SA1124")] internal static class ApiEndpoints { + #region ApiTakumiAccountSessionApp + + /// + /// 通过 GameToken 获取 SToken (V2) + /// + public const string STokenByGameToken = $"{ApiTakumiAccountSessionApp}/getTokenByGameToken"; + + #endregion + #region ApiTakumiAuthApi /// @@ -40,6 +49,7 @@ internal static class ApiEndpoints { return $"{ApiTakumiAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3"; } + #endregion #region ApiTaKumiBindingApi @@ -357,9 +367,18 @@ internal static class ApiEndpoints // https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?key=eYd89JmJ&language=zh-cn&launcher_id=18 #endregion + #region Hk4eSdk + + public const string QrCodeFetch = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/fetch"; + + public const string QrCodeQuery = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/query"; + + #endregion + #region Hosts | Queries private const string ApiTakumi = "https://api-takumi.mihoyo.com"; private const string ApiTakumiAuthApi = $"{ApiTakumi}/auth/api"; + private const string ApiTakumiAccountSessionApp = $"{ApiTakumi}/account/ma-cn-session/app"; private const string ApiTaKumiBindingApi = $"{ApiTakumi}/binding/api"; private const string ApiTakumiCardApi = $"{ApiTakumiRecord}/game_record/app/card/api"; @@ -382,6 +401,8 @@ internal static class ApiEndpoints private const string Hk4eApi = "https://hk4e-api.mihoyo.com"; private const string Hk4eApiAnnouncementApi = $"{Hk4eApi}/common/hk4e_cn/announcement/api"; + private const string Hk4eSdk = "https://hk4e-sdk.mihoyo.com"; + private const string PassportApi = "https://passport-api.mihoyo.com"; private const string PassportApiAuthApi = $"{PassportApi}/account/auth/api"; private const string PassportApiV4 = "https://passport-api-v4.mihoyo.com"; @@ -402,4 +423,4 @@ internal static class ApiEndpoints private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000"; #endregion -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs new file mode 100644 index 00000000..9c48cd45 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeAccount +{ + [JsonPropertyName("uid")] + public string Stuid { get; set; } = default!; + + [JsonPropertyName("token")] + public string GameToken { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs new file mode 100644 index 00000000..7c5d5020 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs @@ -0,0 +1,63 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Web.Request.Builder; +using Snap.Hutao.Web.Request.Builder.Abstraction; +using Snap.Hutao.Web.Response; +using System.Net.Http; + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +[ConstructorGenerated(ResolveHttpClient = true)] +[HttpClient(HttpClientConfiguration.Default)] +internal sealed partial class QrCodeClient +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + private readonly string device = Core.Random.GetLetterAndNumberString(64); + + /// + /// 异步获取扫码链接 + /// + /// 取消令牌 + /// login url + public async ValueTask> PostQrCodeFetchAsync(CancellationToken token = default) + { + QrCodeFetchOptions options = new(4, device); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeFetch) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + /// + /// 异步获取扫码状态 + /// + /// 扫码链接中的ticket + /// 取消令牌/param> + /// 扫码状态 + public async ValueTask> PostQrCodeQueryAsync(string ticket, CancellationToken token = default) + { + QrCodeQueryOptions options = new(4, device, ticket); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeQuery) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs new file mode 100644 index 00000000..bac70add --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeFetch +{ + [JsonPropertyName("url")] + public string Url { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs new file mode 100644 index 00000000..df689141 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeFetchOptions +{ + public QrCodeFetchOptions(int appId, string device) + { + AppId = appId; + Device = device; + } + + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs new file mode 100644 index 00000000..3091ec4e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeQuery +{ + [JsonPropertyName("stat")] + public string Stat { get; set; } = default!; + + [JsonPropertyName("payload")] + public QrCodeQueryPayload Payload { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs new file mode 100644 index 00000000..9f2806d8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeQueryOptions +{ + public QrCodeQueryOptions(int appId, string device, string ticket) + { + AppId = appId; + Device = device; + Ticket = ticket; + } + + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; + + [JsonPropertyName("ticket")] + public string Ticket { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs new file mode 100644 index 00000000..d71edfd4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +/// +/// Proto 常量 +/// +internal sealed partial class QrCodeQueryPayload +{ + public const string ACCOUNT = "Account"; + public const string RAW = "Raw"; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs new file mode 100644 index 00000000..e66b64ba --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed partial class QrCodeQueryPayload +{ + [JsonPropertyName("proto")] + public string Proto { get; set; } = default!; + + [JsonPropertyName("raw")] + public string Raw { get; set; } = default!; + + [JsonPropertyName("ext")] + public string Ext { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs new file mode 100644 index 00000000..ce0e9813 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +/// +/// 扫码状态 +/// +internal sealed partial class QrCodeQueryStatus +{ + public const string INIT = "Init"; + public const string SCANNED = "Scanned"; + public const string CONFIRMED = "Confirmed"; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs new file mode 100644 index 00000000..1b62d7cb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Account; + +internal sealed class GameTokenWrapper +{ + [JsonPropertyName("account_id")] + public int Stuid { get; set; } = default!; + + [JsonPropertyName("game_token")] + public string GameToken { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs new file mode 100644 index 00000000..6cd5b0bf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs @@ -0,0 +1,48 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +using Snap.Hutao.Web.Hoyolab.Passport; +using Snap.Hutao.Web.Request.Builder; +using Snap.Hutao.Web.Request.Builder.Abstraction; +using Snap.Hutao.Web.Response; +using System.Globalization; +using System.Net.Http; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Account; + +[HighQuality] +[ConstructorGenerated(ResolveHttpClient = true)] +[HttpClient(HttpClientConfiguration.XRpc2)] +internal sealed partial class SessionAppClient +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + /// + /// 通过 GameToken 获取 SToken (V2) + /// + /// 扫码获得的账户信息 + /// 取消令牌 + /// 登录结果 + public async ValueTask> PostSTokenByGameTokenAsync(QrCodeAccount account, CancellationToken token = default) + { + GameTokenWrapper wrapper = new() + { + Stuid = int.Parse(account.Stuid, CultureInfo.CurrentCulture), + GameToken = account.GameToken, + }; + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.STokenByGameToken) + .PostJson(wrapper); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index b5cc3540..d1f701b1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -138,4 +138,9 @@ internal enum KnownReturnCode /// 实时便笺 /// CODE10104 = 10104, + + /// + /// 二维码已过期 + /// + QrCodeExpired = -106, } From 2fb6cd344174861410cce4f54def1bb6f0868452 Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Mon, 11 Dec 2023 18:47:41 +0800 Subject: [PATCH 40/47] code style (?) --- .../Factory/QrCode/IQrCodeFactory.cs | 2 +- .../Factory/QrCode/QrCodeFactory.cs | 2 +- .../Snap.Hutao/View/Dialog/QrCodeDialog.xaml | 1 - .../View/Dialog/QrCodeDialog.xaml.cs | 71 ++++++++++--------- src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 2 +- .../ViewModel/User/UserViewModel.cs | 9 ++- src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs | 15 ++-- .../Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs | 63 ---------------- .../Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs | 20 ------ .../Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs | 14 ---- .../Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs | 24 ------- .../Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs | 14 ---- .../Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs | 5 ++ .../Passport/GameLoginRequestOptions.cs | 26 +++++++ .../GameLoginRequestResult.cs} | 7 +- .../Web/Hoyolab/Passport/GameLoginResult.cs | 21 ++++++ .../Passport/GameLoginResultOptions.cs | 30 ++++++++ .../GameLoginResultPayload.ProtoTypes.cs} | 4 +- .../GameLoginResultPayload.cs} | 5 +- .../Hoyolab/Passport/GameLoginResultStatus.cs | 14 ++++ .../Web/Hoyolab/Passport/PassportClient2.cs | 41 +++++++++++ .../UidGameToken.cs} | 6 +- .../Takumi/Account/SessionAppClient.cs | 7 +- .../Web/Response/KnownReturnCode.cs | 10 +-- 24 files changed, 209 insertions(+), 204 deletions(-) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Hk4e/QrCode/QrCodeFetch.cs => Passport/GameLoginRequestResult.cs} (56%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Hk4e/QrCode/QrCodeQueryPayload.Constant.cs => Passport/GameLoginResultPayload.ProtoTypes.cs} (76%) rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Hk4e/QrCode/QrCodeQueryPayload.cs => Passport/GameLoginResultPayload.cs} (77%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Hk4e/QrCode/QrCodeAccount.cs => Passport/UidGameToken.cs} (63%) diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs index 3381dc01..0ab732fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs @@ -5,5 +5,5 @@ namespace Snap.Hutao.Factory.QrCode; internal interface IQrCodeFactory { - byte[] CreateByteArr(string source); + byte[] CreateQrCodeByteArray(string source); } diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs index b16d0d29..4ee567fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Factory.QrCode; [Injection(InjectAs.Singleton, typeof(IQrCodeFactory))] internal class QrCodeFactory : IQrCodeFactory { - public byte[] CreateByteArr(string source) + public byte[] CreateQrCodeByteArray(string source) { using (QRCodeGenerator generator = new()) { diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml index bc8e44cb..177a814c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml @@ -5,7 +5,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:shcm="using:Snap.Hutao.Control.Markup" - xmlns:shvd="using:Snap.Hutao.View.Dialog" Title="{shcm:ResourceString Name=ViewDialogQrCodeTitle}" CloseButtonText="{shcm:ResourceString Name=ContentDialogCancelCloseButtonText}" DefaultButton="Close" diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs index e14bf962..235f766b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs @@ -3,10 +3,10 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Snap.Hutao.Factory.QrCode; using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +using Snap.Hutao.Web.Hoyolab.Passport; using Snap.Hutao.Web.Response; using System.IO; using System.Text.RegularExpressions; @@ -20,10 +20,10 @@ namespace Snap.Hutao.View.Dialog; internal sealed partial class QrCodeDialog : ContentDialog { private readonly ITaskContext taskContext; - private readonly QrCodeClient qrCodeClient; + private readonly PassportClient2 passportClient2; private readonly IQrCodeFactory qrCodeFactory; - private QrCodeAccount? account; + private UidGameToken? account; /// /// 构造一个新的扫描二维码对话框 @@ -34,26 +34,27 @@ internal sealed partial class QrCodeDialog : ContentDialog InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - qrCodeClient = serviceProvider.GetRequiredService(); + passportClient2 = serviceProvider.GetRequiredService(); qrCodeFactory = serviceProvider.GetRequiredService(); - Initialize().SafeForget(); + FetchQrCodeAsync().SafeForget(); } /// /// 获取登录的用户 /// /// QrCodeAccount - public async ValueTask> GetAccountAsync() + [SuppressMessage("", "SH007")] + public async ValueTask> GetAccountAsync() { await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); return new(account is not null, account!); } - private async ValueTask Initialize() + private async ValueTask FetchQrCodeAsync() { - Response fetch = await qrCodeClient.PostQrCodeFetchAsync().ConfigureAwait(false); + Response fetch = await passportClient2.PostQrCodeFetchAsync().ConfigureAwait(false); if (fetch.IsOk()) { string url = Regex.Unescape(fetch.Data.Url); @@ -61,7 +62,7 @@ internal sealed partial class QrCodeDialog : ContentDialog await taskContext.SwitchToMainThreadAsync(); BitmapImage bitmap = new(); - await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.CreateByteArr(url)).AsRandomAccessStream()); + await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.CreateQrCodeByteArray(url)).AsRandomAccessStream()); ImageView.Source = bitmap; if (bitmap is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) @@ -70,37 +71,41 @@ internal sealed partial class QrCodeDialog : ContentDialog } await taskContext.SwitchToBackgroundAsync(); + await CheckStatusAsync(ticket).ConfigureAwait(false); + } + } - using (PeriodicTimer timer = new(TimeSpan.FromSeconds(3))) + private async ValueTask CheckStatusAsync(string ticket) + { + using (PeriodicTimer timer = new(TimeSpan.FromSeconds(3))) + { + while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { - while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) + Response query = await passportClient2.PostQrCodeQueryAsync(ticket).ConfigureAwait(false); + if (query.IsOk(false)) { - Response query = await qrCodeClient.PostQrCodeQueryAsync(ticket).ConfigureAwait(false); - if (query.IsOk(false)) + switch (query.Data.Stat) { - switch (query.Data.Stat) - { - case QrCodeQueryStatus.INIT: - case QrCodeQueryStatus.SCANNED: - break; // @switch - case QrCodeQueryStatus.CONFIRMED: - if (query.Data.Payload.Proto == QrCodeQueryPayload.ACCOUNT) - { - account = JsonSerializer.Deserialize(query.Data.Payload.Raw); - await taskContext.SwitchToMainThreadAsync(); - Hide(); - return; - } + case GameLoginResultStatus.Init: + case GameLoginResultStatus.Scanned: + break; // @switch + case GameLoginResultStatus.Confirmed: + if (query.Data.Payload.Proto == GameLoginResultPayload.ACCOUNT) + { + account = JsonSerializer.Deserialize(query.Data.Payload.Raw); + await taskContext.SwitchToMainThreadAsync(); + Hide(); + return; // Stop timer + } - break; // @switch - } - } - else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) - { - Initialize().SafeForget(); - break; // @while + break; // @switch } } + else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) + { + FetchQrCodeAsync().SafeForget(); + break; // @while + } } } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index fe59a7b2..d58233b3 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -158,7 +158,7 @@ Width="{StaticResource LargeAppBarButtonWidth}" MaxWidth="{StaticResource LargeAppBarButtonWidth}" Margin="2,-4" - Command="{Binding LoginQrCodeCommand}" + Command="{Binding LoginByQrCodeCommand}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}" Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQrCodeAction}"/> ().ConfigureAwait(false); - ValueResult result = await dialog.GetAccountAsync().ConfigureAwait(false); + ValueResult result = await dialog.GetAccountAsync().ConfigureAwait(false); - if (result.TryGetValue(out QrCodeAccount account)) + if (result.TryGetValue(out UidGameToken account)) { Response gameTokenResp = await sessionAppClient.PostSTokenByGameTokenAsync(account).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index e19db0fc..95032e1f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -16,15 +16,6 @@ namespace Snap.Hutao.Web; [SuppressMessage("", "SA1124")] internal static class ApiEndpoints { - #region ApiTakumiAccountSessionApp - - /// - /// 通过 GameToken 获取 SToken (V2) - /// - public const string STokenByGameToken = $"{ApiTakumiAccountSessionApp}/getTokenByGameToken"; - - #endregion - #region ApiTakumiAuthApi /// @@ -304,6 +295,11 @@ internal static class ApiEndpoints /// public const string AccountGetLTokenBySToken = $"{PassportApiAuthApi}/getLTokenBySToken"; + /// + /// 通过GameToken获取V2SToken + /// + public const string AccountGetSTokenByGameToken = $"{PassportApi}/account/ma-cn-session/app/getTokenByGameToken"; + /// /// 获取V2SToken /// @@ -378,7 +374,6 @@ internal static class ApiEndpoints #region Hosts | Queries private const string ApiTakumi = "https://api-takumi.mihoyo.com"; private const string ApiTakumiAuthApi = $"{ApiTakumi}/auth/api"; - private const string ApiTakumiAccountSessionApp = $"{ApiTakumi}/account/ma-cn-session/app"; private const string ApiTaKumiBindingApi = $"{ApiTakumi}/binding/api"; private const string ApiTakumiCardApi = $"{ApiTakumiRecord}/game_record/app/card/api"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs deleted file mode 100644 index 7c5d5020..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; -using Snap.Hutao.Web.Request.Builder; -using Snap.Hutao.Web.Request.Builder.Abstraction; -using Snap.Hutao.Web.Response; -using System.Net.Http; - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -[HighQuality] -[ConstructorGenerated(ResolveHttpClient = true)] -[HttpClient(HttpClientConfiguration.Default)] -internal sealed partial class QrCodeClient -{ - private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; - private readonly HttpClient httpClient; - - private readonly string device = Core.Random.GetLetterAndNumberString(64); - - /// - /// 异步获取扫码链接 - /// - /// 取消令牌 - /// login url - public async ValueTask> PostQrCodeFetchAsync(CancellationToken token = default) - { - QrCodeFetchOptions options = new(4, device); - - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.QrCodeFetch) - .PostJson(options); - - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) - .ConfigureAwait(false); - - return Response.Response.DefaultIfNull(resp); - } - - /// - /// 异步获取扫码状态 - /// - /// 扫码链接中的ticket - /// 取消令牌/param> - /// 扫码状态 - public async ValueTask> PostQrCodeQueryAsync(string ticket, CancellationToken token = default) - { - QrCodeQueryOptions options = new(4, device, ticket); - - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.QrCodeQuery) - .PostJson(options); - - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) - .ConfigureAwait(false); - - return Response.Response.DefaultIfNull(resp); - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs deleted file mode 100644 index df689141..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -[HighQuality] -internal sealed class QrCodeFetchOptions -{ - public QrCodeFetchOptions(int appId, string device) - { - AppId = appId; - Device = device; - } - - [JsonPropertyName("app_id")] - public int AppId { get; set; } - - [JsonPropertyName("device")] - public string Device { get; set; } = default!; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs deleted file mode 100644 index 3091ec4e..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -[HighQuality] -internal sealed class QrCodeQuery -{ - [JsonPropertyName("stat")] - public string Stat { get; set; } = default!; - - [JsonPropertyName("payload")] - public QrCodeQueryPayload Payload { get; set; } = default!; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs deleted file mode 100644 index 9f2806d8..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -[HighQuality] -internal sealed class QrCodeQueryOptions -{ - public QrCodeQueryOptions(int appId, string device, string ticket) - { - AppId = appId; - Device = device; - Ticket = ticket; - } - - [JsonPropertyName("app_id")] - public int AppId { get; set; } - - [JsonPropertyName("device")] - public string Device { get; set; } = default!; - - [JsonPropertyName("ticket")] - public string Ticket { get; set; } = default!; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs deleted file mode 100644 index ce0e9813..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -/// -/// 扫码状态 -/// -internal sealed partial class QrCodeQueryStatus -{ - public const string INIT = "Init"; - public const string SCANNED = "Scanned"; - public const string CONFIRMED = "Confirmed"; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs index f5e391a6..4610b2c9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs @@ -37,6 +37,11 @@ internal sealed class HoyolabOptions : IOptions /// public static string DeviceId { get; } = Guid.NewGuid().ToString(); + /// + /// 扫码登录设备Id + /// + public static string Device { get; } = Core.Random.GetLetterAndNumberString(64); + /// /// 盐 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs new file mode 100644 index 00000000..fb3b77a4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +/// +/// 扫码登录请求配置 +/// +[HighQuality] +internal sealed class GameLoginRequestOptions +{ + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; + + public static GameLoginRequestOptions Create(int appId, string device) + { + return new GameLoginRequestOptions + { + AppId = appId, + Device = device, + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs similarity index 56% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs index bac70add..13f067b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs @@ -1,10 +1,13 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +namespace Snap.Hutao.Web.Hoyolab.Passport; +/// +/// 扫码登录请求结果 +/// [HighQuality] -internal sealed class QrCodeFetch +internal sealed class GameLoginRequestResult { [JsonPropertyName("url")] public string Url { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs new file mode 100644 index 00000000..c3757f69 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Json.Annotation; +using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +/// +/// 扫码登录结果 +/// +[HighQuality] +internal sealed class GameLoginResult +{ + [JsonPropertyName("stat")] + [JsonEnum(JsonSerializeType.String)] + public GameLoginResultStatus Stat { get; set; } = default!; + + [JsonPropertyName("payload")] + public GameLoginResultPayload Payload { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs new file mode 100644 index 00000000..d5f17cd6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +/// +/// 扫码登录结果请求配置 +/// +[HighQuality] +internal sealed class GameLoginResultOptions +{ + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; + + [JsonPropertyName("ticket")] + public string Ticket { get; set; } = default!; + + public static GameLoginResultOptions Create(int appId, string device, string ticket) + { + return new GameLoginResultOptions + { + AppId = appId, + Device = device, + Ticket = ticket, + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs similarity index 76% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs index d71edfd4..179b7501 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs @@ -4,9 +4,9 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; /// -/// Proto 常量 +/// Proto 类型常量 /// -internal sealed partial class QrCodeQueryPayload +internal sealed partial class GameLoginResultPayload { public const string ACCOUNT = "Account"; public const string RAW = "Raw"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs similarity index 77% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs index e66b64ba..8d7cd653 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs @@ -3,8 +3,11 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +/// +/// 扫码登录结果Payload +/// [HighQuality] -internal sealed partial class QrCodeQueryPayload +internal sealed partial class GameLoginResultPayload { [JsonPropertyName("proto")] public string Proto { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs new file mode 100644 index 00000000..9f95988f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +/// +/// 扫码状态 +/// +internal enum GameLoginResultStatus +{ + Init, + Scanned, + Confirmed, +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs index f6e3713f..be261a77 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs @@ -68,6 +68,47 @@ internal sealed partial class PassportClient2 return Response.Response.DefaultIfNull(resp); } + /// + /// 异步获取扫码链接 + /// + /// 取消令牌 + /// 二维码原始链接 + public async ValueTask> PostQrCodeFetchAsync(CancellationToken token = default) + { + GameLoginRequestOptions options = GameLoginRequestOptions.Create(4, HoyolabOptions.Device); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeFetch) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + /// + /// 异步获取扫码状态 + /// + /// 扫码链接中的ticket + /// 取消令牌 + /// 扫码结果 + public async ValueTask> PostQrCodeQueryAsync(string ticket, CancellationToken token = default) + { + GameLoginResultOptions options = GameLoginResultOptions.Create(4, HoyolabOptions.Device, ticket); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeQuery) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + private class Timestamp { [JsonPropertyName("t")] diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs similarity index 63% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs index 9c48cd45..7f2ffc47 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs @@ -1,13 +1,13 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +namespace Snap.Hutao.Web.Hoyolab.Passport; [HighQuality] -internal sealed class QrCodeAccount +internal sealed class UidGameToken { [JsonPropertyName("uid")] - public string Stuid { get; set; } = default!; + public string Uid { get; set; } = default!; [JsonPropertyName("token")] public string GameToken { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs index 6cd5b0bf..57f1c63d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; -using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; using Snap.Hutao.Web.Hoyolab.Passport; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; @@ -27,16 +26,16 @@ internal sealed partial class SessionAppClient /// 扫码获得的账户信息 /// 取消令牌 /// 登录结果 - public async ValueTask> PostSTokenByGameTokenAsync(QrCodeAccount account, CancellationToken token = default) + public async ValueTask> PostSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default) { GameTokenWrapper wrapper = new() { - Stuid = int.Parse(account.Stuid, CultureInfo.CurrentCulture), + Stuid = int.Parse(account.Uid, CultureInfo.CurrentCulture), GameToken = account.GameToken, }; HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.STokenByGameToken) + .SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken) .PostJson(wrapper); Response? resp = await builder diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index d1f701b1..5725680a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -84,6 +84,11 @@ internal enum KnownReturnCode /// AppIdError = -109, + /// + /// 二维码已过期 + /// + QrCodeExpired = -106, + /// /// 验证密钥过期 /// @@ -138,9 +143,4 @@ internal enum KnownReturnCode /// 实时便笺 /// CODE10104 = 10104, - - /// - /// 二维码已过期 - /// - QrCodeExpired = -106, } From 217586fece779c2add1719a52be6858c88b1764f Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Mon, 11 Dec 2023 22:55:47 +0800 Subject: [PATCH 41/47] Device needs rework --- .../Snap.Hutao/Control/Theme/Glyph.xaml | 3 +- ...EnumerableExtension.NameValueCollection.cs | 2 +- .../IQRCodeFactory.cs} | 6 +- .../QRCodeFactory.cs} | 6 +- .../Snap.Hutao/Resource/Localization/SH.resx | 4 +- .../GachaLogQueryManualInputProvider.cs | 2 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 4 +- .../View/Dialog/QrCodeDialog.xaml.cs | 112 ------------- ...rCodeDialog.xaml => UserQRCodeDialog.xaml} | 11 +- .../View/Dialog/UserQRCodeDialog.xaml.cs | 156 ++++++++++++++++++ src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 2 +- .../ViewModel/User/UserViewModel.cs | 4 +- src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs | 18 +- .../Web/Hoyolab/Passport/GameLoginResult.cs | 4 +- .../GameLoginResultPayload.ProtoTypes.cs | 13 -- .../Hoyolab/Passport/GameLoginResultStatus.cs | 14 -- .../Web/Hoyolab/Passport/PassportClient2.cs | 8 +- ...ameLoginRequestResult.cs => UrlWrapper.cs} | 6 +- 18 files changed, 194 insertions(+), 181 deletions(-) rename src/Snap.Hutao/Snap.Hutao/Factory/{QrCode/IQrCodeFactory.cs => QuickResponse/IQRCodeFactory.cs} (60%) rename src/Snap.Hutao/Snap.Hutao/Factory/{QrCode/QrCodeFactory.cs => QuickResponse/QRCodeFactory.cs} (76%) delete mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs rename src/Snap.Hutao/Snap.Hutao/View/Dialog/{QrCodeDialog.xaml => UserQRCodeDialog.xaml} (69%) create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/{GameLoginRequestResult.cs => UrlWrapper.cs} (64%) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml index d658fc29..4f8f0190 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml @@ -1,4 +1,5 @@ + @@ -12,10 +13,10 @@ + - \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs index 9d3840c0..2f4bafda 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs @@ -7,7 +7,7 @@ namespace Snap.Hutao.Extension; internal static partial class EnumerableExtension { - public static bool TryGetValue(this NameValueCollection collection, string name, [NotNullWhen(true)] out string? value) + public static bool TryGetSingleValue(this NameValueCollection collection, string name, [NotNullWhen(true)] out string? value) { if (collection.AllKeys.Contains(name)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs similarity index 60% rename from src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs rename to src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs index 0ab732fa..8ba3ea82 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs @@ -3,7 +3,7 @@ namespace Snap.Hutao.Factory.QrCode; -internal interface IQrCodeFactory +internal interface IQRCodeFactory { - byte[] CreateQrCodeByteArray(string source); -} + byte[] Create(string source); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs similarity index 76% rename from src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs rename to src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs index 4ee567fc..fb28e857 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs @@ -6,10 +6,10 @@ using QRCoder; namespace Snap.Hutao.Factory.QrCode; [ConstructorGenerated] -[Injection(InjectAs.Singleton, typeof(IQrCodeFactory))] -internal class QrCodeFactory : IQrCodeFactory +[Injection(InjectAs.Singleton, typeof(IQRCodeFactory))] +internal class QRCodeFactory : IQRCodeFactory { - public byte[] CreateQrCodeByteArray(string source) + public byte[] Create(string source) { using (QRCodeGenerator generator = new()) { diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 42907257..bcbcfdb2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1265,7 +1265,7 @@ 正在转换客户端 - + 使用米游社扫描二维码 @@ -2621,7 +2621,7 @@ 网页登录 - + 扫码登录 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs index 9e7b98fc..45be4b6b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -30,7 +30,7 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP { NameValueCollection query = HttpUtility.ParseQueryString(queryString); - if (query.TryGetValue("auth_appid", out string? appId) && appId is "webview_gacha") + if (query.TryGetSingleValue("auth_appid", out string? appId) && appId is "webview_gacha") { string? queryLanguageCode = query["lang"]; if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode)) diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 7f248f3e..10c37a81 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -173,8 +173,8 @@ - + @@ -332,7 +332,7 @@ - + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs deleted file mode 100644 index 235f766b..00000000 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media.Imaging; -using Snap.Hutao.Factory.QrCode; -using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; -using Snap.Hutao.Web.Hoyolab.Passport; -using Snap.Hutao.Web.Response; -using System.IO; -using System.Text.RegularExpressions; - -namespace Snap.Hutao.View.Dialog; - -/// -/// 扫描二维码对话框 -/// -[HighQuality] -internal sealed partial class QrCodeDialog : ContentDialog -{ - private readonly ITaskContext taskContext; - private readonly PassportClient2 passportClient2; - private readonly IQrCodeFactory qrCodeFactory; - - private UidGameToken? account; - - /// - /// 构造一个新的扫描二维码对话框 - /// - /// 服务提供器 - public QrCodeDialog(IServiceProvider serviceProvider) - { - InitializeComponent(); - - taskContext = serviceProvider.GetRequiredService(); - passportClient2 = serviceProvider.GetRequiredService(); - qrCodeFactory = serviceProvider.GetRequiredService(); - - FetchQrCodeAsync().SafeForget(); - } - - /// - /// 获取登录的用户 - /// - /// QrCodeAccount - [SuppressMessage("", "SH007")] - public async ValueTask> GetAccountAsync() - { - await taskContext.SwitchToMainThreadAsync(); - ContentDialogResult result = await ShowAsync(); - return new(account is not null, account!); - } - - private async ValueTask FetchQrCodeAsync() - { - Response fetch = await passportClient2.PostQrCodeFetchAsync().ConfigureAwait(false); - if (fetch.IsOk()) - { - string url = Regex.Unescape(fetch.Data.Url); - string ticket = url.ToUri().Query.Split('&').Last().Split('=').Last(); - - await taskContext.SwitchToMainThreadAsync(); - BitmapImage bitmap = new(); - await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.CreateQrCodeByteArray(url)).AsRandomAccessStream()); - - ImageView.Source = bitmap; - if (bitmap is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) - { - VisualStateManager.GoToState(this, "Loaded", true); - } - - await taskContext.SwitchToBackgroundAsync(); - await CheckStatusAsync(ticket).ConfigureAwait(false); - } - } - - private async ValueTask CheckStatusAsync(string ticket) - { - using (PeriodicTimer timer = new(TimeSpan.FromSeconds(3))) - { - while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) - { - Response query = await passportClient2.PostQrCodeQueryAsync(ticket).ConfigureAwait(false); - if (query.IsOk(false)) - { - switch (query.Data.Stat) - { - case GameLoginResultStatus.Init: - case GameLoginResultStatus.Scanned: - break; // @switch - case GameLoginResultStatus.Confirmed: - if (query.Data.Payload.Proto == GameLoginResultPayload.ACCOUNT) - { - account = JsonSerializer.Deserialize(query.Data.Payload.Raw); - await taskContext.SwitchToMainThreadAsync(); - Hide(); - return; // Stop timer - } - - break; // @switch - } - } - else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) - { - FetchQrCodeAsync().SafeForget(); - break; // @while - } - } - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml similarity index 69% rename from src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml rename to src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml index 177a814c..62e88581 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml @@ -1,11 +1,12 @@ + Width="320" + Height="320" + Source="{x:Bind QRCodeSource}"/> diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs new file mode 100644 index 00000000..a9f55887 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs @@ -0,0 +1,156 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using Snap.Hutao.Factory.QrCode; +using Snap.Hutao.Web.Hoyolab.Passport; +using Snap.Hutao.Web.Response; +using System.Collections.Specialized; +using System.IO; +using System.Web; + +namespace Snap.Hutao.View.Dialog; + +[DependencyProperty("QRCodeSource", typeof(ImageSource))] +internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable +{ + private readonly ITaskContext taskContext; + private readonly PassportClient2 passportClient2; + private readonly IQRCodeFactory qrCodeFactory; + + private readonly CancellationTokenSource userManualCancellationTokenSource = new(); + private bool disposed; + + public UserQRCodeDialog(IServiceProvider serviceProvider) + { + InitializeComponent(); + + taskContext = serviceProvider.GetRequiredService(); + passportClient2 = serviceProvider.GetRequiredService(); + qrCodeFactory = serviceProvider.GetRequiredService(); + } + + ~UserQRCodeDialog() + { + Dispose(); + } + + public void Dispose() + { + if (!disposed) + { + userManualCancellationTokenSource.Dispose(); + disposed = true; + } + + GC.SuppressFinalize(this); + } + + public async ValueTask> GetUidGameTokenAsync() + { + try + { + return await GetUidGameTokenCoreAsync().ConfigureAwait(false); + } + finally + { + userManualCancellationTokenSource.Dispose(); + } + } + + [Command("CancelCommand")] + private void Cancel() + { + userManualCancellationTokenSource.Cancel(); + } + + private async ValueTask> GetUidGameTokenCoreAsync() + { + await taskContext.SwitchToMainThreadAsync(); + await ShowAsync(); + + while (!userManualCancellationTokenSource.IsCancellationRequested) + { + try + { + CancellationToken token = userManualCancellationTokenSource.Token; + string ticket = await FetchQRCodeAndSetImageAsync(token).ConfigureAwait(false); + UidGameToken? uidGameToken = await WaitQueryQRCodeConfirmAsync(ticket, token).ConfigureAwait(false); + + if (uidGameToken is null) + { + continue; + } + + await taskContext.SwitchToMainThreadAsync(); + Hide(); + return new(true, uidGameToken); + } + catch (OperationCanceledException) + { + break; + } + } + + return new(false, default!); + } + + private async ValueTask FetchQRCodeAndSetImageAsync(CancellationToken token) + { + Response fetchResponse = await passportClient2.QRCodeFetchAsync(token).ConfigureAwait(false); + if (!fetchResponse.IsOk()) + { + return string.Empty; + } + + string url = fetchResponse.Data.Url; + string ticket = GetTicketFromUrl(fetchResponse.Data.Url); + + await taskContext.SwitchToMainThreadAsync(); + + BitmapImage bitmap = new(); + await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.Create(url)).AsRandomAccessStream()); + QRCodeSource = bitmap; + + return ticket; + + static string GetTicketFromUrl(in ReadOnlySpan urlSpan) + { + ReadOnlySpan querySpan = urlSpan[urlSpan.IndexOf('?')..]; + NameValueCollection queryCollection = HttpUtility.ParseQueryString(querySpan.ToString()); + if (queryCollection.TryGetSingleValue("ticket", out string? ticket)) + { + return ticket; + } + + return string.Empty; + } + } + + private async ValueTask WaitQueryQRCodeConfirmAsync(string ticket, CancellationToken token) + { + using (PeriodicTimer timer = new(new(0, 0, 3))) + { + while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) + { + Response query = await passportClient2.QRCodeQueryAsync(ticket, token).ConfigureAwait(false); + + if (query is { ReturnCode: 0, Data: { Stat: "Confirmed", Payload.Proto: "Account" } }) + { + UidGameToken? uidGameToken = JsonSerializer.Deserialize(query.Data.Payload.Raw); + ArgumentNullException.ThrowIfNull(uidGameToken); + return uidGameToken; + } + else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) + { + break; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index d58233b3..bff2ca0b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -160,7 +160,7 @@ Margin="2,-4" Command="{Binding LoginByQrCodeCommand}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}" - Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQrCodeAction}"/> + Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/> ().ConfigureAwait(false); - ValueResult result = await dialog.GetAccountAsync().ConfigureAwait(false); + UserQRCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + ValueResult result = await dialog.GetUidGameTokenAsync().ConfigureAwait(false); if (result.TryGetValue(out UidGameToken account)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index 95032e1f..2774436b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -283,6 +283,14 @@ internal static class ApiEndpoints public const string AnnContent = $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery}"; #endregion + #region Hk4eSdk + + public const string QrCodeFetch = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/fetch"; + + public const string QrCodeQuery = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/query"; + + #endregion + #region PassportApi | PassportApiV4 /// @@ -363,14 +371,6 @@ internal static class ApiEndpoints // https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?key=eYd89JmJ&language=zh-cn&launcher_id=18 #endregion - #region Hk4eSdk - - public const string QrCodeFetch = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/fetch"; - - public const string QrCodeQuery = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/query"; - - #endregion - #region Hosts | Queries private const string ApiTakumi = "https://api-takumi.mihoyo.com"; private const string ApiTakumiAuthApi = $"{ApiTakumi}/auth/api"; @@ -418,4 +418,4 @@ internal static class ApiEndpoints private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000"; #endregion -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs index c3757f69..bd52a99c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Core.Json.Annotation; using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; namespace Snap.Hutao.Web.Hoyolab.Passport; @@ -13,8 +12,7 @@ namespace Snap.Hutao.Web.Hoyolab.Passport; internal sealed class GameLoginResult { [JsonPropertyName("stat")] - [JsonEnum(JsonSerializeType.String)] - public GameLoginResultStatus Stat { get; set; } = default!; + public string Stat { get; set; } = default!; [JsonPropertyName("payload")] public GameLoginResultPayload Payload { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs deleted file mode 100644 index 179b7501..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -/// -/// Proto 类型常量 -/// -internal sealed partial class GameLoginResultPayload -{ - public const string ACCOUNT = "Account"; - public const string RAW = "Raw"; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs deleted file mode 100644 index 9f95988f..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Passport; - -/// -/// 扫码状态 -/// -internal enum GameLoginResultStatus -{ - Init, - Scanned, - Confirmed, -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs index be261a77..8516723b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs @@ -73,7 +73,7 @@ internal sealed partial class PassportClient2 /// /// 取消令牌 /// 二维码原始链接 - public async ValueTask> PostQrCodeFetchAsync(CancellationToken token = default) + public async ValueTask> QRCodeFetchAsync(CancellationToken token = default) { GameLoginRequestOptions options = GameLoginRequestOptions.Create(4, HoyolabOptions.Device); @@ -81,8 +81,8 @@ internal sealed partial class PassportClient2 .SetRequestUri(ApiEndpoints.QrCodeFetch) .PostJson(options); - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) .ConfigureAwait(false); return Response.Response.DefaultIfNull(resp); @@ -94,7 +94,7 @@ internal sealed partial class PassportClient2 /// 扫码链接中的ticket /// 取消令牌 /// 扫码结果 - public async ValueTask> PostQrCodeQueryAsync(string ticket, CancellationToken token = default) + public async ValueTask> QRCodeQueryAsync(string ticket, CancellationToken token = default) { GameLoginResultOptions options = GameLoginResultOptions.Create(4, HoyolabOptions.Device, ticket); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs similarity index 64% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs index 13f067b2..bda18383 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs @@ -3,11 +3,7 @@ namespace Snap.Hutao.Web.Hoyolab.Passport; -/// -/// 扫码登录请求结果 -/// -[HighQuality] -internal sealed class GameLoginRequestResult +internal sealed class UrlWrapper { [JsonPropertyName("url")] public string Url { get; set; } = default!; From dcf1b01566d64f34f84a4be597d805835dbb9e4a Mon Sep 17 00:00:00 2001 From: qhy040404 <45379733+qhy040404@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:42:08 +0800 Subject: [PATCH 42/47] Update azure-pipelines.yml --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 22beb308..ca0b5b85 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,6 +8,7 @@ # 6. Run trigger: none +pr: none # trigger: # branches: # include: From f4547b60de6e1b81bc604eddde47a4d9e9804bb5 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Tue, 12 Dec 2023 14:22:15 +0800 Subject: [PATCH 43/47] completing --- .../IocHttpClientConfiguration.cs | 6 +- src/Snap.Hutao/Snap.Hutao/Core/Random.cs | 5 -- src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs | 61 +++++++++++++++++++ .../Service/User/UserFingerprintService.cs | 2 +- .../View/Dialog/UserQRCodeDialog.xaml.cs | 9 +-- src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 2 +- .../ViewModel/User/UserViewModel.cs | 33 +++++----- .../Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs | 2 +- .../Sdk/Combo/GameLoginRequest.cs} | 13 ++-- .../Sdk/Combo}/GameLoginResult.cs | 4 +- .../Sdk/Combo}/GameLoginResultPayload.cs | 5 +- .../Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs | 49 +++++++++++++++ .../Sdk/Combo}/UrlWrapper.cs | 2 +- .../Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs | 22 +++++-- .../AccountIdGameToken.cs} | 6 +- .../Passport/GameLoginRequestOptions.cs | 26 -------- .../Web/Hoyolab/Passport/PassportClient2.cs | 44 ++++--------- .../Takumi/Account/SessionAppClient.cs | 47 -------------- 18 files changed, 177 insertions(+), 161 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Passport/GameLoginResultOptions.cs => Hk4e/Sdk/Combo/GameLoginRequest.cs} (55%) rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Passport => Hk4e/Sdk/Combo}/GameLoginResult.cs (81%) rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Passport => Hk4e/Sdk/Combo}/GameLoginResultPayload.cs (79%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Passport => Hk4e/Sdk/Combo}/UrlWrapper.cs (80%) rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Takumi/Account/GameTokenWrapper.cs => Passport/AccountIdGameToken.cs} (62%) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs index 2a20113f..1d091b32 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -46,7 +46,7 @@ internal static partial class IocHttpClientConfiguration client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson); client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion); client.DefaultRequestHeaders.Add("x-rpc-client_type", "5"); - client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId); + client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36); } /// @@ -62,7 +62,7 @@ internal static partial class IocHttpClientConfiguration client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8"); client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion); client.DefaultRequestHeaders.Add("x-rpc-client_type", "2"); - client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId); + client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36); client.DefaultRequestHeaders.Add("x-rpc-device_name", string.Empty); client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn"); client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0"); @@ -81,7 +81,7 @@ internal static partial class IocHttpClientConfiguration client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.OSVersion); client.DefaultRequestHeaders.Add("x-rpc-client_type", "5"); client.DefaultRequestHeaders.Add("x-rpc-language", "zh-cn"); - client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId); + client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Random.cs b/src/Snap.Hutao/Snap.Hutao/Core/Random.cs index f1427404..74afc366 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Random.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Random.cs @@ -19,9 +19,4 @@ internal static class Random { return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyz".AsSpan(), length)); } - - public static string GetLetterAndNumberString(int length) - { - return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".AsSpan(), length)); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs b/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs new file mode 100644 index 00000000..b0d74b4f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs @@ -0,0 +1,61 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; + +namespace Snap.Hutao.Core; + +internal static class Uuid +{ + public static Guid NewV5(string name, Guid namespaceId) + { + Span namespaceBuffer = stackalloc byte[16]; + Verify.Operation(namespaceId.TryWriteBytes(namespaceBuffer), "Failed to copy namespace guid bytes"); + Span nameBytes = Encoding.UTF8.GetBytes(name); + + if (BitConverter.IsLittleEndian) + { + ReverseEndianness(namespaceBuffer); + } + + Span data = stackalloc byte[namespaceBuffer.Length + nameBytes.Length]; + namespaceBuffer.CopyTo(data); + nameBytes.CopyTo(data[namespaceBuffer.Length..]); + + Span temp = stackalloc byte[20]; + Verify.Operation(SHA1.TryHashData(data, temp, out _), "Failed to compute SHA1 hash of UUID"); + + Span hash = temp[..16]; + + if (BitConverter.IsLittleEndian) + { + ReverseEndianness(hash); + } + + hash[8] &= 0x3F; + hash[8] |= 0x80; + + int versionIndex = BitConverter.IsLittleEndian ? 7 : 6; + + hash[versionIndex] &= 0x0F; + hash[versionIndex] |= 0x50; + + return new(hash); + } + + private static void ReverseEndianness(in Span guidByte) + { + ExchangeBytes(guidByte, 0, 3); + ExchangeBytes(guidByte, 1, 2); + ExchangeBytes(guidByte, 4, 5); + ExchangeBytes(guidByte, 6, 7); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ExchangeBytes(in Span guid, int left, int right) + { + (guid[right], guid[left]) = (guid[left], guid[right]); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs index 864346e5..a8a1f59e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs @@ -74,7 +74,7 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService SeedTime = $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}", ExtFields = JsonSerializer.Serialize(extendProperties), AppName = "bbs_cn", - BbsDeviceId = HoyolabOptions.DeviceId, + BbsDeviceId = HoyolabOptions.DeviceId36, DeviceFp = string.IsNullOrEmpty(user.Fingerprint) ? Core.Random.GetLowerHexString(13) : user.Fingerprint, }; 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 a9f55887..dcbaa0fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs @@ -6,6 +6,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Snap.Hutao.Factory.QrCode; +using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; using Snap.Hutao.Web.Hoyolab.Passport; using Snap.Hutao.Web.Response; using System.Collections.Specialized; @@ -18,7 +19,7 @@ namespace Snap.Hutao.View.Dialog; internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable { private readonly ITaskContext taskContext; - private readonly PassportClient2 passportClient2; + private readonly PandaClient pandaClient; private readonly IQRCodeFactory qrCodeFactory; private readonly CancellationTokenSource userManualCancellationTokenSource = new(); @@ -29,7 +30,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - passportClient2 = serviceProvider.GetRequiredService(); + pandaClient = serviceProvider.GetRequiredService(); qrCodeFactory = serviceProvider.GetRequiredService(); } @@ -100,7 +101,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable private async ValueTask FetchQRCodeAndSetImageAsync(CancellationToken token) { - Response fetchResponse = await passportClient2.QRCodeFetchAsync(token).ConfigureAwait(false); + Response fetchResponse = await pandaClient.QRCodeFetchAsync(token).ConfigureAwait(false); if (!fetchResponse.IsOk()) { return string.Empty; @@ -136,7 +137,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable { while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) { - Response query = await passportClient2.QRCodeQueryAsync(ticket, token).ConfigureAwait(false); + Response query = await pandaClient.QRCodeQueryAsync(ticket, token).ConfigureAwait(false); if (query is { ReturnCode: 0, Data: { Stat: "Confirmed", Payload.Proto: "Account" } }) { diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index bff2ca0b..eb88a523 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -158,7 +158,7 @@ Width="{StaticResource LargeAppBarButtonWidth}" MaxWidth="{StaticResource LargeAppBarButtonWidth}" Margin="2,-4" - Command="{Binding LoginByQrCodeCommand}" + Command="{Binding LoginByQRCodeCommand}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}" Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/> ? users; @@ -177,26 +175,29 @@ internal sealed partial class UserViewModel : ObservableObject } } - [Command("LoginByQrCodeCommand")] - private async Task LoginByQrCode() + [Command("LoginByQRCodeCommand")] + private async Task LoginByQRCode() { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - UserQRCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - ValueResult result = await dialog.GetUidGameTokenAsync().ConfigureAwait(false); + (bool isOk, UidGameToken? token) = await dialog.GetUidGameTokenAsync().ConfigureAwait(false); - if (result.TryGetValue(out UidGameToken account)) + if (!isOk) { - Response gameTokenResp = await sessionAppClient.PostSTokenByGameTokenAsync(account).ConfigureAwait(false); + return; + } - if (gameTokenResp.IsOk()) - { - Cookie stokenV2 = Cookie.FromLoginResult(gameTokenResp.Data); - (UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false); + Response sTokenResponse = await serviceProvider + .GetRequiredService() + .GetSTokenByGameTokenAsync(token) + .ConfigureAwait(false); - await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false); - } + 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); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs index d9992754..3466901d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs @@ -233,7 +233,7 @@ internal class MiHoYoJSBridge // Skip x-rpc-lifecycle_id { "x-rpc-app_id", "bll8iq97cem8" }, { "x-rpc-client_type", "5" }, - { "x-rpc-device_id", HoyolabOptions.DeviceId }, + { "x-rpc-device_id", HoyolabOptions.DeviceId36 }, { "x-rpc-app_version", userAndUid.IsOversea ? SaltConstants.OSVersion : SaltConstants.CNVersion }, { "x-rpc-sdk_version", "2.16.0" }, }; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs similarity index 55% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs index d5f17cd6..479d4218 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs @@ -1,13 +1,10 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Passport; +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; -/// -/// 扫码登录结果请求配置 -/// [HighQuality] -internal sealed class GameLoginResultOptions +internal sealed class GameLoginRequest { [JsonPropertyName("app_id")] public int AppId { get; set; } @@ -16,11 +13,11 @@ internal sealed class GameLoginResultOptions public string Device { get; set; } = default!; [JsonPropertyName("ticket")] - public string Ticket { get; set; } = default!; + public string? Ticket { get; set; } - public static GameLoginResultOptions Create(int appId, string device, string ticket) + public static GameLoginRequest Create(int appId, string device, string? ticket = null) { - return new GameLoginResultOptions + return new() { AppId = appId, Device = device, diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs similarity index 81% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs index bd52a99c..92e6456e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs @@ -1,9 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -namespace Snap.Hutao.Web.Hoyolab.Passport; +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; /// /// 扫码登录结果 diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs similarity index 79% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs index 8d7cd653..80580331 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs @@ -1,11 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; -/// -/// 扫码登录结果Payload -/// [HighQuality] internal sealed partial class GameLoginResultPayload { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs new file mode 100644 index 00000000..90ec1cfc --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs @@ -0,0 +1,49 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Web.Request.Builder; +using Snap.Hutao.Web.Request.Builder.Abstraction; +using Snap.Hutao.Web.Response; +using System.Net.Http; + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; + +[ConstructorGenerated(ResolveHttpClient = true)] +[HttpClient(HttpClientConfiguration.XRpc2)] +internal sealed partial class PandaClient +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + public async ValueTask> QRCodeFetchAsync(CancellationToken token = default) + { + GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeFetch) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + public async ValueTask> QRCodeQueryAsync(string ticket, CancellationToken token = default) + { + GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40, ticket); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeQuery) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs similarity index 80% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs index bda18383..9a962e6a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Passport; +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; internal sealed class UrlWrapper { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs index 4610b2c9..ad3dff78 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs @@ -1,16 +1,16 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.Extensions.Options; using Snap.Hutao.Web.Hoyolab.DataSigning; using System.Collections.Frozen; +using System.Security.Cryptography; namespace Snap.Hutao.Web.Hoyolab; /// /// 米游社选项 /// -internal sealed class HoyolabOptions : IOptions +internal static class HoyolabOptions { /// /// 米游社请求UA @@ -35,12 +35,12 @@ internal sealed class HoyolabOptions : IOptions /// /// 米游社设备Id /// - public static string DeviceId { get; } = Guid.NewGuid().ToString(); + public static string DeviceId36 { get; } = Guid.NewGuid().ToString(); /// /// 扫码登录设备Id /// - public static string Device { get; } = Core.Random.GetLetterAndNumberString(64); + public static string DeviceId40 { get; } = GenerateDeviceId40(); /// /// 盐 @@ -61,6 +61,16 @@ internal sealed class HoyolabOptions : IOptions [SaltType.OSX6] = "okr4obncj8bw5a65hbnn5oo6ixjc3l9w", }.ToFrozenDictionary(); - /// - public HoyolabOptions Value { get => this; } + [SuppressMessage("", "CA1308")] + private static string GenerateDeviceId40() + { + Guid uuid = Core.Uuid.NewV5(DeviceId36, new("9450ea74-be9c-35c0-9568-f97407856768")); + + Span uuidSpan = stackalloc byte[16]; + Span hash = stackalloc byte[20]; + + Verify.Operation(uuid.TryWriteBytes(uuidSpan), "Failed to write UUID bytes"); + Verify.Operation(SHA1.TryHashData(uuidSpan, hash, out _), "Failed to write SHA1 hash"); + return Convert.ToHexString(hash).ToLowerInvariant(); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs similarity index 62% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs index 1b62d7cb..0c76cc52 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs @@ -1,12 +1,12 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Takumi.Account; +namespace Snap.Hutao.Web.Hoyolab.Passport; -internal sealed class GameTokenWrapper +internal sealed class AccountIdGameToken { [JsonPropertyName("account_id")] - public int Stuid { get; set; } = default!; + public int AccountId { get; set; } = default!; [JsonPropertyName("game_token")] public string GameToken { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs deleted file mode 100644 index fb3b77a4..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Passport; - -/// -/// 扫码登录请求配置 -/// -[HighQuality] -internal sealed class GameLoginRequestOptions -{ - [JsonPropertyName("app_id")] - public int AppId { get; set; } - - [JsonPropertyName("device")] - public string Device { get; set; } = default!; - - public static GameLoginRequestOptions Create(int appId, string device) - { - return new GameLoginRequestOptions - { - AppId = appId, - Device = device, - }; - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs index 8516723b..be1ad870 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs @@ -5,9 +5,11 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Model.Entity; using Snap.Hutao.Web.Hoyolab.Annotation; using Snap.Hutao.Web.Hoyolab.DataSigning; +using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using Snap.Hutao.Web.Response; +using System.Globalization; using System.Net.Http; namespace Snap.Hutao.Web.Hoyolab.Passport; @@ -68,42 +70,20 @@ internal sealed partial class PassportClient2 return Response.Response.DefaultIfNull(resp); } - /// - /// 异步获取扫码链接 - /// - /// 取消令牌 - /// 二维码原始链接 - public async ValueTask> QRCodeFetchAsync(CancellationToken token = default) + public async ValueTask> GetSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default) { - GameLoginRequestOptions options = GameLoginRequestOptions.Create(4, HoyolabOptions.Device); + AccountIdGameToken data = new() + { + AccountId = int.Parse(account.Uid, CultureInfo.InvariantCulture), + GameToken = account.GameToken, + }; HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.QrCodeFetch) - .PostJson(options); + .SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken) + .PostJson(data); - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) - .ConfigureAwait(false); - - return Response.Response.DefaultIfNull(resp); - } - - /// - /// 异步获取扫码状态 - /// - /// 扫码链接中的ticket - /// 取消令牌 - /// 扫码结果 - public async ValueTask> QRCodeQueryAsync(string ticket, CancellationToken token = default) - { - GameLoginResultOptions options = GameLoginResultOptions.Create(4, HoyolabOptions.Device, ticket); - - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.QrCodeQuery) - .PostJson(options); - - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) .ConfigureAwait(false); return Response.Response.DefaultIfNull(resp); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs deleted file mode 100644 index 57f1c63d..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; -using Snap.Hutao.Web.Hoyolab.Passport; -using Snap.Hutao.Web.Request.Builder; -using Snap.Hutao.Web.Request.Builder.Abstraction; -using Snap.Hutao.Web.Response; -using System.Globalization; -using System.Net.Http; - -namespace Snap.Hutao.Web.Hoyolab.Takumi.Account; - -[HighQuality] -[ConstructorGenerated(ResolveHttpClient = true)] -[HttpClient(HttpClientConfiguration.XRpc2)] -internal sealed partial class SessionAppClient -{ - private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; - private readonly HttpClient httpClient; - - /// - /// 通过 GameToken 获取 SToken (V2) - /// - /// 扫码获得的账户信息 - /// 取消令牌 - /// 登录结果 - public async ValueTask> PostSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default) - { - GameTokenWrapper wrapper = new() - { - Stuid = int.Parse(account.Uid, CultureInfo.CurrentCulture), - GameToken = account.GameToken, - }; - - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken) - .PostJson(wrapper); - - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) - .ConfigureAwait(false); - - return Response.Response.DefaultIfNull(resp); - } -} From ad20b83b4eab3c8ef1f6573eb02da6b44eb27e79 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Tue, 12 Dec 2023 14:25:05 +0800 Subject: [PATCH 44/47] minor fix --- src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index eb88a523..8b119393 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -159,7 +159,7 @@ MaxWidth="{StaticResource LargeAppBarButtonWidth}" Margin="2,-4" Command="{Binding LoginByQRCodeCommand}" - Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}" + Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQRCode}}" Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/> Date: Tue, 12 Dec 2023 15:23:59 +0800 Subject: [PATCH 45/47] Update appveyor signing --- appveyor.yml | 3 +-- build.cake | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index fd432319..223fdb9e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,7 +15,6 @@ artifacts: type: file deploy: - provider: Webhook - url: https://app.signpath.io/API/v1/7a941fa3-64d8-4c45-bd03-92a02bcd4964/Integrations/AppVeyor?ProjectSlug=Snap.Hutao&SigningPolicySlug=test-signing&ArtifactConfigurationSlug=msix - # url: https://app.signpath.io/API/v1/7a941fa3-64d8-4c45-bd03-92a02bcd4964/Integrations/AppVeyor?ProjectSlug=Snap.Hutao&SigningPolicySlug=release-signing&ArtifactConfigurationSlug=msix + url: https://app.signpath.io/API/v1/7a941fa3-64d8-4c45-bd03-92a02bcd4964/Integrations/AppVeyor?ProjectSlug=Snap.Hutao&SigningPolicySlug=release-signing&ArtifactConfigurationSlug=msix authorization: secure: j8srQ5/UYWhI+jlm3Vo3D3QfXoRyQ9hOn3ynJGtwusKui4+uDi4gykdUFYCITZxK+C/fOCAZNJ+YaKSm/OaiXw== diff --git a/build.cake b/build.cake index 789958e9..68cb399c 100644 --- a/build.cake +++ b/build.cake @@ -100,9 +100,7 @@ Task("Generate AppxManifest") else if (AppVeyor.IsRunningOnAppVeyor) { Information("Using Release configuration"); - content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=Test certificate for 'Snap Hutao [OSS]'\""); - // release - // content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, C=US, S=DE, L=Lewes\""); + content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, C=US, S=DE, L=Lewes\""); } System.IO.File.WriteAllText(manifest, content); From 57f7ac944c95314a57bd3ef7121bab4155f9098b Mon Sep 17 00:00:00 2001 From: qhy040404 <45379733+qhy040404@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:31:37 +0800 Subject: [PATCH 46/47] fix signing --- build.cake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.cake b/build.cake index 68cb399c..26a16afc 100644 --- a/build.cake +++ b/build.cake @@ -100,7 +100,7 @@ Task("Generate AppxManifest") else if (AppVeyor.IsRunningOnAppVeyor) { Information("Using Release configuration"); - content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, C=US, S=DE, L=Lewes\""); + content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\""); } System.IO.File.WriteAllText(manifest, content); From 1c991aa120cac91f5e005113cbfcc17fffea4f17 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Tue, 12 Dec 2023 17:07:11 +0800 Subject: [PATCH 47/47] user service refactor --- .../CollectionsMarshalTest.cs | 14 -- .../BaseClassLibrary/JsonSerializeTest.cs | 30 +-- .../BaseClassLibrary/LinqTest.cs | 3 +- .../GeniusInvokationDecoding.cs | 8 - .../SpiralAbyssScheduleIdTest.cs | 1 + .../Extension/EnumerableExtension.List.cs | 2 +- .../Service/User/IUserCollectionService.cs | 25 +++ .../Snap.Hutao/Service/User/IUserService.cs | 7 - .../Service/User/UserCollectionService.cs | 185 +++++++++++++++++ .../Snap.Hutao/Service/User/UserService.cs | 192 +++--------------- .../Service/User/UserServiceExtension.cs | 14 ++ .../ViewModel/User/UserViewModel.cs | 8 +- 12 files changed, 269 insertions(+), 220 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs index a7090302..1d22ed83 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs @@ -10,17 +10,10 @@ public class CollectionsMarshalTest [TestMethod] public void DictionaryMarshalGetValueRefOrNullRefIsNullRef() { -#if NET8_0_OR_GREATER Dictionary dictionaryValueKeyRefValue = []; Dictionary dictionaryValueKeyValueValue = []; Dictionary dictionaryRefKeyValueValue = []; Dictionary dictionaryRefKeyRefValue = []; -#else - Dictionary dictionaryValueKeyRefValue = new(); - Dictionary dictionaryValueKeyValueValue = new(); - Dictionary dictionaryRefKeyValueValue = new(); - Dictionary dictionaryRefKeyRefValue = new(); -#endif Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U))); Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U))); @@ -31,17 +24,10 @@ public class CollectionsMarshalTest [TestMethod] public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault() { -#if NET8_0_OR_GREATER Dictionary dictionaryValueKeyRefValue = []; Dictionary dictionaryValueKeyValueValue = []; Dictionary dictionaryRefKeyValueValue = []; Dictionary dictionaryRefKeyRefValue = []; -#else - Dictionary dictionaryValueKeyRefValue = new(); - Dictionary dictionaryValueKeyValueValue = new(); - Dictionary dictionaryRefKeyValueValue = new(); - Dictionary dictionaryRefKeyRefValue = new(); -#endif Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default); Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default); diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs index 656294cb..42b0ca1d 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; @@ -7,8 +8,6 @@ namespace Snap.Hutao.Test.BaseClassLibrary; [TestClass] public sealed class JsonSerializeTest { - public TestContext? TestContext { get; set; } - private readonly JsonSerializerOptions AlowStringNumberOptions = new() { NumberHandling = JsonNumberHandling.AllowReadingFromString, @@ -36,7 +35,7 @@ public sealed class JsonSerializeTest [TestMethod] public void DelegatePropertyCanSerialize() { - Sample sample = JsonSerializer.Deserialize(SmapleObjectJson)!; + SampleDelegatePropertyClass sample = JsonSerializer.Deserialize(SmapleObjectJson)!; Assert.AreEqual(sample.B, 1); } @@ -44,7 +43,7 @@ public sealed class JsonSerializeTest [ExpectedException(typeof(JsonException))] public void EmptyStringCannotSerializeAsNumber() { - StringNumberSample sample = JsonSerializer.Deserialize(SmapleEmptyStringObjectJson)!; + SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize(SmapleEmptyStringObjectJson)!; Assert.AreEqual(sample.A, 0); } @@ -58,37 +57,28 @@ public sealed class JsonSerializeTest [TestMethod] public void ByteArraySerializeAsBase64() { - byte[] array = -#if NET8_0_OR_GREATER - [1, 2, 3, 4, 5]; -#else - { 1, 2, 3, 4, 5 }; -#endif - ByteArraySample sample = new() + SampleByteArrayPropertyClass sample = new() { - Array = array, + Array = [1, 2, 3, 4, 5], }; string result = JsonSerializer.Serialize(sample); - TestContext!.WriteLine($"ByteArray Serialize Result: {result}"); - Assert.AreEqual(result, """ - {"Array":"AQIDBAU="} - """); + Assert.AreEqual(result, """{"Array":"AQIDBAU="}"""); } - private sealed class Sample + private sealed class SampleDelegatePropertyClass { public int A { get => B; set => B = value; } public int B { get; set; } } - private sealed class StringNumberSample + private sealed class SampleStringReadWriteNumberPropertyClass { [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] public int A { get; set; } } - private sealed class ByteArraySample + private sealed class SampleByteArrayPropertyClass { public byte[]? Array { get; set; } } diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs index e5f4f20b..c189ac21 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs @@ -9,11 +9,10 @@ public sealed class LinqTest { [TestMethod] [ExpectedException(typeof(InvalidOperationException))] - public void LinqOrderByWithWrapperStruct() + public void LinqOrderByWithWrapperStructThrow() { List list = [1, 5, 2, 6, 3, 7, 4, 8]; string result = string.Join(", ", list.OrderBy(i => i).Select(i => i.Value)); - Console.WriteLine(result); } diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs index aaa2b7ce..7a72c3e5 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs @@ -159,19 +159,11 @@ public sealed class GeniusInvokationDecoding result.CopyTo(resultArray); ushort[] testKnownResult = -#if NET8_0_OR_GREATER [ 060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153, 181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201, 217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270, ]; -#else - { - 060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153, - 181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201, - 217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270, - }; -#endif CollectionAssert.AreEqual(resultArray, testKnownResult); } diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs index 31d30681..ac4b993d 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs @@ -15,6 +15,7 @@ public class SpiralAbyssScheduleIdTest DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8); Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期"); } + public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset) { // Force time in UTC+08 diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs index d1519d60..049bc56e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs @@ -167,7 +167,7 @@ internal static partial class EnumerableExtension return results; } - public static async ValueTask> SelectListAsync(this List list, Func> selector, CancellationToken token) + public static async ValueTask> SelectListAsync(this List list, Func> selector, CancellationToken token = default) { List results = new(list.Count); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs new file mode 100644 index 00000000..8db6973f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.ViewModel.User; +using Snap.Hutao.Web.Hoyolab; +using Snap.Hutao.Web.Hoyolab.Takumi.Binding; +using System.Collections.ObjectModel; +using BindingUser = Snap.Hutao.ViewModel.User.User; + +namespace Snap.Hutao.Service.User; + +internal interface IUserCollectionService +{ + BindingUser? CurrentUser { get; set; } + + ValueTask> GetUserAndUidCollectionAsync(); + + ValueTask> GetUserCollectionAsync(); + + 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/IUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs index e814d7d1..e89d0494 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs @@ -50,13 +50,6 @@ internal interface IUserService /// 处理的结果 ValueTask> ProcessInputCookieAsync(Cookie cookie, bool isOversea); - /// - /// 异步刷新 Cookie 的 CookieToken - /// - /// 用户 - /// 是否刷新成功 - ValueTask RefreshCookieTokenAsync(BindingUser user); - ValueTask RefreshCookieTokenAsync(Model.Entity.User user); /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs new file mode 100644 index 00000000..d0f689cb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs @@ -0,0 +1,185 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.Messaging; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Message; +using Snap.Hutao.ViewModel.User; +using Snap.Hutao.Web.Hoyolab; +using Snap.Hutao.Web.Hoyolab.Takumi.Binding; +using System.Collections.ObjectModel; +using BindingUser = Snap.Hutao.ViewModel.User.User; + +namespace Snap.Hutao.Service.User; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IUserCollectionService))] +internal sealed partial class UserCollectionService : IUserCollectionService +{ + private readonly ScopedDbCurrent dbCurrent; + private readonly IUserInitializationService userInitializationService; + private readonly IUserDbService userDbService; + private readonly ITaskContext taskContext; + private readonly IMessenger messenger; + + private readonly Throttler throttler = new(); + + private ObservableCollection? userCollection; + private Dictionary? midUserMap; + + private ObservableCollection? userAndUidCollection; + private Dictionary? uidUserGameRoleMap; + + public BindingUser? CurrentUser + { + get => dbCurrent.Current; + set => dbCurrent.Current = value; + } + + public async ValueTask> GetUserCollectionAsync() + { + using (await throttler.ThrottleAsync().ConfigureAwait(false)) + { + if (userCollection is null) + { + List entities = await userDbService.GetUserListAsync().ConfigureAwait(false); + List users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync).ConfigureAwait(false); + + midUserMap = []; + foreach (BindingUser user in users) + { + if (user.Entity.Mid is not null) + { + midUserMap[user.Entity.Mid] = user; + } + + if (user.NeedDbUpdateAfterResume) + { + await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); + user.NeedDbUpdateAfterResume = false; + } + } + + userCollection = users.ToObservableCollection(); + + try + { + CurrentUser = users.SelectedOrDefault(); + } + catch (InvalidOperationException ex) + { + ThrowHelper.UserdataCorrupted(SH.ServiceUserCurrentMultiMatched, ex); + } + } + } + + return userCollection; + } + + public async ValueTask> GetUserAndUidCollectionAsync() + { + if (userAndUidCollection is null) + { + await taskContext.SwitchToBackgroundAsync(); + ObservableCollection users = await GetUserCollectionAsync().ConfigureAwait(false); + List roles = []; + uidUserGameRoleMap = []; + + foreach (BindingUser user in users) + { + foreach (UserGameRole role in user.UserGameRoles) + { + roles.Add(UserAndUid.From(user.Entity, role)); + uidUserGameRoleMap[role.GameUid] = role; + } + } + + userAndUidCollection = roles.ToObservableCollection(); + } + + return userAndUidCollection; + } + + public async ValueTask RemoveUserAsync(BindingUser user) + { + // Sync cache + await taskContext.SwitchToMainThreadAsync(); + ArgumentNullException.ThrowIfNull(userCollection); + userCollection.Remove(user); + userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid); + if (user.Entity.Mid is not null) + { + midUserMap?.Remove(user.Entity.Mid); + } + + // Sync database + await taskContext.SwitchToBackgroundAsync(); + await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false); + + messenger.Send(new UserRemovedMessage(user.Entity)); + } + + public UserGameRole? GetUserGameRoleByUid(string uid) + { + if (uidUserGameRoleMap is null) + { + return default; + } + + try + { + return uidUserGameRoleMap[uid]; + } + catch (InvalidOperationException) + { + // Sequence contains more than one matching element + // TODO: return a specialize UserGameRole to indicate error + } + + return default; + } + + public bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user) + { + ArgumentNullException.ThrowIfNull(midUserMap); + return midUserMap.TryGetValue(mid, out user); + } + + public async ValueTask> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea) + { + await taskContext.SwitchToBackgroundAsync(); + BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false); + + if (newUser is null) + { + return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed); + } + + await GetUserCollectionAsync().ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(userCollection); + + // Sync cache + await taskContext.SwitchToMainThreadAsync(); + userCollection.Add(newUser); + if (newUser.Entity.Mid is not null) + { + midUserMap?.Add(newUser.Entity.Mid, newUser); + } + + if (userAndUidCollection is not null) + { + foreach (UserGameRole role in newUser.UserGameRoles) + { + userAndUidCollection.Add(new(newUser.Entity, role)); + uidUserGameRoleMap?.Add(role.GameUid, role); + } + } + + // Sync database + await taskContext.SwitchToBackgroundAsync(); + await userDbService.AddUserAsync(newUser.Entity).ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(newUser.UserInfo); + return new(UserOptionResult.Added, newUser.UserInfo.Uid); + } +} \ 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 6ec475cf..ae2dc00d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -1,11 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.Mvvm.Messaging; -using Snap.Hutao.Core.Database; using Snap.Hutao.Core.DependencyInjection.Abstraction; -using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Message; using Snap.Hutao.ViewModel.User; using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab.Passport; @@ -23,39 +19,20 @@ namespace Snap.Hutao.Service.User; [Injection(InjectAs.Singleton, typeof(IUserService))] internal sealed partial class UserService : IUserService, IUserServiceUnsafe { - private readonly Throttler throttler = new(); - - private readonly ScopedDbCurrent dbCurrent; - private readonly IUserInitializationService userInitializationService; + private readonly IUserCollectionService userCollectionService; private readonly IServiceProvider serviceProvider; private readonly IUserDbService userDbService; private readonly ITaskContext taskContext; - private readonly IMessenger messenger; - private ObservableCollection? userCollection; - private ObservableCollection? userAndUidCollection; - - /// public BindingUser? Current { - get => dbCurrent.Current; - set => dbCurrent.Current = value; + get => userCollectionService.CurrentUser; + set => userCollectionService.CurrentUser = value; } - /// - public async ValueTask RemoveUserAsync(BindingUser user) + public ValueTask RemoveUserAsync(BindingUser user) { - // Sync cache - await taskContext.SwitchToMainThreadAsync(); - ArgumentNullException.ThrowIfNull(userCollection); - userCollection.Remove(user); - userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid); - - // Sync database - await taskContext.SwitchToBackgroundAsync(); - await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false); - - messenger.Send(new UserRemovedMessage(user.Entity)); + return userCollectionService.RemoveUserAsync(user); } public async ValueTask UnsafeRemoveUsersAsync() @@ -64,76 +41,21 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe await userDbService.RemoveUsersAsync().ConfigureAwait(false); } - /// - public async ValueTask> GetUserCollectionAsync() + public ValueTask> GetUserCollectionAsync() { - using (await throttler.ThrottleAsync().ConfigureAwait(false)) - { - if (userCollection is null) - { - List entities = await userDbService.GetUserListAsync().ConfigureAwait(false); - List users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync, default).ConfigureAwait(false); - - foreach (BindingUser user in users) - { - if (user.NeedDbUpdateAfterResume) - { - await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); - user.NeedDbUpdateAfterResume = false; - } - } - - userCollection = users.ToObservableCollection(); - - try - { - Current = users.SelectedOrDefault(); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceUserCurrentMultiMatched, ex); - } - } - } - - return userCollection; + return userCollectionService.GetUserCollectionAsync(); } - /// - public async ValueTask> GetRoleCollectionAsync() + public ValueTask> GetRoleCollectionAsync() { - await taskContext.SwitchToBackgroundAsync(); - if (userAndUidCollection is null) - { - ObservableCollection users = await GetUserCollectionAsync().ConfigureAwait(false); - userAndUidCollection = users - .SelectMany(user => user.UserGameRoles.Select(role => UserAndUid.From(user.Entity, role))) - .ToObservableCollection(); - } - - return userAndUidCollection; + return userCollectionService.GetUserAndUidCollectionAsync(); } - /// public UserGameRole? GetUserGameRoleByUid(string uid) { - if (userCollection is not null) - { - try - { - return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid); - } - catch (InvalidOperationException) - { - // Sequence contains more than one matching element - // TODO: return a specialize UserGameRole to indicate error - } - } - - return default; + return userCollectionService.GetUserGameRoleByUid(uid); } - /// public async ValueTask> ProcessInputCookieAsync(Cookie cookie, bool isOversea) { await taskContext.SwitchToBackgroundAsync(); @@ -145,33 +67,22 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe } // 检查 mid 对应用户是否存在 - ArgumentNullException.ThrowIfNull(userCollection); - if (TryGetUser(userCollection, mid, out BindingUser? user)) + if (!userCollectionService.TryGetUserByMid(mid, out BindingUser? user)) { - if (cookie.TryGetSToken(isOversea, out Cookie? stoken)) - { - user.SToken = stoken; - user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken; - user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; - - await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); - return new(UserOptionResult.Updated, mid); - } - else - { - return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken); - } + return await userCollectionService.TryCreateAndAddUserFromCookieAsync(cookie, isOversea).ConfigureAwait(false); } - else + + if (!cookie.TryGetSToken(isOversea, out Cookie? stoken)) { - return await TryCreateUserAndAddAsync(cookie, isOversea).ConfigureAwait(false); + return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken); } - } - /// - public ValueTask RefreshCookieTokenAsync(BindingUser user) - { - return RefreshCookieTokenAsync(user.Entity); + user.SToken = stoken; + user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken; + user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; + + await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); + return new(UserOptionResult.Updated, mid); } public async ValueTask RefreshCookieTokenAsync(Model.Entity.User user) @@ -183,65 +94,20 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe .GetCookieAccountInfoBySTokenAsync(user) .ConfigureAwait(false); - if (cookieTokenResponse.IsOk()) - { - string cookieToken = cookieTokenResponse.Data.CookieToken; - - // Check null and create a new one to avoid System.NullReferenceException - user.CookieToken ??= new(); - - // Sync ui and database - user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken; - await userDbService.UpdateUserAsync(user).ConfigureAwait(false); - - return true; - } - else + if (!cookieTokenResponse.IsOk()) { return false; } - } - private static bool TryGetUser(ObservableCollection users, string mid, [NotNullWhen(true)] out BindingUser? user) - { - // TODO: System.InvalidOperationException: Sequence contains more than one matching element - user = users.SingleOrDefault(u => u.Entity.Mid == mid); - return user is not null; - } + string cookieToken = cookieTokenResponse.Data.CookieToken; - private async ValueTask> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea) - { - await taskContext.SwitchToBackgroundAsync(); - BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false); + // Check null and create a new one to avoid System.NullReferenceException + user.CookieToken ??= new(); - if (newUser is not null) - { - // Sync cache - if (userCollection is not null) - { - await taskContext.SwitchToMainThreadAsync(); - { - userCollection.Add(newUser); + // Sync ui and database + user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken; + await userDbService.UpdateUserAsync(user).ConfigureAwait(false); - if (userAndUidCollection is not null) - { - foreach (UserGameRole role in newUser.UserGameRoles) - { - userAndUidCollection.Add(new(newUser.Entity, role)); - } - } - } - } - - // Sync database - await taskContext.SwitchToBackgroundAsync(); - await userDbService.AddUserAsync(newUser.Entity).ConfigureAwait(false); - ArgumentNullException.ThrowIfNull(newUser.UserInfo); - return new(UserOptionResult.Added, newUser.UserInfo.Uid); - } - else - { - return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed); - } + return true; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs new file mode 100644 index 00000000..d29f3272 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using BindingUser = Snap.Hutao.ViewModel.User.User; + +namespace Snap.Hutao.Service.User; + +internal static class UserServiceExtension +{ + public static ValueTask RefreshCookieTokenAsync(this IUserService userService, BindingUser user) + { + return userService.RefreshCookieTokenAsync(user.Entity); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs index fdd3550b..d02df2ad 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs @@ -52,14 +52,12 @@ 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 (SetProperty(ref selectedUser, value)) { userService.Current = value; - - if (value is not null) - { - value.SelectedUserGameRole = value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen); - } } } }