diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs new file mode 100644 index 00000000..562da130 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs @@ -0,0 +1,138 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +namespace Snap.Hutao.SourceGeneration.DedendencyInjection; + +/// +/// 注入HttpClient代码生成器 +/// 旨在使用源生成器提高注入效率 +/// 防止在运行时动态查找注入类型 +/// +[Generator] +public class HttpClientGenerator : ISourceGenerator +{ + private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.Default"; + private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc"; + + private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute"; + + /// + public void Initialize(GeneratorInitializationContext context) + { + // Register a syntax receiver that will be created for each generation pass + context.RegisterForSyntaxNotifications(() => new HttpClientSyntaxContextReceiver()); + } + + /// + public void Execute(GeneratorExecutionContext context) + { + // retrieve the populated receiver + if (context.SyntaxContextReceiver is not HttpClientSyntaxContextReceiver receiver) + { + return; + } + + string toolName = this.GetGeneratorType().FullName; + + StringBuilder sourceCodeBuilder = new(); + sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +// This class is generated by Snap.Hutao.SourceGeneration + +using Microsoft.Extensions.DependencyInjection; +using System.Net.Http; + +namespace Snap.Hutao.Core.DependencyInjection; + +internal static partial class IocHttpClientConfiguration +{{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + public static partial IServiceCollection AddHttpClients(this IServiceCollection services) + {{"); + + FillWithInjectionServices(receiver, sourceCodeBuilder); + sourceCodeBuilder.Append(@" + return services; + } +}"); + + context.AddSource("IocHttpClientConfiguration.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8)); + } + + private static void FillWithInjectionServices(HttpClientSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder) + { + List lines = new(); + StringBuilder lineBuilder = new(); + + foreach (INamedTypeSymbol classSymbol in receiver.Classes) + { + lineBuilder + .Clear() + .Append("\r\n"); + + lineBuilder.Append(@" services.AddHttpClient<"); + lineBuilder.Append($"{classSymbol.ToDisplayString()}>("); + + AttributeData httpClientInfo = classSymbol + .GetAttributes() + .Single(attr => attr.AttributeClass!.ToDisplayString() == HttpClientSyntaxContextReceiver.AttributeName); + ImmutableArray arguments = httpClientInfo.ConstructorArguments; + + TypedConstant injectAs = arguments[0]; + + string injectAsName = injectAs.ToCSharpString(); + switch (injectAsName) + { + case DefaultName: + lineBuilder.Append(@"DefaultConfiguration)"); + break; + case XRpcName: + lineBuilder.Append(@"XRpcConfiguration)"); + break; + default: + throw new InvalidOperationException($"非法的HttpClientConfigration值: [{injectAsName}]"); + } + + AttributeData? handlerInfo = classSymbol + .GetAttributes() + .SingleOrDefault(attr => attr.AttributeClass!.ToDisplayString() == PrimaryHttpMessageHandlerAttributeName); + + if (handlerInfo != null) + { + ImmutableArray> properties = handlerInfo.NamedArguments; + lineBuilder.Append(@".ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {"); + + foreach (KeyValuePair property in properties) + { + lineBuilder.Append(" "); + lineBuilder.Append(property.Key); + lineBuilder.Append(" = "); + lineBuilder.Append(property.Value.ToCSharpString()); + lineBuilder.Append(","); + } + + lineBuilder.Append(" })"); + } + + lineBuilder.Append(";"); + + lines.Add(lineBuilder.ToString()); + } + + foreach (string line in lines.OrderBy(x => x)) + { + sourceCodeBuilder.Append(line); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientSyntaxContextReceiver.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientSyntaxContextReceiver.cs new file mode 100644 index 00000000..f80d8f94 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientSyntaxContextReceiver.cs @@ -0,0 +1,42 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Linq; + +namespace Snap.Hutao.SourceGeneration.DedendencyInjection; + +/// +/// Created on demand before each generation pass +/// +public class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver +{ + /// + /// 注入特性的名称 + /// + public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute"; + + /// + /// 所有需要注入的类型符号 + /// + public List Classes { get; } = new(); + + /// + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + // any class with at least one attribute is a candidate for injection generation + if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0) + { + // get as named type symbol + if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol) + { + if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName)) + { + Classes.Add(classSymbol); + } + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionGenerator.cs index 40154b3e..9a00c488 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionGenerator.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionGenerator.cs @@ -20,8 +20,8 @@ namespace Snap.Hutao.SourceGeneration.DedendencyInjection; [Generator] public class InjectionGenerator : ISourceGenerator { - private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.InjectAs.Singleton"; - private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.InjectAs.Transient"; + private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton"; + private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient"; /// public void Initialize(GeneratorInitializationContext context) @@ -39,8 +39,10 @@ public class InjectionGenerator : ISourceGenerator return; } + string toolName = this.GetGeneratorType().FullName; + StringBuilder sourceCodeBuilder = new(); - sourceCodeBuilder.Append(@"// Copyright (c) DGP Studio. All rights reserved. + sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. // This class is generated by Snap.Hutao.SourceGeneration @@ -50,15 +52,15 @@ using Microsoft.Extensions.DependencyInjection; namespace Snap.Hutao.Core.DependencyInjection; internal static partial class ServiceCollectionExtensions -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Snap.Hutao.SourceGeneration.DedendencyInjection.InjectionGenerator"","" 1.0.0.0"")] +{{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] public static partial IServiceCollection AddInjections(this IServiceCollection services) - { - return services"); + {{"); FillWithInjectionServices(receiver, sourceCodeBuilder); - sourceCodeBuilder.Append(@"; + sourceCodeBuilder.Append(@" + return services; } }"); @@ -87,13 +89,13 @@ internal static partial class ServiceCollectionExtensions switch (injectAsName) { case InjectAsSingletonName: - lineBuilder.Append(@" .AddSingleton("); + lineBuilder.Append(@" services.AddSingleton("); break; case InjectAsTransientName: - lineBuilder.Append(@" .AddTransient("); + lineBuilder.Append(@" services.AddTransient("); break; default: - throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]。"); + throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]"); } if (arguments.Length == 2) @@ -102,7 +104,7 @@ internal static partial class ServiceCollectionExtensions lineBuilder.Append($"{interfaceType.ToCSharpString()}, "); } - lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}))"); + lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}));"); lines.Add(lineBuilder.ToString()); } diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionSyntaxContextReceiver.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionSyntaxContextReceiver.cs index 09b1d460..2a9fd846 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionSyntaxContextReceiver.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionSyntaxContextReceiver.cs @@ -39,4 +39,4 @@ public class InjectionSyntaxContextReceiver : ISyntaxContextReceiver } } } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Foundation/HWND.cs b/src/Snap.Hutao/Snap.Hutao.Win32/Foundation/HWND.cs deleted file mode 100644 index 804f9404..00000000 --- a/src/Snap.Hutao/Snap.Hutao.Win32/Foundation/HWND.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Windows.Win32.Foundation; - -public readonly partial struct HWND -{ - public static HWND Zero => (HWND)IntPtr.Zero; -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt index c56775e8..50ff08a6 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt +++ b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt @@ -16,4 +16,4 @@ RemoveWindowSubclass // User32 FindWindowEx -GetDpiForWindow +GetDpiForWindow \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj b/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj index 9081fd0f..10c2cd7a 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj +++ b/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj @@ -11,7 +11,7 @@ all - + diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs b/src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs index 3006942d..1c57ab21 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs +++ b/src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs @@ -1,6 +1,10 @@ using Windows.Win32.UI.WindowsAndMessaging; namespace Snap.Hutao.Win32; + +/// +/// 包装不安全的代码 +/// public class Unsafe { /// @@ -9,7 +13,7 @@ public class Unsafe /// lParam /// 最小宽度 /// 最小高度 - public static unsafe void SetMinTrackSize(nint lParam, float minWidth, float minHeight) + public static unsafe void SetMinTrackSize(nint lParam, double minWidth, double minHeight) { MINMAXINFO* info = (MINMAXINFO*)lParam; info->ptMinTrackSize.x = (int)Math.Max(minWidth, info->ptMinTrackSize.x); diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index aafb025c..6321b25d 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -12,6 +12,7 @@ using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Metadata; using System.Diagnostics; using Windows.Storage; +using Windows.UI.ViewManagement; namespace Snap.Hutao; @@ -52,17 +53,13 @@ public partial class App : Application get => (App)Application.Current; } - /// /// - /// public static StorageFolder CacheFolder { get => ApplicationData.Current.TemporaryFolder; } - /// /// - /// public static ApplicationDataContainer Settings { get => ApplicationData.Current.LocalSettings; @@ -78,11 +75,12 @@ public partial class App : Application if (firstInstance.IsCurrent) { firstInstance.Activated += OnActivated; - Window = Ioc.Default.GetRequiredService(); logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path); + OnActivated(firstInstance, activatedEventArgs); + Ioc.Default .GetRequiredService() .ImplictAs()? @@ -109,25 +107,31 @@ public partial class App : Application // Hutao extensions .AddInjections() - .AddDatebase() .AddHttpClients() + .AddDatebase() .AddJsonSerializerOptions() // Discrete services .AddSingleton(WeakReferenceMessenger.Default) + .AddSingleton(new UISettings()) .BuildServiceProvider(); Ioc.Default.ConfigureServices(services); } - private void OnActivated(object? sender, AppActivationArguments args) + [SuppressMessage("", "VSTHRD100")] + private async void OnActivated(object? sender, AppActivationArguments args) { + IInfoBarService infoBarService = Ioc.Default.GetRequiredService(); + await infoBarService.WaitInitializationAsync(); + infoBarService.Information("OnActivated"); + if (args.Kind == ExtendedActivationKind.Protocol) { if (args.TryGetProtocolActivatedUri(out Uri? uri)) { - Ioc.Default.GetRequiredService().Information(uri.ToString()); + infoBarService.Information(uri.ToString()); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Context/Database/LogDbContext.cs b/src/Snap.Hutao/Snap.Hutao/Context/Database/LogDbContext.cs index cbc4021d..c1d9fbcd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Context/Database/LogDbContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Context/Database/LogDbContext.cs @@ -8,6 +8,8 @@ namespace Snap.Hutao.Context.Database; /// /// 日志数据库上下文 +/// 由于写入日志的行为需要锁定数据库上下文 +/// 所以将日志单独分离出来进行读写 /// public class LogDbContext : DbContext { diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs index 1e6d5eac..124b4362 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs @@ -132,6 +132,10 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control { imageSurface = await LoadImageSurfaceAsync(storageFile, token); } + catch (COMException ex) when (ex.Is(COMError.STG_E_FILENOTFOUND)) + { + // Image file not found. + } catch (COMException ex) when (ex.Is(COMError.WINCODEC_ERR_COMPONENTNOTFOUND)) { // Image is broken, remove it diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs index 8dd0f2a4..a844cdfa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.UI.Xaml.Media.Imaging; +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using System.Collections.Generic; using System.Net.Http; using Windows.Storage; @@ -14,6 +15,8 @@ namespace Snap.Hutao.Core.Caching; /// The class's name will become the cache folder's name /// [Injection(InjectAs.Singleton, typeof(IImageCache))] +[HttpClient(HttpClientConfigration.Default)] +[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 20)] public class ImageCache : CacheBase, IImageCache { private const string DateAccessedProperty = "System.DateAccessed"; @@ -24,9 +27,9 @@ public class ImageCache : CacheBase, IImageCache /// Initializes a new instance of the class. /// /// 日志器 - /// http客户端 - public ImageCache(ILogger logger, HttpClient httpClient) - : base(logger, httpClient) + /// http客户端工厂 + public ImageCache(ILogger logger, IHttpClientFactory httpClientFactory) + : base(logger, httpClientFactory.CreateClient(nameof(ImageCache))) { } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs index e885bff0..83edd12f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs @@ -13,6 +13,27 @@ namespace Snap.Hutao.Core; /// internal static class CoreEnvironment { + /// + /// 动态密钥1的盐 + /// + public const string DynamicSecret1Salt = "9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7"; + + /// + /// 动态密钥2的盐 + /// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd + /// + public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"; + + /// + /// 米游社请求UA + /// + public const string HoyolabUA = $"miHoYoBBS/2.34.1"; + + /// + /// 标准UA + /// + public static readonly string CommonUA; + /// /// 当前版本 /// @@ -23,13 +44,20 @@ internal static class CoreEnvironment /// public static readonly string DeviceId; + /// + /// 米游社设备Id + /// + public static readonly string HoyolabDeviceId; + private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography"; private const string MachineGuidValue = "MachineGuid"; static CoreEnvironment() { Version = Package.Current.Id.Version.ToVersion(); + CommonUA = $"Snap Hutao/{Version}"; DeviceId = GetDeviceId(); + HoyolabDeviceId = Guid.NewGuid().ToString(); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientAttribute.cs new file mode 100644 index 00000000..b93b5506 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientAttribute.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; + +/// +/// 指示被标注的类型可注入 HttpClient +/// 由源生成器生成注入代码 +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class HttpClientAttribute : Attribute +{ + /// + /// 构造一个新的特性 + /// + /// 配置 + public HttpClientAttribute(HttpClientConfigration configration) + { + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs new file mode 100644 index 00000000..b86c2970 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; + +/// +/// Http客户端配置 +/// +public enum HttpClientConfigration +{ + /// + /// 默认配置 + /// + Default, + + /// + /// 米游社请求配置 + /// + XRpc, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/PrimaryHttpMessageHandlerAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/PrimaryHttpMessageHandlerAttribute.cs new file mode 100644 index 00000000..b53dc184 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/PrimaryHttpMessageHandlerAttribute.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; + +/// +/// 配置首选Http消息处理器特性 +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class PrimaryHttpMessageHandlerAttribute : Attribute +{ + /// + public int MaxConnectionsPerServer { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/InjectAs.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/InjectAs.cs similarity index 85% rename from src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/InjectAs.cs rename to src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/InjectAs.cs index a9b0adcd..a9f36cfc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/InjectAs.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/InjectAs.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Core.DependencyInjection; +namespace Snap.Hutao.Core.DependencyInjection.Annotation; /// /// 注入方法 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/InjectionAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/InjectionAttribute.cs index ffc1cbbb..741f2dfe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/InjectionAttribute.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/InjectionAttribute.cs @@ -26,4 +26,4 @@ public class InjectionAttribute : Attribute public InjectionAttribute(InjectAs injectAs, Type interfaceType) { } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/IocConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs similarity index 89% rename from src/Snap.Hutao/Snap.Hutao/IocConfiguration.cs rename to src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs index ca806d8c..7039f8ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/IocConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs @@ -10,7 +10,7 @@ using System.Linq; using System.Text.Encodings.Web; using System.Text.Json.Serialization; -namespace Snap.Hutao; +namespace Snap.Hutao.Core.DependencyInjection; /// /// 配置 @@ -18,7 +18,7 @@ namespace Snap.Hutao; internal static class IocConfiguration { /// - /// 添加默认的 配置 + /// 添加默认的 /// /// 集合 /// 可继续操作的集合 @@ -56,7 +56,6 @@ internal static class IocConfiguration } } - return services - .AddDbContextPool(builder => builder.UseSqlite(sqlConnectionString)); + return services.AddDbContextPool(builder => builder.UseSqlite(sqlConnectionString)); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs new file mode 100644 index 00000000..b9a10389 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -0,0 +1,52 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Snap.Hutao.Core.Caching; +using Snap.Hutao.Service.Metadata; +using Snap.Hutao.Web.Enka; +using Snap.Hutao.Web.Hoyolab.Bbs.User; +using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement; +using Snap.Hutao.Web.Hoyolab.Takumi.Binding; +using Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; +using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; +using Snap.Hutao.Web.Hutao; +using System.Net.Http; + +namespace Snap.Hutao.Core.DependencyInjection; + +/// +/// 配置 +/// +internal static partial class IocHttpClientConfiguration +{ + /// + /// 添加 + /// + /// 集合 + /// 可继续操作的集合 + public static partial IServiceCollection AddHttpClients(this IServiceCollection services); + + /// + /// 默认配置 + /// + /// 配置后的客户端 + private static void DefaultConfiguration(HttpClient client) + { + client.Timeout = Timeout.InfiniteTimeSpan; + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.CommonUA); + } + + /// + /// 对于需要添加动态密钥的客户端使用此配置 + /// + /// 配置后的客户端 + private static void XRpcConfiguration(HttpClient client) + { + client.Timeout = Timeout.InfiniteTimeSpan; + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA); + client.DefaultRequestHeaders.Add("x-rpc-app_version", "2.34.1"); + client.DefaultRequestHeaders.Add("x-rpc-client_type", "5"); + client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Exception/COMError.cs b/src/Snap.Hutao/Snap.Hutao/Core/Exception/COMError.cs index d09245c5..d1df0a42 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Exception/COMError.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Exception/COMError.cs @@ -8,6 +8,11 @@ namespace Snap.Hutao.Core.Exception; /// public enum COMError : uint { + /// + /// could not be found. + /// + STG_E_FILENOTFOUND = 0x80030002, + /// /// The component cannot be found. /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs index fa3d5771..d0167cd9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs @@ -36,7 +36,6 @@ internal sealed partial class DatebaseLogger : ILogger /// public bool IsEnabled(LogLevel logLevel) { - // If the filter is null, everything is enabled return logLevel != LogLevel.None; } @@ -55,6 +54,7 @@ internal sealed partial class DatebaseLogger : ILogger return; } + // DbContext is not a thread safe class, so we have to lock the wirte procedure lock (logDbContextLock) { logDbContext.Logs.Add(new LogEntry @@ -73,7 +73,7 @@ internal sealed partial class DatebaseLogger : ILogger /// /// An empty scope without any logic /// - private struct NullScope : IDisposable + private class NullScope : IDisposable { public NullScope() { diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs index 47db1914..749f172d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs @@ -52,4 +52,4 @@ public static class ProcessHelper { return Start(uri.AbsolutePath, useShellExecute); } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ThemeHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/ThemeHelper.cs new file mode 100644 index 00000000..02518511 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/ThemeHelper.cs @@ -0,0 +1,60 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Composition.SystemBackdrops; +using Microsoft.UI.Xaml; + +namespace Snap.Hutao.Core; + +/// +/// 主题帮助工具类 +/// +public static class ThemeHelper +{ + /// + /// 判断主题是否相等 + /// + /// 应用主题 + /// 元素主题 + /// 主题是否相等 + public static bool Equals(ApplicationTheme applicationTheme, ElementTheme elementTheme) + { + return (applicationTheme, elementTheme) switch + { + (ApplicationTheme.Light, ElementTheme.Light) => true, + (ApplicationTheme.Dark, ElementTheme.Dark) => true, + _ => false, + }; + } + + /// + /// 从 转换到 + /// + /// 应用主题 + /// 元素主题 + public static ElementTheme ApplicationToElement(ApplicationTheme applicationTheme) + { + return applicationTheme switch + { + ApplicationTheme.Light => ElementTheme.Light, + ApplicationTheme.Dark => ElementTheme.Dark, + _ => throw Must.NeverHappen(), + }; + } + + /// + /// 从 转换到 + /// + /// 元素主题 + /// 背景主题 + public static SystemBackdropTheme ElementToSystemBackdrop(ElementTheme elementTheme) + { + return elementTheme switch + { + ElementTheme.Default => SystemBackdropTheme.Default, + ElementTheme.Light => SystemBackdropTheme.Light, + ElementTheme.Dark => SystemBackdropTheme.Dark, + _ => throw Must.NeverHappen(), + }; +} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Validation/Must.cs b/src/Snap.Hutao/Snap.Hutao/Core/Validation/Must.cs index fadfb946..9c79c058 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Validation/Must.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Validation/Must.cs @@ -10,6 +10,16 @@ namespace Snap.Hutao.Core.Validation; /// public static class Must { + /// + /// Unconditionally throws an . + /// + /// Nothing. This method always throws. + [DoesNotReturn] + public static System.Exception NeverHappen() + { + throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认"); + } + /// /// Throws an if the specified parameter's value is null. /// @@ -25,25 +35,20 @@ public static class Must } /// - /// Unconditionally throws an . + /// Throws an if the specified parameter's value is IntPtr.Zero. /// - /// Nothing. This method always throws. - [DoesNotReturn] - public static System.Exception NeverHappen() + /// The value of the argument. + /// The name of the parameter to include in any thrown exception. + /// The value of the parameter. + /// Thrown if is . + public static Windows.Win32.Foundation.HWND NotNull(Windows.Win32.Foundation.HWND value, [CallerArgumentExpression("value")] string? parameterName = null) { - throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认"); - } + if (value == default) + { + throw new ArgumentNullException(parameterName); + } - /// - /// Unconditionally throws an . - /// - /// The type that the method should be typed to return (although it never returns anything). - /// Nothing. This method always throws. - [DoesNotReturn] - [return: MaybeNull] - public static T NeverHappen() - { - throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认"); + return value; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs index 1bca1719..70d0edca 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs @@ -19,7 +19,7 @@ public class SystemBackdrop private DispatcherQueueHelper? dispatcherQueueHelper; private MicaController? backdropController; - private SystemBackdropConfiguration? configurationSource; + private SystemBackdropConfiguration? configuration; /// /// 构造一个新的系统背景帮助类 @@ -42,35 +42,34 @@ public class SystemBackdrop } else { - dispatcherQueueHelper = new DispatcherQueueHelper(); - dispatcherQueueHelper.EnsureWindowsSystemDispatcherQueueController(); + dispatcherQueueHelper = new(); + dispatcherQueueHelper.Ensure(); // Hooking up the policy object - configurationSource = new SystemBackdropConfiguration(); - window.Activated += WindowActivated; - window.Closed += WindowClosed; - ((FrameworkElement)window.Content).ActualThemeChanged += WindowThemeChanged; + configuration = new(); + window.Activated += OnWindowActivated; + window.Closed += OnWindowClosed; + ((FrameworkElement)window.Content).ActualThemeChanged += OnWindowThemeChanged; // Initial configuration state. - configurationSource.IsInputActive = true; - SetConfigurationSourceTheme(); + configuration.IsInputActive = true; + SetConfigurationSourceTheme(configuration); - backdropController = new MicaController(); + backdropController = new(); backdropController.AddSystemBackdropTarget(window.As()); - backdropController.SetSystemBackdropConfiguration(configurationSource); + backdropController.SetSystemBackdropConfiguration(configuration); return true; } } - private void WindowActivated(object sender, WindowActivatedEventArgs args) + private void OnWindowActivated(object sender, WindowActivatedEventArgs args) { - Must.NotNull(configurationSource!); - configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated; + Must.NotNull(configuration!).IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated; } - private void WindowClosed(object sender, WindowEventArgs args) + private void OnWindowClosed(object sender, WindowEventArgs args) { // Make sure any Mica/Acrylic controller is disposed so it doesn't try to // use this closed window. @@ -80,27 +79,21 @@ public class SystemBackdrop backdropController = null; } - window.Activated -= WindowActivated; - configurationSource = null; + window.Activated -= OnWindowActivated; + configuration = null; } - private void WindowThemeChanged(FrameworkElement sender, object args) + private void OnWindowThemeChanged(FrameworkElement sender, object args) { - if (configurationSource != null) + if (configuration != null) { - SetConfigurationSourceTheme(); + SetConfigurationSourceTheme(configuration); } } - private void SetConfigurationSourceTheme() + private void SetConfigurationSourceTheme(SystemBackdropConfiguration configuration) { - Must.NotNull(configurationSource!).Theme = ((FrameworkElement)window.Content).ActualTheme switch - { - ElementTheme.Default => SystemBackdropTheme.Default, - ElementTheme.Light => SystemBackdropTheme.Light, - ElementTheme.Dark => SystemBackdropTheme.Dark, - _ => throw Must.NeverHappen(), - }; + configuration.Theme = ThemeHelper.ElementToSystemBackdrop(((FrameworkElement)window.Content).ActualTheme); } private class DispatcherQueueHelper @@ -110,7 +103,7 @@ public class SystemBackdrop /// /// 确保系统调度队列控制器存在 /// - public void EnsureWindowsSystemDispatcherQueueController() + public void Ensure() { if (DispatcherQueue.GetForCurrentThread() != null) { @@ -122,7 +115,7 @@ public class SystemBackdrop { DispatcherQueueOptions options = new() { - DwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions)), + DwSize = Marshal.SizeOf(), ThreadType = 2, // DQTYPE_THREAD_CURRENT ApartmentType = 2, // DQTAT_COM_STA }; @@ -136,7 +129,6 @@ public class SystemBackdrop [In] DispatcherQueueOptions options, [In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object? dispatcherQueueController); - [StructLayout(LayoutKind.Sequential)] private struct DispatcherQueueOptions { internal int DwSize; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs index 26821782..29d4a3d6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs @@ -33,6 +33,7 @@ internal class WindowSubclassManager : IDisposable /// 是否为经典标题栏区域 public WindowSubclassManager(HWND hwnd, bool isLegacyDragBar) { + Must.NotNull(hwnd); this.hwnd = hwnd; this.isLegacyDragBar = isLegacyDragBar; } @@ -44,20 +45,23 @@ internal class WindowSubclassManager : IDisposable public bool TrySetWindowSubclass() { windowProc = new(OnSubclassProcedure); - bool minSize = SetWindowSubclass(hwnd, windowProc, WindowSubclassId, 0); + bool windowHooked = SetWindowSubclass(hwnd, windowProc, WindowSubclassId, 0); - bool hideSystemMenu = true; + bool titleBarHooked = true; // only hook up drag bar proc when use legacy Window.ExtendsContentIntoTitleBar if (isLegacyDragBar) { - dragBarProc = new(OnDragBarProcedure); - hwndDragBar = FindWindowEx(hwnd, HWND.Zero, "DRAG_BAR_WINDOW_CLASS", string.Empty); + hwndDragBar = FindWindowEx(hwnd, default, "DRAG_BAR_WINDOW_CLASS", string.Empty); - hideSystemMenu = SetWindowSubclass(hwndDragBar, dragBarProc, DragBarSubclassId, 0); + if (!hwndDragBar.IsNull) + { + dragBarProc = new(OnDragBarProcedure); + titleBarHooked = SetWindowSubclass(hwndDragBar, dragBarProc, DragBarSubclassId, 0); + } } - return minSize && hideSystemMenu; + return windowHooked && titleBarHooked; } /// @@ -79,7 +83,7 @@ internal class WindowSubclassManager : IDisposable { case WM_GETMINMAXINFO: { - float scalingFactor = (float)Persistence.GetScaleForWindow(hwnd); + double scalingFactor = Persistence.GetScaleForWindow(hwnd); Win32.Unsafe.SetMinTrackSize(lParam, MinWidth * scalingFactor, MinHeight * scalingFactor); break; } @@ -87,7 +91,7 @@ internal class WindowSubclassManager : IDisposable case WM_NCRBUTTONDOWN: case WM_NCRBUTTONUP: { - return (LRESULT)IntPtr.Zero; + return new(0); } } @@ -101,7 +105,7 @@ internal class WindowSubclassManager : IDisposable case WM_NCRBUTTONDOWN: case WM_NCRBUTTONUP: { - return (LRESULT)IntPtr.Zero; + return new(0); } } diff --git a/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs deleted file mode 100644 index 0e18fd63..00000000 --- a/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.Extensions.DependencyInjection; -using Snap.Hutao.Core.Caching; -using Snap.Hutao.Service.Metadata; -using Snap.Hutao.Web.Enka; -using Snap.Hutao.Web.Hoyolab.Bbs.User; -using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement; -using Snap.Hutao.Web.Hoyolab.Takumi.Binding; -using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; -using Snap.Hutao.Web.Hutao; -using System.Net.Http; - -namespace Snap.Hutao; - -/// -/// 配置 -/// -internal static class IocHttpClientConfiguration -{ - private static readonly string CommonUA = $"Snap Hutao/{Core.CoreEnvironment.Version}"; - - /// - /// 添加 - /// - /// 集合 - /// 可继续操作的集合 - public static IServiceCollection AddHttpClients(this IServiceCollection services) - { - // services - services.AddHttpClient(DefaultConfiguration); - services.AddHttpClient(DefaultConfiguration) - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { MaxConnectionsPerServer = 20 }); - - // normal clients - services.AddHttpClient(DefaultConfiguration); - services.AddHttpClient(DefaultConfiguration); - services.AddHttpClient(DefaultConfiguration); - services.AddHttpClient(DefaultConfiguration); - - // x-rpc clients - services.AddHttpClient(XRpcConfiguration); - services.AddHttpClient(XRpcConfiguration); - - return services; - } - - /// - /// 默认配置 - /// - /// 配置后的客户端 - private static void DefaultConfiguration(this HttpClient client) - { - client.Timeout = Timeout.InfiniteTimeSpan; - client.DefaultRequestHeaders.UserAgent.ParseAdd(CommonUA); - } - - /// - /// 对于需要添加动态密钥的客户端使用此配置 - /// - /// 配置后的客户端 - private static void XRpcConfiguration(this HttpClient client) - { - client.DefaultConfiguration(); - client.DefaultRequestHeaders.Add("x-rpc-app_version", "2.30.1"); - client.DefaultRequestHeaders.Add("x-rpc-client_type", "5"); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml index d5036a55..7c525ec4 100644 --- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml +++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml @@ -13,6 +13,7 @@ Margin="48,0,0,0" Height="44" x:Name="TitleBarView"/> + diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs index 3ce2257e..d2f7be1a 100644 --- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs @@ -16,6 +16,8 @@ public sealed partial class MainWindow : Window private readonly AppDbContext appDbContext; private readonly WindowManager windowManager; + private readonly TaskCompletionSource initializaionCompletionSource = new(); + /// /// 构造一个新的主窗体 /// @@ -26,8 +28,12 @@ public sealed partial class MainWindow : Window this.appDbContext = appDbContext; InitializeComponent(); windowManager = new WindowManager(this, TitleBarView.DragableArea); + + initializaionCompletionSource.TrySetResult(); } + + private void MainWindowClosed(object sender, WindowEventArgs args) { windowManager?.Dispose(); diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index 0457ea18..ce80236f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -9,7 +9,7 @@ + Version="1.0.26.0" /> 胡桃 @@ -18,8 +18,8 @@ - - + + diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs index 86d35622..3d8f8210 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs @@ -76,6 +76,13 @@ public interface IInfoBarService /// 关闭延迟 void Success(string title, string message, int delay = 5000); + /// + /// 异步等待加载完成 + /// + /// 取消令牌 + /// 任务 + Task WaitInitializationAsync(CancellationToken token = default); + /// /// 显示警告信息 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs index 718d05da..ad62f22e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs @@ -10,12 +10,25 @@ namespace Snap.Hutao.Service; [Injection(InjectAs.Singleton, typeof(IInfoBarService))] internal class InfoBarService : IInfoBarService { + private readonly TaskCompletionSource initializaionCompletionSource = new(); private StackPanel? infoBarStack; + /// public void Initialize(StackPanel container) { infoBarStack = container; + initializaionCompletionSource.TrySetResult(); + } + + /// + /// 异步等待主窗体加载完成 + /// + /// 取消令牌 + /// 任务 + public Task WaitInitializationAsync(CancellationToken token = default) + { + return initializaionCompletionSource.Task; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataInitializer.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataInitializer.cs index 1427fbbc..72eec155 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataInitializer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataInitializer.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.Abstraction; + namespace Snap.Hutao.Service.Metadata; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index 331984de..4e3a7fe5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Caching.Memory; using Snap.Hutao.Context.FileSystem; using Snap.Hutao.Core.Abstraction; +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.Logging; using Snap.Hutao.Model.Metadata.Achievement; @@ -23,6 +24,7 @@ namespace Snap.Hutao.Service.Metadata; /// 元数据服务 /// [Injection(InjectAs.Singleton, typeof(IMetadataService))] +[HttpClient(HttpClientConfigration.Default)] internal class MetadataService : IMetadataService, IMetadataInitializer, ISupportAsyncInitialization { private const string MetaAPIHost = "http://hutao-metadata.snapgenshin.com"; @@ -46,25 +48,25 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor /// 构造一个新的元数据服务 /// /// 信息条服务 - /// http客户端 + /// http客户端工厂 /// 我的文档上下文 /// json序列化选项 /// 日志器 /// 内存缓存 public MetadataService( IInfoBarService infoBarService, - HttpClient httpClient, + IHttpClientFactory httpClientFactory, MetadataContext metadataContext, JsonSerializerOptions options, ILogger logger, IMemoryCache memoryCache) { this.infoBarService = infoBarService; - this.httpClient = httpClient; this.metadataContext = metadataContext; this.options = options; this.logger = logger; this.memoryCache = memoryCache; + httpClient = httpClientFactory.CreateClient(nameof(MetadataService)); } /// @@ -73,63 +75,63 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor /// public async ValueTask InitializeAsync(CancellationToken token = default) { - await initializeCompletionSource.Task; + await initializeCompletionSource.Task.ConfigureAwait(false); return IsInitialized; } /// public async Task InitializeInternalAsync(CancellationToken token = default) { - logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion begin"); ValueStopwatch stopwatch = ValueStopwatch.StartNew(); + logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion begin"); + IsInitialized = await TryUpdateMetadataAsync(token) .ConfigureAwait(false); initializeCompletionSource.SetResult(); - - logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion completed"); + logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion completed in {time}ms", stopwatch.GetElapsedTime().TotalMilliseconds); } /// public ValueTask> GetAchievementGoalsAsync(CancellationToken token = default) { - return GetMetadataAsync>("AchievementGoal", token); + return FromCacheOrFileAsync>("AchievementGoal", token); } /// public ValueTask> GetAchievementsAsync(CancellationToken token = default) { - return GetMetadataAsync>("Achievement", token); + return FromCacheOrFileAsync>("Achievement", token); } /// public ValueTask> GetAvatarsAsync(CancellationToken token = default) { - return GetMetadataAsync>("Avatar", token); + return FromCacheOrFileAsync>("Avatar", token); } /// public ValueTask> GetReliquariesAsync(CancellationToken token = default) { - return GetMetadataAsync>("Reliquary", token); + return FromCacheOrFileAsync>("Reliquary", token); } /// public ValueTask> GetReliquaryAffixesAsync(CancellationToken token = default) { - return GetMetadataAsync>("ReliquaryAffix", token); + return FromCacheOrFileAsync>("ReliquaryAffix", token); } /// public ValueTask> GetReliquaryMainAffixesAsync(CancellationToken token = default) { - return GetMetadataAsync>("ReliquaryMainAffix", token); + return FromCacheOrFileAsync>("ReliquaryMainAffix", token); } /// public ValueTask> GetWeaponsAsync(CancellationToken token = default) { - return GetMetadataAsync>("Weapon", token); + return FromCacheOrFileAsync>("Weapon", token); } private async Task TryUpdateMetadataAsync(CancellationToken token = default) @@ -171,20 +173,18 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor { (string fileName, string md5) = pair; string fileFullName = $"{fileName}.json"; - bool skip = false; + bool skip = false; if (metadataContext.FileExists(fileFullName)) { - skip = md5 == await GetFileMd5Async(fileFullName, token) - .ConfigureAwait(false); + skip = md5 == await GetFileMd5Async(fileFullName, token).ConfigureAwait(false); } if (!skip) { - logger.LogInformation(EventIds.MetadataFileMD5Check, "MD5 of {file} not matched", fileFullName); + logger.LogInformation(EventIds.MetadataFileMD5Check, "MD5 of {file} not matched, begin downloading", fileFullName); - await DownloadMetadataAsync(fileFullName, token) - .ConfigureAwait(false); + await DownloadMetadataAsync(fileFullName, token).ConfigureAwait(false); } }); } @@ -214,8 +214,8 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor { while (await streamReader.ReadLineAsync().ConfigureAwait(false) is string line) { - Func writeMethod = streamReader.EndOfStream ? streamWriter.WriteAsync : streamWriter.WriteLineAsync; - await writeMethod(line).ConfigureAwait(false); + Func write = streamReader.EndOfStream ? streamWriter.WriteAsync : streamWriter.WriteLineAsync; + await write(line).ConfigureAwait(false); } } } @@ -223,7 +223,7 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor logger.LogInformation("Download {file} completed", fileFullName); } - private async ValueTask GetMetadataAsync(string fileName, CancellationToken token) + private async ValueTask FromCacheOrFileAsync(string fileName, CancellationToken token) where T : class { Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败"); @@ -234,10 +234,10 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor return Must.NotNull((value as T)!); } - T? result = await JsonSerializer - .DeserializeAsync(metadataContext.OpenRead($"{fileName}.json"), options, token) - .ConfigureAwait(false); - - return memoryCache.Set(cacheKey, Must.NotNull(result!)); + using (Stream fileStream = metadataContext.OpenRead($"{fileName}.json")) + { + T? result = await JsonSerializer.DeserializeAsync(fileStream, options, token).ConfigureAwait(false); + return memoryCache.Set(cacheKey, Must.NotNull(result!)); + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index b5d861dc..f60bf593 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -24,7 +24,13 @@ Never 0 Snap.Hutao.Program - DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT + $(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT + + + embedded + + + embedded diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml index 0ffb7ebb..349e2f87 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml @@ -16,9 +16,8 @@ @@ -37,9 +36,9 @@ shvh:NavHelper.NavigateTo="shvp:WikiAvatarPage" Icon="{cwu:BitmapIcon ShowAsMonochrome=True,Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/> - + - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs index 65581190..9cc4db1e 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Core; using Snap.Hutao.Core.Logging; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Navigation; @@ -27,7 +28,8 @@ public sealed partial class MainView : UserControl { InitializeComponent(); - uISettings = new(); + // 由于 PopupRoot 的 BUG, 需要手动响应主题色更改 + uISettings = Ioc.Default.GetRequiredService(); uISettings.ColorValuesChanged += OnUISettingsColorValuesChanged; infoBarService = Ioc.Default.GetRequiredService(); @@ -46,20 +48,13 @@ public sealed partial class MainView : UserControl private void UpdateTheme() { - if (RequestedTheme.ToString() == App.Current.RequestedTheme.ToString()) + if (!ThemeHelper.Equals(App.Current.RequestedTheme, RequestedTheme)) { - return; + ILogger logger = Ioc.Default.GetRequiredService>(); + logger.LogInformation(EventIds.CommonLog, "Element Theme [{element}] App Theme [{app}]", RequestedTheme, App.Current.RequestedTheme); + + // Update controls' theme which presents in the PopupRoot + RequestedTheme = ThemeHelper.ApplicationToElement(App.Current.RequestedTheme); } - - ILogger logger = Ioc.Default.GetRequiredService>(); - logger.LogInformation(EventIds.CommonLog, "Element Theme [{element}] App Theme [{app}]", RequestedTheme, App.Current.RequestedTheme); - - // Update controls' theme which presents in the PopupRoot - RequestedTheme = App.Current.RequestedTheme switch - { - ApplicationTheme.Light => ElementTheme.Light, - ApplicationTheme.Dark => ElementTheme.Dark, - _ => throw Must.NeverHappen(), - }; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index d1b39138..2b45b625 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -41,7 +41,8 @@ + Header="打开 数据 文件夹" + Description="用户数据/日志/元数据在此处存放"> public sealed partial class UserView : UserControl { - private static readonly DependencyProperty IsExpandedProperty = Property.Depend(nameof(IsExpanded), true); - /// /// 构造一个新的用户视图 /// @@ -23,13 +21,4 @@ public sealed partial class UserView : UserControl InitializeComponent(); DataContext = Ioc.Default.GetRequiredService(); } - - /// - /// 当前用户控件是否处于展开状态 - /// - public bool IsExpanded - { - get => (bool)GetValue(IsExpandedProperty); - set => SetValue(IsExpandedProperty, value); - } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/ExperimentalFeaturesViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/ExperimentalFeaturesViewModel.cs index 0c64aa1e..4dd8d39c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/ExperimentalFeaturesViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/ExperimentalFeaturesViewModel.cs @@ -4,6 +4,10 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Context.FileSystem.Location; using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Web.Hoyolab.Takumi.Binding; +using Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; +using Snap.Hutao.Web.Response; using Windows.System; namespace Snap.Hutao.ViewModel; @@ -15,18 +19,33 @@ namespace Snap.Hutao.ViewModel; internal class ExperimentalFeaturesViewModel : ObservableObject { private readonly IFileSystemLocation hutaoLocation; + private readonly IUserService userService; + private readonly SignClient signClient; + private readonly IInfoBarService infoBarService; /// /// 构造一个新的实验性功能视图模型 /// /// 异步命令工厂 /// 数据文件夹 - public ExperimentalFeaturesViewModel(IAsyncRelayCommandFactory asyncRelayCommandFactory, HutaoLocation hutaoLocation) + /// 用户服务 + /// 签到客户端 + /// 信息栏服务 + public ExperimentalFeaturesViewModel( + IAsyncRelayCommandFactory asyncRelayCommandFactory, + HutaoLocation hutaoLocation, + IUserService userService, + SignClient signClient, + IInfoBarService infoBarService) { this.hutaoLocation = hutaoLocation; + this.userService = userService; + this.signClient = signClient; + this.infoBarService = infoBarService; OpenCacheFolderCommand = asyncRelayCommandFactory.Create(OpenCacheFolderAsync); OpenDataFolderCommand = asyncRelayCommandFactory.Create(OpenDataFolderAsync); + SignAllUserGameRolesCommand = asyncRelayCommandFactory.Create(SignAllUserGameRolesAsync); } /// @@ -39,13 +58,35 @@ internal class ExperimentalFeaturesViewModel : ObservableObject /// public ICommand OpenDataFolderCommand { get; } - private Task OpenCacheFolderAsync(CancellationToken token) + /// + /// 签到全部角色命令 + /// + public ICommand SignAllUserGameRolesCommand { get; } + + private Task OpenCacheFolderAsync() { - return Launcher.LaunchFolderAsync(App.CacheFolder).AsTask(token); + return Launcher.LaunchFolderAsync(App.CacheFolder).AsTask(); } - private Task OpenDataFolderAsync(CancellationToken token) + private Task OpenDataFolderAsync() { - return Launcher.LaunchFolderPathAsync(hutaoLocation.GetPath()).AsTask(token); + return Launcher.LaunchFolderPathAsync(hutaoLocation.GetPath()).AsTask(); + } + + private async Task SignAllUserGameRolesAsync() + { + foreach (Model.Binding.User user in await userService.GetUserCollectionAsync()) + { + foreach (UserGameRole role in user.UserGameRoles) + { + Response? result = await signClient.SignAsync(user, role); + if (result != null) + { + infoBarService.Information(result.Message); + } + + await Task.Delay(TimeSpan.FromSeconds(15)); + } + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs index d24f964f..86280e20 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Web.Enka.Model; using Snap.Hutao.Web.Hoyolab; using System.Net.Http; @@ -11,7 +12,7 @@ namespace Snap.Hutao.Web.Enka; /// /// Enka API 客户端 /// -[Injection(InjectAs.Transient)] +[HttpClient(HttpClientConfigration.Default)] internal class EnkaClient { private const string EnkaAPI = "https://enka.shinshin.moe/u/{0}/__data.json"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs index a6424c6c..d0450e9d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs @@ -19,6 +19,11 @@ internal static class ApiEndpoints /// public const string AnnContent = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery}"; + /// + /// 角色信息 + /// + public const string GameRecordCharacter = $"{ApiTakumiRecordApi}/character"; + /// /// 游戏记录主页 /// @@ -33,17 +38,53 @@ internal static class ApiEndpoints /// /// 深渊信息 /// - public const string SpiralAbyss = $"{ApiTakumiRecordApi}/spiralAbyss?schedule_type={{0}}&role_id={{1}}&server={{2}}"; + /// 深渊类型 + /// Uid + /// 深渊信息字符串 + public static string GameRecordSpiralAbyss(Takumi.GameRecord.SpiralAbyssSchedule scheduleType, PlayerUid uid) + { + return $"{ApiTakumiRecordApi}/spiralAbyss?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}"; + } /// - /// 角色信息 + /// 签到活动Id /// - public const string Character = $"{ApiTakumiRecordApi}/character"; + public const string SignInRewardActivityId = "e202009291139501"; /// - /// 用户游戏角色 + /// 签到 /// - public const string UserGameRoles = $"{ApiTakumi}/binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn"; + public const string SignInRewardHome = $"{ApiTakumi}/event/bbs_sign_reward/home?act_id={SignInRewardActivityId}"; + + /// + /// 签到信息 + /// + /// uid + /// 签到信息字符串 + public static string SignInRewardInfo(PlayerUid uid) + { + return $"{ApiTakumi}/event/bbs_sign_reward/info?act_id={SignInRewardActivityId}®ion={uid.Region}&uid={uid.Value}"; + } + + /// + /// 签到 + /// + public const string SignInRewardReSign = $"{ApiTakumi}/event/bbs_sign_reward/resign"; + + /// + /// 补签信息 + /// + /// uid + /// 补签信息字符串 + public static string SignInRewardResignInfo(PlayerUid uid) + { + return $"{ApiTakumi}/event/bbs_sign_reward/resign_info?act_id=e202009291139501®ion={uid.Region}&uid={uid.Value}"; + } + + /// + /// 签到 + /// + public const string SignInRewardSign = $"{ApiTakumi}/event/bbs_sign_reward/sign"; /// /// 用户详细信息 @@ -53,8 +94,19 @@ internal static class ApiEndpoints /// /// 查询其他用户详细信息 /// - public const string UserFullInfoQuery = $"{BbsApiUserApi}/getUserFullInfo?uid={{0}}&gids=2"; + /// bbs Uid + /// 查询其他用户详细信息字符串 + public static string UserFullInfoQuery(string bbsUid) + { + return $"{BbsApiUserApi}/getUserFullInfo?uid={bbsUid}&gids=2"; + } + /// + /// 用户游戏角色 + /// + public const string UserGameRoles = $"{ApiTakumi}/binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn"; + + // consts private const string ApiTakumi = "https://api-takumi.mihoyo.com"; private const string ApiTakumiRecord = "https://api-takumi-record.mihoyo.com"; private const string ApiTakumiRecordApi = $"{ApiTakumiRecord}/game_record/app/genshin/api"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs index 86a04145..aa57db0d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Web.Hoyolab.DynamicSecret; using Snap.Hutao.Web.Response; using System.Net.Http; @@ -11,7 +12,7 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User; /// /// 用户信息客户端 /// -[Injection(InjectAs.Transient)] +[HttpClient(HttpClientConfigration.XRpc)] internal class UserClient { private readonly HttpClient httpClient; @@ -20,7 +21,6 @@ internal class UserClient /// /// 构造一个新的用户信息客户端 /// - /// 用户服务 /// http客户端 /// Json序列化选项 public UserClient(HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions) @@ -47,17 +47,18 @@ internal class UserClient } /// - /// 获取当前用户详细信息 + /// 获取其他用户详细信息 /// + /// 当前用户 /// 米游社Uid /// 取消令牌 /// 详细信息 - public async Task GetUserFullInfoAsync(string uid, CancellationToken token = default) + public async Task GetUserFullInfoAsync(Model.Binding.User user, string uid, CancellationToken token = default) { Response? resp = await httpClient .UsingDynamicSecret() - /*.SetUser(userService.CurrentUser)*/ - .GetFromJsonAsync>(string.Format(ApiEndpoints.UserFullInfoQuery, uid), jsonSerializerOptions, token) + .SetUser(user) + .GetFromJsonAsync>(ApiEndpoints.UserFullInfoQuery(uid), jsonSerializerOptions, token) .ConfigureAwait(false); return resp?.Data?.UserInfo; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs index d4eb034e..219b90f0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs @@ -11,9 +11,6 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret; /// internal abstract class DynamicSecretProvider : Md5Convert { - // @Azure99 respect original author - private static readonly string Salt = "4a8knnbk5pbjqsrudp3dq484m9axoc5g"; - /// /// 创建动态密钥 /// @@ -22,8 +19,10 @@ internal abstract class DynamicSecretProvider : Md5Convert { // unix timestamp long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + string r = GetRandomString(); - string check = ToHexString($"salt={Salt}&t={t}&r={r}"); + + string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret1Salt}&t={t}&r={r}").ToLowerInvariant(); return $"{t},{r},{check}"; } @@ -31,22 +30,17 @@ internal abstract class DynamicSecretProvider : Md5Convert private static string GetRandomString() { StringBuilder sb = new(6); - Random random = new(); for (int i = 0; i < 6; i++) { - int offset = random.Next(0, 32768) % 26; - - // 实际上只能取到前16个小写字母 - int target = 'a' - 10; - - // 取数字 - if (offset < 10) + int v8 = Random.Shared.Next(0, 32768) % 26; + int v9 = 87; + if (v8 < 10) { - target = '0'; + v9 = 48; } - _ = sb.Append((char)(offset + target)); + _ = sb.Append((char)(v8 + v9)); } return sb.ToString(); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs index f6ce4d12..9f605926 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs @@ -11,19 +11,6 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret; /// internal abstract class DynamicSecretProvider2 : Md5Convert { - /// - /// salt - /// - public const string AppVersion = "2.34.1"; - - /// - /// 米游社的盐 - /// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd - /// libxxxx.so - /// - private static readonly string Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"; - private static readonly Random Random = new(); - /// /// 创建动态密钥 /// @@ -43,15 +30,10 @@ internal abstract class DynamicSecretProvider2 : Md5Convert string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options); // query - string q = string.Empty; - string? query = new UriBuilder(queryUrl).Query; - if (!string.IsNullOrEmpty(query)) - { - q = string.Join("&", query.Split('&').OrderBy(x => x)); - } + string q = string.Join("&", new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x)); // check - string check = ToHexString($"salt={Salt}&t={t}&r={r}&b={b}&q={q}"); + string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret2Salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant(); return $"{t},{r},{check}"; } @@ -66,7 +48,7 @@ internal abstract class DynamicSecretProvider2 : Md5Convert // v18 = v17; // else // v18 = v17 + 542367; - int rand = Random.Next(100000, 200000); + int rand = Random.Shared.Next(100000, 200000); if (rand == 100000) { rand = 642367; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/DynamicSecret2HttpClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/DynamicSecret2HttpClient.cs new file mode 100644 index 00000000..9f4bd3fa --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/DynamicSecret2HttpClient.cs @@ -0,0 +1,77 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Net.Http; +using System.Net.Http.Json; + +namespace Snap.Hutao.Web.Hoyolab.DynamicSecret.Http; + +/// +/// 使用动态密钥2的Http客户端默认实现 +/// +/// 请求提交的数据的的格式 +internal class DynamicSecret2HttpClient : IDynamicSecret2HttpClient +{ + private readonly HttpClient httpClient; + private readonly JsonSerializerOptions options; + private readonly string url; + + /// + /// 构造一个新的使用动态密钥2的Http客户端默认实现的实例 + /// + /// 请求使用的客户端 + /// Json序列化选项 + /// url + /// 请求的数据 + public DynamicSecret2HttpClient(HttpClient httpClient, JsonSerializerOptions options, string url) + { + this.httpClient = httpClient; + this.options = options; + this.url = url; + + httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider2.Create(options, url, null)); + } + + /// + public Task GetFromJsonAsync(CancellationToken token) + { + return httpClient.GetFromJsonAsync(url, options, token); + } +} + +/// +/// 使用动态密钥2的Http客户端默认实现 +/// +/// 请求提交的数据的的格式 +[SuppressMessage("", "SA1402")] +internal class DynamicSecret2HttpClient : IDynamicSecret2HttpClient + where TValue : class +{ + private readonly HttpClient httpClient; + private readonly JsonSerializerOptions options; + private readonly string url; + private readonly TValue? data = null; + + /// + /// 构造一个新的使用动态密钥2的Http客户端默认实现的实例 + /// + /// 请求使用的客户端 + /// Json序列化选项 + /// url + /// 请求的数据 + public DynamicSecret2HttpClient(HttpClient httpClient, JsonSerializerOptions options, string url, TValue? data) + { + this.httpClient = httpClient; + this.options = options; + this.url = url; + this.data = data; + + httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider2.Create(options, url, data)); + } + + /// + public Task PostAsJsonAsync(CancellationToken token) + { + return httpClient.PostAsJsonAsync(url, data, options, token); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/IDynamicSecret2HttpClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/IDynamicSecret2HttpClient.cs new file mode 100644 index 00000000..ddfe3376 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/IDynamicSecret2HttpClient.cs @@ -0,0 +1,35 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Net.Http; + +namespace Snap.Hutao.Web.Hoyolab.DynamicSecret.Http; + +/// +/// 使用动态密钥2的Http客户端抽象 +/// +internal interface IDynamicSecret2HttpClient +{ + /// + /// Sends a GET request to the specified Uri and returns the value that results from deserializing the response body as JSON in an asynchronous operation. + /// + /// The target type to deserialize to. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The task object representing the asynchronous operation. + Task GetFromJsonAsync(CancellationToken token); +} + +/// +/// 使用动态密钥2的Http客户端抽象 +/// +/// 请求数据的类型 +internal interface IDynamicSecret2HttpClient + where TValue : class +{ + /// + /// Sends a POST request to the specified Uri containing the value serialized as JSON in the request body. + /// + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The task object representing the asynchronous operation. + Task PostAsJsonAsync(CancellationToken token); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs index 26206390..41f16e28 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Web.Hoyolab.DynamicSecret.Http; using System.Net.Http; namespace Snap.Hutao.Web.Hoyolab.DynamicSecret; @@ -29,9 +30,24 @@ internal static class HttpClientDynamicSecretExtensions /// 地址 /// post数据 /// 响应 - public static HttpClient UsingDynamicSecret2(this HttpClient httpClient, JsonSerializerOptions options, string url, object? data = null) + public static IDynamicSecret2HttpClient UsingDynamicSecret2(this HttpClient httpClient, JsonSerializerOptions options, string url) + { + return new DynamicSecret2HttpClient(httpClient, options, url); + } + + /// + /// 使用二代动态密钥执行 GET 操作 + /// + /// 请求数据的类型 + /// 请求器 + /// 选项 + /// 地址 + /// post数据 + /// 响应 + public static IDynamicSecret2HttpClient UsingDynamicSecret2(this HttpClient httpClient, JsonSerializerOptions options, string url, TValue data) + where TValue : class { httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider2.Create(options, url, data)); - return httpClient; + return new DynamicSecret2HttpClient(httpClient, options, url, data); } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementClient.cs index d677fdef..aaa24901 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementClient.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Extension; using Snap.Hutao.Web.Response; using System.Collections.Generic; @@ -12,7 +13,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement; /// /// 公告客户端 /// -[Injection(InjectAs.Transient)] +[HttpClient(HttpClientConfigration.Default)] internal class AnnouncementClient { private readonly HttpClient httpClient; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRole.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRole.cs index e0ca6238..a57f0fc0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRole.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRole.cs @@ -8,7 +8,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding; /// /// 用户游戏角色 /// -public record UserGameRole +public class UserGameRole { /// /// hk4e_cn for Genshin Impact @@ -68,16 +68,7 @@ public record UserGameRole public static explicit operator PlayerUid(UserGameRole userGameRole) { - return userGameRole.AsPlayerUid(); - } - - /// - /// 转化为 - /// - /// 一个等价的 实例 - public PlayerUid AsPlayerUid() - { - return new PlayerUid(GameUid, Region); + return new PlayerUid(userGameRole.GameUid, userGameRole.Region); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRoleClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRoleClient.cs index 6464e9c2..b156e8cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRoleClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRoleClient.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Extension; using Snap.Hutao.Model.Binding; using Snap.Hutao.Web.Response; @@ -13,7 +14,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding; /// /// 用户游戏角色提供器 /// -[Injection(InjectAs.Transient)] +[HttpClient(HttpClientConfigration.Default)] internal class UserGameRoleClient { private readonly HttpClient httpClient; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Award.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Award.cs new file mode 100644 index 00000000..1cb80951 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Award.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json.Serialization; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; + +/// +/// 奖励物品 +/// +public class Award +{ + /// + /// 图标 + /// + [JsonPropertyName("icon")] + public string Icon { get; set; } = default!; + + /// + /// 名称 + /// + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + + /// + /// 个数 + /// + [JsonPropertyName("cnt")] + public string Count { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Reward.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Reward.cs new file mode 100644 index 00000000..416a7365 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Reward.cs @@ -0,0 +1,28 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; + +public class Reward +{ + /// + /// 月份 + /// + [JsonPropertyName("month")] + public string? Month { get; set; } + + /// + /// 奖励列表 + /// + [JsonPropertyName("awards")] + public List? Awards { get; set; } + + /// + /// 支持补签 + /// + [JsonPropertyName("resign")] + public bool Resign { get; set; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignClient.cs new file mode 100644 index 00000000..d7cc6e25 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignClient.cs @@ -0,0 +1,129 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Model.Binding; +using Snap.Hutao.Web.Hoyolab.DynamicSecret; +using Snap.Hutao.Web.Hoyolab.Takumi.Binding; +using Snap.Hutao.Web.Response; +using System.Net.Http; +using System.Net.Http.Json; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; + +/// +/// 签到客户端 +/// +[HttpClient(HttpClientConfigration.XRpc)] +internal class SignClient +{ + private readonly HttpClient httpClient; + private readonly JsonSerializerOptions options; + + /// + /// 构造一个新的签到客户端 + /// + /// http客户端 + /// 选项 + public SignClient(HttpClient httpClient, JsonSerializerOptions options) + { + this.httpClient = httpClient; + this.options = options; + } + + /// + /// 异步获取签到信息 + /// + /// 用户 + /// 角色 + /// 取消令牌 + /// 签到信息 + public async Task GetInfoAsync(User user, UserGameRole role, CancellationToken token = default) + { + Response? resp = await httpClient + .SetUser(user) + .UsingDynamicSecret() + .GetFromJsonAsync>(ApiEndpoints.SignInRewardInfo((PlayerUid)role), options, token) + .ConfigureAwait(false); + + return resp?.Data; + } + + /// + /// 异步获取签到信息 + /// + /// 用户 + /// 角色 + /// 取消令牌 + /// 签到信息 + public async Task GetResignInfoAsync(User user, UserGameRole role, CancellationToken token = default) + { + Response? resp = await httpClient + .SetUser(user) + .UsingDynamicSecret() + .GetFromJsonAsync>(ApiEndpoints.SignInRewardResignInfo((PlayerUid)role), options, token) + .ConfigureAwait(false); + + return resp?.Data; + } + + /// + /// 获取签到奖励 + /// + /// 用户 + /// 取消令牌 + /// 奖励信息 + public async Task GetRewardAsync(User user, CancellationToken token = default) + { + Response? resp = await httpClient + .SetUser(user) + .GetFromJsonAsync>(ApiEndpoints.SignInRewardHome, options, token) + .ConfigureAwait(false); + + return resp?.Data; + } + + /// + /// 补签 + /// + /// 用户 + /// 角色 + /// 取消令牌 + /// 签到消息 + public async Task?> ReSignAsync(User user, UserGameRole role, CancellationToken token = default) + { + SignInData data = new((PlayerUid)role); + + HttpResponseMessage response = await httpClient + .SetUser(user) + .UsingDynamicSecret() + .PostAsJsonAsync(ApiEndpoints.SignInRewardReSign, data, options, token) + .ConfigureAwait(false); + Response? resp = await response.Content + .ReadFromJsonAsync>(options, token) + .ConfigureAwait(false); + + return resp; + } + + /// + /// 签到 + /// + /// 用户 + /// 角色 + /// 取消令牌 + /// 签到消息 + public async Task?> SignAsync(User user, UserGameRole role, CancellationToken token = default) + { + HttpResponseMessage response = await httpClient + .SetUser(user) + .UsingDynamicSecret() + .PostAsJsonAsync(ApiEndpoints.SignInRewardSign, new SignInData((PlayerUid)role), options, token) + .ConfigureAwait(false); + Response? resp = await response.Content + .ReadFromJsonAsync>(options, token) + .ConfigureAwait(false); + + return resp; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInData.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInData.cs new file mode 100644 index 00000000..c861dc77 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInData.cs @@ -0,0 +1,40 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json.Serialization; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; + +/// +/// 签到提交数据 +/// +public class SignInData +{ + /// + /// 构造一个新的签到提交数据 + /// + /// uid + public SignInData(PlayerUid uid) + { + Region = uid.Region; + Uid = uid.Value; + } + + /// + /// 活动Id + /// + [JsonPropertyName("act_id")] + public string ActivityId { get; } = ApiEndpoints.SignInRewardActivityId; + + /// + /// 地区代码 + /// + [JsonPropertyName("region")] + public string Region { get; } + + /// + /// Uid + /// + [JsonPropertyName("uid")] + public string Uid { get; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInResult.cs new file mode 100644 index 00000000..1d473885 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInResult.cs @@ -0,0 +1,42 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json.Serialization; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; + +/// +/// 签到结果 +/// +public class SignInResult +{ + /// + /// 通常是 "" + /// + [JsonPropertyName("code")] + public string Code { get; set; } = default!; + + /// + /// 通常是 "" + /// + [JsonPropertyName("risk_code")] + public int RiskCode { get; set; } + + /// + /// 通常是 "" + /// + [JsonPropertyName("gt")] + public string Gt { get; set; } = default!; + + /// + /// 通常是 "" + /// + [JsonPropertyName("challenge")] + public string Challenge { get; set; } = default!; + + /// + /// 通常是 "" + /// + [JsonPropertyName("success")] + public int Success { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInRewardInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInRewardInfo.cs new file mode 100644 index 00000000..b9a8ab18 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInRewardInfo.cs @@ -0,0 +1,54 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json.Serialization; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; + +/// +/// 签到信息 +/// +public class SignInRewardInfo +{ + /// + /// 累积签到天数 + /// + [JsonPropertyName("total_sign_day")] + public int TotalSignDay { get; set; } + + /// + /// yyyy-MM-dd + /// + [JsonPropertyName("today")] + public string? Today { get; set; } + + /// + /// 今日是否已签到 + /// + [JsonPropertyName("is_sign")] + public bool IsSign { get; set; } + + /// + /// ? + /// + [JsonPropertyName("is_sub")] + public bool IsSub { get; set; } + + /// + /// 是否首次绑定 + /// + [JsonPropertyName("first_bind")] + public bool FirstBind { get; set; } + + /// + /// 是否为当月第一次 + /// + [JsonPropertyName("month_first")] + public bool MonthFirst { get; set; } + + /// + /// 漏签天数 + /// + [JsonPropertyName("sign_cnt_missed")] + public bool SignCountMissed { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInRewardReSignInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInRewardReSignInfo.cs new file mode 100644 index 00000000..845c5cd8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInRewardReSignInfo.cs @@ -0,0 +1,60 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json.Serialization; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; + +/// +/// 补签说明 +/// +public class SignInRewardReSignInfo +{ + /// + /// 当日补签次数 + /// + [JsonPropertyName("resign_cnt_daily")] + public bool ResignCountDaily { get; set; } + + /// + /// 当月补签次数 + /// + [JsonPropertyName("resign_cnt_monthly")] + public bool ResignCountMonthly { get; set; } + + /// + /// 当日补签次数限制 + /// + [JsonPropertyName("resign_limit_daily")] + public bool ResignLimitDaily { get; set; } + + /// + /// 当月补签次数限制 + /// + [JsonPropertyName("resign_limit_monthly")] + public bool ResignLimitMonthly { get; set; } + + /// + /// 漏签次数 + /// + [JsonPropertyName("sign_cnt_missed")] + public bool SignCountMissed { get; set; } + + /// + /// 米游币个数 + /// + [JsonPropertyName("coin_cnt")] + public bool CoinCount { get; set; } + + /// + /// 补签需要的米游币个数 + /// + [JsonPropertyName("coin_cost")] + public bool CoinCost { get; set; } + + /// + /// 规则 + /// + [JsonPropertyName("rule")] + public string Rule { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs index fe3b13eb..84ef349b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Extension; using Snap.Hutao.Model.Binding; using Snap.Hutao.Web.Hoyolab.DynamicSecret; @@ -17,7 +18,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; /// /// 游戏记录提供器 /// -[Injection(InjectAs.Transient)] +[HttpClient(HttpClientConfigration.XRpc)] internal class GameRecordClient { private readonly HttpClient httpClient; @@ -42,7 +43,7 @@ internal class GameRecordClient /// 玩家的基础信息 public Task GetPlayerInfoAsync(User user, CancellationToken token = default) { - PlayerUid uid = Must.NotNull(user.SelectedUserGameRole!).AsPlayerUid(); + PlayerUid uid = (PlayerUid)Must.NotNull(user.SelectedUserGameRole!); return GetPlayerInfoAsync(user, uid, token); } @@ -55,12 +56,10 @@ internal class GameRecordClient /// 玩家的基础信息 public async Task GetPlayerInfoAsync(User user, PlayerUid uid, CancellationToken token = default) { - string url = string.Format(ApiEndpoints.GameRecordIndex(uid.Value, uid.Region)); - Response? resp = await httpClient .SetUser(user) - .UsingDynamicSecret2(jsonSerializerOptions, url) - .GetFromJsonAsync>(url, jsonSerializerOptions, token) + .UsingDynamicSecret2(jsonSerializerOptions, ApiEndpoints.GameRecordIndex(uid.Value, uid.Region)) + .GetFromJsonAsync>(token) .ConfigureAwait(false); return resp?.Data; @@ -75,7 +74,7 @@ internal class GameRecordClient /// 深渊信息 public Task GetSpiralAbyssAsync(User user, SpiralAbyssSchedule schedule, CancellationToken token = default) { - PlayerUid uid = Must.NotNull(user.SelectedUserGameRole!).AsPlayerUid(); + PlayerUid uid = (PlayerUid)Must.NotNull(user.SelectedUserGameRole!); return GetSpiralAbyssAsync(user, uid, schedule, token); } @@ -89,12 +88,10 @@ internal class GameRecordClient /// 深渊信息 public async Task GetSpiralAbyssAsync(User user, PlayerUid uid, SpiralAbyssSchedule schedule, CancellationToken token = default) { - string url = string.Format(ApiEndpoints.SpiralAbyss, (int)schedule, uid.Value, uid.Region); - Response? resp = await httpClient .SetUser(user) - .UsingDynamicSecret2(jsonSerializerOptions, url) - .GetFromJsonAsync>(url, jsonSerializerOptions, token) + .UsingDynamicSecret2(jsonSerializerOptions, ApiEndpoints.GameRecordSpiralAbyss(schedule, uid)) + .GetFromJsonAsync>(token) .ConfigureAwait(false); return resp?.Data; @@ -109,7 +106,7 @@ internal class GameRecordClient /// 角色列表 public Task> GetCharactersAsync(User user, PlayerInfo playerInfo, CancellationToken token = default) { - PlayerUid uid = Must.NotNull(user.SelectedUserGameRole!).AsPlayerUid(); + PlayerUid uid = (PlayerUid)Must.NotNull(user.SelectedUserGameRole!); return GetCharactersAsync(user, uid, playerInfo, token); } @@ -127,11 +124,13 @@ internal class GameRecordClient HttpResponseMessage? response = await httpClient .SetUser(user) - .UsingDynamicSecret2(jsonSerializerOptions, ApiEndpoints.Character, data) - .PostAsJsonAsync(ApiEndpoints.Character, data, token) + .UsingDynamicSecret2(jsonSerializerOptions, ApiEndpoints.GameRecordCharacter, data) + .PostAsJsonAsync(token) .ConfigureAwait(false); - Response? resp = await response.Content.ReadFromJsonAsync>(jsonSerializerOptions, token); + Response? resp = await response.Content + .ReadFromJsonAsync>(jsonSerializerOptions, token) + .ConfigureAwait(false); return EnumerableExtensions.EmptyIfNull(resp?.Data?.Avatars); } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/HttpRequestHeadersExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/HttpRequestHeadersExtensions.cs index 988db68a..38329d90 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/HttpRequestHeadersExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/HttpRequestHeadersExtensions.cs @@ -18,7 +18,7 @@ public static class HttpRequestHeadersExtensions /// 值 public static void Set(this HttpRequestHeaders headers, string name, string? value) { - headers.Clear(); + headers.Remove(name); headers.Add(name, value); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoClient.cs index 976c9387..ee3fae77 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoClient.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core.Abstraction; +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Extension; using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; @@ -19,7 +20,8 @@ namespace Snap.Hutao.Web.Hutao; /// /// 胡桃API客户端 /// -[Injection(InjectAs.Transient)] +// [Injection(InjectAs.Transient)] +[HttpClient(HttpClientConfigration.Default)] internal class HutaoClient : ISupportAsyncInitialization { private const string AuthHost = "https://auth.snapgenshin.com"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index 43903f69..b18fb6ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -9,19 +9,9 @@ namespace Snap.Hutao.Web.Response; public enum KnownReturnCode { /// - /// Json 异常 + /// 无效请求 /// - JsonParseIssue = -2000000002, - - /// - /// Url为 空 - /// - UrlIsEmpty = -2000000001, - - /// - /// 内部错误 - /// - InternalFailure = -2000000000, + InvalidRequest = -10001, /// /// 已经签到过了 diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs index 4e455988..8bf19529 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs @@ -25,16 +25,12 @@ public class Response : ISupportValidation if (!Validate()) { - Ioc.Default - .GetRequiredService() - .Error(ToString()); + Ioc.Default.GetRequiredService().Error(ToString()); } if (ReturnCode != 0) { - Ioc.Default - .GetRequiredService() - .Warning(ToString()); + Ioc.Default.GetRequiredService().Warning(ToString()); } } @@ -71,4 +67,31 @@ public class Response : ISupportValidation { return $"状态:{ReturnCode} | 信息:{Message}"; } +} + +/// +/// Mihoyo 标准API响应 +/// +/// 数据类型 +[SuppressMessage("", "SA1402")] +public class Response : Response +{ + /// + /// 构造一个新的 Mihoyo 标准API响应 + /// + /// 返回代码 + /// 消息 + /// 数据 + [JsonConstructor] + public Response(int returnCode, string message, TData? data) + : base(returnCode, message) + { + Data = data; + } + + /// + /// 数据 + /// + [JsonPropertyName("data")] + public TData? Data { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs deleted file mode 100644 index 34fa102b..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.Text.Json.Serialization; - -namespace Snap.Hutao.Web.Response; - -/// -/// Mihoyo 标准API响应 -/// -/// 数据类型 -public class Response : Response -{ - /// - /// 构造一个新的 Mihoyo 标准API响应 - /// - /// 返回代码 - /// 消息 - /// 数据 - [JsonConstructor] - public Response(int returnCode, string message, TData? data) - : base(returnCode, message) - { - Data = data; - } - - /// - /// 数据 - /// - [JsonPropertyName("data")] - public TData? Data { get; set; } -}