remove manual dependency property

This commit is contained in:
Lightczx
2023-07-30 00:35:41 +08:00
parent 4226598442
commit c5ab707b66
90 changed files with 1001 additions and 1191 deletions

View File

@@ -4,6 +4,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
@@ -58,41 +59,80 @@ internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
{
foreach (AttributeData propertyInfo in context2.Attributes.Where(attr => attr.AttributeClass!.ToDisplayString() == AttributeName))
{
string owner = context2.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
Dictionary<string, TypedConstant> namedArguments = propertyInfo.NamedArguments.ToDictionary();
bool isAttached = namedArguments.TryGetValue("IsAttached", out TypedConstant constant) && (bool)constant.Value!;
string register = isAttached ? "RegisterAttached" : "Register";
ImmutableArray<TypedConstant> arguments = propertyInfo.ConstructorArguments;
string propertyName = (string)arguments[0].Value!;
string propertyType = arguments[0].Type!.ToDisplayString();
string type = arguments[1].Value!.ToString();
string defaultValue = arguments.Length > 2
? GetLiteralString(arguments[2])
: "default";
string className = context2.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
string propertyType = arguments[1].Value!.ToString();
string defaultValue = GetLiteralString(arguments.ElementAtOrDefault(2)) ?? "default";
string propertyChangedCallback = arguments.ElementAtOrDefault(3) is { IsNull: false } arg3 ? $", {arg3.Value}" : string.Empty;
string code = $$"""
using Microsoft.UI.Xaml;
string code;
if (isAttached)
{
string objType = namedArguments.TryGetValue("AttachedType", out TypedConstant attachedType)
? attachedType.Value!.ToString()
: "object";
namespace {{context2.Symbol.ContainingNamespace}};
code = $$"""
using Microsoft.UI.Xaml;
partial class {{className}}
{
private DependencyProperty {{propertyName}}Property =
DependencyProperty.Register(nameof({{propertyName}}), typeof({{type}}), typeof({{className}}), new PropertyMetadata(({{type}}){{defaultValue}}));
namespace {{context2.Symbol.ContainingNamespace}};
public {{type}} {{propertyName}}
partial class {{owner}}
{
get => ({{type}})GetValue({{propertyName}}Property);
set => SetValue({{propertyName}}Property, value);
private static readonly DependencyProperty {{propertyName}}Property =
DependencyProperty.RegisterAttached("{{propertyName}}", typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
public static {{propertyType}} Get{{propertyName}}({{objType}} obj)
{
return obj?.GetValue({{propertyName}}Property) as Type;
}
public static void Set{{propertyName}}({{objType}} obj, {{propertyType}} value)
{
obj.SetValue({{propertyName}}Property, value);
}
}
}
""";
""";
}
else
{
code = $$"""
using Microsoft.UI.Xaml;
namespace {{context2.Symbol.ContainingNamespace}};
partial class {{owner}}
{
private readonly DependencyProperty {{propertyName}}Property =
DependencyProperty.Register(nameof({{propertyName}}), typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
public {{propertyType}} {{propertyName}}
{
get => ({{propertyType}})GetValue({{propertyName}}Property);
set => SetValue({{propertyName}}Property, value);
}
}
""";
}
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
production.AddSource($"{normalizedClassName}.{propertyName}.g.cs", code);
}
}
private static string GetLiteralString(TypedConstant typedConstant)
private static string? GetLiteralString(TypedConstant typedConstant)
{
if (typedConstant.IsNull)
{
return default;
}
if (typedConstant.Value is bool boolValue)
{
return boolValue ? "true" : "false";

View File

@@ -24,6 +24,8 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
private const string UseDynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
private const string CRLF = "\r\n";
private static readonly DiagnosticDescriptor injectionShouldOmitDescriptor = new("SH201", "Injection 特性可以省略", "HttpClient 特性已将 {0} 注册为 Transient 服务", "Quality", DiagnosticSeverity.Warning, true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
@@ -91,6 +93,17 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
{
if (context.SingleOrDefaultAttribute(InjectionGenerator.AttributeName) is AttributeData injectionData)
{
if (injectionData.ConstructorArguments[0].ToCSharpString() == InjectionGenerator.InjectAsTransientName)
{
if (injectionData.ConstructorArguments.Length < 2)
{
production.ReportDiagnostic(Diagnostic.Create(injectionShouldOmitDescriptor, context.Context.Node.GetLocation(), context.Context.Node));
}
}
}
lineBuilder.Clear().Append(CRLF);
lineBuilder.Append(@" services.AddHttpClient<");

View File

@@ -16,11 +16,10 @@ namespace Snap.Hutao.SourceGeneration.DependencyInjection;
[Generator(LanguageNames.CSharp)]
internal sealed class InjectionGenerator : IIncrementalGenerator
{
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
public const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
private const string CRLF = "\r\n";
private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
@@ -87,7 +86,7 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
{
lineBuilder.Clear().Append(CRLF);
lineBuilder.Clear().AppendLine();
AttributeData injectionInfo = context.SingleAttribute(AttributeName);
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;

View File

@@ -14,7 +14,7 @@ internal class LocalizedEnumGenerator : IIncrementalGenerator
{
private const string AttributeName = "Snap.Hutao.Resource.Localization.LocalizationAttribute";
private const string LocalizationKeyName = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<GeneratorSyntaxContext2> localizationEnums = context.SyntaxProvider

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.Primitive;
@@ -32,4 +33,9 @@ internal static class EnumerableExtension
while (enumerator.MoveNext());
}
}
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source)
{
return source.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
}

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<None Remove="stylecop.json" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
</Project>

View File

@@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -16,6 +17,7 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
@@ -25,6 +27,7 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
{
typeInternalDescriptor,
readOnlyStructRefDescriptor,
useValueTaskIfPossibleDescriptor,
}.ToImmutableArray();
}
}
@@ -90,9 +93,22 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
private void HandleMethodDeclaration(SyntaxNodeAnalysisContext context)
{
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
INamedTypeSymbol? returnTypeSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax.ReturnType) as INamedTypeSymbol;
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
{
Location location = methodSyntax.ReturnType.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useValueTaskIfPossibleDescriptor, location);
context.ReportDiagnostic(diagnostic);
return;
}
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.ValueTask"))
{
return;
}
// 跳过异步方法,因为异步方法无法使用 ref in out
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.AsyncKeyword)) || IsTaskOrValueTask(returnTypeSymbol))
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.AsyncKeyword)))
{
return;
}
@@ -185,23 +201,4 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
_ => false,
};
}
private static bool IsTaskOrValueTask(INamedTypeSymbol? symbol)
{
if (symbol == null)
{
return false;
}
string typeName = symbol.MetadataName;
if (typeName == "System.Threading.Tasks.Task" ||
typeName == "System.Threading.Tasks.Task`1" ||
typeName == "System.Threading.Tasks.ValueTask" ||
typeName == "System.Threading.Tasks.ValueTask`1")
{
return true;
}
return false;
}
}

View File

@@ -22,10 +22,10 @@ namespace Snap.Hutao.Control.Image;
/// 为其他图像类控件提供基类
/// </summary>
[HighQuality]
[DependencyProperty("EnableLazyLoading", typeof(bool), true)]
[DependencyProperty("EnableLazyLoading", typeof(bool), true, nameof(OnSourceChanged))]
[DependencyProperty("Source", typeof(Uri), default!, nameof(OnSourceChanged))]
internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
private static readonly DependencyProperty SourceProperty = Property<CompositionImage>.Depend(nameof(Source), default(Uri), OnSourceChanged);
private readonly ConcurrentCancellationTokenSource loadingTokenSource = new();
private readonly IServiceProvider serviceProvider;
@@ -55,15 +55,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
loadedImageSourceLoadCompletedEventHandler = OnLoadImageSurfaceLoadCompleted;
}
/// <summary>
/// 源
/// </summary>
public Uri Source
{
get => (Uri)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// 合成组合视觉
/// </summary>
@@ -131,7 +122,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
}
}
private async Task ApplyImageAsync(Uri? uri, CancellationToken token)
private async ValueTask ApplyImageAsync(Uri? uri, CancellationToken token)
{
await HideAsync(token).ConfigureAwait(true);
@@ -170,7 +161,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
}
}
private async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
private async ValueTask<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
{
surfaceLoadTaskCompletionSource = new();
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
@@ -180,7 +171,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
return surface;
}
private async Task ShowAsync(CancellationToken token)
private async ValueTask ShowAsync(CancellationToken token)
{
if (!isShow)
{
@@ -201,7 +192,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
}
}
private async Task HideAsync(CancellationToken token)
private async ValueTask HideAsync(CancellationToken token)
{
if (isShow)
{

View File

@@ -11,12 +11,11 @@ namespace Snap.Hutao.Control.Panel;
/// 面板选择器
/// </summary>
[HighQuality]
[DependencyProperty("Current", typeof(string), List, nameof(OnCurrentChanged))]
internal sealed partial class PanelSelector : SplitButton
{
private const string List = nameof(List);
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), List, OnCurrentChanged);
private readonly RoutedEventHandler loadedEventHandler;
private readonly RoutedEventHandler unloadedEventHandler;
private readonly TypedEventHandler<SplitButton, SplitButtonClickEventArgs> clickEventHandler;
@@ -41,15 +40,6 @@ internal sealed partial class PanelSelector : SplitButton
Unloaded += unloadedEventHandler;
}
/// <summary>
/// 当前选择
/// </summary>
public string Current
{
get => (string)GetValue(CurrentProperty);
set => SetValue(CurrentProperty, value);
}
private static void InitializeItems(PanelSelector selector)
{
MenuFlyout menuFlyout = (MenuFlyout)selector.Flyout;

View File

@@ -1,105 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Control;
/// <summary>
/// 快速创建 <see cref="TOwner"/> 的 <see cref="DependencyProperty"/>
/// </summary>
/// <typeparam name="TOwner">所有者的类型</typeparam>
[HighQuality]
[Obsolete("Use DependencyPropertyAttribute whenever possible")]
internal static class Property<TOwner>
{
/// <summary>
/// 注册依赖属性
/// </summary>
/// <typeparam name="TProperty">属性的类型</typeparam>
/// <param name="name">属性名称</param>
/// <returns>注册的依赖属性</returns>
public static DependencyProperty Depend<TProperty>(string name)
{
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(default(TProperty)));
}
/// <summary>
/// 注册依赖属性
/// </summary>
/// <typeparam name="TProperty">属性的类型</typeparam>
/// <param name="name">属性名称</param>
/// <param name="defaultValue">默认值</param>
/// <returns>注册的依赖属性</returns>
public static DependencyProperty Depend<TProperty>(string name, TProperty defaultValue)
{
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue));
}
/// <summary>
/// 注册依赖属性
/// </summary>
/// <typeparam name="TProperty">属性的类型</typeparam>
/// <param name="name">属性名称</param>
/// <param name="defaultValue">封装的默认值</param>
/// <returns>注册的依赖属性</returns>
public static DependencyProperty DependBoxed<TProperty>(string name, object defaultValue)
{
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue));
}
/// <summary>
/// 注册依赖属性
/// </summary>
/// <typeparam name="TProperty">属性的类型</typeparam>
/// <param name="name">属性名称</param>
/// <param name="defaultValue">默认值</param>
/// <param name="callback">属性更改回调</param>
/// <returns>注册的依赖属性</returns>
public static DependencyProperty Depend<TProperty>(
string name,
TProperty defaultValue,
Action<DependencyObject, DependencyPropertyChangedEventArgs> callback)
{
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue, new(callback)));
}
/// <summary>
/// 注册附加属性
/// </summary>
/// <typeparam name="TProperty">属性的类型</typeparam>
/// <param name="name">属性名称</param>
/// <returns>注册的附加属性</returns>
public static DependencyProperty Attach<TProperty>(string name)
{
return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new(default(TProperty)));
}
/// <summary>
/// 注册附加属性
/// </summary>
/// <typeparam name="TProperty">属性的类型</typeparam>
/// <param name="name">属性名称</param>
/// <param name="defaultValue">默认值</param>
/// <returns>注册的附加属性</returns>
public static DependencyProperty Attach<TProperty>(string name, TProperty defaultValue)
{
return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new(defaultValue));
}
/// <summary>
/// 注册附加属性
/// </summary>
/// <typeparam name="TProperty">属性的类型</typeparam>
/// <param name="name">属性名称</param>
/// <param name="defaultValue">默认值</param>
/// <param name="callback">属性更改回调</param>
/// <returns>注册的附加属性</returns>
public static DependencyProperty Attach<TProperty>(
string name,
TProperty defaultValue,
Action<DependencyObject, DependencyPropertyChangedEventArgs> callback)
{
return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new(defaultValue, new(callback)));
}
}

View File

@@ -21,10 +21,9 @@ namespace Snap.Hutao.Control.Text;
/// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
/// </summary>
[HighQuality]
internal sealed class DescriptionTextBlock : ContentControl
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
internal sealed partial class DescriptionTextBlock : ContentControl
{
private static readonly DependencyProperty DescriptionProperty = Property<DescriptionTextBlock>.Depend(nameof(Description), string.Empty, OnDescriptionChanged);
private static readonly int ColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
private static readonly int ColorTagLeftLength = "<color=#FFFFFFFF>".Length;
@@ -49,15 +48,6 @@ internal sealed class DescriptionTextBlock : ContentControl
ActualThemeChanged += actualThemeChangedEventHandler;
}
/// <summary>
/// 可绑定的描述文本
/// </summary>
public string Description
{
get => (string)GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;

View File

@@ -13,4 +13,12 @@ internal sealed class DependencyPropertyAttribute : Attribute
public DependencyPropertyAttribute(string name, Type type, object defaultValue)
{
}
public DependencyPropertyAttribute(string name, Type type, object defaultValue, string valueChangedCallbackName)
{
}
public bool IsAttached { get; set; }
public Type AttachedType { get; set; } = default!;
}

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
/// 有名称的对象
/// 指示该对象可通过名称区分
/// </summary>
[Obsolete("无意义的接口")]
[HighQuality]
internal interface INamedService
{

View File

@@ -6,6 +6,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
/// <summary>
/// 海外服/HoYoLAB 可区分
/// </summary>
[Obsolete("Use IOverseaSupportFactory instead")]
internal interface IOverseaSupport
{
/// <summary>

View File

@@ -11,20 +11,6 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// </summary>
internal static class EnumerableServiceExtension
{
/// <summary>
/// 选择对应的服务
/// </summary>
/// <typeparam name="TService">服务类型</typeparam>
/// <param name="services">服务集合</param>
/// <param name="name">名称</param>
/// <returns>对应的服务</returns>
[Obsolete("该方法会导致不必要的服务实例化")]
public static TService Pick<TService>(this IEnumerable<TService> services, string name)
where TService : INamedService
{
return services.Single(s => s.Name == name);
}
/// <summary>
/// 选择对应的服务
/// </summary>

View File

@@ -32,7 +32,8 @@ internal sealed class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEn
private static IEnumerable<int> EnumerateNumbers(string source)
{
foreach (StringSegment id in new StringTokenizer(source, new[] { Comma })) // TODO: Use CL
// TODO: Use Collection Literals
foreach (StringSegment id in new StringTokenizer(source, new[] { Comma }))
{
yield return int.Parse(id.AsSpan());
}

View File

@@ -30,7 +30,7 @@ internal class AsyncBarrier
// Allocate the stack so no resizing is necessary.
// We don't need space for the last participant, since we never have to store it.
waiters = new Stack<TaskCompletionSource>(participants - 1);
waiters = new Queue<TaskCompletionSource>(participants - 1);
}
/// <summary>

View File

@@ -110,4 +110,102 @@ internal static class TaskExtension
onException?.Invoke(e);
}
}
/// <summary>
/// 安全的触发任务
/// </summary>
/// <param name="task">任务</param>
public static async void SafeForget(this ValueTask task)
{
try
{
await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Do nothing
}
#if DEBUG
catch (Exception ex)
{
if (System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debug.WriteLine(ExceptionFormat.Format(ex));
System.Diagnostics.Debugger.Break();
}
}
#else
catch
{
}
#endif
}
/// <summary>
/// 安全的触发任务
/// </summary>
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
public static async void SafeForget(this ValueTask task, ILogger logger)
{
try
{
await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Do nothing
}
catch (Exception e)
{
logger?.LogError(e, "{Caller}:\r\n{Exception}", nameof(SafeForget), ExceptionFormat.Format(e.GetBaseException()));
}
}
/// <summary>
/// 安全的触发任务
/// </summary>
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this ValueTask task, ILogger logger, Action<Exception> onException)
{
try
{
await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Do nothing
}
catch (Exception e)
{
logger?.LogError(e, "{Caller}:\r\n{Exception}", nameof(SafeForget), ExceptionFormat.Format(e.GetBaseException()));
onException?.Invoke(e);
}
}
/// <summary>
/// 安全的触发任务
/// </summary>
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
/// <param name="onCanceled">任务取消时调用</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this ValueTask task, ILogger logger, Action onCanceled, Action<Exception>? onException = null)
{
try
{
await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
onCanceled?.Invoke();
}
catch (Exception e)
{
logger?.LogError(e, "{Caller}:\r\n{Exception}", nameof(SafeForget), ExceptionFormat.Format(e.GetBaseException()));
onException?.Invoke(e);
}
}
}

View File

@@ -22,7 +22,6 @@ internal sealed class FlyoutStateChangedMessage
public static FlyoutStateChangedMessage Close { get; } = new(false);
/// <summary>
/// 是否为开启状态
/// </summary>

View File

@@ -2,11 +2,9 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Cultivation;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
using Snap.Hutao.Model.Intrinsic.Immutable;
using Snap.Hutao.ViewModel.Cultivation;
using System.Text.RegularExpressions;
namespace Snap.Hutao.Model.Metadata.Item;

View File

@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;

View File

@@ -0,0 +1,205 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IGachaLogDbService))]
internal sealed partial class GachaLogDbService : IGachaLogDbService
{
private readonly IServiceProvider serviceProvider;
public ObservableCollection<GachaArchive> GetGachaArchiveCollection()
{
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
}
catch (SqliteException ex)
{
string message = string.Format(SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage, ex.Message);
throw ThrowHelper.UserdataCorrupted(message, ex);
}
}
public List<GachaItem> GetGachaItemListByArchiveId(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.ToList();
}
}
public async ValueTask DeleteGachaArchiveByIdAsync(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaArchives
.ExecuteDeleteWhereAsync(a => a.InnerId == archiveId)
.ConfigureAwait(false);
}
}
public async ValueTask<long> GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(Guid archiveId, GachaConfigType queryType, CancellationToken token)
{
GachaItem? item = null;
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = await appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.FirstOrDefaultAsync(token)
.ConfigureAwait(false);
}
}
catch (SqliteException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex);
}
return item?.Id ?? 0L;
}
public long GetNewestGachaItemIdByArchiveIdAndQueryType(Guid archiveId, GachaConfigType queryType)
{
GachaItem? item = null;
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.FirstOrDefault();
}
}
catch (SqliteException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex);
}
return item?.Id ?? 0L;
}
public long GetOldestGachaItemIdByArchiveId(Guid archiveId)
{
GachaItem? item = null;
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.FirstOrDefault();
}
return item?.Id ?? long.MaxValue;
}
public async ValueTask AddGachaArchiveAsync(GachaArchive archive)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaArchives.AddAndSaveAsync(archive);
}
}
public void AddGachaArchive(GachaArchive archive)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GachaArchives.AddAndSave(archive);
}
}
public List<Web.Hutao.GachaLog.GachaItem> GetHutaoGachaItemList(Guid archiveId, GachaConfigType queryType, long endId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.Where(i => i.Id > endId)
// Keep this to make SQL generates correctly
.Select(i => new Web.Hutao.GachaLog.GachaItem()
{
GachaType = i.GachaType,
QueryType = i.QueryType,
ItemId = i.ItemId,
Time = i.Time,
Id = i.Id,
})
.ToList();
}
}
public async ValueTask<GachaArchive?> GetGachaArchiveByUidAsync(string uid, CancellationToken token)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.GachaArchives
.AsNoTracking()
.SingleOrDefaultAsync(a => a.Uid == uid, token)
.ConfigureAwait(false);
}
}
public async ValueTask AddGachaItemsAsync(List<GachaItem> items)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaItems.AddRangeAndSaveAsync(items).ConfigureAwait(false);
}
}
}

View File

@@ -102,7 +102,7 @@ internal struct GachaLogFetchContext
GachaArchiveOperation.GetOrAdd(serviceProvider, item.Uid, archives, out TargetArchive);
}
DbEndId ??= gachaLogDbService.GetLastGachaItemIdByArchiveIdAndQueryType(TargetArchive.InnerId, CurrentType);
DbEndId ??= gachaLogDbService.GetNewestGachaItemIdByArchiveIdAndQueryType(TargetArchive.InnerId, CurrentType);
}
/// <summary>

View File

@@ -0,0 +1,134 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.GachaLog.Factory;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.GachaLog;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿记录胡桃云服务
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IGachaLogHutaoCloudService))]
internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudService
{
private readonly HomaGachaLogClient homaGachaLogClient;
private readonly IGachaLogDbService gachaLogDbService;
private readonly IServiceProvider serviceProvider;
/// <inheritdoc/>
public ValueTask<Response<List<GachaEntry>>> GetGachaEntriesAsync(CancellationToken token = default)
{
return homaGachaLogClient.GetGachaEntriesAsync(token);
}
/// <inheritdoc/>
public async ValueTask<ValueResult<bool, string>> UploadGachaItemsAsync(GachaArchive gachaArchive, CancellationToken token = default)
{
string uid = gachaArchive.Uid;
if (await GetEndIdsFromCloudAsync(uid, token).ConfigureAwait(false) is { } endIds)
{
List<Web.Hutao.GachaLog.GachaItem> items = new();
foreach ((GachaConfigType type, long endId) in endIds)
{
List<Web.Hutao.GachaLog.GachaItem> part = gachaLogDbService.GetHutaoGachaItemList(gachaArchive.InnerId, type, endId);
items.AddRange(part);
}
return await homaGachaLogClient.UploadGachaItemsAsync(uid, items, token).ConfigureAwait(false);
}
return new(false, SH.ServiceGachaLogHutaoCloudEndIdFetchFailed);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, GachaArchive?>> RetrieveGachaItemsAsync(string uid, CancellationToken token = default)
{
GachaArchive? archive = await gachaLogDbService
.GetGachaArchiveByUidAsync(uid, token)
.ConfigureAwait(false);
EndIds endIds = await CreateEndIdsAsync(archive, token).ConfigureAwait(false);
Response<List<Web.Hutao.GachaLog.GachaItem>> resp = await homaGachaLogClient.RetrieveGachaItemsAsync(uid, endIds, token).ConfigureAwait(false);
if (!resp.IsOk())
{
return new(false, null);
}
if (archive == null)
{
archive = GachaArchive.From(uid);
await gachaLogDbService.AddGachaArchiveAsync(archive).ConfigureAwait(false);
}
List<Model.Entity.GachaItem> gachaItems = resp.Data.SelectList(i => Model.Entity.GachaItem.From(archive.InnerId, i));
await gachaLogDbService.AddGachaItemsAsync(gachaItems).ConfigureAwait(false);
return new(true, archive);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> DeleteGachaItemsAsync(string uid, CancellationToken token = default)
{
return await homaGachaLogClient.DeleteGachaItemsAsync(uid, token).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default)
{
Response<GachaEventStatistics> response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false);
if (response.IsOk())
{
IMetadataService metadataService = serviceProvider.GetRequiredService<IMetadataService>();
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
Dictionary<WeaponId, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
List<GachaEvent> gachaEvents = await metadataService.GetGachaEventsAsync(token).ConfigureAwait(false);
HutaoStatisticsFactoryMetadataContext context = new(idAvatarMap, idWeaponMap, gachaEvents);
GachaEventStatistics raw = response.Data;
HutaoStatisticsFactory factory = new(context);
HutaoStatistics statistics = factory.Create(raw);
return new(true, statistics);
}
}
return new(false, default!);
}
private async Task<EndIds?> GetEndIdsFromCloudAsync(string uid, CancellationToken token = default)
{
Response<EndIds> resp = await homaGachaLogClient.GetEndIdsAsync(uid, token).ConfigureAwait(false);
return resp.IsOk() ? resp.Data : default;
}
private async ValueTask<EndIds> CreateEndIdsAsync(GachaArchive? archive, CancellationToken token)
{
EndIds endIds = new();
foreach (GachaConfigType type in GachaLog.QueryTypes)
{
if (archive != null)
{
endIds[type] = await gachaLogDbService
.GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(archive.InnerId, type, token)
.ConfigureAwait(false);
}
}
return endIds;
}
}

View File

@@ -1,13 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.GachaLog.Factory;
@@ -123,11 +119,12 @@ internal sealed partial class GachaLogService : IGachaLogService
/// <inheritdoc/>
public async ValueTask ImportFromUIGFAsync(UIGF uigf)
{
CurrentArchive = await gachaLogImportService.ImportAsync(context, uigf).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(ArchiveCollection);
CurrentArchive = await gachaLogImportService.ImportAsync(context, uigf, ArchiveCollection).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
public async ValueTask<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
{
bool isLazy = strategy switch
{
@@ -147,7 +144,7 @@ internal sealed partial class GachaLogService : IGachaLogService
}
/// <inheritdoc/>
public async Task RemoveArchiveAsync(GachaArchive archive)
public async ValueTask RemoveArchiveAsync(GachaArchive archive)
{
ArgumentNullException.ThrowIfNull(archiveCollection);
@@ -160,7 +157,7 @@ internal sealed partial class GachaLogService : IGachaLogService
await gachaLogDbService.DeleteGachaArchiveByIdAsync(archive.InnerId).ConfigureAwait(false);
}
private async Task<ValueResult<bool, GachaArchive?>> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
private async ValueTask<ValueResult<bool, GachaArchive?>> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(ArchiveCollection);
GachaLogFetchContext fetchContext = new(serviceProvider, context, isLazy);
@@ -225,103 +222,4 @@ internal sealed partial class GachaLogService : IGachaLogService
return new(!fetchContext.FetchStatus.AuthKeyTimeout, fetchContext.TargetArchive);
}
}
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IGachaLogDbService))]
internal sealed partial class GachaLogDbService : IGachaLogDbService
{
private readonly IServiceProvider serviceProvider;
public ObservableCollection<GachaArchive> GetGachaArchiveCollection()
{
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
}
catch (SqliteException ex)
{
string message = string.Format(SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage, ex.Message);
throw ThrowHelper.UserdataCorrupted(message, ex);
}
}
public List<GachaItem> GetGachaItemListByArchiveId(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.ToList();
}
}
public async ValueTask DeleteGachaArchiveByIdAsync(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaArchives
.ExecuteDeleteWhereAsync(a => a.InnerId == archiveId)
.ConfigureAwait(false);
}
}
public long GetLastGachaItemIdByArchiveIdAndQueryType(Guid archiveId, GachaConfigType queryType)
{
GachaItem? item = null;
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.FirstOrDefault();
}
}
catch (SqliteException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex);
}
return item?.Id ?? 0L;
}
public void AddGachaArchive(GachaArchive archive)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GachaArchives.AddAndSave(archive);
}
}
}
internal interface IGachaLogDbService
{
void AddGachaArchive(GachaArchive archive);
ValueTask DeleteGachaArchiveByIdAsync(Guid archiveId);
ObservableCollection<GachaArchive> GetGachaArchiveCollection();
List<GachaItem> GetGachaItemListByArchiveId(Guid archiveId);
long GetLastGachaItemIdByArchiveIdAndQueryType(Guid archiveId, GachaConfigType queryType);
}

View File

@@ -42,12 +42,6 @@ internal readonly struct GachaLogServiceMetadataContext
/// </summary>
public readonly Dictionary<string, Weapon> NameWeaponMap;
/// <summary>
/// 存档集合
/// </summary>
[Obsolete]
public readonly ObservableCollection<GachaArchive> ArchiveCollection;
/// <summary>
/// 是否初始化完成
/// </summary>

View File

@@ -1,150 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.GachaLog.Factory;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.GachaLog;
using Snap.Hutao.Web.Response;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 胡桃云服务
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IHutaoCloudService))]
internal sealed partial class HutaoCloudService : IHutaoCloudService
{
private readonly HomaGachaLogClient homaGachaLogClient;
private readonly IServiceProvider serviceProvider;
/// <inheritdoc/>
public Task<Response<List<string>>> GetUidsAsync(CancellationToken token = default)
{
return homaGachaLogClient.GetUidsAsync(token);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> UploadGachaItemsAsync(Model.Entity.GachaArchive gachaArchive, CancellationToken token = default)
{
string uid = gachaArchive.Uid;
EndIds? endIds = await GetEndIdsFromCloudAsync(uid, token).ConfigureAwait(false);
if (endIds != null)
{
List<GachaItem> items = new();
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
foreach ((GachaConfigType type, long endId) in endIds)
{
IEnumerable<GachaItem> part = QueryArchiveGachaItemsByTypeAndEndId(appDbContext, gachaArchive, type, endId);
items.AddRange(part);
}
}
return await homaGachaLogClient.UploadGachaItemsAsync(uid, items, token).ConfigureAwait(false);
}
return new(false, SH.ServiceGachaLogHutaoCloudEndIdFetchFailed);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, Model.Entity.GachaArchive?>> RetrieveGachaItemsAsync(string uid, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
Model.Entity.GachaArchive? archive = await appDbContext.GachaArchives
.SingleOrDefaultAsync(a => a.Uid == uid, token)
.ConfigureAwait(false);
EndIds endIds = await EndIds.CreateAsync(appDbContext, archive, token).ConfigureAwait(false);
Response<List<GachaItem>> resp = await homaGachaLogClient.RetrieveGachaItemsAsync(uid, endIds, token).ConfigureAwait(false);
if (resp.IsOk())
{
if (archive == null)
{
archive = Model.Entity.GachaArchive.From(uid);
await appDbContext.GachaArchives.AddAndSaveAsync(archive).ConfigureAwait(false);
}
List<Model.Entity.GachaItem> gachaItems = resp.Data.SelectList(i => Model.Entity.GachaItem.From(archive.InnerId, i));
await appDbContext.GachaItems.AddRangeAndSaveAsync(gachaItems).ConfigureAwait(false);
return new(true, archive);
}
}
return new(false, null);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> DeleteGachaItemsAsync(string uid, CancellationToken token = default)
{
return await homaGachaLogClient.DeleteGachaItemsAsync(uid, token).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default)
{
IMetadataService metadataService = serviceProvider.GetRequiredService<IMetadataService>();
Response<GachaEventStatistics> response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false);
if (response.IsOk())
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
Dictionary<WeaponId, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
List<GachaEvent> gachaEvents = await metadataService.GetGachaEventsAsync(token).ConfigureAwait(false);
HutaoStatisticsFactoryMetadataContext context = new(idAvatarMap, idWeaponMap, gachaEvents);
GachaEventStatistics raw = response.Data;
HutaoStatisticsFactory factory = new(context);
HutaoStatistics statistics = factory.Create(raw);
return new(true, statistics);
}
}
return new(false, default!);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static IEnumerable<GachaItem> QueryArchiveGachaItemsByTypeAndEndId(AppDbContext appDbContext, Model.Entity.GachaArchive gachaArchive, GachaConfigType type, long endId)
{
return appDbContext.GachaItems
.Where(i => i.ArchiveId == gachaArchive.InnerId)
.Where(i => i.QueryType == type)
.OrderByDescending(i => i.Id)
.Where(i => i.Id > endId)
// Keep this to make SQL generates correctly
.Select(i => new GachaItem()
{
GachaType = i.GachaType,
QueryType = i.QueryType,
ItemId = i.ItemId,
Time = i.Time,
Id = i.Id,
});
}
private async Task<EndIds?> GetEndIdsFromCloudAsync(string uid, CancellationToken token = default)
{
Response<EndIds> resp = await homaGachaLogClient.GetEndIdsAsync(uid, token).ConfigureAwait(false);
_ = resp.IsOk();
return resp.Data;
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
internal interface IGachaLogDbService
{
void AddGachaArchive(GachaArchive archive);
ValueTask AddGachaArchiveAsync(GachaArchive archive);
ValueTask AddGachaItemsAsync(List<GachaItem> items);
ValueTask DeleteGachaArchiveByIdAsync(Guid archiveId);
ValueTask<GachaArchive?> GetGachaArchiveByUidAsync(string uid, CancellationToken token);
ObservableCollection<GachaArchive> GetGachaArchiveCollection();
List<GachaItem> GetGachaItemListByArchiveId(Guid archiveId);
List<Web.Hutao.GachaLog.GachaItem> GetHutaoGachaItemList(Guid archiveId, GachaConfigType queryType, long endId);
long GetNewestGachaItemIdByArchiveIdAndQueryType(Guid archiveId, GachaConfigType queryType);
ValueTask<long> GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(Guid archiveId, GachaConfigType queryType, CancellationToken token);
long GetOldestGachaItemIdByArchiveId(Guid archiveId);
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Model.Entity;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hutao.GachaLog;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.GachaLog;
@@ -10,7 +11,7 @@ namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 胡桃云服务
/// </summary>
internal interface IHutaoCloudService
internal interface IGachaLogHutaoCloudService
{
/// <summary>
/// 异步删除服务器上的祈愿记录
@@ -25,14 +26,9 @@ internal interface IHutaoCloudService
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>祈愿统计信息</returns>
Task<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default(CancellationToken));
Task<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default);
/// <summary>
/// 异步获取服务器上的 Uid 列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>服务器上的 Uid 列表</returns>
Task<Response<List<string>>> GetUidsAsync(CancellationToken token = default);
ValueTask<Response<List<GachaEntry>>> GetGachaEntriesAsync(CancellationToken token = default);
/// <summary>
/// 异步获取祈愿记录
@@ -48,5 +44,5 @@ internal interface IHutaoCloudService
/// <param name="gachaArchive">祈愿档案</param>
/// <param name="token">取消令牌</param>
/// <returns>是否上传成功</returns>
Task<ValueResult<bool, string>> UploadGachaItemsAsync(GachaArchive gachaArchive, CancellationToken token = default);
ValueTask<ValueResult<bool, string>> UploadGachaItemsAsync(GachaArchive gachaArchive, CancellationToken token = default);
}

View File

@@ -69,12 +69,12 @@ internal interface IGachaLogService
/// <param name="progress">进度</param>
/// <param name="token">取消令牌</param>
/// <returns>验证密钥是否有效</returns>
Task<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress<GachaLogFetchStatus> progress, CancellationToken token);
ValueTask<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress<GachaLogFetchStatus> progress, CancellationToken token);
/// <summary>
/// 删除存档
/// </summary>
/// <param name="archive">存档</param>
/// <returns>任务</returns>
Task RemoveArchiveAsync(GachaArchive archive);
ValueTask RemoveArchiveAsync(GachaArchive archive);
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
@@ -16,6 +17,7 @@ internal interface IUIGFImportService
/// </summary>
/// <param name="context">祈愿记录服务上下文</param>
/// <param name="uigf">数据</param>
/// <param name="archives">存档集合</param>
/// <returns>存档</returns>
Task<GachaArchive> ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf);
ValueTask<GachaArchive> ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, ObservableCollection<GachaArchive> archives);
}

View File

@@ -20,10 +20,7 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public string Name { get => nameof(GachaLogQueryManualInputProvider); }
/// <inheritdoc/>
public async Task<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
public async ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();

View File

@@ -23,10 +23,7 @@ internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvid
private readonly IUserService userService;
/// <inheritdoc/>
public string Name { get => nameof(GachaLogQuerySTokenProvider); }
/// <inheritdoc/>
public async Task<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
public async ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{

View File

@@ -23,9 +23,6 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
private readonly IGameService gameService;
private readonly MetadataOptions metadataOptions;
/// <inheritdoc/>
public string Name { get => nameof(GachaLogQueryWebCacheProvider); }
/// <summary>
/// 获取缓存文件路径
/// </summary>
@@ -50,7 +47,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
}
/// <inheritdoc/>
public async Task<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
public async ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
{
(bool isOk, string path) = await gameService.GetGamePathAsync().ConfigureAwait(false);

View File

@@ -1,20 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// <summary>
/// 祈愿记录Url提供器
/// </summary>
[HighQuality]
internal interface IGachaLogQueryProvider : INamedService
internal interface IGachaLogQueryProvider
{
/// <summary>
/// 异步获取包含验证密钥的查询语句
/// 查询语句可以仅包含?后的内容
/// </summary>
/// <returns>包含验证密钥的查询语句</returns>
Task<ValueResult<bool, GachaLogQuery>> GetQueryAsync();
ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync();
}

View File

@@ -15,29 +15,24 @@ namespace Snap.Hutao.Service.GachaLog;
internal sealed partial class UIGFExportService : IUIGFExportService
{
private readonly IServiceProvider serviceProvider;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async ValueTask<UIGF> ExportAsync(GachaLogServiceMetadataContext context, GachaArchive archive)
{
await taskContext.SwitchToBackgroundAsync();
using (IServiceScope scope = serviceProvider.CreateScope())
List<UIGFItem> list = gachaLogDbService
.GetGachaItemListByArchiveId(archive.InnerId)
.SelectList(i => UIGFItem.From(i, context.GetNameQualityByItemId(i.ItemId)));
UIGF uigf = new()
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
Info = UIGFInfo.From(serviceProvider, archive.Uid),
List = list,
};
List<UIGFItem> list = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.AsEnumerable()
.Select(i => UIGFItem.From(i, context.GetNameQualityByItemId(i.ItemId)))
.ToList();
UIGF uigf = new()
{
Info = UIGFInfo.From(serviceProvider, archive.Uid),
List = list,
};
return uigf;
}
return uigf;
}
}

View File

@@ -1,15 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.InterChange.GachaLog;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// v2.1 v2.2 祈愿记录导入服务
/// 祈愿记录导入服务
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IUIGFImportService))]
@@ -17,43 +16,39 @@ internal sealed partial class UIGFImportService : IUIGFImportService
{
private readonly ILogger<UIGFImportService> logger;
private readonly IServiceProvider serviceProvider;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async Task<GachaArchive> ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf)
public async ValueTask<GachaArchive> ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, ObservableCollection<GachaArchive> archives)
{
await taskContext.SwitchToBackgroundAsync();
using (IServiceScope scope = serviceProvider.CreateScope())
GachaArchiveOperation.GetOrAdd(serviceProvider, uigf.Info.Uid, archives, out GachaArchive? archive);
Guid archiveId = archive.InnerId;
long trimId = gachaLogDbService.GetOldestGachaItemIdByArchiveId(archiveId);
logger.LogInformation("Last Id to trim with: [{Id}]", trimId);
_ = uigf.IsCurrentVersionSupported(out UIGFVersion version);
List<GachaItem> toAdd = version switch
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
UIGFVersion.Major2Minor3OrHigher => uigf.List
.OrderByDescending(i => i.Id)
.Where(i => i.Id < trimId)
.Select(i => GachaItem.From(archiveId, i))
.ToList(),
UIGFVersion.Major2Minor2OrLower => uigf.List
.OrderByDescending(i => i.Id)
.Where(i => i.Id < trimId)
.Select(i => GachaItem.From(archiveId, i, context.GetItemId(i)))
.ToList(),
_ => new(),
};
GachaArchiveOperation.GetOrAdd(serviceProvider, uigf.Info.Uid, context.ArchiveCollection, out GachaArchive? archive);
Guid archiveId = archive.InnerId;
long trimId = appDbContext.GachaItems
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.FirstOrDefault()?.Id ?? long.MaxValue;
logger.LogInformation("Last Id to trim with: [{Id}]", trimId);
_ = uigf.IsCurrentVersionSupported(out UIGFVersion version);
IEnumerable<GachaItem> toAdd = version switch
{
UIGFVersion.Major2Minor3OrHigher => uigf.List
.OrderByDescending(i => i.Id)
.Where(i => i.Id < trimId)
.Select(i => GachaItem.From(archiveId, i)),
UIGFVersion.Major2Minor2OrLower => uigf.List
.OrderByDescending(i => i.Id)
.Where(i => i.Id < trimId)
.Select(i => GachaItem.From(archiveId, i, context.GetItemId(i))),
_ => Enumerable.Empty<GachaItem>(),
};
await appDbContext.GachaItems.AddRangeAndSaveAsync(toAdd).ConfigureAwait(false);
return archive;
}
await gachaLogDbService.AddGachaItemsAsync(toAdd).ConfigureAwait(false);
return archive;
}
}

View File

@@ -65,19 +65,19 @@ internal sealed partial class GameService : IGameService
// Cannot find in setting
if (string.IsNullOrEmpty(appOptions.GamePath))
{
IEnumerable<IGameLocator> gameLocators = scope.ServiceProvider.GetRequiredService<IEnumerable<IGameLocator>>();
IGameLocatorFactory locatorFactory = scope.ServiceProvider.GetRequiredService<IGameLocatorFactory>();
// Try locate by unity log
ValueResult<bool, string> result = await gameLocators
.Pick(nameof(UnityLogGameLocator))
ValueResult<bool, string> result = await locatorFactory
.Create(GameLocationSource.UnityLog)
.LocateGamePathAsync()
.ConfigureAwait(false);
if (!result.IsOk)
{
// Try locate by registry
result = await gameLocators
.Pick(nameof(RegistryLauncherLocator))
result = await locatorFactory
.Create(GameLocationSource.Registry)
.LocateGamePathAsync()
.ConfigureAwait(false);
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Locator;
internal enum GameLocationSource
{
Registry,
UnityLog,
Manual,
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Locator;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocatorFactory))]
internal sealed partial class GameLocatorFactory : IGameLocatorFactory
{
private readonly IServiceProvider serviceProvider;
public IGameLocator Create(GameLocationSource source)
{
return source switch
{
GameLocationSource.Registry => serviceProvider.GetRequiredService<RegistryLauncherLocator>(),
GameLocationSource.UnityLog => serviceProvider.GetRequiredService<UnityLogGameLocator>(),
GameLocationSource.Manual => serviceProvider.GetRequiredService<ManualGameLocator>(),
_ => throw Must.NeverHappen(),
};
}
}

View File

@@ -9,12 +9,12 @@ namespace Snap.Hutao.Service.Game.Locator;
/// 游戏位置定位器
/// </summary>
[HighQuality]
internal interface IGameLocator : INamedService
internal interface IGameLocator
{
/// <summary>
/// 异步获取游戏位置
/// 路径应当包含游戏文件名称
/// </summary>
/// <returns>游戏位置</returns>
Task<ValueResult<bool, string>> LocateGamePathAsync();
ValueTask<ValueResult<bool, string>> LocateGamePathAsync();
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Locator;
internal interface IGameLocatorFactory
{
IGameLocator Create(GameLocationSource source);
}

View File

@@ -12,17 +12,14 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocator))]
[Injection(InjectAs.Transient)]
internal sealed partial class ManualGameLocator : IGameLocator
{
private readonly ITaskContext taskContext;
private readonly IPickerFactory pickerFactory;
/// <inheritdoc/>
public string Name { get => nameof(ManualGameLocator); }
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> LocateGamePathAsync()
public async ValueTask<ValueResult<bool, string>> LocateGamePathAsync()
{
await taskContext.SwitchToMainThreadAsync();

View File

@@ -13,16 +13,13 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocator))]
[Injection(InjectAs.Transient)]
internal sealed partial class RegistryLauncherLocator : IGameLocator
{
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public string Name { get => nameof(RegistryLauncherLocator); }
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> LocateGamePathAsync()
public async ValueTask<ValueResult<bool, string>> LocateGamePathAsync()
{
await taskContext.SwitchToBackgroundAsync();

View File

@@ -12,16 +12,13 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocator))]
[Injection(InjectAs.Transient)]
internal sealed partial class UnityLogGameLocator : IGameLocator
{
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public string Name { get => nameof(UnityLogGameLocator); }
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> LocateGamePathAsync()
public async ValueTask<ValueResult<bool, string>> LocateGamePathAsync()
{
await taskContext.SwitchToBackgroundAsync();
@@ -32,11 +29,11 @@ internal sealed partial class UnityLogGameLocator : IGameLocator
// Fallback to the CN server.
string logFilePathFinal = File.Exists(logFilePathOversea) ? logFilePathOversea : logFilePathChinese;
using (TempFile? tempFile = TempFile.CopyFrom(logFilePathFinal))
if (TempFile.CopyFrom(logFilePathFinal) is TempFile file)
{
if (tempFile != null)
using (file)
{
string content = await File.ReadAllTextAsync(tempFile.Path).ConfigureAwait(false);
string content = await File.ReadAllTextAsync(file.Path).ConfigureAwait(false);
Match matchResult = WarmupFileLine().Match(content);
if (!matchResult.Success)
@@ -44,17 +41,17 @@ internal sealed partial class UnityLogGameLocator : IGameLocator
return new(false, SH.ServiceGameLocatorUnityLogGamePathNotFound);
}
string entryName = matchResult.Groups[0].Value.Replace("_Data", ".exe");
string entryName = $"{matchResult.Value}.exe";
string fullPath = Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
return new(true, fullPath);
}
else
{
return new(false, SH.ServiceGameLocatorUnityLogFileNotFound);
}
}
else
{
return new(false, SH.ServiceGameLocatorUnityLogFileNotFound);
}
}
[GeneratedRegex(@"(?m).:/.+(GenshinImpact_Data|YuanShen_Data)")]
[GeneratedRegex(@".:/.+(?:GenshinImpact|YuanShen)(?=_Data)")]
private static partial Regex WarmupFileLine();
}

View File

@@ -19,7 +19,6 @@ namespace Snap.Hutao.Service.Game.Package;
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[Injection(InjectAs.Transient)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed partial class PackageConverter
{

View File

@@ -0,0 +1,71 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Options;
using Snap.Hutao.Core;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service.Metadata;
/// <summary>
/// 本地化名称
/// </summary>
internal static class LocaleNames
{
public const string DE = "DE"; // German
public const string EN = "EN"; // English
public const string ES = "ES"; // Spanish
public const string FR = "FR"; // French
public const string ID = "ID"; // Indonesian
public const string IT = "IT"; // Italian
public const string JP = "JP"; // Japanese
public const string KR = "KR"; // Korean
public const string PT = "PT"; // Portuguese
public const string RU = "RU"; // Russian
public const string TH = "TH"; // Thai
public const string TR = "TR"; // Turkish
public const string VI = "VI"; // Vietnamese
public const string CHS = "CHS"; // Chinese (Simplified)
public const string CHT = "CHT"; // Chinese (Traditional)
public static readonly ImmutableDictionary<string, string> LanguageNameLocaleNameMap = new Dictionary<string, string>()
{
["de"] = DE,
["en"] = EN,
["es"] = ES,
["fr"] = FR,
["id"] = ID,
["it"] = IT,
["ja"] = JP,
["ko"] = KR,
["pt"] = PT,
["ru"] = RU,
["th"] = TH,
["tr"] = TR,
["vi"] = VI,
["zh-Hans"] = CHS,
["zh-Hant"] = CHT,
[string.Empty] = CHS, // Fallback to Chinese.
}.ToImmutableDictionary();
public static readonly ImmutableDictionary<string, string> LocaleNameLanguageCodeMap = new Dictionary<string, string>()
{
[DE] = "de-de",
[EN] = "en-us",
[ES] = "es-es",
[FR] = "fr-fr",
[ID] = "id-id",
[IT] = "it-it",
[JP] = "ja-jp",
[KR] = "ko-kr",
[PT] = "pt-pt",
[RU] = "ru-ru",
[TH] = "th-th",
[TR] = "tr-tr",
[VI] = "vi-vn",
[CHS] = "zh-cn",
[CHT] = "zh-tw",
}.ToImmutableDictionary();
}

View File

@@ -16,8 +16,6 @@ namespace Snap.Hutao.Service.Metadata;
[Injection(InjectAs.Singleton)]
internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
{
private readonly AppOptions appOptions;
private readonly RuntimeOptions hutaoOptions;
@@ -132,65 +130,4 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
return Web.HutaoEndpoints.HutaoMetadata2File(LocaleName, fileNameWithExtension);
#endif
}
}
/// <summary>
/// 本地化名称
/// </summary>
internal static class LocaleNames
{
public const string DE = "DE"; // German
public const string EN = "EN"; // English
public const string ES = "ES"; // Spanish
public const string FR = "FR"; // French
public const string ID = "ID"; // Indonesian
public const string IT = "IT"; // Italian
public const string JP = "JP"; // Japanese
public const string KR = "KR"; // Korean
public const string PT = "PT"; // Portuguese
public const string RU = "RU"; // Russian
public const string TH = "TH"; // Thai
public const string TR = "TR"; // Turkish
public const string VI = "VI"; // Vietnamese
public const string CHS = "CHS"; // Chinese (Simplified)
public const string CHT = "CHT"; // Chinese (Traditional)
public static readonly ImmutableDictionary<string, string> LanguageNameLocaleNameMap = new Dictionary<string, string>()
{
["de"] = DE,
["en"] = EN,
["es"] = ES,
["fr"] = FR,
["id"] = ID,
["it"] = IT,
["ja"] = JP,
["ko"] = KR,
["pt"] = PT,
["ru"] = RU,
["th"] = TH,
["tr"] = TR,
["vi"] = VI,
["zh-Hans"] = CHS,
["zh-Hant"] = CHT,
[string.Empty] = CHS, // Fallback to Chinese.
}.ToImmutableDictionary();
public static readonly ImmutableDictionary<string, string> LocaleNameLanguageCodeMap = new Dictionary<string, string>()
{
[DE] = "de-de",
[EN] = "en-us",
[ES] = "es-es",
[FR] = "fr-fr",
[ID] = "id-id",
[IT] = "it-it",
[JP] = "ja-jp",
[KR] = "ko-kr",
[PT] = "pt-pt",
[RU] = "ru-ru",
[TH] = "th-th",
[TR] = "tr-tr",
[VI] = "vi-vn",
[CHS] = "zh-cn",
[CHT] = "zh-tw",
}.ToImmutableDictionary();
}

View File

@@ -3,6 +3,7 @@
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.ViewModel.User;
@@ -63,7 +64,8 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
private async Task RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule)
{
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await serviceProvider
.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea)
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(userAndUid.User.IsOversea)
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Message;
using Snap.Hutao.Model.Entity.Database;
@@ -220,7 +221,8 @@ internal sealed partial class UserService : IUserService
using (IServiceScope scope = serviceProvider.CreateScope())
{
Response<UidCookieToken> cookieTokenResponse = await scope.ServiceProvider
.PickRequiredService<IPassportClient>(user.Entity.IsOversea)
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.Entity.IsOversea)
.GetCookieAccountInfoBySTokenAsync(user.Entity)
.ConfigureAwait(false);

View File

@@ -14,6 +14,7 @@ namespace Snap.Hutao.View.Control;
/// 公告内容页面
/// </summary>
[HighQuality]
[DependencyProperty("Announcement", typeof(Announcement))]
internal sealed partial class AnnouncementContentViewer : Microsoft.UI.Xaml.Controls.UserControl
{
// apply in dark mode, Dark theme
@@ -42,8 +43,6 @@ internal sealed partial class AnnouncementContentViewer : Microsoft.UI.Xaml.Cont
}
""";
private static readonly DependencyProperty AnnouncementProperty = Property<AnnouncementContentViewer>.Depend<Announcement>(nameof(Announcement));
/// <summary>
/// 构造一个新的公告窗体
/// </summary>
@@ -52,15 +51,6 @@ internal sealed partial class AnnouncementContentViewer : Microsoft.UI.Xaml.Cont
InitializeComponent();
}
/// <summary>
/// 目标公告
/// </summary>
public Announcement Announcement
{
get => (Announcement)GetValue(AnnouncementProperty);
set => SetValue(AnnouncementProperty, value);
}
private static string? GenerateHtml(Announcement? announcement, ElementTheme theme)
{
if (announcement == null)

View File

@@ -11,11 +11,10 @@ namespace Snap.Hutao.View.Control;
/// <summary>
/// 基础数值滑动条
/// </summary>
[DependencyProperty("BaseValueInfo", typeof(BaseValueInfo))]
[DependencyProperty("IsPromoteVisible", typeof(bool), true)]
internal sealed partial class BaseValueSlider : UserControl
{
private static readonly DependencyProperty BaseValueInfoProperty = Property<BaseValueSlider>.Depend<BaseValueInfo>(nameof(BaseValueInfo));
private static readonly DependencyProperty IsPromoteVisibleProperty = Property<BaseValueSlider>.DependBoxed<bool>(nameof(IsPromoteVisible), BoxedValues.True);
/// <summary>
/// 构造一个新的基础数值滑动条
/// </summary>
@@ -23,22 +22,4 @@ internal sealed partial class BaseValueSlider : UserControl
{
InitializeComponent();
}
/// <summary>
/// 基础数值信息
/// </summary>
public BaseValueInfo BaseValueInfo
{
get => (BaseValueInfo)GetValue(BaseValueInfoProperty);
set => SetValue(BaseValueInfoProperty, value);
}
/// <summary>
/// 提升按钮是否可见
/// </summary>
public bool IsPromoteVisible
{
get => (bool)GetValue(IsPromoteVisibleProperty);
set => SetValue(IsPromoteVisibleProperty, value);
}
}

View File

@@ -14,12 +14,11 @@ namespace Snap.Hutao.View.Control;
/// </summary>
[HighQuality]
[ContentProperty(Name = nameof(TopContent))]
[DependencyProperty("Text", typeof(string), "", nameof(OnTextChanged))]
[DependencyProperty("TopContent", typeof(UIElement), default!, nameof(OnContentChanged))]
[DependencyProperty("Fill", typeof(Brush), default!, nameof(OnFillChanged))]
internal sealed partial class BottomTextControl : ContentControl
{
private static readonly DependencyProperty TextProperty = Property<BottomTextControl>.Depend(nameof(Text), string.Empty, OnTextChanged);
private static readonly DependencyProperty TopContentProperty = Property<BottomTextControl>.Depend<UIElement>(nameof(TopContent), default!, OnContentChanged);
private static readonly DependencyProperty FillProperty = Property<BottomTextControl>.Depend(nameof(Fill), default(Brush), OnFillChanged);
/// <summary>
/// 构造一个新的底部带有文本的控件
/// </summary>
@@ -28,33 +27,6 @@ internal sealed partial class BottomTextControl : ContentControl
InitializeComponent();
}
/// <summary>
/// 顶部内容
/// </summary>
public UIElement TopContent
{
get => (UIElement)GetValue(TopContentProperty);
set => SetValue(TopContentProperty, value);
}
/// <summary>
/// 文本
/// </summary>
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>
/// 填充
/// </summary>
public Brush Fill
{
get => (Brush)GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
private static void OnTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
((BottomTextControl)sender).TextHost.Text = (string)args.NewValue;

View File

@@ -12,14 +12,10 @@ namespace Snap.Hutao.View.Control;
/// 描述参数组合框
/// </summary>
[HighQuality]
[DependencyProperty("Source", typeof(List<LevelParameters<string, ParameterDescription>>), default!, nameof(OnSourceChanged))]
[DependencyProperty("PreferredSelectedIndex", typeof(int), 0)]
internal sealed partial class DescParamComboBox : UserControl
{
private static readonly DependencyProperty SourceProperty = Property<DescParamComboBox>
.Depend<List<LevelParameters<string, ParameterDescription>>>(nameof(Source), default!, OnSourceChanged);
private static readonly DependencyProperty PreferredSelectedIndexProperty = Property<DescParamComboBox>
.DependBoxed<int>(nameof(PreferredSelectedIndex), BoxedValues.Int32Zero);
/// <summary>
/// 构造一个新的描述参数组合框
/// </summary>
@@ -28,31 +24,13 @@ internal sealed partial class DescParamComboBox : UserControl
InitializeComponent();
}
/// <summary>
/// 技能列表
/// </summary>
public List<LevelParameters<string, ParameterDescription>> Source
{
get => (List<LevelParameters<string, ParameterDescription>>)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// 期望的选中索引
/// </summary>
public int PreferredSelectedIndex
{
get => (int)GetValue(PreferredSelectedIndexProperty);
set => SetValue(PreferredSelectedIndexProperty, value);
}
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
// Some of the {x:Bind} feature is not working properly,
// so we use this simple code behind approach to achieve selection function
if (sender is DescParamComboBox descParamComboBox)
{
if (args.NewValue != args.OldValue && args.NewValue is IList<LevelParameters<string, ParameterDescription>> list)
if (args.NewValue != args.OldValue && args.NewValue is List<LevelParameters<string, ParameterDescription>> list)
{
descParamComboBox.ItemHost.ItemsSource = list;
descParamComboBox.ItemHost.SelectedIndex = Math.Min(descParamComboBox.PreferredSelectedIndex, list.Count - 1);

View File

@@ -12,12 +12,11 @@ namespace Snap.Hutao.View.Control;
/// 物品图标
/// </summary>
[HighQuality]
[DependencyProperty("Quality", typeof(QualityType), QualityType.QUALITY_NONE)]
[DependencyProperty("Icon", typeof(Uri))]
[DependencyProperty("Badge", typeof(Uri))]
internal sealed partial class ItemIcon : UserControl
{
private static readonly DependencyProperty QualityProperty = Property<ItemIcon>.Depend(nameof(Quality), QualityType.QUALITY_NONE);
private static readonly DependencyProperty IconProperty = Property<ItemIcon>.Depend<Uri>(nameof(Icon));
private static readonly DependencyProperty BadgeProperty = Property<ItemIcon>.Depend<Uri>(nameof(Badge));
/// <summary>
/// 构造一个新的物品图标
/// </summary>
@@ -25,31 +24,4 @@ internal sealed partial class ItemIcon : UserControl
{
InitializeComponent();
}
/// <summary>
/// 等阶
/// </summary>
public QualityType Quality
{
get => (QualityType)GetValue(QualityProperty);
set => SetValue(QualityProperty, value);
}
/// <summary>
/// 图标
/// </summary>
public Uri Icon
{
get => (Uri)GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
/// <summary>
/// 角标
/// </summary>
public Uri Badge
{
get => (Uri)GetValue(BadgeProperty);
set => SetValue(BadgeProperty, value);
}
}

View File

@@ -12,12 +12,11 @@ namespace Snap.Hutao.View.Control;
/// 技能展柜
/// </summary>
[HighQuality]
[DependencyProperty("Skills", typeof(IList))]
[DependencyProperty("Selected", typeof(object))]
[DependencyProperty("ItemTemplate", typeof(DataTemplate))]
internal sealed partial class SkillPivot : UserControl
{
private static readonly DependencyProperty SkillsProperty = Property<SkillPivot>.Depend<IList>(nameof(Skills));
private static readonly DependencyProperty SelectedProperty = Property<SkillPivot>.Depend<object>(nameof(Selected));
private static readonly DependencyProperty ItemTemplateProperty = Property<SkillPivot>.Depend<DataTemplate>(nameof(ItemTemplate));
/// <summary>
/// 创建一个新的技能展柜
/// </summary>
@@ -25,31 +24,4 @@ internal sealed partial class SkillPivot : UserControl
{
InitializeComponent();
}
/// <summary>
/// 技能列表
/// </summary>
public IList Skills
{
get => (IList)GetValue(SkillsProperty);
set => SetValue(SkillsProperty, value);
}
/// <summary>
/// 选中的项
/// </summary>
public object Selected
{
get => GetValue(SelectedProperty);
set => SetValue(SelectedProperty, value);
}
/// <summary>
/// 项目模板
/// </summary>
public DataTemplate ItemTemplate
{
get => (DataTemplate)GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
}

View File

@@ -11,10 +11,9 @@ namespace Snap.Hutao.View.Control;
/// 统计卡片
/// </summary>
[HighQuality]
[DependencyProperty("ShowUpPull", typeof(bool), true)]
internal sealed partial class StatisticsCard : UserControl
{
private static readonly DependencyProperty ShowUpPullProperty = Property<StatisticsCard>.DependBoxed<bool>(nameof(ShowUpPull), BoxedValues.True);
/// <summary>
/// 构造一个新的统计卡片
/// </summary>
@@ -22,13 +21,4 @@ internal sealed partial class StatisticsCard : UserControl
{
InitializeComponent();
}
/// <summary>
/// 显示Up抽数
/// </summary>
public bool ShowUpPull
{
get => (bool)GetValue(ShowUpPullProperty);
set => SetValue(ShowUpPullProperty, value);
}
}

View File

@@ -13,53 +13,21 @@ namespace Snap.Hutao.View.Converter;
/// <summary>
/// Int32 转 色阶颜色
/// </summary>
internal sealed class Int32ToGradientColorConverter : DependencyObject, IValueConverter
[DependencyProperty("MaximumValue", typeof(int), 90)]
[DependencyProperty("MinimumValue", typeof(int), 1)]
[DependencyProperty("Maximum", typeof(Color))]
[DependencyProperty("Minimum", typeof(Color))]
internal sealed partial class Int32ToGradientColorConverter : DependencyValueConverter<int, Color>
{
private static readonly DependencyProperty MaximumProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(Maximum), StructMarshal.Color(0xFFFF4949));
private static readonly DependencyProperty MinimumProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(Minimum), StructMarshal.Color(0xFF48FF7A));
private static readonly DependencyProperty MaximumValueProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(MaximumValue), 90);
private static readonly DependencyProperty MinimumValueProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(MinimumValue), 1);
/// <summary>
/// 最小颜色
/// </summary>
public Color Minimum
public Int32ToGradientColorConverter()
{
get => (Color)GetValue(MinimumProperty);
set => SetValue(MinimumProperty, value);
Minimum = StructMarshal.Color(0xFFFF4949);
Maximum = StructMarshal.Color(0xFF48FF7A);
}
/// <summary>
/// 最大颜色
/// </summary>
public Color Maximum
public override Color Convert(int from)
{
get => (Color)GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);
}
/// <summary>
/// 最小值
/// </summary>
public int MinimumValue
{
get => (int)GetValue(MinimumValueProperty);
set => SetValue(MinimumValueProperty, value);
}
/// <summary>
/// 最大值
/// </summary>
public int MaximumValue
{
get => (int)GetValue(MaximumValueProperty);
set => SetValue(MaximumValueProperty, value);
}
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
double n = (value != null ? (int)value : MinimumValue) - MinimumValue;
double n = Math.Clamp(from, MinimumValue, MaximumValue) - MinimumValue;
int step = MaximumValue - MinimumValue;
double a = Minimum.A + ((Maximum.A - Minimum.A) * n / step);
double r = Minimum.R + ((Maximum.R - Minimum.R) * n / step);
@@ -73,10 +41,4 @@ internal sealed class Int32ToGradientColorConverter : DependencyObject, IValueCo
color.B = (byte)b;
return color;
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -9,29 +9,10 @@ namespace Snap.Hutao.View.Converter;
/// <summary>
/// 条件转换器
/// </summary>
internal sealed class PanelSelectorModeConverter : DependencyValueConverter<string, object>
[DependencyProperty("ListValue", typeof(object))]
[DependencyProperty("GridValue", typeof(object))]
internal sealed partial class PanelSelectorModeConverter : DependencyValueConverter<string, object>
{
private static readonly DependencyProperty ListValueProperty = Property<PanelSelectorModeConverter>.Depend<object>(nameof(ListValue));
private static readonly DependencyProperty GridValueProperty = Property<PanelSelectorModeConverter>.Depend<object>(nameof(GridValue));
/// <summary>
/// 列表值
/// </summary>
public object ListValue
{
get => GetValue(ListValueProperty);
set => SetValue(ListValueProperty, value);
}
/// <summary>
/// 网格值
/// </summary>
public object GridValue
{
get => GetValue(GridValueProperty);
set => SetValue(GridValueProperty, value);
}
/// <inheritdoc/>
public override object Convert(string from)
{

View File

@@ -13,10 +13,9 @@ namespace Snap.Hutao.View.Dialog;
/// 成就对话框
/// </summary>
[HighQuality]
[DependencyProperty("UIAF", typeof(UIAF))]
internal sealed partial class AchievementImportDialog : ContentDialog
{
private static readonly DependencyProperty UIAFProperty = Property<AchievementImportDialog>.Depend(nameof(UIAF), default(UIAF));
private readonly ITaskContext taskContext;
/// <summary>
@@ -33,15 +32,6 @@ internal sealed partial class AchievementImportDialog : ContentDialog
UIAF = uiaf;
}
/// <summary>
/// UIAF数据
/// </summary>
public UIAF UIAF
{
get => (UIAF)GetValue(UIAFProperty);
set => SetValue(UIAFProperty, value);
}
/// <summary>
/// 异步获取导入选项
/// </summary>

View File

@@ -13,11 +13,10 @@ namespace Snap.Hutao.View.Dialog;
/// 养成计算对话框
/// </summary>
[HighQuality]
[DependencyProperty("Avatar", typeof(ICalculableAvatar))]
[DependencyProperty("Weapon", typeof(ICalculableWeapon))]
internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
{
private static readonly DependencyProperty AvatarProperty = Property<CultivatePromotionDeltaDialog>.Depend<ICalculableAvatar?>(nameof(Avatar));
private static readonly DependencyProperty WeaponProperty = Property<CultivatePromotionDeltaDialog>.Depend<ICalculableWeapon?>(nameof(Weapon));
private readonly ITaskContext taskContext;
/// <summary>
@@ -37,24 +36,6 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
DataContext = this;
}
/// <summary>
/// 角色
/// </summary>
public ICalculableAvatar? Avatar
{
get => (ICalculableAvatar?)GetValue(AvatarProperty);
set => SetValue(AvatarProperty, value);
}
/// <summary>
/// 武器
/// </summary>
public ICalculableWeapon? Weapon
{
get => (ICalculableWeapon?)GetValue(WeaponProperty);
set => SetValue(WeaponProperty, value);
}
/// <summary>
/// 异步获取提升差异
/// </summary>

View File

@@ -12,10 +12,9 @@ namespace Snap.Hutao.View.Dialog;
/// 祈愿记录导入对话框
/// </summary>
[HighQuality]
[DependencyProperty("UIGF", typeof(UIGF))]
internal sealed partial class GachaLogImportDialog : ContentDialog
{
private static readonly DependencyProperty UIGFProperty = Property<AchievementImportDialog>.Depend(nameof(UIGF), default(UIGF));
private readonly ITaskContext taskContext;
/// <summary>
@@ -32,15 +31,6 @@ internal sealed partial class GachaLogImportDialog : ContentDialog
UIGF = uigf;
}
/// <summary>
/// UIAF数据
/// </summary>
public UIGF UIGF
{
get => (UIGF)GetValue(UIGFProperty);
set => SetValue(UIGFProperty, value);
}
/// <summary>
/// 异步获取导入选项
/// </summary>

View File

@@ -14,10 +14,9 @@ namespace Snap.Hutao.View.Dialog;
/// 祈愿记录刷新进度对话框
/// </summary>
[HighQuality]
[DependencyProperty("Status", typeof(GachaLogFetchStatus))]
internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog
{
private static readonly DependencyProperty StatusProperty = Property<GachaLogRefreshProgressDialog>.Depend<GachaLogFetchStatus>(nameof(Status));
/// <summary>
/// 构造一个新的对话框
/// </summary>
@@ -28,15 +27,6 @@ internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog
XamlRoot = serviceProvider.GetRequiredService<MainWindow>().Content.XamlRoot;
}
/// <summary>
/// 刷新状态
/// </summary>
public GachaLogFetchStatus Status
{
get => (GachaLogFetchStatus)GetValue(StatusProperty);
set => SetValue(StatusProperty, value);
}
/// <summary>
/// 接收进度更新
/// </summary>

View File

@@ -12,10 +12,9 @@ namespace Snap.Hutao.View.Dialog;
/// 启动游戏客户端转换对话框
/// </summary>
[HighQuality]
[DependencyProperty("State", typeof(PackageReplaceStatus))]
internal sealed partial class LaunchGamePackageConvertDialog : ContentDialog
{
private static readonly DependencyProperty StateProperty = Property<LaunchGamePackageConvertDialog>.Depend<PackageReplaceStatus>(nameof(State));
/// <summary>
/// 构造一个新的启动游戏客户端转换对话框
/// </summary>
@@ -27,13 +26,4 @@ internal sealed partial class LaunchGamePackageConvertDialog : ContentDialog
DataContext = this;
}
/// <summary>
/// 描述
/// </summary>
public PackageReplaceStatus State
{
get => (PackageReplaceStatus)GetValue(StateProperty);
set => SetValue(StateProperty, value);
}
}

View File

@@ -13,48 +13,8 @@ namespace Snap.Hutao.View.Helper;
/// </summary>
[HighQuality]
[SuppressMessage("", "SH001")]
public sealed class NavHelper
[DependencyProperty("NavigateTo", typeof(Type), IsAttached = true, AttachedType = typeof(NavigationViewItem))]
[DependencyProperty("ExtraData", typeof(object), IsAttached = true, AttachedType = typeof(NavigationViewItem))]
public sealed partial class NavHelper
{
private static readonly DependencyProperty NavigateToProperty = Property<NavHelper>.Attach<Type>("NavigateTo");
private static readonly DependencyProperty ExtraDataProperty = Property<NavHelper>.Attach<object>("ExtraData");
/// <summary>
/// 获取导航项的目标页面类型
/// </summary>
/// <param name="item">待获取的导航项</param>
/// <returns>目标页面类型</returns>
public static Type? GetNavigateTo(NavigationViewItem? item)
{
return item?.GetValue(NavigateToProperty) as Type;
}
/// <summary>
/// 设置导航项的目标页面类型
/// </summary>
/// <param name="item">待设置的导航项</param>
/// <param name="value">新的目标页面类型</param>
public static void SetNavigateTo(NavigationViewItem item, Type value)
{
item.SetValue(NavigateToProperty, value);
}
/// <summary>
/// 获取导航项的目标页面的额外数据
/// </summary>
/// <param name="item">待获取的导航项</param>
/// <returns>目标页面类型的额外数据</returns>
public static object? GetExtraData(NavigationViewItem? item)
{
return item?.GetValue(ExtraDataProperty);
}
/// <summary>
/// 设置导航项的目标页面类型
/// </summary>
/// <param name="item">待设置的导航项</param>
/// <param name="value">新的目标页面类型</param>
public static void SetExtraData(NavigationViewItem item, object value)
{
item.SetValue(ExtraDataProperty, value);
}
}
}

View File

@@ -14,9 +14,9 @@ namespace Snap.Hutao.View;
/// <summary>
/// 信息条视图
/// </summary>
[DependencyProperty("InfoBars", typeof(ObservableCollection<InfoBar>))]
internal sealed partial class InfoBarView : UserControl
{
private static readonly DependencyProperty InfoBarsProperty = Property<InfoBarView>.Depend<ObservableCollection<InfoBar>>(nameof(InfoBars));
private readonly IInfoBarService infoBarService;
/// <summary>
@@ -32,15 +32,6 @@ internal sealed partial class InfoBarView : UserControl
VisibilityButton.IsChecked = LocalSetting.Get(SettingKeys.IsInfoBarToggleChecked, true);
}
/// <summary>
/// 信息条
/// </summary>
public ObservableCollection<InfoBar> InfoBars
{
get => (ObservableCollection<InfoBar>)GetValue(InfoBarsProperty);
set => SetValue(InfoBarsProperty, value);
}
private void OnVisibilityButtonCheckedChanged(object sender, RoutedEventArgs e)
{
LocalSetting.Set(SettingKeys.IsInfoBarToggleChecked, ((ToggleButton)sender).IsChecked ?? false);

View File

@@ -71,7 +71,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Cookie stokenV1 = Cookie.FromSToken(loginTicketCookie[Cookie.LOGIN_UID], multiTokenMap[Cookie.STOKEN]);
Response<LoginResult> loginResultResponse = await Ioc.Default
.GetRequiredService<PassportClient>()
.GetRequiredService<PassportClient2>()
.LoginBySTokenAsync(stokenV1)
.ConfigureAwait(false);

View File

@@ -99,6 +99,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
{
if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(false))
{
ArgumentNullException.ThrowIfNull(gachaLogService.ArchiveCollection);
ObservableCollection<GachaArchive> archives = gachaLogService.ArchiveCollection;
await taskContext.SwitchToMainThreadAsync();
Archives = archives;

View File

@@ -1,28 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hutao.GachaLog;
namespace Snap.Hutao.ViewModel.GachaLog;
/// <summary>
/// 胡桃云Uid操作视图模型
/// 胡桃云记录操作视图模型
/// </summary>
internal sealed class HutaoCloudUidOperationViewModel
internal sealed class HutaoCloudEntryOperationViewModel
{
/// <summary>
/// 构造一个新的 胡桃云Uid操作视图模型
/// 构造一个新的胡桃云记录操作视图模型
/// </summary>
/// <param name="uid">Uid</param>
/// <param name="entry">记录</param>
/// <param name="retrieve">获取记录</param>
/// <param name="delete">删除记录</param>
public HutaoCloudUidOperationViewModel(string uid, ICommand retrieve, ICommand delete)
public HutaoCloudEntryOperationViewModel(GachaEntry entry, ICommand retrieve, ICommand delete)
{
Uid = uid;
Uid = entry.Uid;
ItemCount = entry.ItemCount;
RetrieveCommand = retrieve;
DeleteCommand = delete;
}
public string Uid { get; }
public int ItemCount { get; }
/// <summary>
/// 获取云端数据
/// </summary>

View File

@@ -32,7 +32,7 @@ internal sealed class HutaoCloudStatisticsViewModel : Abstraction.ViewModelSlim
{
ITaskContext taskContext = ServiceProvider.GetRequiredService<ITaskContext>();
await taskContext.SwitchToBackgroundAsync();
IHutaoCloudService hutaoCloudService = ServiceProvider.GetRequiredService<IHutaoCloudService>();
IGachaLogHutaoCloudService hutaoCloudService = ServiceProvider.GetRequiredService<IGachaLogHutaoCloudService>();
(bool isOk, HutaoStatistics statistics) = await hutaoCloudService.GetCurrentEventStatisticsAsync().ConfigureAwait(false);
if (isOk)
{

View File

@@ -9,6 +9,7 @@ using Snap.Hutao.Service.GachaLog;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Web.Hutao.GachaLog;
using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel;
@@ -22,18 +23,18 @@ namespace Snap.Hutao.ViewModel.GachaLog;
internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IHutaoCloudService hutaoCloudService;
private readonly IGachaLogHutaoCloudService hutaoCloudService;
private readonly IServiceProvider serviceProvider;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
private readonly HutaoUserOptions options;
private ObservableCollection<HutaoCloudUidOperationViewModel>? uidOperations;
private ObservableCollection<HutaoCloudEntryOperationViewModel>? uidOperations;
/// <summary>
/// Uid集合
/// </summary>
public ObservableCollection<HutaoCloudUidOperationViewModel>? UidOperations { get => uidOperations; set => SetProperty(ref uidOperations, value); }
public ObservableCollection<HutaoCloudEntryOperationViewModel>? UidOperations { get => uidOperations; set => SetProperty(ref uidOperations, value); }
/// <summary>
/// 选项
@@ -71,6 +72,12 @@ internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel
IsInitialized = true;
}
[Command("NavigateToAfdianSkuCommand")]
private static async Task NavigateToAfdianSkuAsync()
{
await Windows.System.Launcher.LaunchUriAsync("https://afdian.net/item/80d3b9decf9011edb5f452540025c377".ToUri());
}
[Command("UploadCommand")]
private async Task UploadAsync(GachaArchive? gachaArchive)
{
@@ -127,23 +134,17 @@ internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel
.Navigate<View.Page.SpiralAbyssRecordPage>(INavigationAwaiter.Default);
}
[Command("NavigateToAfdianSkuCommand")]
private async Task NavigateToAfdianSkuAsync()
{
await Windows.System.Launcher.LaunchUriAsync("https://afdian.net/item/80d3b9decf9011edb5f452540025c377".ToUri());
}
private async Task RefreshUidCollectionAsync()
{
if (Options.IsCloudServiceAllowed)
{
Response<List<string>> resp = await hutaoCloudService.GetUidsAsync().ConfigureAwait(false);
Response<List<GachaEntry>> resp = await hutaoCloudService.GetGachaEntriesAsync().ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
if (resp.IsOk())
{
UidOperations = resp.Data!
.SelectList(uid => new HutaoCloudUidOperationViewModel(uid, RetrieveCommand, DeleteCommand))
.SelectList(entry => new HutaoCloudEntryOperationViewModel(entry, RetrieveCommand, DeleteCommand))
.ToObservableCollection();
}
}

View File

@@ -109,7 +109,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
[Command("SetGamePathCommand")]
private async Task SetGamePathAsync()
{
IGameLocator locator = serviceProvider.GetRequiredService<IEnumerable<IGameLocator>>().Pick(nameof(ManualGameLocator));
IGameLocator locator = serviceProvider.GetRequiredService<IGameLocatorFactory>().Create(GameLocationSource.Manual);
(bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
if (isOk)

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Passport;
@@ -195,7 +196,8 @@ internal sealed class User : ObservableObject
}
Response<LTokenWrapper> lTokenResponse = await provider
.PickRequiredService<IPassportClient>(Entity.IsOversea)
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(Entity.IsOversea)
.GetLTokenBySTokenAsync(Entity, token)
.ConfigureAwait(false);
@@ -218,7 +220,8 @@ internal sealed class User : ObservableObject
}
Response<UidCookieToken> cookieTokenResponse = await provider
.PickRequiredService<IPassportClient>(Entity.IsOversea)
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(Entity.IsOversea)
.GetCookieAccountInfoBySTokenAsync(Entity, token)
.ConfigureAwait(false);
@@ -236,7 +239,8 @@ internal sealed class User : ObservableObject
private async Task<bool> TrySetUserInfoAsync(IServiceProvider provider, CancellationToken token)
{
Response<UserFullInfoWrapper> response = await provider
.PickRequiredService<IUserClient>(Entity.IsOversea)
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(Entity.IsOversea)
.GetUserFullInfoAsync(Entity, token)
.ConfigureAwait(false);

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
@@ -9,7 +8,7 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
/// <summary>
/// 用户信息客户端
/// </summary>
internal interface IUserClient : IOverseaSupport
internal interface IUserClient
{
/// <summary>
/// 获取当前用户详细信息
@@ -18,4 +17,4 @@ internal interface IUserClient : IOverseaSupport
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
Task<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default);
}
}

View File

@@ -16,19 +16,12 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
[UseDynamicSecret]
[ConstructorGenerated]
[HttpClient(HttpClientConfiguration.XRpc)]
[Injection(InjectAs.Transient, typeof(IUserClient))]
internal sealed partial class UserClient : IUserClient
{
private readonly JsonSerializerOptions options;
private readonly ILogger<UserClient> logger;
private readonly HttpClient httpClient;
/// <inheritdoc/>
public bool IsOversea
{
get => false;
}
/// <summary>
/// 获取当前用户详细信息
/// </summary>

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
[Injection(InjectAs.Transient)]
[ConstructorGenerated(CallBaseConstructor = true)]
internal sealed partial class UserClientFactory : OverseaSupportFactory<IUserClient, UserClient, UserClientOversea>
{
}

View File

@@ -15,19 +15,12 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
[UseDynamicSecret]
[ConstructorGenerated]
[HttpClient(HttpClientConfiguration.Default)]
[Injection(InjectAs.Transient, typeof(IUserClient))]
internal sealed partial class UserClientOversea : IUserClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<UserClientOversea> logger;
/// <inheritdoc/>
public bool IsOversea
{
get => true;
}
/// <summary>
/// 获取当前用户详细信息,使用 LToken
/// </summary>

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Response;
@@ -10,7 +9,7 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 通行证客户端
/// </summary>
internal interface IPassportClient : IOverseaSupport
internal interface IPassportClient
{
/// <summary>
/// 异步获取 CookieToken

View File

@@ -7,65 +7,55 @@ using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
using System.Net.Http.Json;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 通行证客户端
/// 通行证客户端 XRPC 版
/// </summary>
[HighQuality]
[UseDynamicSecret]
[ConstructorGenerated(ResolveHttpClient = true)]
[ConstructorGenerated]
[HttpClient(HttpClientConfiguration.XRpc2)]
internal sealed partial class PassportClient
internal sealed partial class PassportClient : IPassportClient
{
private readonly ILogger<PassportClient> logger;
private readonly ILogger<PassportClient2> logger;
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
/// <summary>
/// 异步验证 LToken
/// 异步获取 CookieToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>验证信息</returns>
[ApiInformation(Cookie = CookieType.LToken)]
public async Task<Response<UserInfoWrapper>> VerifyLtokenAsync(User user, CancellationToken token)
/// <returns>cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.PROD)]
public async Task<Response<UidCookieToken>> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default)
{
Response<UserInfoWrapper>? response = await httpClient
.SetUser(user, CookieType.LToken)
.TryCatchPostAsJsonAsync<Timestamp, Response<UserInfoWrapper>>(ApiEndpoints.AccountVerifyLtoken, new(), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(response);
}
/// <summary>
/// V1 SToken 登录
/// </summary>
/// <param name="stokenV1">v1 SToken</param>
/// <param name="token">取消令牌</param>
/// <returns>登录数据</returns>
[ApiInformation(Salt = SaltType.PROD)]
public async Task<Response<LoginResult>> LoginBySTokenAsync(Cookie stokenV1, CancellationToken token = default)
{
HttpResponseMessage message = await httpClient
.SetHeader("Cookie", stokenV1.ToString())
Response<UidCookieToken>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.PostAsync(ApiEndpoints.AccountGetSTokenByOldToken, null, token)
.ConfigureAwait(false);
Response<LoginResult>? resp = await message.Content
.ReadFromJsonAsync<Response<LoginResult>>(options, token)
.TryCatchGetFromJsonAsync<Response<UidCookieToken>>(ApiEndpoints.AccountGetCookieTokenBySToken, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
private class Timestamp
/// <summary>
/// 异步获取 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>uid 与 cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.PROD)]
public async Task<Response<LTokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
{
[JsonPropertyName("t")]
public long Time { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
Response<LTokenWrapper>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.TryCatchGetFromJsonAsync<Response<LTokenWrapper>>(ApiEndpoints.AccountGetLTokenBySToken, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -7,62 +7,65 @@ using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
using System.Net.Http.Json;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 通行证客户端 XRPC 版
/// 通行证客户端
/// </summary>
[HighQuality]
[UseDynamicSecret]
[ConstructorGenerated]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc2)]
[Injection(InjectAs.Transient, typeof(IPassportClient))]
internal sealed partial class PassportClient2 : IPassportClient
internal sealed partial class PassportClient2
{
private readonly ILogger<PassportClient> logger;
private readonly ILogger<PassportClient2> logger;
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
/// <inheritdoc/>
public bool IsOversea
{
get => false;
}
/// <summary>
/// 异步获取 CookieToken
/// 异步验证 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.PROD)]
public async Task<Response<UidCookieToken>> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default)
/// <returns>验证信息</returns>
[ApiInformation(Cookie = CookieType.LToken)]
public async Task<Response<UserInfoWrapper>> VerifyLtokenAsync(User user, CancellationToken token)
{
Response<UidCookieToken>? resp = await httpClient
.SetUser(user, CookieType.SToken)
Response<UserInfoWrapper>? response = await httpClient
.SetUser(user, CookieType.LToken)
.TryCatchPostAsJsonAsync<Timestamp, Response<UserInfoWrapper>>(ApiEndpoints.AccountVerifyLtoken, new(), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(response);
}
/// <summary>
/// V1 SToken 登录
/// </summary>
/// <param name="stokenV1">v1 SToken</param>
/// <param name="token">取消令牌</param>
/// <returns>登录数据</returns>
[ApiInformation(Salt = SaltType.PROD)]
public async Task<Response<LoginResult>> LoginBySTokenAsync(Cookie stokenV1, CancellationToken token = default)
{
HttpResponseMessage message = await httpClient
.SetHeader("Cookie", stokenV1.ToString())
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.TryCatchGetFromJsonAsync<Response<UidCookieToken>>(ApiEndpoints.AccountGetCookieTokenBySToken, options, logger, token)
.PostAsync(ApiEndpoints.AccountGetSTokenByOldToken, null, token)
.ConfigureAwait(false);
Response<LoginResult>? resp = await message.Content
.ReadFromJsonAsync<Response<LoginResult>>(options, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>uid 与 cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.PROD)]
public async Task<Response<LTokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
private class Timestamp
{
Response<LTokenWrapper>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.TryCatchGetFromJsonAsync<Response<LTokenWrapper>>(ApiEndpoints.AccountGetLTokenBySToken, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
[JsonPropertyName("t")]
public long Time { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
namespace Snap.Hutao.Web.Hoyolab.Passport;
[ConstructorGenerated(CallBaseConstructor = true)]
[Injection(InjectAs.Transient, typeof(IOverseaSupportFactory<IPassportClient>))]
internal sealed partial class PassportClientFactory : OverseaSupportFactory<IPassportClient, PassportClient, PassportClientOversea>
{
}

View File

@@ -14,19 +14,12 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
/// </summary>
[ConstructorGenerated]
[HttpClient(HttpClientConfiguration.XRpc3)]
[Injection(InjectAs.Transient, typeof(IPassportClient))]
internal sealed partial class PassportClientOversea : IPassportClient
{
private readonly ILogger<PassportClientOversea> logger;
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
/// <inheritdoc/>
public bool IsOversea
{
get => true;
}
/// <summary>
/// 异步获取 CookieToken
/// </summary>

View File

@@ -19,7 +19,6 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
[ConstructorGenerated]
[HttpClient(HttpClientConfiguration.XRpc)]
[PrimaryHttpMessageHandler(UseCookies = false)]
[Injection(InjectAs.Transient)]
internal sealed partial class GameRecordClient : IGameRecordClient
{
private readonly IServiceProvider serviceProvider;
@@ -27,12 +26,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
/// <inheritdoc/>
public bool IsOversea
{
get => false;
}
/// <summary>
/// 异步获取实时便笺
/// </summary>

View File

@@ -18,19 +18,12 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
[ConstructorGenerated]
[HttpClient(HttpClientConfiguration.XRpc3)]
[PrimaryHttpMessageHandler(UseCookies = false)]
[Injection(InjectAs.Transient)]
internal sealed partial class GameRecordClientOversea : IGameRecordClient
{
private readonly ILogger<GameRecordClient> logger;
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
/// <inheritdoc/>
public bool IsOversea
{
get => true;
}
/// <summary>
/// 异步获取实时便笺
/// </summary>

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 游戏记录提供器
/// </summary>
internal interface IGameRecordClient : IOverseaSupport
internal interface IGameRecordClient
{
/// <summary>
/// 获取玩家角色详细信息

View File

@@ -76,37 +76,6 @@ internal sealed class EndIds
}
}
/// <summary>
/// 异步创建一个新的 End Id集合
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="archive">存档</param>
/// <param name="token">取消令牌</param>
/// <returns>新的 End Id集合</returns>
public static async Task<EndIds> CreateAsync(AppDbContext appDbContext, GachaArchive? archive, CancellationToken token)
{
EndIds endIds = new();
foreach (GachaConfigType type in Service.GachaLog.GachaLog.QueryTypes)
{
if (archive != null)
{
Snap.Hutao.Model.Entity.GachaItem? item = await appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.Where(i => i.QueryType == type)
.OrderBy(i => i.Id)
.FirstOrDefaultAsync(token)
.ConfigureAwait(false);
if (item != null)
{
endIds[type] = item.Id;
}
}
}
return endIds;
}
/// <summary>
/// 获取枚举器
/// </summary>

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.GachaLog;
internal sealed class GachaEntry
{
/// <summary>
/// Uid
/// </summary>
public string Uid { get; set; } = default!;
/// <summary>
/// 物品个数
/// </summary>
public int ItemCount { get; set; }
}

View File

@@ -69,6 +69,7 @@ internal sealed class HomaGachaLogClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>Uid 列表</returns>
[Obsolete("Use GetGachaEntriesAsync instead")]
public async Task<Response<List<string>>> GetUidsAsync(CancellationToken token = default)
{
Response<List<string>>? resp = await httpClient
@@ -78,6 +79,20 @@ internal sealed class HomaGachaLogClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取 Uid 列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>Uid 列表</returns>
public async ValueTask<Response<List<GachaEntry>>> GetGachaEntriesAsync(CancellationToken token = default)
{
Response<List<GachaEntry>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<List<GachaEntry>>>(HutaoEndpoints.GachaLogUids, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取末尾 Id
/// </summary>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.ViewModel.User;
@@ -173,7 +174,9 @@ internal sealed partial class HomaSpiralAbyssClient
/// <returns>玩家记录</returns>
public async Task<SimpleRecord?> GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default)
{
IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea);
IGameRecordClient gameRecordClient = serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(userAndUid.User.IsOversea);
Response<PlayerInfo> playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)

View File

@@ -45,6 +45,11 @@ internal static class HutaoEndpoints
/// </summary>
public const string GachaLogUids = $"{HomaSnapGenshinApi}/GachaLog/Uids";
/// <summary>
/// 获取Uid列表
/// </summary>
public const string GachaLogEntries = $"{HomaSnapGenshinApi}/GachaLog/Entries";
/// <summary>
/// 删除祈愿记录
/// </summary>