mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
use source generator to simplify HttpClient Injection
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 注入HttpClient代码生成器
|
||||
/// 旨在使用源生成器提高注入效率
|
||||
/// 防止在运行时动态查找注入类型
|
||||
/// </summary>
|
||||
[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";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
{
|
||||
// Register a syntax receiver that will be created for each generation pass
|
||||
context.RegisterForSyntaxNotifications(() => new HttpClientSyntaxContextReceiver());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<string> 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<TypedConstant> 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<KeyValuePair<string, TypedConstant>> properties = handlerInfo.NamedArguments;
|
||||
lineBuilder.Append(@".ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {");
|
||||
|
||||
foreach (KeyValuePair<string, TypedConstant> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Created on demand before each generation pass
|
||||
/// </summary>
|
||||
public class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// 注入特性的名称
|
||||
/// </summary>
|
||||
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
|
||||
|
||||
/// <summary>
|
||||
/// 所有需要注入的类型符号
|
||||
/// </summary>
|
||||
public List<INamedTypeSymbol> Classes { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
/// <inheritdoc/>
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@ public class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Windows.Win32.Foundation;
|
||||
|
||||
public readonly partial struct HWND
|
||||
{
|
||||
public static HWND Zero => (HWND)IntPtr.Zero;
|
||||
}
|
||||
@@ -16,4 +16,4 @@ RemoveWindowSubclass
|
||||
|
||||
// User32
|
||||
FindWindowEx
|
||||
GetDpiForWindow
|
||||
GetDpiForWindow
|
||||
@@ -11,7 +11,7 @@
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.10-beta">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.Win32Metadata" Version="24.0.1-preview" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.Win32Metadata" Version="25.0.28-preview" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao.Win32;
|
||||
|
||||
/// <summary>
|
||||
/// 包装不安全的代码
|
||||
/// </summary>
|
||||
public class Unsafe
|
||||
{
|
||||
/// <summary>
|
||||
@@ -9,7 +13,7 @@ public class Unsafe
|
||||
/// <param name="lParam">lParam</param>
|
||||
/// <param name="minWidth">最小宽度</param>
|
||||
/// <param name="minHeight">最小高度</param>
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="ApplicationData.Current.TemporaryFolder"/>
|
||||
/// </summary>
|
||||
public static StorageFolder CacheFolder
|
||||
{
|
||||
get => ApplicationData.Current.TemporaryFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="ApplicationData.Current.LocalSettings"/>
|
||||
/// </summary>
|
||||
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<MainWindow>();
|
||||
|
||||
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path);
|
||||
|
||||
OnActivated(firstInstance, activatedEventArgs);
|
||||
|
||||
Ioc.Default
|
||||
.GetRequiredService<IMetadataService>()
|
||||
.ImplictAs<IMetadataInitializer>()?
|
||||
@@ -109,25 +107,31 @@ public partial class App : Application
|
||||
|
||||
// Hutao extensions
|
||||
.AddInjections()
|
||||
.AddDatebase()
|
||||
.AddHttpClients()
|
||||
.AddDatebase()
|
||||
.AddJsonSerializerOptions()
|
||||
|
||||
// Discrete services
|
||||
.AddSingleton<IMessenger>(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<IInfoBarService>();
|
||||
await infoBarService.WaitInitializationAsync();
|
||||
infoBarService.Information("OnActivated");
|
||||
|
||||
if (args.Kind == ExtendedActivationKind.Protocol)
|
||||
{
|
||||
if (args.TryGetProtocolActivatedUri(out Uri? uri))
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Information(uri.ToString());
|
||||
infoBarService.Information(uri.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Snap.Hutao.Context.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 日志数据库上下文
|
||||
/// 由于写入日志的行为需要锁定数据库上下文
|
||||
/// 所以将日志单独分离出来进行读写
|
||||
/// </summary>
|
||||
public class LogDbContext : DbContext
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton, typeof(IImageCache))]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 20)]
|
||||
public class ImageCache : CacheBase<BitmapImage>, IImageCache
|
||||
{
|
||||
private const string DateAccessedProperty = "System.DateAccessed";
|
||||
@@ -24,9 +27,9 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
|
||||
/// Initializes a new instance of the <see cref="ImageCache"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
public ImageCache(ILogger<ImageCache> logger, HttpClient httpClient)
|
||||
: base(logger, httpClient)
|
||||
/// <param name="httpClientFactory">http客户端工厂</param>
|
||||
public ImageCache(ILogger<ImageCache> logger, IHttpClientFactory httpClientFactory)
|
||||
: base(logger, httpClientFactory.CreateClient(nameof(ImageCache)))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,27 @@ namespace Snap.Hutao.Core;
|
||||
/// </summary>
|
||||
internal static class CoreEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// 动态密钥1的盐
|
||||
/// </summary>
|
||||
public const string DynamicSecret1Salt = "9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7";
|
||||
|
||||
/// <summary>
|
||||
/// 动态密钥2的盐
|
||||
/// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
|
||||
/// </summary>
|
||||
public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabUA = $"miHoYoBBS/2.34.1";
|
||||
|
||||
/// <summary>
|
||||
/// 标准UA
|
||||
/// </summary>
|
||||
public static readonly string CommonUA;
|
||||
|
||||
/// <summary>
|
||||
/// 当前版本
|
||||
/// </summary>
|
||||
@@ -23,13 +44,20 @@ internal static class CoreEnvironment
|
||||
/// </summary>
|
||||
public static readonly string DeviceId;
|
||||
|
||||
/// <summary>
|
||||
/// 米游社设备Id
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
|
||||
/// <summary>
|
||||
/// 指示被标注的类型可注入 HttpClient
|
||||
/// 由源生成器生成注入代码
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class HttpClientAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的特性
|
||||
/// </summary>
|
||||
/// <param name="configration">配置</param>
|
||||
public HttpClientAttribute(HttpClientConfigration configration)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Http客户端配置
|
||||
/// </summary>
|
||||
public enum HttpClientConfigration
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// 米游社请求配置
|
||||
/// </summary>
|
||||
XRpc,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
|
||||
/// <summary>
|
||||
/// 配置首选Http消息处理器特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class PrimaryHttpMessageHandlerAttribute : Attribute
|
||||
{
|
||||
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.MaxConnectionsPerServer"/>
|
||||
public int MaxConnectionsPerServer { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 注入方法
|
||||
@@ -26,4 +26,4 @@ public class InjectionAttribute : Attribute
|
||||
public InjectionAttribute(InjectAs injectAs, Type interfaceType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Ioc"/> 配置
|
||||
@@ -18,7 +18,7 @@ namespace Snap.Hutao;
|
||||
internal static class IocConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加默认的 <see cref="JsonSerializer"/> 配置
|
||||
/// 添加默认的 <see cref="JsonSerializerOptions"/>
|
||||
/// </summary>
|
||||
/// <param name="services">集合</param>
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
@@ -56,7 +56,6 @@ internal static class IocConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
return services
|
||||
.AddDbContextPool<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
|
||||
return services.AddDbContextPool<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Ioc"/> 与 <see cref="HttpClient"/> 配置
|
||||
/// </summary>
|
||||
internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 <see cref="HttpClient"/>
|
||||
/// </summary>
|
||||
/// <param name="services">集合</param>
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services);
|
||||
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void DefaultConfiguration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.CommonUA);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,11 @@ namespace Snap.Hutao.Core.Exception;
|
||||
/// </summary>
|
||||
public enum COMError : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// could not be found.
|
||||
/// </summary>
|
||||
STG_E_FILENOTFOUND = 0x80030002,
|
||||
|
||||
/// <summary>
|
||||
/// The component cannot be found.
|
||||
/// </summary>
|
||||
|
||||
@@ -36,7 +36,6 @@ internal sealed partial class DatebaseLogger : ILogger
|
||||
/// <inheritdoc />
|
||||
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
|
||||
/// <summary>
|
||||
/// An empty scope without any logic
|
||||
/// </summary>
|
||||
private struct NullScope : IDisposable
|
||||
private class NullScope : IDisposable
|
||||
{
|
||||
public NullScope()
|
||||
{
|
||||
|
||||
@@ -52,4 +52,4 @@ public static class ProcessHelper
|
||||
{
|
||||
return Start(uri.AbsolutePath, useShellExecute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
60
src/Snap.Hutao/Snap.Hutao/Core/ThemeHelper.cs
Normal file
60
src/Snap.Hutao/Snap.Hutao/Core/ThemeHelper.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 主题帮助工具类
|
||||
/// </summary>
|
||||
public static class ThemeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断主题是否相等
|
||||
/// </summary>
|
||||
/// <param name="applicationTheme">应用主题</param>
|
||||
/// <param name="elementTheme">元素主题</param>
|
||||
/// <returns>主题是否相等</returns>
|
||||
public static bool Equals(ApplicationTheme applicationTheme, ElementTheme elementTheme)
|
||||
{
|
||||
return (applicationTheme, elementTheme) switch
|
||||
{
|
||||
(ApplicationTheme.Light, ElementTheme.Light) => true,
|
||||
(ApplicationTheme.Dark, ElementTheme.Dark) => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 <see cref="ApplicationTheme"/> 转换到 <see cref="ElementTheme"/>
|
||||
/// </summary>
|
||||
/// <param name="applicationTheme">应用主题</param>
|
||||
/// <returns>元素主题</returns>
|
||||
public static ElementTheme ApplicationToElement(ApplicationTheme applicationTheme)
|
||||
{
|
||||
return applicationTheme switch
|
||||
{
|
||||
ApplicationTheme.Light => ElementTheme.Light,
|
||||
ApplicationTheme.Dark => ElementTheme.Dark,
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="SystemBackdropTheme"/>
|
||||
/// </summary>
|
||||
/// <param name="elementTheme">元素主题</param>
|
||||
/// <returns>背景主题</returns>
|
||||
public static SystemBackdropTheme ElementToSystemBackdrop(ElementTheme elementTheme)
|
||||
{
|
||||
return elementTheme switch
|
||||
{
|
||||
ElementTheme.Default => SystemBackdropTheme.Default,
|
||||
ElementTheme.Light => SystemBackdropTheme.Light,
|
||||
ElementTheme.Dark => SystemBackdropTheme.Dark,
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,16 @@ namespace Snap.Hutao.Core.Validation;
|
||||
/// </summary>
|
||||
public static class Must
|
||||
{
|
||||
/// <summary>
|
||||
/// Unconditionally throws an <see cref="NotSupportedException"/>.
|
||||
/// </summary>
|
||||
/// <returns>Nothing. This method always throws.</returns>
|
||||
[DoesNotReturn]
|
||||
public static System.Exception NeverHappen()
|
||||
{
|
||||
throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentNullException"/> if the specified parameter's value is null.
|
||||
/// </summary>
|
||||
@@ -25,25 +35,20 @@ public static class Must
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unconditionally throws an <see cref="NotSupportedException"/>.
|
||||
/// Throws an <see cref="ArgumentNullException"/> if the specified parameter's value is IntPtr.Zero.
|
||||
/// </summary>
|
||||
/// <returns>Nothing. This method always throws.</returns>
|
||||
[DoesNotReturn]
|
||||
public static System.Exception NeverHappen()
|
||||
/// <param name="value">The value of the argument.</param>
|
||||
/// <param name="parameterName">The name of the parameter to include in any thrown exception.</param>
|
||||
/// <returns>The value of the parameter.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is <see cref="IntPtr.Zero"/>.</exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unconditionally throws an <see cref="NotSupportedException"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type that the method should be typed to return (although it never returns anything).</typeparam>
|
||||
/// <returns>Nothing. This method always throws.</returns>
|
||||
[DoesNotReturn]
|
||||
[return: MaybeNull]
|
||||
public static T NeverHappen<T>()
|
||||
{
|
||||
throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认");
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -19,7 +19,7 @@ public class SystemBackdrop
|
||||
|
||||
private DispatcherQueueHelper? dispatcherQueueHelper;
|
||||
private MicaController? backdropController;
|
||||
private SystemBackdropConfiguration? configurationSource;
|
||||
private SystemBackdropConfiguration? configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的系统背景帮助类
|
||||
@@ -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<ICompositionSupportsSystemBackdrop>());
|
||||
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
|
||||
/// <summary>
|
||||
/// 确保系统调度队列控制器存在
|
||||
/// </summary>
|
||||
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<DispatcherQueueOptions>(),
|
||||
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;
|
||||
|
||||
@@ -33,6 +33,7 @@ internal class WindowSubclassManager : IDisposable
|
||||
/// <param name="isLegacyDragBar">是否为经典标题栏区域</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Ioc"/> 与 <see cref="HttpClient"/> 配置
|
||||
/// </summary>
|
||||
internal static class IocHttpClientConfiguration
|
||||
{
|
||||
private static readonly string CommonUA = $"Snap Hutao/{Core.CoreEnvironment.Version}";
|
||||
|
||||
/// <summary>
|
||||
/// 添加 <see cref="HttpClient"/>
|
||||
/// </summary>
|
||||
/// <param name="services">集合</param>
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{
|
||||
// services
|
||||
services.AddHttpClient<MetadataService>(DefaultConfiguration);
|
||||
services.AddHttpClient<ImageCache>(DefaultConfiguration)
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { MaxConnectionsPerServer = 20 });
|
||||
|
||||
// normal clients
|
||||
services.AddHttpClient<AnnouncementClient>(DefaultConfiguration);
|
||||
services.AddHttpClient<EnkaClient>(DefaultConfiguration);
|
||||
services.AddHttpClient<HutaoClient>(DefaultConfiguration);
|
||||
services.AddHttpClient<UserGameRoleClient>(DefaultConfiguration);
|
||||
|
||||
// x-rpc clients
|
||||
services.AddHttpClient<GameRecordClient>(XRpcConfiguration);
|
||||
services.AddHttpClient<UserClient>(XRpcConfiguration);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void DefaultConfiguration(this HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CommonUA);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
Margin="48,0,0,0"
|
||||
Height="44"
|
||||
x:Name="TitleBarView"/>
|
||||
|
||||
<view:MainView/>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed partial class MainWindow : Window
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly WindowManager windowManager;
|
||||
|
||||
private readonly TaskCompletionSource initializaionCompletionSource = new();
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的主窗体
|
||||
/// </summary>
|
||||
@@ -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();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.0.23.0" />
|
||||
Version="1.0.26.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
@@ -18,8 +18,8 @@
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<!--<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />-->
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.22000.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
|
||||
@@ -76,6 +76,13 @@ public interface IInfoBarService
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Success(string title, string message, int delay = 5000);
|
||||
|
||||
/// <summary>
|
||||
/// 异步等待加载完成
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>任务</returns>
|
||||
Task WaitInitializationAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 显示警告信息
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(StackPanel container)
|
||||
{
|
||||
infoBarStack = container;
|
||||
initializaionCompletionSource.TrySetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步等待主窗体加载完成
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>任务</returns>
|
||||
public Task WaitInitializationAsync(CancellationToken token = default)
|
||||
{
|
||||
return initializaionCompletionSource.Task;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
/// 元数据服务
|
||||
/// </summary>
|
||||
[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
|
||||
/// 构造一个新的元数据服务
|
||||
/// </summary>
|
||||
/// <param name="infoBarService">信息条服务</param>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="httpClientFactory">http客户端工厂</param>
|
||||
/// <param name="metadataContext">我的文档上下文</param>
|
||||
/// <param name="options">json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="memoryCache">内存缓存</param>
|
||||
public MetadataService(
|
||||
IInfoBarService infoBarService,
|
||||
HttpClient httpClient,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
MetadataContext metadataContext,
|
||||
JsonSerializerOptions options,
|
||||
ILogger<MetadataService> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -73,63 +75,63 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeAsync(CancellationToken token = default)
|
||||
{
|
||||
await initializeCompletionSource.Task;
|
||||
await initializeCompletionSource.Task.ConfigureAwait(false);
|
||||
return IsInitialized;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<List<AchievementGoal>>("AchievementGoal", token);
|
||||
return FromCacheOrFileAsync<List<AchievementGoal>>("AchievementGoal", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Model.Metadata.Achievement.Achievement>> GetAchievementsAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<List<Model.Metadata.Achievement.Achievement>>("Achievement", token);
|
||||
return FromCacheOrFileAsync<List<Model.Metadata.Achievement.Achievement>>("Achievement", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<List<Avatar>>("Avatar", token);
|
||||
return FromCacheOrFileAsync<List<Avatar>>("Avatar", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<List<Reliquary>>("Reliquary", token);
|
||||
return FromCacheOrFileAsync<List<Reliquary>>("Reliquary", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<List<ReliquaryAffix>>("ReliquaryAffix", token);
|
||||
return FromCacheOrFileAsync<List<ReliquaryAffix>>("ReliquaryAffix", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<List<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
|
||||
return FromCacheOrFileAsync<List<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<List<Weapon>>("Weapon", token);
|
||||
return FromCacheOrFileAsync<List<Weapon>>("Weapon", token);
|
||||
}
|
||||
|
||||
private async Task<bool> 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<string?, Task> writeMethod = streamReader.EndOfStream ? streamWriter.WriteAsync : streamWriter.WriteLineAsync;
|
||||
await writeMethod(line).ConfigureAwait(false);
|
||||
Func<string?, Task> 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<T> GetMetadataAsync<T>(string fileName, CancellationToken token)
|
||||
private async ValueTask<T> FromCacheOrFileAsync<T>(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<T>(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<T>(fileStream, options, token).ConfigureAwait(false);
|
||||
return memoryCache.Set(cacheKey, Must.NotNull(result!));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,13 @@
|
||||
<AppxBundle>Never</AppxBundle>
|
||||
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
|
||||
<StartupObject>Snap.Hutao.Program</StartupObject>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants>
|
||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
<NavigationView
|
||||
x:Name="NavView"
|
||||
CompactPaneLength="48"
|
||||
OpenPaneLength="244"
|
||||
CompactModeThresholdWidth="16"
|
||||
ExpandedModeThresholdWidth="16"
|
||||
OpenPaneLength="188"
|
||||
PaneDisplayMode="Left"
|
||||
IsPaneOpen="True"
|
||||
IsBackEnabled="{Binding ElementName=ContentFrame,Path=CanGoBack}">
|
||||
<NavigationView.MenuItems>
|
||||
@@ -37,9 +36,9 @@
|
||||
shvh:NavHelper.NavigateTo="shvp:WikiAvatarPage"
|
||||
Icon="{cwu:BitmapIcon ShowAsMonochrome=True,Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/>
|
||||
</NavigationView.MenuItems>
|
||||
|
||||
|
||||
<NavigationView.PaneFooter>
|
||||
<shv:UserView IsExpanded="{Binding ElementName=NavView,Path=IsPaneOpen}"/>
|
||||
<shv:UserView/>
|
||||
</NavigationView.PaneFooter>
|
||||
|
||||
<Frame x:Name="ContentFrame">
|
||||
|
||||
@@ -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>();
|
||||
uISettings.ColorValuesChanged += OnUISettingsColorValuesChanged;
|
||||
|
||||
infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
@@ -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<MainView> logger = Ioc.Default.GetRequiredService<ILogger<MainView>>();
|
||||
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<MainView> logger = Ioc.Default.GetRequiredService<ILogger<MainView>>();
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,8 @@
|
||||
<sc:SettingsGroup Header="测试功能">
|
||||
<sc:Setting
|
||||
Icon=""
|
||||
Header="打开 数据 文件夹">
|
||||
Header="打开 数据 文件夹"
|
||||
Description="用户数据/日志/元数据在此处存放">
|
||||
<sc:Setting.ActionContent>
|
||||
<Button Content="打开" Command="{Binding Experimental.OpenDataFolderCommand}"/>
|
||||
</sc:Setting.ActionContent>
|
||||
@@ -49,11 +50,21 @@
|
||||
|
||||
<sc:Setting
|
||||
Icon=""
|
||||
Header="打开 缓存 文件夹">
|
||||
Header="打开 缓存 文件夹"
|
||||
Description="图片缓存在此处存放">
|
||||
<sc:Setting.ActionContent>
|
||||
<Button Content="打开" Command="{Binding Experimental.OpenCacheFolderCommand}"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
|
||||
<sc:Setting
|
||||
Icon=""
|
||||
Header="米游社签到"
|
||||
Description="所有账号的所有角色都会签到,每次间隔 15s">
|
||||
<sc:Setting.ActionContent>
|
||||
<Button Content="签到" Command="{Binding Experimental.SignAllUserGameRolesCommand}"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
</sc:SettingsGroup>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -13,8 +13,6 @@ namespace Snap.Hutao.View;
|
||||
/// </summary>
|
||||
public sealed partial class UserView : UserControl
|
||||
{
|
||||
private static readonly DependencyProperty IsExpandedProperty = Property<UserView>.Depend(nameof(IsExpanded), true);
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户视图
|
||||
/// </summary>
|
||||
@@ -23,13 +21,4 @@ public sealed partial class UserView : UserControl
|
||||
InitializeComponent();
|
||||
DataContext = Ioc.Default.GetRequiredService<UserViewModel>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户控件是否处于展开状态
|
||||
/// </summary>
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => (bool)GetValue(IsExpandedProperty);
|
||||
set => SetValue(IsExpandedProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的实验性功能视图模型
|
||||
/// </summary>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
/// <param name="hutaoLocation">数据文件夹</param>
|
||||
public ExperimentalFeaturesViewModel(IAsyncRelayCommandFactory asyncRelayCommandFactory, HutaoLocation hutaoLocation)
|
||||
/// <param name="userService">用户服务</param>
|
||||
/// <param name="signClient">签到客户端</param>
|
||||
/// <param name="infoBarService">信息栏服务</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -39,13 +58,35 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand OpenDataFolderCommand { get; }
|
||||
|
||||
private Task OpenCacheFolderAsync(CancellationToken token)
|
||||
/// <summary>
|
||||
/// 签到全部角色命令
|
||||
/// </summary>
|
||||
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<SignInResult>? result = await signClient.SignAsync(user, role);
|
||||
if (result != null)
|
||||
{
|
||||
infoBarService.Information(result.Message);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(15));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// Enka API 客户端
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class EnkaClient
|
||||
{
|
||||
private const string EnkaAPI = "https://enka.shinshin.moe/u/{0}/__data.json";
|
||||
|
||||
@@ -19,6 +19,11 @@ internal static class ApiEndpoints
|
||||
/// </summary>
|
||||
public const string AnnContent = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery}";
|
||||
|
||||
/// <summary>
|
||||
/// 角色信息
|
||||
/// </summary>
|
||||
public const string GameRecordCharacter = $"{ApiTakumiRecordApi}/character";
|
||||
|
||||
/// <summary>
|
||||
/// 游戏记录主页
|
||||
/// </summary>
|
||||
@@ -33,17 +38,53 @@ internal static class ApiEndpoints
|
||||
/// <summary>
|
||||
/// 深渊信息
|
||||
/// </summary>
|
||||
public const string SpiralAbyss = $"{ApiTakumiRecordApi}/spiralAbyss?schedule_type={{0}}&role_id={{1}}&server={{2}}";
|
||||
/// <param name="scheduleType">深渊类型</param>
|
||||
/// <param name="uid">Uid</param>
|
||||
/// <returns>深渊信息字符串</returns>
|
||||
public static string GameRecordSpiralAbyss(Takumi.GameRecord.SpiralAbyssSchedule scheduleType, PlayerUid uid)
|
||||
{
|
||||
return $"{ApiTakumiRecordApi}/spiralAbyss?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角色信息
|
||||
/// 签到活动Id
|
||||
/// </summary>
|
||||
public const string Character = $"{ApiTakumiRecordApi}/character";
|
||||
public const string SignInRewardActivityId = "e202009291139501";
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// 签到
|
||||
/// </summary>
|
||||
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}";
|
||||
|
||||
/// <summary>
|
||||
/// 签到信息
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>签到信息字符串</returns>
|
||||
public static string SignInRewardInfo(PlayerUid uid)
|
||||
{
|
||||
return $"{ApiTakumi}/event/bbs_sign_reward/info?act_id={SignInRewardActivityId}®ion={uid.Region}&uid={uid.Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 签到
|
||||
/// </summary>
|
||||
public const string SignInRewardReSign = $"{ApiTakumi}/event/bbs_sign_reward/resign";
|
||||
|
||||
/// <summary>
|
||||
/// 补签信息
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>补签信息字符串</returns>
|
||||
public static string SignInRewardResignInfo(PlayerUid uid)
|
||||
{
|
||||
return $"{ApiTakumi}/event/bbs_sign_reward/resign_info?act_id=e202009291139501®ion={uid.Region}&uid={uid.Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 签到
|
||||
/// </summary>
|
||||
public const string SignInRewardSign = $"{ApiTakumi}/event/bbs_sign_reward/sign";
|
||||
|
||||
/// <summary>
|
||||
/// 用户详细信息
|
||||
@@ -53,8 +94,19 @@ internal static class ApiEndpoints
|
||||
/// <summary>
|
||||
/// 查询其他用户详细信息
|
||||
/// </summary>
|
||||
public const string UserFullInfoQuery = $"{BbsApiUserApi}/getUserFullInfo?uid={{0}}&gids=2";
|
||||
/// <param name="bbsUid">bbs Uid</param>
|
||||
/// <returns>查询其他用户详细信息字符串</returns>
|
||||
public static string UserFullInfoQuery(string bbsUid)
|
||||
{
|
||||
return $"{BbsApiUserApi}/getUserFullInfo?uid={bbsUid}&gids=2";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 用户信息客户端
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
[HttpClient(HttpClientConfigration.XRpc)]
|
||||
internal class UserClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
@@ -20,7 +21,6 @@ internal class UserClient
|
||||
/// <summary>
|
||||
/// 构造一个新的用户信息客户端
|
||||
/// </summary>
|
||||
/// <param name="userService">用户服务</param>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="jsonSerializerOptions">Json序列化选项</param>
|
||||
public UserClient(HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions)
|
||||
@@ -47,17 +47,18 @@ internal class UserClient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户详细信息
|
||||
/// 获取其他用户详细信息
|
||||
/// </summary>
|
||||
/// <param name="user">当前用户</param>
|
||||
/// <param name="uid">米游社Uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>详细信息</returns>
|
||||
public async Task<UserInfo?> GetUserFullInfoAsync(string uid, CancellationToken token = default)
|
||||
public async Task<UserInfo?> GetUserFullInfoAsync(Model.Binding.User user, string uid, CancellationToken token = default)
|
||||
{
|
||||
Response<UserFullInfoWrapper>? resp = await httpClient
|
||||
.UsingDynamicSecret()
|
||||
/*.SetUser(userService.CurrentUser)*/
|
||||
.GetFromJsonAsync<Response<UserFullInfoWrapper>>(string.Format(ApiEndpoints.UserFullInfoQuery, uid), jsonSerializerOptions, token)
|
||||
.SetUser(user)
|
||||
.GetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiEndpoints.UserFullInfoQuery(uid), jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data?.UserInfo;
|
||||
|
||||
@@ -11,9 +11,6 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
/// </summary>
|
||||
internal abstract class DynamicSecretProvider : Md5Convert
|
||||
{
|
||||
// @Azure99 respect original author
|
||||
private static readonly string Salt = "4a8knnbk5pbjqsrudp3dq484m9axoc5g";
|
||||
|
||||
/// <summary>
|
||||
/// 创建动态密钥
|
||||
/// </summary>
|
||||
@@ -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();
|
||||
|
||||
@@ -11,19 +11,6 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
/// </summary>
|
||||
internal abstract class DynamicSecretProvider2 : Md5Convert
|
||||
{
|
||||
/// <summary>
|
||||
/// salt
|
||||
/// </summary>
|
||||
public const string AppVersion = "2.34.1";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社的盐
|
||||
/// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
|
||||
/// libxxxx.so
|
||||
/// </summary>
|
||||
private static readonly string Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
|
||||
private static readonly Random Random = new();
|
||||
|
||||
/// <summary>
|
||||
/// 创建动态密钥
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 使用动态密钥2的Http客户端默认实现
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">请求提交的数据的的格式</typeparam>
|
||||
internal class DynamicSecret2HttpClient : IDynamicSecret2HttpClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly string url;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的使用动态密钥2的Http客户端默认实现的实例
|
||||
/// </summary>
|
||||
/// <param name="httpClient">请求使用的客户端</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="url">url</param>
|
||||
/// <param name="data">请求的数据</param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<TValue?> GetFromJsonAsync<TValue>(CancellationToken token)
|
||||
{
|
||||
return httpClient.GetFromJsonAsync<TValue>(url, options, token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用动态密钥2的Http客户端默认实现
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">请求提交的数据的的格式</typeparam>
|
||||
[SuppressMessage("", "SA1402")]
|
||||
internal class DynamicSecret2HttpClient<TValue> : IDynamicSecret2HttpClient<TValue>
|
||||
where TValue : class
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly string url;
|
||||
private readonly TValue? data = null;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的使用动态密钥2的Http客户端默认实现的实例
|
||||
/// </summary>
|
||||
/// <param name="httpClient">请求使用的客户端</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="url">url</param>
|
||||
/// <param name="data">请求的数据</param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<HttpResponseMessage> PostAsJsonAsync(CancellationToken token)
|
||||
{
|
||||
return httpClient.PostAsJsonAsync(url, data, options, token);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 使用动态密钥2的Http客户端抽象
|
||||
/// </summary>
|
||||
internal interface IDynamicSecret2HttpClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The target type to deserialize to.</typeparam>
|
||||
/// <param name="token">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
Task<TValue?> GetFromJsonAsync<TValue>(CancellationToken token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用动态密钥2的Http客户端抽象
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">请求数据的类型</typeparam>
|
||||
internal interface IDynamicSecret2HttpClient<TValue>
|
||||
where TValue : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a POST request to the specified Uri containing the value serialized as JSON in the request body.
|
||||
/// </summary>
|
||||
/// <param name="token">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
Task<HttpResponseMessage> PostAsJsonAsync(CancellationToken token);
|
||||
}
|
||||
@@ -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
|
||||
/// <param name="url">地址</param>
|
||||
/// <param name="data">post数据</param>
|
||||
/// <returns>响应</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用二代动态密钥执行 GET 操作
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">请求数据的类型</typeparam>
|
||||
/// <param name="httpClient">请求器</param>
|
||||
/// <param name="options">选项</param>
|
||||
/// <param name="url">地址</param>
|
||||
/// <param name="data">post数据</param>
|
||||
/// <returns>响应</returns>
|
||||
public static IDynamicSecret2HttpClient<TValue> UsingDynamicSecret2<TValue>(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<TValue>(httpClient, options, url, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 公告客户端
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class AnnouncementClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// </summary>
|
||||
public record UserGameRole
|
||||
public class UserGameRole
|
||||
{
|
||||
/// <summary>
|
||||
/// hk4e_cn for Genshin Impact
|
||||
@@ -68,16 +68,7 @@ public record UserGameRole
|
||||
|
||||
public static explicit operator PlayerUid(UserGameRole userGameRole)
|
||||
{
|
||||
return userGameRole.AsPlayerUid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转化为 <see cref="PlayerUid"/>
|
||||
/// </summary>
|
||||
/// <returns>一个等价的 <see cref="PlayerUid"/> 实例</returns>
|
||||
public PlayerUid AsPlayerUid()
|
||||
{
|
||||
return new PlayerUid(GameUid, Region);
|
||||
return new PlayerUid(userGameRole.GameUid, userGameRole.Region);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 用户游戏角色提供器
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class UserGameRoleClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 奖励物品
|
||||
/// </summary>
|
||||
public class Award
|
||||
{
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
[JsonPropertyName("icon")]
|
||||
public string Icon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 个数
|
||||
/// </summary>
|
||||
[JsonPropertyName("cnt")]
|
||||
public string Count { get; set; } = default!;
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 月份
|
||||
/// </summary>
|
||||
[JsonPropertyName("month")]
|
||||
public string? Month { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励列表
|
||||
/// </summary>
|
||||
[JsonPropertyName("awards")]
|
||||
public List<Award>? Awards { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支持补签
|
||||
/// </summary>
|
||||
[JsonPropertyName("resign")]
|
||||
public bool Resign { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 签到客户端
|
||||
/// </summary>
|
||||
[HttpClient(HttpClientConfigration.XRpc)]
|
||||
internal class SignClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的签到客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="options">选项</param>
|
||||
public SignClient(HttpClient httpClient, JsonSerializerOptions options)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取签到信息
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="role">角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>签到信息</returns>
|
||||
public async Task<SignInRewardInfo?> GetInfoAsync(User user, UserGameRole role, CancellationToken token = default)
|
||||
{
|
||||
Response<SignInRewardInfo>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.UsingDynamicSecret()
|
||||
.GetFromJsonAsync<Response<SignInRewardInfo>>(ApiEndpoints.SignInRewardInfo((PlayerUid)role), options, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取签到信息
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="role">角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>签到信息</returns>
|
||||
public async Task<SignInRewardReSignInfo?> GetResignInfoAsync(User user, UserGameRole role, CancellationToken token = default)
|
||||
{
|
||||
Response<SignInRewardReSignInfo>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.UsingDynamicSecret()
|
||||
.GetFromJsonAsync<Response<SignInRewardReSignInfo>>(ApiEndpoints.SignInRewardResignInfo((PlayerUid)role), options, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取签到奖励
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>奖励信息</returns>
|
||||
public async Task<Reward?> GetRewardAsync(User user, CancellationToken token = default)
|
||||
{
|
||||
Response<Reward>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.GetFromJsonAsync<Response<Reward>>(ApiEndpoints.SignInRewardHome, options, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 补签
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="role">角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>签到消息</returns>
|
||||
public async Task<Response<SignInResult>?> 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<SignInResult>? resp = await response.Content
|
||||
.ReadFromJsonAsync<Response<SignInResult>>(options, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 签到
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="role">角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>签到消息</returns>
|
||||
public async Task<Response<SignInResult>?> 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<SignInResult>? resp = await response.Content
|
||||
.ReadFromJsonAsync<Response<SignInResult>>(options, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 签到提交数据
|
||||
/// </summary>
|
||||
public class SignInData
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的签到提交数据
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
public SignInData(PlayerUid uid)
|
||||
{
|
||||
Region = uid.Region;
|
||||
Uid = uid.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 活动Id
|
||||
/// </summary>
|
||||
[JsonPropertyName("act_id")]
|
||||
public string ActivityId { get; } = ApiEndpoints.SignInRewardActivityId;
|
||||
|
||||
/// <summary>
|
||||
/// 地区代码
|
||||
/// </summary>
|
||||
[JsonPropertyName("region")]
|
||||
public string Region { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Uid
|
||||
/// </summary>
|
||||
[JsonPropertyName("uid")]
|
||||
public string Uid { get; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 签到结果
|
||||
/// </summary>
|
||||
public class SignInResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 通常是 ""
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public string Code { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 通常是 ""
|
||||
/// </summary>
|
||||
[JsonPropertyName("risk_code")]
|
||||
public int RiskCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通常是 ""
|
||||
/// </summary>
|
||||
[JsonPropertyName("gt")]
|
||||
public string Gt { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 通常是 ""
|
||||
/// </summary>
|
||||
[JsonPropertyName("challenge")]
|
||||
public string Challenge { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 通常是 ""
|
||||
/// </summary>
|
||||
[JsonPropertyName("success")]
|
||||
public int Success { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 签到信息
|
||||
/// </summary>
|
||||
public class SignInRewardInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 累积签到天数
|
||||
/// </summary>
|
||||
[JsonPropertyName("total_sign_day")]
|
||||
public int TotalSignDay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// yyyy-MM-dd
|
||||
/// </summary>
|
||||
[JsonPropertyName("today")]
|
||||
public string? Today { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 今日是否已签到
|
||||
/// </summary>
|
||||
[JsonPropertyName("is_sign")]
|
||||
public bool IsSign { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ?
|
||||
/// </summary>
|
||||
[JsonPropertyName("is_sub")]
|
||||
public bool IsSub { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否首次绑定
|
||||
/// </summary>
|
||||
[JsonPropertyName("first_bind")]
|
||||
public bool FirstBind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为当月第一次
|
||||
/// </summary>
|
||||
[JsonPropertyName("month_first")]
|
||||
public bool MonthFirst { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 漏签天数
|
||||
/// </summary>
|
||||
[JsonPropertyName("sign_cnt_missed")]
|
||||
public bool SignCountMissed { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 补签说明
|
||||
/// </summary>
|
||||
public class SignInRewardReSignInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 当日补签次数
|
||||
/// </summary>
|
||||
[JsonPropertyName("resign_cnt_daily")]
|
||||
public bool ResignCountDaily { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当月补签次数
|
||||
/// </summary>
|
||||
[JsonPropertyName("resign_cnt_monthly")]
|
||||
public bool ResignCountMonthly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当日补签次数限制
|
||||
/// </summary>
|
||||
[JsonPropertyName("resign_limit_daily")]
|
||||
public bool ResignLimitDaily { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当月补签次数限制
|
||||
/// </summary>
|
||||
[JsonPropertyName("resign_limit_monthly")]
|
||||
public bool ResignLimitMonthly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 漏签次数
|
||||
/// </summary>
|
||||
[JsonPropertyName("sign_cnt_missed")]
|
||||
public bool SignCountMissed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 米游币个数
|
||||
/// </summary>
|
||||
[JsonPropertyName("coin_cnt")]
|
||||
public bool CoinCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 补签需要的米游币个数
|
||||
/// </summary>
|
||||
[JsonPropertyName("coin_cost")]
|
||||
public bool CoinCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 规则
|
||||
/// </summary>
|
||||
[JsonPropertyName("rule")]
|
||||
public string Rule { get; set; } = default!;
|
||||
}
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 游戏记录提供器
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
[HttpClient(HttpClientConfigration.XRpc)]
|
||||
internal class GameRecordClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
@@ -42,7 +43,7 @@ internal class GameRecordClient
|
||||
/// <returns>玩家的基础信息</returns>
|
||||
public Task<PlayerInfo?> 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
|
||||
/// <returns>玩家的基础信息</returns>
|
||||
public async Task<PlayerInfo?> GetPlayerInfoAsync(User user, PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
string url = string.Format(ApiEndpoints.GameRecordIndex(uid.Value, uid.Region));
|
||||
|
||||
Response<PlayerInfo>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.UsingDynamicSecret2(jsonSerializerOptions, url)
|
||||
.GetFromJsonAsync<Response<PlayerInfo>>(url, jsonSerializerOptions, token)
|
||||
.UsingDynamicSecret2(jsonSerializerOptions, ApiEndpoints.GameRecordIndex(uid.Value, uid.Region))
|
||||
.GetFromJsonAsync<Response<PlayerInfo>>(token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
@@ -75,7 +74,7 @@ internal class GameRecordClient
|
||||
/// <returns>深渊信息</returns>
|
||||
public Task<SpiralAbyss.SpiralAbyss?> 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
|
||||
/// <returns>深渊信息</returns>
|
||||
public async Task<SpiralAbyss.SpiralAbyss?> GetSpiralAbyssAsync(User user, PlayerUid uid, SpiralAbyssSchedule schedule, CancellationToken token = default)
|
||||
{
|
||||
string url = string.Format(ApiEndpoints.SpiralAbyss, (int)schedule, uid.Value, uid.Region);
|
||||
|
||||
Response<SpiralAbyss.SpiralAbyss>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.UsingDynamicSecret2(jsonSerializerOptions, url)
|
||||
.GetFromJsonAsync<Response<SpiralAbyss.SpiralAbyss>>(url, jsonSerializerOptions, token)
|
||||
.UsingDynamicSecret2(jsonSerializerOptions, ApiEndpoints.GameRecordSpiralAbyss(schedule, uid))
|
||||
.GetFromJsonAsync<Response<SpiralAbyss.SpiralAbyss>>(token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
@@ -109,7 +106,7 @@ internal class GameRecordClient
|
||||
/// <returns>角色列表</returns>
|
||||
public Task<List<Character>> 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<CharacterWrapper>? resp = await response.Content.ReadFromJsonAsync<Response<CharacterWrapper>>(jsonSerializerOptions, token);
|
||||
Response<CharacterWrapper>? resp = await response.Content
|
||||
.ReadFromJsonAsync<Response<CharacterWrapper>>(jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data?.Avatars);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public static class HttpRequestHeadersExtensions
|
||||
/// <param name="value">值</param>
|
||||
public static void Set(this HttpRequestHeaders headers, string name, string? value)
|
||||
{
|
||||
headers.Clear();
|
||||
headers.Remove(name);
|
||||
headers.Add(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 胡桃API客户端
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
// [Injection(InjectAs.Transient)]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class HutaoClient : ISupportAsyncInitialization
|
||||
{
|
||||
private const string AuthHost = "https://auth.snapgenshin.com";
|
||||
|
||||
@@ -9,19 +9,9 @@ namespace Snap.Hutao.Web.Response;
|
||||
public enum KnownReturnCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Json 异常
|
||||
/// 无效请求
|
||||
/// </summary>
|
||||
JsonParseIssue = -2000000002,
|
||||
|
||||
/// <summary>
|
||||
/// Url为 空
|
||||
/// </summary>
|
||||
UrlIsEmpty = -2000000001,
|
||||
|
||||
/// <summary>
|
||||
/// 内部错误
|
||||
/// </summary>
|
||||
InternalFailure = -2000000000,
|
||||
InvalidRequest = -10001,
|
||||
|
||||
/// <summary>
|
||||
/// 已经签到过了
|
||||
|
||||
@@ -25,16 +25,12 @@ public class Response : ISupportValidation
|
||||
|
||||
if (!Validate())
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<IInfoBarService>()
|
||||
.Error(ToString());
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Error(ToString());
|
||||
}
|
||||
|
||||
if (ReturnCode != 0)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<IInfoBarService>()
|
||||
.Warning(ToString());
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Warning(ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +67,31 @@ public class Response : ISupportValidation
|
||||
{
|
||||
return $"状态:{ReturnCode} | 信息:{Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mihoyo 标准API响应
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">数据类型</typeparam>
|
||||
[SuppressMessage("", "SA1402")]
|
||||
public class Response<TData> : Response
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的 Mihoyo 标准API响应
|
||||
/// </summary>
|
||||
/// <param name="returnCode">返回代码</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="data">数据</param>
|
||||
[JsonConstructor]
|
||||
public Response(int returnCode, string message, TData? data)
|
||||
: base(returnCode, message)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public TData? Data { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Mihoyo 标准API响应
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">数据类型</typeparam>
|
||||
public class Response<TData> : Response
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的 Mihoyo 标准API响应
|
||||
/// </summary>
|
||||
/// <param name="returnCode">返回代码</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="data">数据</param>
|
||||
[JsonConstructor]
|
||||
public Response(int returnCode, string message, TData? data)
|
||||
: base(returnCode, message)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public TData? Data { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user