Merge pull request #1114 from DGP-Studio/develop

This commit is contained in:
DismissedLight
2023-11-21 14:10:59 +08:00
committed by GitHub
297 changed files with 2669 additions and 2255 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,8 +140,28 @@ 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);
string resourceLocalization = """
namespace Snap.Hutao.Resource.Localization;
[AttributeUsage(AttributeTargets.Enum)]
internal sealed class LocalizationAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Field)]
internal sealed class LocalizationKeyAttribute : Attribute
{
public LocalizationKeyAttribute(string key)
{
}
}
""";
context.AddSource("Snap.Hutao.Resource.Localization.Attributes.g.cs", resourceLocalization);
}
}

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()
@@ -314,12 +317,6 @@ public sealed class ResxGenerator : IIncrementalGenerator
string callParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "arg" + arg.ToString(CultureInfo.InvariantCulture)));
sb.AppendLine($$"""
/// {{comment}}
public static string Format{{ToCSharpNameIdentifier(entry.Name!)}}(global::System.Globalization.CultureInfo? provider, {{inParams}})
{
return GetString(provider, "{{entry.Name}}", {{callParams}})!;
}
/// {{comment}}
public static string Format{{ToCSharpNameIdentifier(entry.Name!)}}({{inParams}})
{
@@ -366,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));
@@ -406,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;
}
@@ -429,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 });
}
}
}
@@ -538,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; }
@@ -559,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];
@@ -585,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

@@ -7,17 +7,10 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
<Configurations>Debug;Release</Configurations>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</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.7.0" />

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

@@ -2,11 +2,20 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Test;
namespace Snap.Hutao.Test.BaseClassLibrary;
[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

@@ -1,7 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Snap.Hutao.Test;
namespace Snap.Hutao.Test.PlatformExtensions;
[TestClass]
public sealed class DependencyInjectionTest

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,11 +6,35 @@ 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)
{
Assert.AreEqual(uint.MaxValue, *(uint*)pBytes);
}
}
}
[TestClass]
public sealed class NewModifierRuntimeBehaviorTest
{
private interface IBase
{
int GetValue();
}
private interface IBaseImpl : IBase
{
new int GetValue();
}
private sealed class Impl : IBaseImpl
{
public int GetValue() => 1;
}
}

View File

@@ -1,18 +1,18 @@
<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>
<IsTestProject>true</IsTestProject>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">

View File

@@ -1,5 +1,11 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
"allowMarshaling": true,
"useSafeHandles": false
"useSafeHandles": false,
"comInterop": {
"preserveSigMethods": [
"IFileOpenDialog.Show",
"IFileSaveDialog.Show"
]
}
}

View File

@@ -28,8 +28,12 @@ WaitForSingleObject
WriteProcessMemory
// OLE32
CoCreateInstance
CoWaitForMultipleObjects
// SHELL32
SHCreateItemFromParsingName
// USER32
AttachThreadInput
FindWindowExW
@@ -46,6 +50,10 @@ SetForegroundWindow
UnregisterHotKey
// COM
FileOpenDialog
FileSaveDialog
IFileOpenDialog
IFileSaveDialog
IPersistFile
IShellLinkW
ShellLink
@@ -56,6 +64,7 @@ IMemoryBufferByteAccess
// Const value
INFINITE
MAX_PATH
WM_GETMINMAXINFO
WM_HOTKEY
WM_NCRBUTTONDOWN
@@ -63,6 +72,8 @@ WM_NCRBUTTONUP
WM_NULL
// Type & Enum definition
HRESULT_FROM_WIN32
SLGP_FLAGS
// System.Threading
LPTHREAD_START_ROUTINE

View File

@@ -0,0 +1,17 @@
using System;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
namespace Windows.Win32;
internal static partial class PInvoke
{
/// <inheritdoc cref="CoCreateInstance(Guid*, object, CLSCTX, Guid*, out object)"/>
internal static unsafe HRESULT CoCreateInstance<TClass, TInterface>(object? pUnkOuter, CLSCTX dwClsContext, out TInterface ppv)
where TInterface : class
{
HRESULT hr = CoCreateInstance(typeof(TClass).GUID, pUnkOuter, dwClsContext, typeof(TInterface).GUID, out object o);
ppv = (TInterface)o;
return hr;
}
}

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.22621.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@@ -19,18 +19,6 @@ internal static class StructMarshal
/// <returns>新的实例</returns>
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
{
return new() { length = SizeOf<WINDOWPLACEMENT>() };
}
/// <summary>
/// 获取结构的大小
/// </summary>
/// <typeparam name="TStruct">结构类型</typeparam>
/// <returns>结构的大小</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe uint SizeOf<TStruct>()
where TStruct : unmanaged
{
return unchecked((uint)sizeof(TStruct));
return new() { length = unchecked((uint)sizeof(WINDOWPLACEMENT)) };
}
}

View File

@@ -69,4 +69,4 @@ internal class WinRTCustomMarshaler : ICustomMarshaler
return Marshal.GetObjectForIUnknown(pNativeData);
}
}
}
}

View File

@@ -20,6 +20,7 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/NumericValue.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>

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

@@ -35,6 +35,11 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
private void TryExecuteCommand()
{
if (AssociatedObject is null)
{
return;
}
if (executed)
{
return;

View File

@@ -11,12 +11,17 @@ namespace Snap.Hutao.Control.Behavior;
/// 打开附着的浮出控件操作
/// </summary>
[HighQuality]
internal sealed class OpenAttachedFlyoutAction : DependencyObject, IAction
internal sealed class ShowAttachedFlyoutAction : DependencyObject, IAction
{
/// <inheritdoc/>
public object Execute(object sender, object parameter)
public object? Execute(object sender, object parameter)
{
if (sender is null)
{
return default;
}
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
return default!;
return default;
}
}

View File

@@ -35,9 +35,9 @@ internal sealed partial class SegmentedBar : ContentControl
double offset = 0;
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(list))
{
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
collection.Add(new() { Color = segment.Color, Offset = offset, });
offset += segment.Value / total;
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
collection.Add(new() { Color = segment.Color, Offset = offset, });
}
}
}

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

@@ -0,0 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))]
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
public sealed partial class ScrollViewerHelper
{
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
internal interface IScopedPageScopeReferenceTracker
{
IServiceScope CreateScope();
}

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

@@ -2,13 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Control.Layout;

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

@@ -17,9 +17,6 @@ namespace Snap.Hutao.Control;
[SuppressMessage("", "CA1001")]
internal class ScopedPage : Page
{
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(default!);
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope currentScope;
@@ -31,22 +28,7 @@ internal class ScopedPage : Page
{
unloadEventHandler = OnUnloaded;
Unloaded += unloadEventHandler;
currentScope = Ioc.Default.CreateScope();
DisposePreviousScope();
// track current
PreviousScopeReference.SetTarget(currentScope);
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposePreviousScope()
{
if (PreviousScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
/// <summary>

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
/// <summary>
/// By injecting into services, we take dvantage of the fact that
/// IServiceProvider disposes all injected services when it is disposed.
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IScopedPageScopeReferenceTracker))]
internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScopeReferenceTracker, IDisposable
{
private readonly IServiceProvider serviceProvider;
private readonly WeakReference<IServiceScope> previousScopeReference = new(default!);
public void Dispose()
{
DisposePreviousScope();
}
public IServiceScope CreateScope()
{
IServiceScope currentScope = serviceProvider.CreateScope();
// In case previous one is not disposed.
DisposePreviousScope();
previousScopeReference.SetTarget(currentScope);
return currentScope;
}
private void DisposePreviousScope()
{
if (previousScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
}

View File

@@ -0,0 +1,287 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shch="using:Snap.Hutao.Control.Helper">
<Style x:Key="TwoPanelScrollViewerStyle" TargetType="ScrollViewer">
<Setter Property="HorizontalScrollMode" Value="Auto"/>
<Setter Property="VerticalScrollMode" Value="Auto"/>
<Setter Property="IsHorizontalRailEnabled" Value="True"/>
<Setter Property="IsVerticalRailEnabled" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="ZoomMode" Value="Disabled"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="VerticalScrollBarVisibility" Value="Visible"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollViewer">
<Border
x:Name="Root"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid
Grid.RowSpan="2"
Grid.ColumnSpan="2"
Margin="{TemplateBinding Padding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MaxWidth="{Binding Path=(shch:ScrollViewerHelper.LeftPanelMaxWidth), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ContentPresenter Grid.Column="1" Content="{Binding Path=(shch:ScrollViewerHelper.RightPanel), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
</Grid>
<Grid Grid.RowSpan="2" Grid.ColumnSpan="2"/>
<Grid
Grid.Column="1"
Padding="{ThemeResource ScrollViewerScrollBarMargin}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}">
<ScrollBar
x:Name="VerticalScrollBar"
HorizontalAlignment="Right"
IsTabStop="False"
Maximum="{TemplateBinding ScrollableHeight}"
Orientation="Vertical"
ViewportSize="{TemplateBinding ViewportHeight}"
Value="{TemplateBinding VerticalOffset}"/>
</Grid>
<Grid
Grid.Row="1"
Padding="{ThemeResource ScrollViewerScrollBarMargin}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}">
<ScrollBar
x:Name="HorizontalScrollBar"
IsTabStop="False"
Maximum="{TemplateBinding ScrollableWidth}"
Orientation="Horizontal"
ViewportSize="{TemplateBinding ViewportWidth}"
Value="{TemplateBinding HorizontalOffset}"/>
</Grid>
<Border
x:Name="ScrollBarSeparator"
Grid.Row="1"
Grid.Column="1"
Background="{ThemeResource ScrollViewerScrollBarSeparatorBackground}"
Opacity="0"/>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ScrollingIndicatorStates">
<VisualStateGroup.Transitions>
<VisualTransition From="MouseIndicator" To="NoIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition From="MouseIndicatorFull" To="NoIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition From="MouseIndicatorFull" To="MouseIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition From="TouchIndicator" To="NoIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0:0:0.5">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0:0:0.5">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="NoIndicator"/>
<VisualState x:Name="TouchIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>TouchIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>TouchIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="MouseIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="MouseIndicatorFull">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ScrollBarSeparatorStates">
<VisualStateGroup.Transitions>
<VisualTransition From="ScrollBarSeparatorExpanded" To="ScrollBarSeparatorCollapsed">
<Storyboard>
<DoubleAnimation
BeginTime="{ThemeResource ScrollViewerSeparatorContractBeginTime}"
Storyboard.TargetName="ScrollBarSeparator"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="{ThemeResource ScrollViewerSeparatorContractDuration}"/>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="ScrollBarSeparatorCollapsed"/>
<VisualState x:Name="ScrollBarSeparatorExpanded">
<Storyboard>
<DoubleAnimation
BeginTime="{ThemeResource ScrollViewerSeparatorExpandBeginTime}"
Storyboard.TargetName="ScrollBarSeparator"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="{ThemeResource ScrollViewerSeparatorExpandDuration}"/>
</Storyboard>
</VisualState>
<VisualState x:Name="ScrollBarSeparatorExpandedWithoutAnimation">
<Storyboard>
<DoubleAnimation
BeginTime="{ThemeResource ScrollViewerSeparatorExpandBeginTime}"
Storyboard.TargetName="ScrollBarSeparator"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="ScrollBarSeparatorCollapsedWithoutAnimation">
<Storyboard>
<DoubleAnimation
BeginTime="{ThemeResource ScrollViewerSeparatorContractBeginTime}"
Storyboard.TargetName="ScrollBarSeparator"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

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));
@@ -125,7 +125,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
/// <inheritdoc/>
public ValueFile GetFileFromCategoryAndName(string category, string fileName)
{
Uri dummyUri = Web.HutaoEndpoints.StaticFile(category, fileName).ToUri();
Uri dummyUri = Web.HutaoEndpoints.StaticRaw(category, fileName).ToUri();
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
}

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

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Windows.ApplicationModel.Resources;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service;
using System.Globalization;
@@ -36,7 +35,6 @@ internal static class DependencyInjection
// Discrete services
.AddSingleton<IMessenger, WeakReferenceMessenger>()
.BuildServiceProvider(true);
Ioc.Default.ConfigureServices(serviceProvider);
@@ -56,6 +54,9 @@ internal static class DependencyInjection
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name;
SH.Culture = cultureInfo;
}
}

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

@@ -88,7 +88,6 @@ internal static partial class IocHttpClientConfiguration
/// HoYoLAB web
/// </summary>
/// <param name="client">配置后的客户端</param>
[SuppressMessage("", "IDE0051")]
private static void XRpc4Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;

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

@@ -1,104 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Notification;
using Windows.Storage;
using Windows.Storage.Pickers;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 选择器拓展
/// </summary>
internal static class PickerExtension
{
/// <inheritdoc cref="FileOpenPicker.PickSingleFileAsync"/>
public static async ValueTask<ValueResult<bool, ValueFile>> TryPickSingleFileAsync(this FileOpenPicker picker)
{
StorageFile? file;
Exception? exception = null;
try
{
file = await picker.PickSingleFileAsync().AsTask().ConfigureAwait(false);
}
catch (Exception ex)
{
exception = ex;
file = null;
}
if (file is not null)
{
return new(true, file.Path);
}
else
{
InfoBarWaringPickerException(exception);
return new(false, default!);
}
}
/// <inheritdoc cref="FileSavePicker.PickSaveFileAsync"/>
public static async ValueTask<ValueResult<bool, ValueFile>> TryPickSaveFileAsync(this FileSavePicker picker)
{
StorageFile? file;
Exception? exception = null;
try
{
file = await picker.PickSaveFileAsync().AsTask().ConfigureAwait(false);
}
catch (Exception ex)
{
exception = ex;
file = null;
}
if (file is not null)
{
return new(true, file.Path);
}
else
{
InfoBarWaringPickerException(exception);
return new(false, default!);
}
}
/// <inheritdoc cref="FolderPicker.PickSingleFolderAsync"/>
public static async ValueTask<ValueResult<bool, string>> TryPickSingleFolderAsync(this FolderPicker picker)
{
StorageFolder? folder;
Exception? exception = null;
try
{
folder = await picker.PickSingleFolderAsync().AsTask().ConfigureAwait(false);
}
catch (Exception ex)
{
exception = ex;
folder = null;
}
if (folder is not null)
{
return new(true, folder.Path);
}
else
{
InfoBarWaringPickerException(exception);
return new(false, default!);
}
}
private static void InfoBarWaringPickerException(Exception? exception)
{
if (exception is not null)
{
Ioc.Default
.GetRequiredService<IInfoBarService>()
.Warning(
SH.CoreIOPickerExtensionPickerExceptionInfoBarTitle,
SH.CoreIOPickerExtensionPickerExceptionInfoBarMessage.Format(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

@@ -2,11 +2,14 @@
// Licensed under the MIT license.
using System.IO;
using System.Runtime.InteropServices;
using Windows.Storage;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Shell;
@@ -37,16 +40,17 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
return false;
}
IShellLinkW shellLink = (IShellLinkW)new ShellLink();
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
Marshal.ThrowExceptionForHR(result);
shellLink.SetPath("powershell");
shellLink.SetArguments($"""
-Command "Start-Process shell:AppsFolder\{runtimeOptions.FamilyName}!App -verb runas"
""");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE);
shellLink.SetIconLocation(targetLogoPath, 0);
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string target = Path.Combine(desktop, $"{SH.AppNameAndVersion.Format(runtimeOptions.Version)}.lnk");
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
IPersistFile persistFile = (IPersistFile)shellLink;
try

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

@@ -36,7 +36,7 @@ internal static class DispatcherQueueExtension
}
catch (Exception ex)
{
ExceptionDispatchInfo.Capture(ex);
exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex);
}
finally
{
@@ -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

@@ -9,11 +9,6 @@ namespace Snap.Hutao.Core.Windowing;
[HighQuality]
internal enum BackdropType
{
/// <summary>
/// 透明
/// </summary>
Transparent = -1,
/// <summary>
/// 无
/// </summary>

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

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Windowing.HotKey;
internal static class VirtualKeys
{
private static readonly List<NameValue<VirtualKey>> Values = CollectionsNameValue.ListFromEnum<VirtualKey>();
private static readonly List<NameValue<VirtualKey>> Values = CollectionsNameValue.FromEnum<VirtualKey>();
public static List<NameValue<VirtualKey>> GetList()
{

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

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core.Windowing.HotKey;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
@@ -34,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

@@ -0,0 +1,211 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.LifeCycle;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.Shell.Common;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Factory.Picker;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IFileSystemPickerInteraction))]
internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInteraction
{
private readonly ICurrentWindowReference currentWindowReference;
public unsafe ValueResult<bool, ValueFile> PickFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters)
{
CoCreateInstance<FileOpenDialog, IFileOpenDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure();
FILEOPENDIALOGOPTIONS options =
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM |
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
dialog.SetOptions(options);
SetDesktopAsStartupFolder(dialog);
if (!string.IsNullOrEmpty(title))
{
dialog.SetTitle(title);
}
if (!string.IsNullOrEmpty(defaultFileName))
{
dialog.SetFileName(defaultFileName);
}
if (filters is { Length: > 0 })
{
SetFileTypes(dialog, filters);
}
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
{
return new(false, default);
}
else
{
Marshal.ThrowExceptionForHR(res);
}
dialog.GetResult(out IShellItem item);
PWSTR displayName = default;
string file;
try
{
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
file = new((char*)displayName);
}
finally
{
Marshal.FreeCoTaskMem((nint)displayName.Value);
}
return new(true, file);
}
public unsafe ValueResult<bool, ValueFile> SaveFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters)
{
CoCreateInstance<FileSaveDialog, IFileSaveDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileSaveDialog dialog).ThrowOnFailure();
FILEOPENDIALOGOPTIONS options =
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM |
FILEOPENDIALOGOPTIONS.FOS_STRICTFILETYPES |
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
dialog.SetOptions(options);
SetDesktopAsStartupFolder(dialog);
if (!string.IsNullOrEmpty(title))
{
dialog.SetTitle(title);
}
if (!string.IsNullOrEmpty(defaultFileName))
{
dialog.SetFileName(defaultFileName);
}
if (filters is { Length: > 0 })
{
SetFileTypes(dialog, filters);
}
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
{
return new(false, default);
}
else
{
Marshal.ThrowExceptionForHR(res);
}
dialog.GetResult(out IShellItem item);
PWSTR displayName = default;
string file;
try
{
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
file = new((char*)displayName);
}
finally
{
Marshal.FreeCoTaskMem((nint)displayName.Value);
}
return new(true, file);
}
public unsafe ValueResult<bool, string> PickFolder(string? title)
{
CoCreateInstance<FileOpenDialog, IFileOpenDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure();
FILEOPENDIALOGOPTIONS options =
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM |
FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS |
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
dialog.SetOptions(options);
SetDesktopAsStartupFolder(dialog);
if (!string.IsNullOrEmpty(title))
{
dialog.SetTitle(title);
}
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
{
return new(false, default!);
}
else
{
Marshal.ThrowExceptionForHR(res);
}
dialog.GetResult(out IShellItem item);
PWSTR displayName = default;
string file;
try
{
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
file = new((char*)displayName);
}
finally
{
Marshal.FreeCoTaskMem((nint)displayName.Value);
}
return new(true, file);
}
private static unsafe void SetFileTypes<TDialog>(TDialog dialog, (string Name, string Type)[] filters)
where TDialog : IFileDialog
{
List<nint> unmanagedStringPtrs = new(filters.Length * 2);
List<COMDLG_FILTERSPEC> filterSpecs = new(filters.Length);
foreach ((string name, string type) in filters)
{
nint pName = Marshal.StringToHGlobalUni(name);
nint pType = Marshal.StringToHGlobalUni(type);
unmanagedStringPtrs.Add(pName);
unmanagedStringPtrs.Add(pType);
COMDLG_FILTERSPEC spec = default;
spec.pszName = *(PCWSTR*)&pName;
spec.pszSpec = *(PCWSTR*)&pType;
filterSpecs.Add(spec);
}
fixed (COMDLG_FILTERSPEC* ptr = CollectionsMarshal.AsSpan(filterSpecs))
{
dialog.SetFileTypes((uint)filterSpecs.Count, ptr);
}
foreach (ref readonly nint ptr in CollectionsMarshal.AsSpan(unmanagedStringPtrs))
{
Marshal.FreeHGlobal(ptr);
}
}
private static unsafe void SetDesktopAsStartupFolder<TDialog>(TDialog dialog)
where TDialog : IFileDialog
{
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
SHCreateItemFromParsingName(desktopPath, default, typeof(IShellItem).GUID, out object shellItem).ThrowOnFailure();
dialog.SetFolder((IShellItem)shellItem);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.IO;
namespace Snap.Hutao.Factory.Picker;
internal static class FileSystemPickerInteractionExtension
{
public static ValueResult<bool, ValueFile> PickFile(this IFileSystemPickerInteraction interaction, string? title, (string Name, string Type)[]? filters)
{
return interaction.PickFile(title, null, filters);
}
public static ValueResult<bool, string> PickFolder(this IFileSystemPickerInteraction interaction)
{
return interaction.PickFolder(null);
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.IO;
namespace Snap.Hutao.Factory.Picker;
internal interface IFileSystemPickerInteraction
{
ValueResult<bool, ValueFile> PickFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters);
ValueResult<bool, string> PickFolder(string? title);
ValueResult<bool, ValueFile> SaveFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters);
}

View File

@@ -1,38 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Storage.Pickers;
namespace Snap.Hutao.Factory.Picker;
/// <summary>
/// 文件选择器工厂
/// </summary>
[HighQuality]
internal interface IPickerFactory
{
/// <summary>
/// 获取 经过初始化的 <see cref="FileOpenPicker"/>
/// </summary>
/// <param name="location">初始位置</param>
/// <param name="commitButton">提交按钮文本</param>
/// <param name="fileTypes">文件类型</param>
/// <returns>经过初始化的 <see cref="FileOpenPicker"/></returns>
FileOpenPicker GetFileOpenPicker(PickerLocationId location, string commitButton, params string[] fileTypes);
/// <summary>
/// 获取 经过初始化的 <see cref="FileSavePicker"/>
/// </summary>
/// <param name="location">初始位置</param>
/// <param name="fileName">文件名</param>
/// <param name="commitButton">提交按钮文本</param>
/// <param name="fileTypes">文件类型</param>
/// <returns>经过初始化的 <see cref="FileSavePicker"/></returns>
FileSavePicker GetFileSavePicker(PickerLocationId location, string fileName, string commitButton, IDictionary<string, IList<string>> fileTypes);
/// <summary>
/// 获取 经过初始化的 <see cref="FolderPicker"/>
/// </summary>
/// <returns>经过初始化的 <see cref="FolderPicker"/></returns>
FolderPicker GetFolderPicker();
}

View File

@@ -1,89 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Windowing;
using Windows.Storage.Pickers;
using Windows.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Factory.Picker;
/// <inheritdoc cref="IPickerFactory"/>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IPickerFactory))]
internal sealed partial class PickerFactory : IPickerFactory
{
private const string AnyType = "*";
private readonly ICurrentWindowReference currentWindowReference;
/// <inheritdoc/>
public FileOpenPicker GetFileOpenPicker(PickerLocationId location, string commitButton, params string[] fileTypes)
{
FileOpenPicker picker = GetInitializedPicker<FileOpenPicker>();
picker.SuggestedStartLocation = location;
picker.CommitButtonText = commitButton;
foreach (string type in fileTypes)
{
picker.FileTypeFilter.Add(type);
}
// below Windows 11
if (!UniversalApiContract.IsPresent(WindowsVersion.Windows11))
{
// https://github.com/microsoft/WindowsAppSDK/issues/2931
picker.FileTypeFilter.Add(AnyType);
}
return picker;
}
/// <inheritdoc/>
public FileSavePicker GetFileSavePicker(PickerLocationId location, string fileName, string commitButton, IDictionary<string, IList<string>> fileTypes)
{
FileSavePicker picker = GetInitializedPicker<FileSavePicker>();
picker.SuggestedStartLocation = location;
picker.SuggestedFileName = fileName;
picker.CommitButtonText = commitButton;
foreach (KeyValuePair<string, IList<string>> kvp in fileTypes)
{
picker.FileTypeChoices.Add(kvp);
}
return picker;
}
/// <inheritdoc/>
public FolderPicker GetFolderPicker()
{
FolderPicker picker = GetInitializedPicker<FolderPicker>();
// below Windows 11
if (!UniversalApiContract.IsPresent(WindowsVersion.Windows11))
{
// https://github.com/microsoft/WindowsAppSDK/issues/2931
picker.FileTypeFilter.Add(AnyType);
}
return picker;
}
private T GetInitializedPicker<T>()
where T : new()
{
// Create a folder picker.
T picker = new();
HWND hwnd = currentWindowReference.GetWindowHandle();
InitializeWithWindow.Initialize(picker, hwnd);
return picker;
}
}

View File

@@ -1,12 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Factory.Progress;
[ConstructorGenerated]

View File

@@ -71,6 +71,12 @@
"Equatable": true,
"EqualityOperators": true
},
{
"Name": "ProfilePictureId",
"Documentation": "角色头像 Id",
"Equatable": true,
"EqualityOperators": true
},
{
"Name": "PromoteId",
"Documentation": "角色突破提升 Id",

View File

@@ -16,8 +16,8 @@ namespace Snap.Hutao;
[SuppressMessage("", "CA1001")]
internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMaxInfoHandler
{
private const int MinWidth = 848;
private const int MinHeight = 524;
private const int MinWidth = 1200;
private const int MinHeight = 750;
private readonly WindowOptions windowOptions;
private readonly ILogger<MainWindow> logger;

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Message;
internal enum UserChangeFlag
{
// This flag is impossible to appear alone
UserChanged = 0b0001,
RoleChanged = 0b0010,
UserAndRoleChanged = UserChanged | RoleChanged,
}

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.User;
using System.Diagnostics;
using System.Text;
namespace Snap.Hutao.Message;
@@ -9,6 +11,49 @@ namespace Snap.Hutao.Message;
/// 用户切换消息
/// </summary>
[HighQuality]
[DebuggerDisplay("{DebuggerDisplay(),nq}")]
internal sealed class UserChangedMessage : ValueChangedMessage<User>
{
// defaults to the UserAndRoleChanged when we raise this message in ScopedDbCurrent
public UserChangeFlag Flag { get; private set; } = UserChangeFlag.UserAndRoleChanged;
public bool IsOnlyRoleChanged { get => Flag == UserChangeFlag.RoleChanged; }
public static UserChangedMessage Create(User oldValue, User newValue, UserChangeFlag flag)
{
return new UserChangedMessage
{
OldValue = oldValue,
NewValue = newValue,
Flag = flag,
};
}
public static UserChangedMessage CreateOnlyRoleChanged(User value)
{
return Create(value, value, UserChangeFlag.RoleChanged);
}
#if DEBUG
private string DebuggerDisplay()
{
StringBuilder stringBuilder = new();
stringBuilder
.Append("Name:")
.Append(OldValue?.UserInfo?.Nickname)
.Append("|Role[")
.Append(OldValue?.UserGameRoles?.Count)
.Append("]:<")
.Append(OldValue?.SelectedUserGameRole)
.Append("> -> Name:")
.Append(NewValue?.UserInfo?.Nickname)
.Append("|Role[")
.Append(NewValue?.UserGameRoles?.Count)
.Append("]:<")
.Append(NewValue?.SelectedUserGameRole)
.Append('>');
return stringBuilder.ToString();
}
#endif
}

View File

@@ -39,4 +39,6 @@ internal abstract class ValueChangedMessage<TValue>
/// 新的值
/// </summary>
public TValue? NewValue { get; set; }
public Guid UniqueMessageId { get; } = Guid.NewGuid();
}

View File

@@ -5,9 +5,14 @@ namespace Snap.Hutao.Model;
internal static class CollectionsNameValue
{
public static List<NameValue<T>> ListFromEnum<T>()
where T : struct, Enum
public static List<NameValue<TEnum>> FromEnum<TEnum>()
where TEnum : struct, Enum
{
return Enum.GetValues<T>().Select(x => new NameValue<T>(x.ToString(), x)).ToList();
return [.. Enum.GetValues<TEnum>().Select(x => new NameValue<TEnum>(x.ToString(), x))];
}
public static List<NameValue<TSource>> From<TSource>(IEnumerable<TSource> sources, Func<TSource, string> nameSelector)
{
return [.. sources.Select(x => new NameValue<TSource>(nameSelector(x), x))];
}
}

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

@@ -0,0 +1,63 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Frozen;
namespace Snap.Hutao.Model.Intrinsic.Frozen;
/// <summary>
/// 本地化的不可变的原生枚举
/// </summary>
[HighQuality]
internal static class IntrinsicFrozen
{
/// <summary>
/// 所属地区
/// </summary>
public static readonly FrozenSet<string> AssociationTypes = Enum.GetValues<AssociationType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 武器类型
/// </summary>
public static readonly FrozenSet<string> WeaponTypes = Enum.GetValues<WeaponType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 物品类型
/// </summary>
public static readonly FrozenSet<string> ItemQualities = Enum.GetValues<QualityType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 身材类型
/// </summary>
public static readonly FrozenSet<string> BodyTypes = Enum.GetValues<BodyType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 战斗属性
/// </summary>
public static readonly FrozenSet<string> FightProperties = Enum.GetValues<FightProperty>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 元素名称
/// </summary>
public static readonly FrozenSet<string> ElementNames = FrozenSet.ToFrozenSet(
[
SH.ModelIntrinsicElementNameFire,
SH.ModelIntrinsicElementNameWater,
SH.ModelIntrinsicElementNameGrass,
SH.ModelIntrinsicElementNameElec,
SH.ModelIntrinsicElementNameWind,
SH.ModelIntrinsicElementNameIce,
SH.ModelIntrinsicElementNameRock,
]);
public static readonly FrozenSet<string> MaterialTypeDescriptions = FrozenSet.ToFrozenSet(
[
SH.ModelMetadataMaterialCharacterAndWeaponEnhancementMaterial,
SH.ModelMetadataMaterialCharacterEXPMaterial,
SH.ModelMetadataMaterialCharacterAscensionMaterial,
SH.ModelMetadataMaterialCharacterTalentMaterial,
SH.ModelMetadataMaterialCharacterLevelUpMaterial,
SH.ModelMetadataMaterialWeaponEnhancementMaterial,
SH.ModelMetadataMaterialWeaponAscensionMaterial,
]);
}

View File

@@ -1,63 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Immutable;
namespace Snap.Hutao.Model.Intrinsic.Immutable;
/// <summary>
/// 本地化的不可变的原生枚举
/// </summary>
[HighQuality]
internal static class IntrinsicImmutable
{
/// <summary>
/// 所属地区
/// </summary>
public static readonly ImmutableHashSet<string> AssociationTypes = Enum.GetValues<AssociationType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 武器类型
/// </summary>
public static readonly ImmutableHashSet<string> WeaponTypes = Enum.GetValues<WeaponType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 物品类型
/// </summary>
public static readonly ImmutableHashSet<string> ItemQualities = Enum.GetValues<QualityType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 身材类型
/// </summary>
public static readonly ImmutableHashSet<string> BodyTypes = Enum.GetValues<BodyType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 战斗属性
/// </summary>
public static readonly ImmutableHashSet<string> FightProperties = Enum.GetValues<FightProperty>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 元素名称
/// </summary>
public static readonly ImmutableHashSet<string> ElementNames = new HashSet<string>(7)
{
SH.ModelIntrinsicElementNameFire,
SH.ModelIntrinsicElementNameWater,
SH.ModelIntrinsicElementNameGrass,
SH.ModelIntrinsicElementNameElec,
SH.ModelIntrinsicElementNameWind,
SH.ModelIntrinsicElementNameIce,
SH.ModelIntrinsicElementNameRock,
}.ToImmutableHashSet();
public static readonly ImmutableHashSet<string> MaterialTypeDescriptions = new HashSet<string>(7)
{
SH.ModelMetadataMaterialCharacterAndWeaponEnhancementMaterial,
SH.ModelMetadataMaterialCharacterEXPMaterial,
SH.ModelMetadataMaterialCharacterAscensionMaterial,
SH.ModelMetadataMaterialCharacterTalentMaterial,
SH.ModelMetadataMaterialCharacterLevelUpMaterial,
SH.ModelMetadataMaterialWeaponEnhancementMaterial,
SH.ModelMetadataMaterialWeaponAscensionMaterial,
}.ToImmutableHashSet();
}

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