mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
remove manual dependency property
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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<");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
/// 有名称的对象
|
||||
/// 指示该对象可通过名称区分
|
||||
/// </summary>
|
||||
[Obsolete("无意义的接口")]
|
||||
[HighQuality]
|
||||
internal interface INamedService
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
/// <summary>
|
||||
/// 海外服/HoYoLAB 可区分
|
||||
/// </summary>
|
||||
[Obsolete("Use IOverseaSupportFactory instead")]
|
||||
internal interface IOverseaSupport
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ internal sealed class FlyoutStateChangedMessage
|
||||
|
||||
public static FlyoutStateChangedMessage Close { get; } = new(false);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 是否为开启状态
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
205
src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs
Normal file
205
src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
71
src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs
Normal file
71
src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
/// <summary>
|
||||
/// 游戏记录提供器
|
||||
/// </summary>
|
||||
internal interface IGameRecordClient : IOverseaSupport
|
||||
internal interface IGameRecordClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取玩家角色详细信息
|
||||
|
||||
@@ -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>
|
||||
|
||||
17
src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/GachaEntry.cs
Normal file
17
src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/GachaEntry.cs
Normal 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; }
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user