mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Merge pull request #1114 from DGP-Studio/develop
This commit is contained in:
@@ -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]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ internal static class EnumerableExtension
|
||||
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
HashSet<TKey> set = new();
|
||||
HashSet<TKey> set = [];
|
||||
|
||||
do
|
||||
{
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test;
|
||||
namespace Snap.Hutao.Test.PlatformExtensions;
|
||||
|
||||
[TestClass]
|
||||
public sealed class DependencyInjectionTest
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
17
src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs
Normal file
17
src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)) };
|
||||
}
|
||||
}
|
||||
@@ -69,4 +69,4 @@ internal class WinRTCustomMarshaler : ICustomMarshaler
|
||||
return Marshal.GetObjectForIUnknown(pNativeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
@@ -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/>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -35,6 +35,11 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
|
||||
|
||||
private void TryExecuteCommand()
|
||||
{
|
||||
if (AssociatedObject is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (executed)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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, });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
287
src/Snap.Hutao/Snap.Hutao/Control/Theme/ScrollViewer.xaml
Normal file
287
src/Snap.Hutao/Snap.Hutao/Control/Theme/ScrollViewer.xaml
Normal 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>
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
/// 当符合条件时添加参数
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.IO.DataTransfer;
|
||||
/// <summary>
|
||||
/// 剪贴板互操作
|
||||
/// </summary>
|
||||
internal interface IClipboardInterop
|
||||
internal interface IClipboardProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 从剪贴板文本中反序列化
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
22
src/Snap.Hutao/Snap.Hutao/Core/Random.cs
Normal file
22
src/Snap.Hutao/Snap.Hutao/Core/Random.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -9,11 +9,6 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
[HighQuality]
|
||||
internal enum BackdropType
|
||||
{
|
||||
/// <summary>
|
||||
/// 透明
|
||||
/// </summary>
|
||||
Transparent = -1,
|
||||
|
||||
/// <summary>
|
||||
/// 无
|
||||
/// </summary>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// 寻找枚举中唯一的值,找不到时
|
||||
/// 回退到首个或默认值
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -67,4 +67,4 @@ internal static class StructExtension
|
||||
{
|
||||
return size.Width * size.Height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs
Normal file
19
src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -71,6 +71,12 @@
|
||||
"Equatable": true,
|
||||
"EqualityOperators": true
|
||||
},
|
||||
{
|
||||
"Name": "ProfilePictureId",
|
||||
"Documentation": "角色头像 Id",
|
||||
"Equatable": true,
|
||||
"EqualityOperators": true
|
||||
},
|
||||
{
|
||||
"Name": "PromoteId",
|
||||
"Documentation": "角色突破提升 Id",
|
||||
|
||||
@@ -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;
|
||||
|
||||
12
src/Snap.Hutao/Snap.Hutao/Message/UserChangeFlag.cs
Normal file
12
src/Snap.Hutao/Snap.Hutao/Message/UserChangeFlag.cs
Normal 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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -39,4 +39,6 @@ internal abstract class ValueChangedMessage<TValue>
|
||||
/// 新的值
|
||||
/// </summary>
|
||||
public TValue? NewValue { get; set; }
|
||||
|
||||
public Guid UniqueMessageId { get; } = Guid.NewGuid();
|
||||
}
|
||||
@@ -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))];
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
/// 值字符串
|
||||
|
||||
@@ -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>
|
||||
/// 信息
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
/// 信息
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user