Merge pull request #1103 from DGP-Studio/dotnet8

This commit is contained in:
DismissedLight
2023-11-16 14:24:57 +08:00
committed by GitHub
196 changed files with 973 additions and 1232 deletions

View File

@@ -108,7 +108,9 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_diagnostic.SA1629.severity = none
dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0005.severity = warning
dotnet_diagnostic.IDE0060.severity = none
dotnet_diagnostic.IDE0290.severity = none
# SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1208.severity = none
@@ -322,6 +324,8 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = true:suggestion
dotnet_diagnostic.SA1010.severity = none
[*.vb]
#### 命名样式 ####

View File

@@ -140,6 +140,8 @@ internal sealed class AttributeGenerator : IIncrementalGenerator
public InjectionAttribute(InjectAs injectAs, Type interfaceType)
{
}
public object Key { get; set; }
}
""";
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.Attributes.g.cs", coreDependencyInjectionAnnotations);

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Generic;
@@ -68,7 +69,8 @@ internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
string propertyName = (string)arguments[0].Value!;
string propertyType = arguments[1].Value!.ToString();
string defaultValue = GetLiteralString(arguments.ElementAtOrDefault(2)) ?? "default";
string defaultValue = arguments.ElementAtOrDefault(2).ToCSharpString() ?? "default";
defaultValue = defaultValue == "null" ? "default" : defaultValue;
string propertyChangedCallback = arguments.ElementAtOrDefault(3) is { IsNull: false } arg3 ? $", {arg3.Value}" : string.Empty;
string code;
@@ -125,25 +127,4 @@ internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
production.AddSource($"{normalizedClassName}.{propertyName}.g.cs", code);
}
}
private static string? GetLiteralString(TypedConstant typedConstant)
{
if (typedConstant.IsNull)
{
return default;
}
if (typedConstant.Value is bool boolValue)
{
return boolValue ? "true" : "false";
}
string result = typedConstant.Value!.ToString();
if (string.IsNullOrEmpty(result))
{
return default;
}
return result;
}
}

View File

@@ -60,7 +60,6 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Net.Http;
namespace Snap.Hutao.Core.DependencyInjection;
@@ -86,7 +85,7 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
private static void FillUpWithAddHttpClient(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
{
List<string> lines = new();
List<string> lines = [];
StringBuilder lineBuilder = new();
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))

View File

@@ -81,7 +81,7 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
private static void FillUpWithAddServices(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
{
List<string> lines = new();
List<string> lines = [];
StringBuilder lineBuilder = new();
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
@@ -92,17 +92,29 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
string injectAsName = arguments[0].ToCSharpString();
switch (injectAsName)
bool hasKey = injectionInfo.TryGetNamedArgumentValue("Key", out TypedConstant key);
switch (injectAsName, hasKey)
{
case InjectAsSingletonName:
case (InjectAsSingletonName, false):
lineBuilder.Append(" services.AddSingleton<");
break;
case InjectAsTransientName:
case (InjectAsSingletonName, true):
lineBuilder.Append(" services.AddKeyedSingleton<");
break;
case (InjectAsTransientName, false):
lineBuilder.Append(" services.AddTransient<");
break;
case InjectAsScopedName:
case (InjectAsTransientName, true):
lineBuilder.Append(" services.AddKeyedTransient<");
break;
case (InjectAsScopedName, false):
lineBuilder.Append(" services.AddScoped<");
break;
case (InjectAsScopedName, true):
lineBuilder.Append(" services.AddKeyedScoped<");
break;
default:
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, context.Context.Node.GetLocation(), injectAsName));
break;
@@ -113,7 +125,14 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
lineBuilder.Append($"{arguments[1].Value}, ");
}
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
if (hasKey)
{
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>({key.ToCSharpString()});");
}
else
{
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
}
lines.Add(lineBuilder.ToString());
}

View File

@@ -41,10 +41,10 @@ public static class JsonParser
public static T? FromJson<T>(this string json)
{
// Initialize, if needed, the ThreadStatic variables
propertyInfoCache ??= new Dictionary<Type, Dictionary<string, PropertyInfo>>();
fieldInfoCache ??= new Dictionary<Type, Dictionary<string, FieldInfo>>();
stringBuilder ??= new StringBuilder();
splitArrayPool ??= new Stack<List<string>>();
propertyInfoCache ??= [];
fieldInfoCache ??= [];
stringBuilder ??= new();
splitArrayPool ??= [];
// Remove all whitespace not within strings to make parsing simpler
stringBuilder.Length = 0;
@@ -99,7 +99,7 @@ public static class JsonParser
// Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
private static List<string> Split(string json)
{
List<string> splitArray = splitArrayPool!.Count > 0 ? splitArrayPool.Pop() : new List<string>();
List<string> splitArray = splitArrayPool!.Count > 0 ? splitArrayPool.Pop() : [];
splitArray.Clear();
if (json.Length == 2)
{

View File

@@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.Primitive;
@@ -13,4 +14,19 @@ internal static class AttributeDataExtension
{
return data.NamedArguments.Any(a => a.Key == key && predicate((TValue)a.Value.Value!));
}
public static bool TryGetNamedArgumentValue(this AttributeData data, string key, out TypedConstant value)
{
foreach (KeyValuePair<string, TypedConstant> pair in data.NamedArguments)
{
if (pair.Key == key)
{
value = pair.Value;
return true;
}
}
value = default;
return false;
}
}

View File

@@ -20,7 +20,7 @@ internal static class EnumerableExtension
if (enumerator.MoveNext())
{
HashSet<TKey> set = new();
HashSet<TKey> set = [];
do
{

View File

@@ -39,44 +39,44 @@ public sealed class ResxGenerator : IIncrementalGenerator
private static void Execute(SourceProductionContext context, AnalyzerConfigOptionsProvider options, string? assemblyName, bool supportNullableReferenceTypes, ImmutableArray<AdditionalText> files)
{
// Group additional file by resource kind ((a.resx, a.en.resx, a.en-us.resx), (b.resx, b.en-us.resx))
List<IGrouping<string, AdditionalText>> resxGroups = files
IOrderedEnumerable<IGrouping<string, AdditionalText>> group = files
.GroupBy(file => GetResourceName(file.Path), StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x.Key, StringComparer.Ordinal)
.ToList();
.OrderBy(x => x.Key, StringComparer.Ordinal);
List<IGrouping<string, AdditionalText>> resxGroups = [.. group];
foreach (IGrouping<string, AdditionalText>? resxGroug in resxGroups)
foreach (IGrouping<string, AdditionalText>? resxGroup in resxGroups)
{
string? rootNamespaceConfiguration = GetMetadataValue(context, options, "RootNamespace", resxGroug);
string? projectDirConfiguration = GetMetadataValue(context, options, "ProjectDir", resxGroug);
string? namespaceConfiguration = GetMetadataValue(context, options, "Namespace", "DefaultResourcesNamespace", resxGroug);
string? resourceNameConfiguration = GetMetadataValue(context, options, "ResourceName", globalName: null, resxGroug);
string? classNameConfiguration = GetMetadataValue(context, options, "ClassName", globalName: null, resxGroug);
string? rootNamespaceConfiguration = GetMetadataValue(context, options, "RootNamespace", resxGroup);
string? projectDirConfiguration = GetMetadataValue(context, options, "ProjectDir", resxGroup);
string? namespaceConfiguration = GetMetadataValue(context, options, "Namespace", "DefaultResourcesNamespace", resxGroup);
string? resourceNameConfiguration = GetMetadataValue(context, options, "ResourceName", globalName: null, resxGroup);
string? classNameConfiguration = GetMetadataValue(context, options, "ClassName", globalName: null, resxGroup);
string rootNamespace = rootNamespaceConfiguration ?? assemblyName ?? "";
string projectDir = projectDirConfiguration ?? assemblyName ?? "";
string? defaultResourceName = ComputeResourceName(rootNamespace, projectDir, resxGroug.Key);
string? defaultNamespace = ComputeNamespace(rootNamespace, projectDir, resxGroug.Key);
string? defaultResourceName = ComputeResourceName(rootNamespace, projectDir, resxGroup.Key);
string? defaultNamespace = ComputeNamespace(rootNamespace, projectDir, resxGroup.Key);
string? ns = namespaceConfiguration ?? defaultNamespace;
string? resourceName = resourceNameConfiguration ?? defaultResourceName;
string className = classNameConfiguration ?? ToCSharpNameIdentifier(Path.GetFileName(resxGroug.Key));
string className = classNameConfiguration ?? ToCSharpNameIdentifier(Path.GetFileName(resxGroup.Key));
if (ns == null)
{
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForNamespace, location: null, resxGroug.First().Path));
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForNamespace, location: null, resxGroup.First().Path));
}
if (resourceName == null)
{
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForResourceName, location: null, resxGroug.First().Path));
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForResourceName, location: null, resxGroup.First().Path));
}
List<ResxEntry>? entries = LoadResourceFiles(context, resxGroug);
List<ResxEntry>? entries = LoadResourceFiles(context, resxGroup);
string content = $"""
// Debug info:
// key: {resxGroug.Key}
// files: {string.Join(", ", resxGroug.Select(f => f.Path))}
// key: {resxGroup.Key}
// files: {string.Join(", ", resxGroup.Select(f => f.Path))}
// RootNamespace (metadata): {rootNamespaceConfiguration}
// ProjectDir (metadata): {projectDirConfiguration}
// Namespace / DefaultResourcesNamespace (metadata): {namespaceConfiguration}
@@ -97,7 +97,7 @@ public sealed class ResxGenerator : IIncrementalGenerator
content += GenerateCode(ns, className, resourceName, entries, supportNullableReferenceTypes);
}
context.AddSource($"{Path.GetFileName(resxGroug.Key)}.resx.g.cs", SourceText.From(content, Encoding.UTF8));
context.AddSource($"{Path.GetFileName(resxGroup.Key)}.resx.g.cs", SourceText.From(content, Encoding.UTF8));
}
}
@@ -285,7 +285,10 @@ public sealed class ResxGenerator : IIncrementalGenerator
if (!entry.IsFileRef)
{
summary.Add(new XElement("para", $"Value: \"{entry.Value}\"."));
foreach((string? each, string locale) in entry.Values.Zip(entry.Locales,(x,y)=>(x,y)))
{
summary.Add(new XElement("para", $"{GetStringWithPadding(locale, 8)} Value: \"{each}\""));
}
}
string comment = summary.ToString().Replace("\r\n", "\r\n /// ", StringComparison.Ordinal);
@@ -299,9 +302,9 @@ public sealed class ResxGenerator : IIncrementalGenerator
""");
if (entry.Value != null)
if (entry.Values.FirstOrDefault() is string value)
{
int args = Regex.Matches(entry.Value, "\\{(?<num>[0-9]+)(\\:[^}]*)?\\}", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant)
int args = Regex.Matches(value, "\\{(?<num>[0-9]+)(\\:[^}]*)?\\}", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant)
.Cast<Match>()
.Select(m => int.Parse(m.Groups["num"].Value, CultureInfo.InvariantCulture))
.Distinct()
@@ -360,6 +363,16 @@ public sealed class ResxGenerator : IIncrementalGenerator
return sb.ToString();
}
private static string GetStringWithPadding(string source, int length)
{
if (source.Length >= length)
{
return source;
}
return source + new string('_', length - source.Length);
}
private static string? ComputeResourceName(string rootNamespace, string projectDir, string resourcePath)
{
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
@@ -400,11 +413,11 @@ public sealed class ResxGenerator : IIncrementalGenerator
private static List<ResxEntry>? LoadResourceFiles(SourceProductionContext context, IGrouping<string, AdditionalText> resxGroug)
{
List<ResxEntry> entries = new();
List<ResxEntry> entries = [];
foreach (AdditionalText? entry in resxGroug.OrderBy(file => file.Path, StringComparer.Ordinal))
{
SourceText? content = entry.GetText(context.CancellationToken);
if (content == null)
if (content is null)
{
continue;
}
@@ -423,10 +436,12 @@ public sealed class ResxGenerator : IIncrementalGenerator
if (existingEntry != null)
{
existingEntry.Comment ??= comment;
existingEntry.Values.Add(value);
existingEntry.Locales.Add(GetLocaleName(entry.Path));
}
else
{
entries.Add(new ResxEntry { Name = name, Value = value, Comment = comment, Type = type });
entries.Add(new() { Name = name, Values = [value], Locales = [GetLocaleName(entry.Path)], Comment = comment, Type = type });
}
}
}
@@ -532,15 +547,28 @@ public sealed class ResxGenerator : IIncrementalGenerator
return pathWithoutExtension;
}
return Regex.IsMatch(pathWithoutExtension.Substring(indexOf + 1), "^[a-zA-Z]{2}(-[a-zA-Z]{2})?$", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1))
return Regex.IsMatch(pathWithoutExtension.Substring(indexOf + 1), "^[a-zA-Z]{2}(-[a-zA-Z]{2,4})?$", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1))
? pathWithoutExtension.Substring(0, indexOf)
: pathWithoutExtension;
}
private static string GetLocaleName(string path)
{
string fileName = Path.GetFileNameWithoutExtension(path);
int indexOf = fileName.LastIndexOf('.');
if (indexOf < 0)
{
return "Neutral";
}
return fileName.Substring(indexOf + 1);
}
private sealed class ResxEntry
{
public string? Name { get; set; }
public string? Value { get; set; }
public List<string?> Values { get; set; } = default!;
public List<string> Locales { get; set; } = default!;
public string? Comment { get; set; }
public string? Type { get; set; }
@@ -553,9 +581,9 @@ public sealed class ResxGenerator : IIncrementalGenerator
return true;
}
if (Value != null)
if (Values.FirstOrDefault() is string value)
{
string[] parts = Value.Split(';');
string[] parts = value.Split(';');
if (parts.Length > 1)
{
string type = parts[1];
@@ -579,9 +607,9 @@ public sealed class ResxGenerator : IIncrementalGenerator
return "string";
}
if (Value != null)
if (Values.FirstOrDefault() is string value)
{
string[] parts = Value.Split(';');
string[] parts = value.Split(';');
if (parts.Length > 1)
{
string type = parts[1];

View File

@@ -8,6 +8,7 @@
<Platforms>x64</Platforms>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Configurations>Debug;Release</Configurations>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

View File

@@ -57,23 +57,13 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
private static void CompilationStart(CompilationStartAnalysisContext context)
{
SyntaxKind[] types =
{
SyntaxKind.ClassDeclaration,
SyntaxKind.InterfaceDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.EnumDeclaration,
};
SyntaxKind[] types = [SyntaxKind.ClassDeclaration, SyntaxKind.InterfaceDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.EnumDeclaration,];
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
SyntaxKind[] expressions =
{
SyntaxKind.EqualsExpression,
SyntaxKind.NotEqualsExpression,
};
SyntaxKind[] expressions = [SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression,];
context.RegisterSyntaxNodeAction(HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching, expressions);
context.RegisterSyntaxNodeAction(HandleIsPatternShouldUseRecursivePattern, SyntaxKind.IsPatternExpression);
context.RegisterSyntaxNodeAction(HandleArgumentNullExceptionThrowIfNull, SyntaxKind.SuppressNullableWarningExpression);

View File

@@ -7,6 +7,15 @@ namespace Snap.Hutao.Test;
[TestClass]
public class JsonSerializeTest
{
private TestContext? testContext;
public TestContext? TestContext { get => testContext; set => testContext = value; }
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
{
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
private const string SmapleObjectJson = """
{
"A" :1
@@ -44,13 +53,29 @@ public class JsonSerializeTest
[TestMethod]
public void NumberStringKeyCanSerializeAsKey()
{
JsonSerializerOptions options = new()
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
Assert.AreEqual(sample[111], "12");
}
[TestMethod]
public void ByteArraySerializeAsBase64()
{
byte[] array =
#if NET8_0_OR_GREATER
[1, 2, 3, 4, 5];
#else
{ 1, 2, 3, 4, 5 };
#endif
ByteArraySample sample = new()
{
NumberHandling = JsonNumberHandling.AllowReadingFromString,
Array = array,
};
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, options)!;
Assert.AreEqual(sample[111], "12");
string result = JsonSerializer.Serialize(sample);
TestContext!.WriteLine($"ByteArray Serialize Result: {result}");
Assert.AreEqual(result, """
{"Array":"AQIDBAU="}
""");
}
private sealed class Sample
@@ -64,4 +89,9 @@ public class JsonSerializeTest
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public int A { get; set; }
}
private sealed class ByteArraySample
{
public byte[]? Array { get; set; }
}
}

View File

@@ -9,11 +9,12 @@ public sealed class ForEachRuntimeBehaviorTest
[TestMethod]
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
{
List<string> strings = new()
{
"a", "b", "c"
};
List<string> strings =
#if NET8_0_OR_GREATER
["a", "b", "c"];
#else
new() { "a", "b", "c" };
#endif
int count = 0;
foreach (ReadOnlySpan<char> chars in strings)
{

View File

@@ -8,8 +8,13 @@ public sealed class RangeRuntimeBehaviorTest
[TestMethod]
public void RangeTrimLastOne()
{
#if NET8_0_OR_GREATER
int[] array = [1, 2, 3, 4];
int[] test = [1, 2, 3];
#else
int[] array = { 1, 2, 3, 4 };
int[] test = { 1, 2, 3 };
#endif
int[] result = array[..^1];
Assert.AreEqual(3, result.Length);
Assert.IsTrue(MemoryExtensions.SequenceEqual<int>(test, result));

View File

@@ -6,7 +6,12 @@ public sealed class UnsafeRuntimeBehaviorTest
[TestMethod]
public unsafe void UInt32AllSetIs()
{
byte[] bytes = { 0xFF, 0xFF, 0xFF, 0xFF, };
byte[] bytes =
#if NET8_0_OR_GREATER
[0xFF, 0xFF, 0xFF, 0xFF];
#else
{ 0xFF, 0xFF, 0xFF, 0xFF, };
#endif
fixed (byte* pBytes = bytes)
{

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@@ -3,12 +3,18 @@
namespace Snap.Hutao.Control.Animation;
/// <summary>
/// 动画时长
/// </summary>
[HighQuality]
internal static class AnimationDurations
internal static class ControlAnimationConstants
{
/// <summary>
/// 1
/// </summary>
public const string One = "1";
/// <summary>
/// 1.1
/// </summary>
public const string OnePointOne = "1.1";
/// <summary>
/// 图片缩放动画
/// </summary>

View File

@@ -19,10 +19,10 @@ internal sealed class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
/// </summary>
public ImageZoomInAnimation()
{
Duration = AnimationDurations.ImageZoom;
Duration = ControlAnimationConstants.ImageZoom;
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
To = Core.StringLiterals.OnePointOne;
To = ControlAnimationConstants.OnePointOne;
}
/// <inheritdoc/>

View File

@@ -19,10 +19,10 @@ internal sealed class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
/// </summary>
public ImageZoomOutAnimation()
{
Duration = AnimationDurations.ImageZoom;
Duration = ControlAnimationConstants.ImageZoom;
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
To = Core.StringLiterals.One;
To = ControlAnimationConstants.One;
}
/// <inheritdoc/>

View File

@@ -1,16 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using System.Diagnostics;
namespace Snap.Hutao.Web.Bridge;
namespace Snap.Hutao.Control.Extension;
/// <summary>
/// Bridge 拓展
/// </summary>
[HighQuality]
internal static class CoreWebView2Extension
internal static class WebView2Extension
{
[Conditional("RELEASE")]
public static void DisableDevToolsForReleaseBuild(this CoreWebView2 webView)
@@ -37,4 +38,9 @@ internal static class CoreWebView2Extension
manager.DeleteCookie(item);
}
}
public static bool IsDisposed(this WebView2 webView2)
{
return WinRTExtension.IsDisposed(webView2);
}
}

View File

@@ -110,7 +110,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
if (exception is HttpRequestException httpRequestException)
{
infoBarService.Error(httpRequestException, SH.ControlImageCompositionImageHttpRequest.Format(uri));
infoBarService.Error(httpRequestException, SH.FormatControlImageCompositionImageHttpRequest(uri));
}
else
{
@@ -196,7 +196,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
await AnimationBuilder
.Create()
.Opacity(from: 0D, to: 1D, duration: AnimationDurations.ImageFadeIn)
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageFadeIn)
.StartAsync(this, token)
.ConfigureAwait(true);
}
@@ -217,7 +217,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
await AnimationBuilder
.Create()
.Opacity(from: 1D, to: 0D, duration: AnimationDurations.ImageFadeOut)
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageFadeOut)
.StartAsync(this, token)
.ConfigureAwait(true);
}

View File

@@ -27,9 +27,9 @@ internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTr
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
{
List<ItemCollectionTransition> addTransitions = new();
List<ItemCollectionTransition> removeTransitions = new();
List<ItemCollectionTransition> moveTransitions = new();
List<ItemCollectionTransition> addTransitions = [];
List<ItemCollectionTransition> removeTransitions = [];
List<ItemCollectionTransition> moveTransitions = [];
foreach (ItemCollectionTransition transition in addTransitions)
{

View File

@@ -118,7 +118,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
Span<double> columnHeights = new double[numberOfColumns];
Span<int> itemsPerColumn = new int[numberOfColumns];
HashSet<int> deadColumns = new();
HashSet<int> deadColumns = [];
for (int i = 0; i < context.ItemCount; i++)
{
@@ -131,7 +131,9 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
// The first element must be force refreshed otherwise
// it will use the old one realized
ElementRealizationOptions options = i == 0 ? ElementRealizationOptions.ForceCreate : ElementRealizationOptions.None;
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Now we need to refresh the first element of each column
ElementRealizationOptions options = i < numberOfColumns ? ElementRealizationOptions.ForceCreate : ElementRealizationOptions.None;
// Item has not been measured yet. Get the element and store the values
UIElement element = context.GetOrCreateElementAt(i, options);

View File

@@ -9,9 +9,9 @@ namespace Snap.Hutao.Control.Layout;
internal sealed class UniformStaggeredLayoutState
{
private readonly List<UniformStaggeredItem> items = new();
private readonly List<UniformStaggeredItem> items = [];
private readonly VirtualizingLayoutContext context;
private readonly Dictionary<int, UniformStaggeredColumnLayout> columnLayout = new();
private readonly Dictionary<int, UniformStaggeredColumnLayout> columnLayout = [];
private double lastAverageHeight;
public UniformStaggeredLayoutState(VirtualizingLayoutContext context)
@@ -32,7 +32,7 @@ internal sealed class UniformStaggeredLayoutState
{
if (!this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout))
{
columnLayout = new();
columnLayout = [];
this.columnLayout[columnIndex] = columnLayout;
}

View File

@@ -11,7 +11,6 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
{
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));
[SuppressMessage("", "IDE0052")]
private FrameworkElement? presenter;
public Loading()

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.IO;
using System.Net;
using System.Net.Http;
@@ -24,17 +25,16 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
// TODO: use FrozenDictionary
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
private static readonly FrozenDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
[2] = TimeSpan.FromSeconds(64),
};
}.ToFrozenDictionary();
private readonly ILogger logger;
private readonly IHttpClientFactory httpClientFactory;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<ImageCache> logger;
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
@@ -62,7 +62,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
/// <inheritdoc/>
public void Remove(Uri uriForCachedItem)
{
Remove(new ReadOnlySpan<Uri>(uriForCachedItem));
Remove(new ReadOnlySpan<Uri>(ref uriForCachedItem));
}
/// <inheritdoc/>
@@ -76,7 +76,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
string folder = GetCacheFolder();
string[] files = Directory.GetFiles(folder);
List<string> filesToDelete = new();
List<string> filesToDelete = [];
foreach (ref readonly Uri uri in uriForCachedItems)
{
string filePath = Path.Combine(folder, GetCacheFileName(uri));

View File

@@ -12,7 +12,8 @@ namespace Snap.Hutao.Core;
internal sealed class CommandLineBuilder
{
private const char WhiteSpace = ' ';
private readonly Dictionary<string, string?> options = new();
private readonly Dictionary<string, string?> options = [];
/// <summary>
/// 当符合条件时添加参数

View File

@@ -38,7 +38,11 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
return;
}
// TODO: Troubeshooting why the serviceProvider will NRE
if (serviceProvider.IsDisposedSlow())
{
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
@@ -92,7 +96,11 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
return;
}
// TODO: Troubeshooting why the serviceProvider will NRE
if (serviceProvider.IsDisposedSlow())
{
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();

View File

@@ -3,6 +3,12 @@
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
/// <summary>
/// 由于 AddHttpClient 不支持 KeyedService, 所以使用工厂模式
/// </summary>
/// <typeparam name="TClient">抽象类型</typeparam>
/// <typeparam name="TClientCN">官服/米游社类型</typeparam>
/// <typeparam name="TClientOS">国际/HoYoLAB类型</typeparam>
internal abstract class OverseaSupportFactory<TClient, TClientCN, TClientOS> : IOverseaSupportFactory<TClient>
where TClientCN : notnull, TClient
where TClientOS : notnull, TClient

View File

@@ -35,7 +35,6 @@ internal static class DependencyInjection
// Discrete services
.AddSingleton<IMessenger, WeakReferenceMessenger>()
.BuildServiceProvider(true);
Ioc.Default.ConfigureServices(serviceProvider);

View File

@@ -1,42 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 服务集合扩展
/// </summary>
internal static class EnumerableServiceExtension
{
/// <summary>
/// 选择对应的服务
/// </summary>
/// <typeparam name="TService">服务类型</typeparam>
/// <param name="services">服务集合</param>
/// <param name="isOversea">是否为海外服/Hoyolab</param>
/// <returns>对应的服务</returns>
[Obsolete("该方法会导致不必要的服务实例化")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TService Pick<TService>(this IEnumerable<TService> services, bool isOversea)
where TService : IOverseaSupport
{
return services.Single(s => s.IsOversea == isOversea);
}
/// <summary>
/// 选择对应的服务
/// </summary>
/// <typeparam name="TService">服务类型</typeparam>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="isOversea">是否为海外服/Hoyolab</param>
/// <returns>对应的服务</returns>
[Obsolete("该方法会导致不必要的服务实例化")]
public static TService PickRequiredService<TService>(this IServiceProvider serviceProvider, bool isOversea)
where TService : IOverseaSupport
{
return serviceProvider.GetRequiredService<IEnumerable<TService>>().Pick(isOversea);
}
}

View File

@@ -16,4 +16,15 @@ internal static class ServiceProviderExtension
{
return ActivatorUtilities.CreateInstance<T>(serviceProvider, parameters);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDisposedSlow(this IServiceProvider? serviceProvider)
{
if (serviceProvider is null)
{
return true;
}
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
}
}

View File

@@ -15,7 +15,7 @@ internal sealed class DatabaseCorruptedException : Exception
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public DatabaseCorruptedException(string message, Exception? innerException)
: base(SH.CoreExceptionServiceDatabaseCorruptedMessage.Format($"{message}\n{innerException?.Message}"), innerException)
: base(SH.FormatCoreExceptionServiceDatabaseCorruptedMessage($"{message}\n{innerException?.Message}"), innerException)
{
}
}

View File

@@ -15,7 +15,7 @@ internal sealed class UserdataCorruptedException : Exception
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public UserdataCorruptedException(string message, Exception? innerException)
: base(SH.CoreExceptionServiceUserdataCorruptedMessage.Format($"{message}\n{innerException?.Message}"), innerException)
: base(SH.FormatCoreExceptionServiceUserdataCorruptedMessage($"{message}\n{innerException?.Message}"), innerException)
{
}
}

View File

@@ -6,12 +6,9 @@ using Windows.Storage.Streams;
namespace Snap.Hutao.Core.IO.DataTransfer;
/// <summary>
/// 剪贴板互操作
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IClipboardInterop))]
internal sealed partial class ClipboardInterop : IClipboardInterop
[Injection(InjectAs.Transient, typeof(IClipboardProvider))]
internal sealed partial class ClipboardProvider : IClipboardProvider
{
private readonly JsonSerializerOptions options;
private readonly ITaskContext taskContext;

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.IO.DataTransfer;
/// <summary>
/// 剪贴板互操作
/// </summary>
internal interface IClipboardInterop
internal interface IClipboardProvider
{
/// <summary>
/// 从剪贴板文本中反序列化

View File

@@ -18,7 +18,7 @@ internal static class IniSerializer
/// <returns>Ini 元素集合</returns>
public static List<IniElement> Deserialize(FileStream fileStream)
{
List<IniElement> results = new();
List<IniElement> results = [];
using (StreamReader reader = new(fileStream))
{
while (reader.ReadLine() is { } line)

View File

@@ -98,7 +98,7 @@ internal static class PickerExtension
.GetRequiredService<IInfoBarService>()
.Warning(
SH.CoreIOPickerExtensionPickerExceptionInfoBarTitle,
SH.CoreIOPickerExtensionPickerExceptionInfoBarMessage.Format(exception.Message));
SH.FormatCoreIOPickerExtensionPickerExceptionInfoBarMessage(exception.Message));
}
}
}

View File

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

View File

@@ -37,10 +37,9 @@ internal static class AppInstanceExtension
SetEvent(redirectEventHandle);
});
ReadOnlySpan<HANDLE> handles = new(redirectEventHandle);
ReadOnlySpan<HANDLE> handles = new(ref redirectEventHandle);
CoWaitForMultipleObjects((uint)CWMO_FLAGS.CWMO_DEFAULT, INFINITE, handles, out uint _);
// TODO: Release handle
CloseHandle(redirectEventHandle);
}
[SuppressMessage("", "SH007")]

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core;
internal static class Random
{
public static string GetLowerHexString(int length)
{
return new(System.Random.Shared.GetItems("0123456789abcdef".AsSpan(), length));
}
public static string GetUpperAndNumberString(int length)
{
return new(System.Random.Shared.GetItems("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".AsSpan(), length));
}
public static string GetLowerAndNumberString(int length)
{
return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyz".AsSpan(), length));
}
}

View File

@@ -11,26 +11,6 @@ namespace Snap.Hutao.Core;
[HighQuality]
internal static class StringLiterals
{
/// <summary>
/// 1
/// </summary>
public const string One = "1";
/// <summary>
/// 1.1
/// </summary>
public const string OnePointOne = "1.1";
/// <summary>
/// True
/// </summary>
public const string True = "True";
/// <summary>
/// False
/// </summary>
public const string False = "False";
/// <summary>
/// CRLF 换行符
/// </summary>

View File

@@ -45,9 +45,7 @@ internal static class DispatcherQueueExtension
});
blockEvent.Wait();
#pragma warning disable CA1508
exceptionDispatchInfo?.Throw();
#pragma warning restore CA1508
}
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
namespace Snap.Hutao.Core;
internal struct UnsafeDateTimeOffset
{

View File

@@ -8,6 +8,7 @@ using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing.HotKey;
[SuppressMessage("", "CA1001")]
[Injection(InjectAs.Singleton, typeof(IHotKeyController))]
[ConstructorGenerated]
internal sealed partial class HotKeyController : IHotKeyController
{
@@ -53,10 +54,10 @@ internal sealed partial class HotKeyController : IHotKeyController
while (!token.IsCancellationRequested)
{
INPUT[] inputs =
{
[
CreateInputForMouseEvent(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN),
CreateInputForMouseEvent(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP),
};
];
if (SendInput(inputs.AsSpan(), sizeof(INPUT)) is 0)
{
@@ -68,7 +69,7 @@ internal sealed partial class HotKeyController : IHotKeyController
return;
}
Thread.Sleep(Random.Shared.Next(100, 150));
Thread.Sleep(System.Random.Shared.Next(100, 150));
}
}

View File

@@ -55,7 +55,7 @@ internal sealed class WindowController
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
window.AppWindow.Title = SH.AppNameAndVersion.Format(hutaoOptions.Version);
window.AppWindow.Title = SH.FormatAppNameAndVersion(hutaoOptions.Version);
window.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico"));
ExtendsContentIntoTitleBar();
@@ -204,6 +204,6 @@ internal sealed class WindowController
// 48 is the navigation button leftInset
RectInt32 dragRect = StructMarshal.RectInt32(48, 0, options.TitleBar.ActualSize).Scale(scale);
appTitleBar.SetDragRectangles(dragRect.ToArray());
appTitleBar.SetDragRectangles([dragRect]);
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Windowing;
internal static class WindowExtension
{
private static readonly ConditionalWeakTable<Window, WindowController> WindowControllers = new();
private static readonly ConditionalWeakTable<Window, WindowController> WindowControllers = [];
public static void InitializeController<TWindow>(this TWindow window, IServiceProvider serviceProvider)
where TWindow : Window, IWindowOptionsSource

View File

@@ -33,7 +33,8 @@ internal sealed class WindowSubclass : IDisposable
this.window = window;
this.options = options;
this.serviceProvider = serviceProvider;
hotKeyController = new HotKeyController(serviceProvider);
hotKeyController = serviceProvider.GetRequiredService<IHotKeyController>();
}
/// <summary>

View File

@@ -80,7 +80,7 @@ internal static partial class EnumerableExtension
public static Dictionary<TKey, TSource> ToDictionaryIgnoringDuplicateKeys<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull
{
Dictionary<TKey, TSource> dictionary = new();
Dictionary<TKey, TSource> dictionary = [];
foreach (TSource value in source)
{
@@ -94,7 +94,7 @@ internal static partial class EnumerableExtension
public static Dictionary<TKey, TValue> ToDictionaryIgnoringDuplicateKeys<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> elementSelector)
where TKey : notnull
{
Dictionary<TKey, TValue> dictionary = new();
Dictionary<TKey, TValue> dictionary = [];
foreach (TSource value in source)
{

View File

@@ -69,7 +69,7 @@ internal static partial class EnumerableExtension
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static List<TSource> EmptyIfNull<TSource>(this List<TSource>? source)
{
return source ?? new();
return source ?? [];
}
public static List<T> GetRange<T>(this List<T> list, in Range range)

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Specialized;
namespace Snap.Hutao.Extension;
internal static partial class EnumerableExtension
{
public static bool TryGetValue(this NameValueCollection collection, string name, [NotNullWhen(true)] out string? value)
{
if (collection.AllKeys.Contains(name))
{
if (collection.GetValues(name) is [string single])
{
value = single;
return true;
}
}
value = string.Empty;
return false;
}
}

View File

@@ -30,20 +30,6 @@ internal static partial class EnumerableExtension
return source ?? Enumerable.Empty<TSource>();
}
/// <summary>
/// 将源转换为仅包含单个元素的枚举
/// </summary>
/// <typeparam name="TSource">源的类型</typeparam>
/// <param name="source">源</param>
/// <returns>集合</returns>
#if NET8_0
[Obsolete("Use C# 12 Collection Literal instead")]
#endif
public static IEnumerable<TSource> Enumerate<TSource>(this TSource source)
{
yield return source;
}
/// <summary>
/// 寻找枚举中唯一的值,找不到时
/// 回退到首个或默认值

View File

@@ -1,29 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Extension;
/// <summary>
/// 对象拓展
/// </summary>
internal static class ObjectExtension
{
/// <summary>
/// 转换到只有1长度的数组
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="source">源</param>
/// <returns>数组</returns>
#if NET8_0
[Obsolete("Use C# 12 Collection Literals")]
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T[] ToArray<T>(this T source)
{
// TODO: use C# 12 collection literals
// [ source ]
return new[] { source };
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Globalization;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Extension;
@@ -28,22 +27,4 @@ internal static class StringExtension
{
return source.AsSpan().TrimEnd(value).ToString();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Format(this string value, object? arg)
{
return string.Format(CultureInfo.CurrentCulture, value, arg);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Format(this string value, object? arg0, object? arg1)
{
return string.Format(CultureInfo.CurrentCulture, value, arg0, arg1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Format(this string value, object? arg0, object? arg1, object? arg2)
{
return string.Format(CultureInfo.CurrentCulture, value, arg0, arg1, arg2);
}
}

View File

@@ -67,4 +67,4 @@ internal static class StructExtension
{
return size.Width * size.Height;
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using WinRT;
namespace Snap.Hutao.Extension;
internal static class WinRTExtension
{
public static bool IsDisposed(this IWinRTObject obj)
{
return GetDisposed(obj.NativeObject);
}
// protected bool disposed;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name ="disposed")]
private static extern ref bool GetDisposed(IObjectReference objRef);
}

View File

@@ -64,7 +64,7 @@ internal sealed class DailyNoteEntry : ObservableObject, IMappingFrom<DailyNoteE
{
return RefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ModelEntityDailyNoteNotRefreshed
: SH.ModelEntityDailyNoteRefreshTimeFormat.Format(RefreshTime);
: SH.FormatModelEntityDailyNoteRefreshTimeFormat(RefreshTime.ToLocalTime());
}
}
@@ -143,7 +143,7 @@ internal sealed class DailyNoteEntry : ObservableObject, IMappingFrom<DailyNoteE
DailyNote = dailyNote;
OnPropertyChanged(nameof(DailyNote));
RefreshTime = DateTimeOffset.Now;
RefreshTime = DateTimeOffset.UtcNow;
OnPropertyChanged(nameof(RefreshTimeFormatted));
}
}

View File

@@ -119,7 +119,7 @@ internal sealed partial class GachaItem
ArchiveId = archiveId,
GachaType = item.GachaType,
QueryType = item.UIGFGachaType,
ItemId = uint.Parse(item.ItemId, CultureInfo.CurrentCulture), // TODO: catch the FormatException and throw v2.3 incompat exception
ItemId = uint.Parse(item.ItemId, CultureInfo.CurrentCulture),
Time = item.Time,
Id = item.Id,
};

View File

@@ -28,7 +28,7 @@ internal sealed class ObjectCacheEntry
/// 获取该对象是否过期
/// </summary>
[NotMapped]
public bool IsExpired { get => ExpireTime < DateTimeOffset.Now; }
public bool IsExpired { get => ExpireTime < DateTimeOffset.UtcNow; }
/// <summary>
/// 值字符串

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Frozen;
namespace Snap.Hutao.Model.InterChange.Achievement;
/// <summary>
@@ -15,11 +17,7 @@ internal sealed class UIAF
/// </summary>
public const string CurrentVersion = "v1.1";
// TODO use FrozenSet
private static readonly HashSet<string> SupportedVersion = new()
{
CurrentVersion,
};
private static readonly FrozenSet<string> SupportedVersion = FrozenSet.ToFrozenSet([CurrentVersion]);
/// <summary>
/// 信息

View File

@@ -49,7 +49,7 @@ internal sealed class UIAFInfo : IMappingFrom<UIAFInfo, RuntimeOptions>
{
return new()
{
ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = runtimeOptions.Version.ToString(),
UIAFVersion = UIAF.CurrentVersion,

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Web.Hoyolab;
using System.Runtime.InteropServices;

View File

@@ -71,7 +71,7 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
{
Uid = uid,
Language = metadataOptions.LanguageCode,
ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = runtimeOptions.Version.ToString(),
UIGFVersion = UIGF.CurrentVersion,

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Immutable;
using System.Collections.Frozen;
namespace Snap.Hutao.Model.InterChange.Inventory;
@@ -16,10 +16,7 @@ internal sealed class UIIF
/// </summary>
public const string CurrentVersion = "v1.0";
private static readonly ImmutableList<string> SupportedVersion = new List<string>()
{
CurrentVersion,
}.ToImmutableList();
private static readonly FrozenSet<string> SupportedVersion = FrozenSet.ToFrozenSet([CurrentVersion]);
/// <summary>
/// 信息

View File

@@ -69,7 +69,7 @@ internal sealed class UIIFInfo
return new()
{
Uid = uid,
ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = hutaoOptions.Version.ToString(),
UIIFVersion = UIIF.CurrentVersion,

View File

@@ -46,7 +46,7 @@ internal sealed class FetterInfo
/// </summary>
public string BirthFormatted
{
get => SH.ModelMetadataFetterInfoBirthdayFormat.Format(BirthMonth, BirthDay);
get => SH.FormatModelMetadataFetterInfoBirthdayFormat(BirthMonth, BirthDay);
}
/// <summary>

View File

@@ -58,8 +58,8 @@ internal sealed class MonsterBaseValue : BaseValue
{
get
{
return new()
{
return
[
FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_FIRE_SUB_HURT, FireSubHurt),
FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_WATER_SUB_HURT, WaterSubHurt),
FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_GRASS_SUB_HURT, GrassSubHurt),
@@ -68,7 +68,7 @@ internal sealed class MonsterBaseValue : BaseValue
FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_ICE_SUB_HURT, IceSubHurt),
FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_ROCK_SUB_HURT, RockSubHurt),
FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, PhysicalSubHurt),
};
];
}
}
}

View File

@@ -19,7 +19,7 @@ internal sealed class LevelDescription
/// 格式化的等级
/// </summary>
[JsonIgnore]
public string LevelFormatted { get => SH.ModelWeaponAffixFormat.Format(Level + 1); }
public string LevelFormatted { get => SH.FormatModelWeaponAffixFormat(Level + 1); }
/// <summary>
/// 描述

View File

@@ -129,10 +129,8 @@ internal sealed partial class AchievementDbService : IAchievementDbService
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.Achievements
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.ToList();
IQueryable<EntityAchievement> result = appDbContext.Achievements.AsNoTracking().Where(i => i.ArchiveId == archiveId);
return [.. result];
}
}
@@ -154,7 +152,8 @@ internal sealed partial class AchievementDbService : IAchievementDbService
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.AchievementArchives.AsNoTracking().ToList();
IQueryable<AchievementArchive> result = appDbContext.AchievementArchives.AsNoTracking();
return [.. result];
}
}

View File

@@ -21,7 +21,7 @@ internal sealed partial class AchievementStatisticsService : IAchievementStatist
{
await taskContext.SwitchToBackgroundAsync();
List<AchievementStatistics> results = new();
List<AchievementStatistics> results = [];
foreach (AchievementArchive archive in await achievementDbService.GetAchievementArchiveListAsync().ConfigureAwait(false))
{
int finishedCount = await achievementDbService

View File

@@ -40,6 +40,6 @@ internal readonly struct ImportResult
/// <inheritdoc/>
public override string ToString()
{
return SH.ServiceAchievementImportResultFormat.Format(Add, Update, Remove);
return SH.FormatServiceAchievementImportResultFormat(Add, Update, Remove);
}
}

View File

@@ -21,14 +21,14 @@ internal sealed partial class AppOptions : DbStoreOptions
{
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = CollectionsNameValue.ListFromEnum<BackdropType>();
private readonly List<NameValue<string>> supportedCulturesInner = new()
{
private readonly List<NameValue<string>> supportedCulturesInner =
[
ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")),
ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")),
ToNameValue(CultureInfo.GetCultureInfo("en")),
ToNameValue(CultureInfo.GetCultureInfo("ko")),
ToNameValue(CultureInfo.GetCultureInfo("ja")),
};
];
private string? gamePath;
private string? powerShellPath;
@@ -126,7 +126,7 @@ internal sealed partial class AppOptions : DbStoreOptions
string? paths = Environment.GetEnvironmentVariable("Path");
if (!string.IsNullOrEmpty(paths))
{
foreach (StringSegment path in new StringTokenizer(paths, ';'.ToArray()))
foreach (StringSegment path in new StringTokenizer(paths, [';']))
{
if (path is { HasValue: true, Length: > 0 })
{

View File

@@ -181,13 +181,13 @@ internal sealed partial class AvatarInfoDbBulkOperation
if (entity is null)
{
entity = EntityAvatarInfo.From(uid, webInfo);
entity.ShowcaseRefreshTime = DateTimeOffset.Now;
entity.ShowcaseRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = webInfo;
entity.ShowcaseRefreshTime = DateTimeOffset.Now;
entity.ShowcaseRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
@@ -200,7 +200,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId };
transformer.Transform(ref avatarInfo, source);
entity = EntityAvatarInfo.From(uid, avatarInfo);
entity.CalculatorRefreshTime = DateTimeOffset.Now;
entity.CalculatorRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
@@ -208,7 +208,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
EnkaAvatarInfo avatarInfo = entity.Info;
transformer.Transform(ref avatarInfo, source);
entity.Info = avatarInfo;
entity.CalculatorRefreshTime = DateTimeOffset.Now;
entity.CalculatorRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
@@ -221,7 +221,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId };
transformer.Transform(ref avatarInfo, source);
entity = EntityAvatarInfo.From(uid, avatarInfo);
entity.GameRecordRefreshTime = DateTimeOffset.Now;
entity.GameRecordRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
@@ -229,7 +229,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
EnkaAvatarInfo avatarInfo = entity.Info;
transformer.Transform(ref avatarInfo, source);
entity.Info = avatarInfo;
entity.GameRecordRefreshTime = DateTimeOffset.Now;
entity.GameRecordRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
@@ -243,7 +243,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
if (distinctCount < dbInfos.Count)
{
avatarInfoDbService.RemoveAvatarInfoRangeByUid(uid);
dbInfos = new();
dbInfos = [];
}
}
}

View File

@@ -19,7 +19,8 @@ internal sealed partial class AvatarInfoDbService : IAvatarInfoDbService
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.AvatarInfos.AsNoTracking().Where(i => i.Uid == uid).ToList();
IQueryable<EntityAvatarInfo> result = appDbContext.AvatarInfos.AsNoTracking().Where(i => i.Uid == uid);
return [.. result];
}
}

View File

@@ -80,13 +80,13 @@ internal sealed class SummaryAvatarFactory
// times
ShowcaseRefreshTimeFormat = showcaseRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryShowcaseNotRefreshed
: SH.ServiceAvatarInfoSummaryShowcaseRefreshTimeFormat.Format(showcaseRefreshTime),
: SH.FormatServiceAvatarInfoSummaryShowcaseRefreshTimeFormat(showcaseRefreshTime.ToLocalTime()),
GameRecordRefreshTimeFormat = gameRecordRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryGameRecordNotRefreshed
: SH.ServiceAvatarInfoSummaryGameRecordRefreshTimeFormat.Format(gameRecordRefreshTime),
: SH.FormatServiceAvatarInfoSummaryGameRecordRefreshTimeFormat(gameRecordRefreshTime.ToLocalTime()),
CalculatorRefreshTimeFormat = calculatorRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryCalculatorNotRefreshed
: SH.ServiceAvatarInfoSummaryCalculatorRefreshTimeFormat.Format(calculatorRefreshTime),
: SH.FormatServiceAvatarInfoSummaryCalculatorRefreshTimeFormat(calculatorRefreshTime.ToLocalTime()),
};
ApplyCostumeIconOrDefault(ref propertyAvatar, avatar);
@@ -110,12 +110,12 @@ internal sealed class SummaryAvatarFactory
}
}
private ReliquaryAndWeapon ProcessEquip(List<Web.Enka.Model.Equip> equipments)
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
{
List<PropertyReliquary> reliquaryList = new();
List<PropertyReliquary> reliquaryList = [];
PropertyWeapon? weapon = null;
foreach (Web.Enka.Model.Equip equip in equipments)
foreach (Equip equip in equipments)
{
switch (equip.Flat.ItemType)
{
@@ -132,7 +132,7 @@ internal sealed class SummaryAvatarFactory
return new(reliquaryList, weapon);
}
private PropertyWeapon CreateWeapon(Web.Enka.Model.Equip equip)
private PropertyWeapon CreateWeapon(Equip equip)
{
MetadataWeapon weapon = metadataContext.IdWeaponMap[equip.ItemId];

View File

@@ -22,7 +22,7 @@ internal static class SummaryAvatarProperties
{
if (fightPropMap is null)
{
return new();
return [];
}
AvatarProperty hpProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_HP, fightPropMap);

View File

@@ -31,17 +31,15 @@ internal sealed partial class SummaryFactory : ISummaryFactory
Reliquaries = await metadataService.GetReliquariesAsync(token).ConfigureAwait(false),
};
IOrderedEnumerable<AvatarView> avatars = avatarInfos
.Where(a => !AvatarIds.IsPlayer(a.Info.AvatarId))
.Select(a => new SummaryAvatarFactory(metadataContext, a).Create())
.OrderByDescending(a => a.LevelNumber)
.ThenBy(a => a.Name);
return new()
{
Avatars = avatarInfos
.Where(a => !AvatarIds.IsPlayer(a.Info.AvatarId))
.Select(a => new SummaryAvatarFactory(metadataContext, a).Create())
.OrderByDescending(a => a.LevelNumber)
.ThenBy(a => a.Name)
.ToList(),
// .ThenByDescending(a => a.Quality)
// .ThenByDescending(a => a.ActivatedConstellationCount)
Avatars = [.. avatars],
};
}
}

View File

@@ -23,7 +23,7 @@ internal static class SummaryHelper
/// <returns>命之座</returns>
public static List<ConstellationView> CreateConstellations(List<Skill> talents, List<SkillId>? talentIds)
{
talentIds ??= new();
talentIds ??= [];
return talents.SelectList(talent => new ConstellationView()
{
@@ -45,7 +45,7 @@ internal static class SummaryHelper
{
if (skillLevelMap.IsNullOrEmpty())
{
return new();
return [];
}
Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap);

View File

@@ -114,7 +114,7 @@ internal sealed class SummaryReliquaryFactory
private List<ReliquaryComposedSubProperty> CreateComposedSubProperties(List<ReliquarySubAffixId> appendProps)
{
List<SummaryReliquarySubPropertyCompositionInfo> infos = new();
List<SummaryReliquarySubPropertyCompositionInfo> infos = [];
foreach (ref readonly ReliquarySubAffixId subAffixId in CollectionsMarshal.AsSpan(appendProps))
{
ReliquarySubAffix subAffix = metadataContext.IdReliquarySubAffixMap[subAffixId];
@@ -128,7 +128,7 @@ internal sealed class SummaryReliquaryFactory
ThrowHelper.InvalidOperation("无效的圣遗物数据");
}
List<ReliquaryComposedSubProperty> results = new();
List<ReliquaryComposedSubProperty> results = [];
foreach (ref readonly SummaryReliquarySubPropertyCompositionInfo info in CollectionsMarshal.AsSpan(infos))
{
results.Add(info.ToReliquaryComposedSubProperty());

View File

@@ -22,7 +22,7 @@ internal sealed class GameRecordCharacterAvatarInfoTransformer : IAvatarInfoTran
avatarInfo.FetterInfo.ExpLevel = source.Fetter;
// update level
avatarInfo.PropMap ??= new Dictionary<PlayerProperty, TypeValue>();
avatarInfo.PropMap ??= [];
avatarInfo.PropMap[PlayerProperty.PROP_LEVEL] = new(PlayerProperty.PROP_LEVEL, source.Level);
// update constellations

View File

@@ -20,10 +20,8 @@ internal sealed partial class CultivationDbService : ICultivationDbService
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.InventoryItems
.AsNoTracking()
.Where(a => a.ProjectId == projectId)
.ToList();
IQueryable<InventoryItem> result = appDbContext.InventoryItems.AsNoTracking().Where(a => a.ProjectId == projectId);
return [.. result];
}
}

View File

@@ -38,7 +38,7 @@ internal sealed partial class CultivationService : ICultivationService
Guid projectId = cultivateProject.InnerId;
List<InventoryItem> entities = cultivationDbService.GetInventoryItemListByProjectId(projectId);
List<InventoryItemView> results = new();
List<InventoryItemView> results = [];
foreach (Material meta in metadata.Where(m => m.IsInventoryItem()).OrderBy(m => m.Id.Value))
{
InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.From(projectId, meta.Id);
@@ -64,7 +64,7 @@ internal sealed partial class CultivationService : ICultivationService
List<CultivateEntryView> resultEntries = new(entries.Count);
foreach (CultivateEntry entry in entries)
{
List<CultivateItemView> entryItems = new();
List<CultivateItemView> entryItems = [];
foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false))
{
entryItems.Add(new(item, materials.Single(m => m.Id == item.ItemId)));
@@ -94,7 +94,7 @@ internal sealed partial class CultivationService : ICultivationService
CancellationToken token)
{
await taskContext.SwitchToBackgroundAsync();
List<StatisticsCultivateItem> resultItems = new();
List<StatisticsCultivateItem> resultItems = [];
Guid projectId = cultivateProject.InnerId;

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
@@ -64,7 +65,8 @@ internal sealed partial class DailyNoteDbService : IDailyNoteDbService
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.DailyNotes.AsNoTracking().Include(n => n.User).ToList();
IIncludableQueryable<DailyNoteEntry, Model.Entity.User> result = appDbContext.DailyNotes.AsNoTracking().Include(n => n.User);
return [.. result];
}
}

View File

@@ -35,7 +35,7 @@ internal sealed partial class DailyNoteNotificationOperation
return;
}
List<DailyNoteNotifyInfo> notifyInfos = new();
List<DailyNoteNotifyInfo> notifyInfos = [];
CheckNotifySuppressed(entry, notifyInfos);
@@ -139,7 +139,7 @@ internal sealed partial class DailyNoteNotificationOperation
SH.ServiceDailyNoteNotifierResin,
Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_210.png"),
$"{entry.DailyNote.CurrentResin}",
SH.ServiceDailyNoteNotifierResinCurrent.Format(entry.DailyNote.CurrentResin)));
SH.FormatServiceDailyNoteNotifierResinCurrent(entry.DailyNote.CurrentResin)));
entry.ResinNotifySuppressed = true;
}
}
@@ -160,7 +160,7 @@ internal sealed partial class DailyNoteNotificationOperation
SH.ServiceDailyNoteNotifierHomeCoin,
Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_204.png"),
$"{entry.DailyNote.CurrentHomeCoin}",
SH.ServiceDailyNoteNotifierHomeCoinCurrent.Format(entry.DailyNote.CurrentHomeCoin)));
SH.FormatServiceDailyNoteNotifierHomeCoinCurrent(entry.DailyNote.CurrentHomeCoin)));
entry.HomeCoinNotifySuppressed = true;
}
}

View File

@@ -20,6 +20,15 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
{
private const int OneMinute = 60;
private readonly List<NameValue<int>> refreshTimes =
[
new(SH.ViewModelDailyNoteRefreshTime4, OneMinute * 4),
new(SH.ViewModelDailyNoteRefreshTime8, OneMinute * 8),
new(SH.ViewModelDailyNoteRefreshTime30, OneMinute * 30),
new(SH.ViewModelDailyNoteRefreshTime40, OneMinute * 40),
new(SH.ViewModelDailyNoteRefreshTime60, OneMinute * 60),
];
private readonly RuntimeOptions runtimeOptions;
private readonly IServiceProvider serviceProvider;
private readonly IScheduleTaskInterop scheduleTaskInterop;
@@ -32,14 +41,7 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
/// <summary>
/// 刷新时间
/// </summary>
public List<NameValue<int>> RefreshTimes { get; } = new()
{
new(SH.ViewModelDailyNoteRefreshTime4, OneMinute * 4),
new(SH.ViewModelDailyNoteRefreshTime8, OneMinute * 8),
new(SH.ViewModelDailyNoteRefreshTime30, OneMinute * 30),
new(SH.ViewModelDailyNoteRefreshTime40, OneMinute * 40),
new(SH.ViewModelDailyNoteRefreshTime60, OneMinute * 60),
};
public List<NameValue<int>> RefreshTimes { get => refreshTimes; }
public bool IsAutoRefreshEnabled
{

View File

@@ -50,10 +50,10 @@ internal static class GachaStatisticsExtension
public static List<StatisticsItem> ToStatisticsList<TItem>(this Dictionary<TItem, int> dict)
where TItem : IStatisticsItemSource
{
return dict
IOrderedEnumerable<StatisticsItem> result = dict
.Select(kvp => kvp.Key.ToStatisticsItem(kvp.Value))
.OrderByDescending(item => item.Count)
.ToList();
.OrderByDescending(item => item.Count);
return [.. result];
}
[SuppressMessage("", "IDE0057")]

View File

@@ -54,11 +54,11 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
TypedWishSummaryBuilderContext weaponContext = TypedWishSummaryBuilderContext.WeaponEventWish(taskContext, gachaLogClient);
TypedWishSummaryBuilder weaponWishBuilder = new(weaponContext);
Dictionary<Avatar, int> orangeAvatarCounter = new();
Dictionary<Avatar, int> purpleAvatarCounter = new();
Dictionary<Weapon, int> orangeWeaponCounter = new();
Dictionary<Weapon, int> purpleWeaponCounter = new();
Dictionary<Weapon, int> blueWeaponCounter = new();
Dictionary<Avatar, int> orangeAvatarCounter = [];
Dictionary<Avatar, int> purpleAvatarCounter = [];
Dictionary<Weapon, int> orangeWeaponCounter = [];
Dictionary<Weapon, int> purpleWeaponCounter = [];
Dictionary<Weapon, int> blueWeaponCounter = [];
// Pre group builders
Dictionary<GachaConfigType, List<HistoryWishBuilder>> historyWishBuilderMap = historyWishBuilders
@@ -132,7 +132,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
default:
// ItemId string length not correct.
ThrowHelper.UserdataCorrupted(SH.ServiceGachaStatisticsFactoryItemIdInvalid.Format(item.ItemId), default!);
ThrowHelper.UserdataCorrupted(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(item.ItemId), default!);
break;
}
}

View File

@@ -16,11 +16,11 @@ internal sealed class HistoryWishBuilder
{
private readonly GachaEvent gachaEvent;
private readonly Dictionary<IStatisticsItemSource, int> orangeUpCounter = new();
private readonly Dictionary<IStatisticsItemSource, int> purpleUpCounter = new();
private readonly Dictionary<IStatisticsItemSource, int> orangeCounter = new();
private readonly Dictionary<IStatisticsItemSource, int> purpleCounter = new();
private readonly Dictionary<IStatisticsItemSource, int> blueCounter = new();
private readonly Dictionary<IStatisticsItemSource, int> orangeUpCounter = [];
private readonly Dictionary<IStatisticsItemSource, int> purpleUpCounter = [];
private readonly Dictionary<IStatisticsItemSource, int> orangeCounter = [];
private readonly Dictionary<IStatisticsItemSource, int> purpleCounter = [];
private readonly Dictionary<IStatisticsItemSource, int> blueCounter = [];
private int totalCountTracker;

View File

@@ -26,7 +26,7 @@ internal sealed class HutaoStatisticsFactory
// TODO: when in new verion
// due to lack of newer metadata
// this can crash
DateTimeOffset now = DateTimeOffset.Now;
DateTimeOffset now = DateTimeOffset.UtcNow;
avatarEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.AvatarEventWish);
avatarEvent2 = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.AvatarEventWish2);
weaponEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.WeaponEventWish);
@@ -44,10 +44,10 @@ internal sealed class HutaoStatisticsFactory
private HutaoWishSummary CreateWishSummary(GachaEvent gachaEvent, List<ItemCount> items)
{
List<StatisticsItem> upItems = new();
List<StatisticsItem> orangeItems = new();
List<StatisticsItem> purpleItems = new();
List<StatisticsItem> blueItems = new();
List<StatisticsItem> upItems = [];
List<StatisticsItem> orangeItems = [];
List<StatisticsItem> purpleItems = [];
List<StatisticsItem> blueItems = [];
foreach (ref readonly ItemCount item in CollectionsMarshal.AsSpan(items))
{
@@ -55,7 +55,7 @@ internal sealed class HutaoStatisticsFactory
{
8U => context.IdAvatarMap[item.Item],
5U => context.IdWeaponMap[item.Item],
_ => throw ThrowHelper.UserdataCorrupted(SH.ServiceGachaStatisticsFactoryItemIdInvalid.Format(item.Item), default!),
_ => throw ThrowHelper.UserdataCorrupted(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(item.Item), default!),
};
StatisticsItem statisticsItem = source.ToStatisticsItem(unchecked((int)item.Count));
@@ -80,10 +80,10 @@ internal sealed class HutaoStatisticsFactory
return new()
{
Event = gachaEvent,
UpItems = upItems.OrderByDescending(i => i.Quality).ThenByDescending(i => i.Count).ToList(),
OrangeItems = orangeItems.OrderByDescending(i => i.Count).ToList(),
PurpleItems = purpleItems.OrderByDescending(i => i.Count).ToList(),
BlueItems = blueItems.OrderByDescending(i => i.Count).ToList(),
UpItems = [.. upItems.OrderByDescending(i => i.Quality).ThenByDescending(i => i.Count)],
OrangeItems = [.. orangeItems.OrderByDescending(i => i.Count)],
PurpleItems = [.. purpleItems.OrderByDescending(i => i.Count)],
BlueItems = [.. blueItems.OrderByDescending(i => i.Count)],
};
}
}

View File

@@ -32,9 +32,9 @@ internal sealed class TypedWishSummaryBuilder
private readonly TypedWishSummaryBuilderContext context;
private readonly List<int> averageOrangePullTracker = new();
private readonly List<int> averageUpOrangePullTracker = new();
private readonly List<SummaryItem> summaryItems = new();
private readonly List<int> averageOrangePullTracker = [];
private readonly List<int> averageUpOrangePullTracker = [];
private readonly List<SummaryItem> summaryItems = [];
private int maxOrangePullTracker;
private int minOrangePullTracker;

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.Immutable;
using System.Collections.Frozen;
namespace Snap.Hutao.Service.GachaLog;
@@ -14,11 +14,11 @@ internal static class GachaLog
/// <summary>
/// 查询类型
/// </summary>
public static readonly ImmutableList<GachaConfigType> QueryTypes = new List<GachaConfigType>
{
public static readonly FrozenSet<GachaConfigType> QueryTypes = FrozenSet.ToFrozenSet(
[
GachaConfigType.NoviceWish,
GachaConfigType.StandardWish,
GachaConfigType.AvatarEventWish,
GachaConfigType.WeaponEventWish,
}.ToImmutableList(); // TODO: FrozenSet
]);
}

View File

@@ -30,7 +30,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
}
catch (SqliteException ex)
{
string message = SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage.Format(ex.Message);
string message = SH.FormatServiceGachaLogArchiveCollectionUserdataCorruptedMessage(ex.Message);
throw ThrowHelper.UserdataCorrupted(message, ex);
}
}
@@ -40,11 +40,11 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GachaItems
IOrderedQueryable<GachaItem> result = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.ToList();
.OrderBy(i => i.Id);
return [.. result];
}
}
@@ -271,7 +271,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GachaItems
IQueryable<Web.Hutao.GachaLog.GachaItem> result = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
@@ -286,8 +286,8 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
ItemId = i.ItemId,
Time = i.Time,
Id = i.Id,
})
.ToList();
});
return [..result];
}
}

View File

@@ -70,7 +70,7 @@ internal struct GachaLogFetchContext
{
DbEndId = null;
CurrentType = configType;
ItemsToAdd = new();
ItemsToAdd = [];
FetchStatus = new(configType);
QueryOptions = new(query, configType);
}

View File

@@ -41,7 +41,7 @@ internal sealed class GachaLogFetchStatus
{
return AuthKeyTimeout
? SH.ViewDialogGachaLogRefreshProgressAuthkeyTimeout
: SH.ViewDialogGachaLogRefreshProgressDescription.Format(ConfigType.GetLocalizedDescription());
: SH.FormatViewDialogGachaLogRefreshProgressDescription(ConfigType.GetLocalizedDescription());
}
}
}

View File

@@ -39,7 +39,7 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
string uid = gachaArchive.Uid;
if (await GetEndIdsFromCloudAsync(uid, token).ConfigureAwait(false) is { } endIds)
{
List<Web.Hutao.GachaLog.GachaItem> items = new();
List<Web.Hutao.GachaLog.GachaItem> items = [];
foreach ((GachaConfigType type, long endId) in endIds)
{
List<Web.Hutao.GachaLog.GachaItem> part = await gachaLogDbService

View File

@@ -99,7 +99,7 @@ internal sealed partial class GachaLogService : IGachaLogService
await InitializeAsync(token).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(ArchiveCollection);
List<GachaStatisticsSlim> statistics = new();
List<GachaStatisticsSlim> statistics = [];
foreach (GachaArchive archive in ArchiveCollection)
{
List<GachaItem> items = await gachaLogDbService.GetGachaItemListByArchiveIdAsync(archive.InnerId).ConfigureAwait(false);

View File

@@ -18,7 +18,7 @@ internal readonly struct GachaLogServiceMetadataContext
/// <summary>
/// 物品缓存
/// </summary>
public readonly Dictionary<string, Item> ItemCache = new();
public readonly Dictionary<string, Item> ItemCache = [];
/// <summary>
/// Id 角色 映射

View File

@@ -4,7 +4,8 @@
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Request.QueryString;
using System.Collections.Specialized;
using System.Web;
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
@@ -27,18 +28,18 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP
if (isOk)
{
QueryString query = QueryString.Parse(queryString);
NameValueCollection query = HttpUtility.ParseQueryString(queryString);
if (query.TryGetValue("auth_appid", out string? appId) && appId is "webview_gacha")
{
string queryLanguageCode = query["lang"];
string? queryLanguageCode = query["lang"];
if (metadataOptions.IsCurrentLocale(queryLanguageCode))
{
return new(true, new(queryString));
}
else
{
string message = SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale
.Format(queryLanguageCode, metadataOptions.LanguageCode);
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, metadataOptions.LanguageCode);
return new(false, message);
}
}

View File

@@ -4,9 +4,10 @@
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.User;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Request;
using Snap.Hutao.Web.Response;
using System.Collections.Specialized;
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
@@ -37,7 +38,7 @@ internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvid
if (authkeyResponse.IsOk())
{
return new(true, new(GachaLogQueryOptions.ToQueryString(data, authkeyResponse.Data, metadataOptions.LanguageCode)));
return new(true, new(ComposeQueryString(data, authkeyResponse.Data, metadataOptions.LanguageCode)));
}
else
{
@@ -49,4 +50,16 @@ internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvid
return new(false, SH.MustSelectUserAndUid);
}
}
private static string ComposeQueryString(GenAuthKeyData genAuthKeyData, GameAuthKey gameAuthKey, string lang)
{
NameValueCollection collection = [];
collection.Set("lang", lang);
collection.Set("auth_appid", genAuthKeyData.AuthAppId);
collection.Set("authkey", gameAuthKey.AuthKey);
collection.Set("authkey_ver", $"{gameAuthKey.AuthKeyVersion:D}");
collection.Set("sign_type", $"{gameAuthKey.SignType:D}");
return collection.ToQueryString();
}
}

Some files were not shown because too many files have changed in this diff Show More