mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Merge pull request #700 from DGP-Studio/refactor/application-model
Refactor Application Model
This commit is contained in:
@@ -33,7 +33,7 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)!
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddHttpClientsImplementation);
|
||||
|
||||
@@ -26,9 +26,9 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)!
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddInjectionsImplementation);
|
||||
@@ -96,13 +96,13 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
switch (injectAsName)
|
||||
{
|
||||
case InjectAsSingletonName:
|
||||
lineBuilder.Append(@" services.AddSingleton<");
|
||||
lineBuilder.Append(" services.AddSingleton<");
|
||||
break;
|
||||
case InjectAsTransientName:
|
||||
lineBuilder.Append(@" services.AddTransient<");
|
||||
lineBuilder.Append(" services.AddTransient<");
|
||||
break;
|
||||
case InjectAsScopedName:
|
||||
lineBuilder.Append(@" services.AddScoped<");
|
||||
lineBuilder.Append(" services.AddScoped<");
|
||||
break;
|
||||
default:
|
||||
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, null, injectAsName));
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Enum;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Resource.Localization.LocalizationAttribute";
|
||||
private const string LocalizationKeyName = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValuesProvider<GeneratorSyntaxContext2> localizationEnums = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedEnums, LocalizationEnum)
|
||||
.Where(GeneratorSyntaxContext2.NotNull);
|
||||
|
||||
context.RegisterSourceOutput(localizationEnums, GenerateGetLocalizedDescriptionImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedEnums(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is EnumDeclarationSyntax enumDeclarationSyntax && enumDeclarationSyntax.AttributeLists.Count > 0;
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 LocalizationEnum(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol enumSymbol)
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = enumSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, enumSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateGetLocalizedDescriptionImplementation(SourceProductionContext context, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}","1.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
internal static class {{context2.Symbol.Name}}Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
public static string GetLocalizedDescription(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return Enum.GetName(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
[return:MaybeNull]
|
||||
public static string GetLocalizedDescriptionOrDefault(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource($"{context2.Symbol.Name}Extension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithSwitchBranches(StringBuilder sourceBuilder, GeneratorSyntaxContext2 context)
|
||||
{
|
||||
EnumDeclarationSyntax enumSyntax = (EnumDeclarationSyntax)context.Context.Node;
|
||||
|
||||
foreach(EnumMemberDeclarationSyntax enumMemberSyntax in enumSyntax.Members)
|
||||
{
|
||||
if (context.Context.SemanticModel.GetDeclaredSymbol(enumMemberSyntax) is IFieldSymbol fieldSymbol)
|
||||
{
|
||||
AttributeData? localizationKeyInfo = fieldSymbol.GetAttributes()
|
||||
.SingleOrDefault(data => data.AttributeClass!.ToDisplayString() == LocalizationKeyName);
|
||||
if (localizationKeyInfo != null)
|
||||
{
|
||||
sourceBuilder.Append(" ").Append(fieldSymbol).Append(" => \"").Append(localizationKeyInfo.ConstructorArguments[0].Value).AppendLine("\",");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceBuilder.Append($"""
|
||||
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,6 @@ internal sealed class IdentityGenerator : IIncrementalGenerator
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExpressionService;
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive.Converter;
|
||||
|
||||
/// <summary>
|
||||
@@ -57,19 +55,25 @@ internal sealed class IdentityGenerator : IIncrementalGenerator
|
||||
/// <typeparam name="TWrapper">包装类型</typeparam>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(IdentityGenerator)}}","1.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
internal sealed class IdentityConverter<TWrapper> : JsonConverter<TWrapper>
|
||||
where TWrapper : struct
|
||||
internal unsafe sealed class IdentityConverter<TWrapper> : JsonConverter<TWrapper>
|
||||
where TWrapper : unmanaged
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override TWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return CastTo<TWrapper>.From(reader.GetInt32());
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
{
|
||||
int value = reader.GetInt32();
|
||||
return *(TWrapper*)&value;
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, TWrapper value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(CastTo<int>.From(value));
|
||||
writer.WriteNumberValue(*(int*)&value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -205,7 +205,7 @@ public static class JsonParser
|
||||
|
||||
try
|
||||
{
|
||||
return Enum.Parse(type, json, false);
|
||||
return System.Enum.Parse(type, json, false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
@@ -61,4 +62,30 @@ public class CSharpLanguageFeatureTest
|
||||
ValueB = 2,
|
||||
ValueC = 3,
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetTwiceOnPropertyResultsNotSame()
|
||||
{
|
||||
Assert.AreNotEqual(UUID, UUID);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
|
||||
{
|
||||
List<string> strings = new()
|
||||
{
|
||||
"a", "b", "c"
|
||||
};
|
||||
|
||||
int count = 0;
|
||||
foreach (ReadOnlySpan<char> chars in strings)
|
||||
{
|
||||
Assert.IsTrue(chars.Length == 1);
|
||||
++count;
|
||||
}
|
||||
|
||||
Assert.AreEqual(3, count);
|
||||
}
|
||||
|
||||
public static Guid UUID { get => Guid.NewGuid(); }
|
||||
}
|
||||
@@ -34,6 +34,16 @@ public class DependencyInjectionTest
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GenericServicesCanBeResolved()
|
||||
{
|
||||
IServiceProvider services = new ServiceCollection()
|
||||
.AddTransient(typeof(IGenericService<>),typeof(GenericService<>))
|
||||
.BuildServiceProvider();
|
||||
|
||||
Assert.IsNotNull(services.GetService<IGenericService<int>>());
|
||||
}
|
||||
|
||||
private interface IService
|
||||
{
|
||||
Guid Id { get; }
|
||||
@@ -54,4 +64,24 @@ public class DependencyInjectionTest
|
||||
get => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private interface IGenericService<T>
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class GenericService<T> : IGenericService<T>
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class NonInjectedServiceA
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class NonInjectedServiceB
|
||||
{
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public NonInjectedServiceB(NonInjectedServiceA? serviceA)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
|
||||
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
|
||||
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
|
||||
<shmmc:ParameterDescriptor x:Key="DescParamDescriptor"/>
|
||||
<shmmc:DescriptionsParametersDescriptor x:Key="DescParamDescriptor"/>
|
||||
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
|
||||
<shmmc:EmotionIconConverter x:Key="EmotionIconConverter"/>
|
||||
<shmmc:EquipIconConverter x:Key="EquipIconConverter"/>
|
||||
@@ -98,7 +98,7 @@
|
||||
<shmmc:GachaEquipIconConverter x:Key="GachaEquipIconConverter"/>
|
||||
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
|
||||
<shmmc:MonsterIconConverter x:Key="MonsterIconConverter"/>
|
||||
<shmmc:PropertyDescriptor x:Key="PropertyDescriptor"/>
|
||||
<shmmc:PropertiesParametersDescriptor x:Key="PropertyDescriptor"/>
|
||||
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
|
||||
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
|
||||
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
|
||||
|
||||
@@ -21,19 +21,23 @@ namespace Snap.Hutao;
|
||||
[SuppressMessage("", "SH001")]
|
||||
public sealed partial class App : Application
|
||||
{
|
||||
private const string AppInstanceKey = "main";
|
||||
private readonly ILogger<App> logger;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object.
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
public App(ILogger<App> logger)
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public App(IServiceProvider serviceProvider)
|
||||
{
|
||||
// load app resource
|
||||
// Load app resource
|
||||
InitializeComponent();
|
||||
this.logger = logger;
|
||||
|
||||
_ = new ExceptionRecorder(this, logger);
|
||||
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
||||
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -42,7 +46,7 @@ public sealed partial class App : Application
|
||||
try
|
||||
{
|
||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main");
|
||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
|
||||
|
||||
if (firstInstance.IsCurrent)
|
||||
{
|
||||
@@ -51,10 +55,8 @@ public sealed partial class App : Application
|
||||
firstInstance.Activated += Activation.Activate;
|
||||
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
|
||||
|
||||
logger.LogInformation("Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
|
||||
logger.LogInformation("Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path);
|
||||
|
||||
JumpListHelper.ConfigureAsync().SafeForget(logger);
|
||||
LogDiagnosticInformation();
|
||||
PostLaunchAsync().SafeForget(logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -69,4 +71,18 @@ public sealed partial class App : Application
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task PostLaunchAsync()
|
||||
{
|
||||
await JumpListHelper.ConfigureAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void LogDiagnosticInformation()
|
||||
{
|
||||
HutaoOptions hutaoOptions = serviceProvider.GetRequiredService<HutaoOptions>();
|
||||
|
||||
logger.LogInformation("Snap Hutao FamilyName: {name}", hutaoOptions.FamilyName);
|
||||
logger.LogInformation("Snap Hutao Version: {version}", hutaoOptions.Version);
|
||||
logger.LogInformation("Snap Hutao LocalCache: {folder}", hutaoOptions.LocalCache);
|
||||
}
|
||||
}
|
||||
28
src/Snap.Hutao/Snap.Hutao/AppResourceProvider.cs
Normal file
28
src/Snap.Hutao/Snap.Hutao/AppResourceProvider.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序资源提供器
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IAppResourceProvider))]
|
||||
internal sealed class AppResourceProvider : IAppResourceProvider
|
||||
{
|
||||
private readonly App app;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的应用程序资源提供器
|
||||
/// </summary>
|
||||
/// <param name="app">应用</param>
|
||||
public AppResourceProvider(App app)
|
||||
{
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T GetResource<T>(string name)
|
||||
{
|
||||
return (T)app.Resources[name];
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
[HighQuality]
|
||||
internal sealed class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetWidth), 320D);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetHeight), 1024D);
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.DependBoxed<double>(nameof(TargetWidth), BoxedValues.DoubleOne);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.DependBoxed<double>(nameof(TargetHeight), BoxedValues.DoubleOne);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
|
||||
@@ -15,30 +15,36 @@ internal static class ContentDialogExtension
|
||||
/// 阻止用户交互
|
||||
/// </summary>
|
||||
/// <param name="contentDialog">对话框</param>
|
||||
/// <param name="taskContext">任务上下文</param>
|
||||
/// <returns>用于恢复用户交互</returns>
|
||||
public static async ValueTask<IDisposable> BlockAsync(this ContentDialog contentDialog)
|
||||
public static async ValueTask<IDisposable> BlockAsync(this ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
contentDialog.ShowAsync().AsTask().SafeForget();
|
||||
|
||||
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019
|
||||
// Only a single ContentDialog can be open at any time.
|
||||
return new ContentDialogHider(contentDialog);
|
||||
return new ContentDialogHider(contentDialog, taskContext);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SA1201")]
|
||||
[SuppressMessage("", "SA1400")]
|
||||
[SuppressMessage("", "SA1600")]
|
||||
file readonly struct ContentDialogHider : IDisposable
|
||||
{
|
||||
private readonly ContentDialog contentDialog;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
public ContentDialogHider(ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
this.contentDialog = contentDialog;
|
||||
this.taskContext = taskContext;
|
||||
}
|
||||
|
||||
private class ContentDialogHider : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
private readonly ContentDialog contentDialog;
|
||||
|
||||
public ContentDialogHider(ContentDialog contentDialog)
|
||||
{
|
||||
this.contentDialog = contentDialog;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Hide() must be called on main thread.
|
||||
ThreadHelper.InvokeOnMainThread(contentDialog.Hide);
|
||||
}
|
||||
// Hide() must be called on main thread.
|
||||
taskContext.InvokeOnMainThread(contentDialog.Hide);
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,7 @@ internal sealed class CachedImage : ImageEx
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
// We can only use Ioc to retrive IImageCache,
|
||||
// no IServiceProvider is available.
|
||||
// We can only use Ioc to retrieve IImageCache, no IServiceProvider is available.
|
||||
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
||||
|
||||
try
|
||||
@@ -47,7 +46,7 @@ internal sealed class CachedImage : ImageEx
|
||||
catch (COMException)
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
imageCache.Remove(imageUri.Enumerate());
|
||||
imageCache.Remove(imageUri);
|
||||
return null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
||||
@@ -35,7 +35,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
/// </summary>
|
||||
public CompositionImage()
|
||||
{
|
||||
serviceProvider = Ioc.Default.GetRequiredService<IServiceProvider>();
|
||||
serviceProvider = Ioc.Default;
|
||||
|
||||
AllowFocusOnInteraction = false;
|
||||
IsDoubleTapEnabled = false;
|
||||
@@ -123,7 +123,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
private static void OnApplyImageFailed(IServiceProvider serviceProvider, Uri? uri, Exception exception)
|
||||
{
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
|
||||
|
||||
if (exception is HttpRequestException httpRequestException)
|
||||
{
|
||||
@@ -157,11 +157,11 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
imageCache.Remove(uri.Enumerate());
|
||||
imageCache.Remove(uri);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
imageCache.Remove(uri.Enumerate());
|
||||
imageCache.Remove(uri);
|
||||
}
|
||||
|
||||
if (imageSurface != null)
|
||||
@@ -183,7 +183,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
if (EnableLazyLoading)
|
||||
{
|
||||
await AnimationBuilder.Create().Opacity(1d, 0d).StartAsync(this, token).ConfigureAwait(true);
|
||||
await AnimationBuilder.Create().Opacity(from: 0D, to: 1D).StartAsync(this, token).ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -200,7 +200,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
if (EnableLazyLoading)
|
||||
{
|
||||
await AnimationBuilder.Create().Opacity(0d, 1d).StartAsync(this, token).ConfigureAwait(true);
|
||||
await AnimationBuilder.Create().Opacity(from: 1D, to: 0D).StartAsync(this, token).ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -9,17 +9,17 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// <param name="Offset">便宜</param>
|
||||
/// <param name="Color">颜色</param>
|
||||
[HighQuality]
|
||||
internal struct GradientStop
|
||||
internal readonly struct GradientStop
|
||||
{
|
||||
/// <summary>
|
||||
/// 便宜
|
||||
/// 偏移
|
||||
/// </summary>
|
||||
public float Offset;
|
||||
public readonly float Offset;
|
||||
|
||||
/// <summary>
|
||||
/// 颜色
|
||||
/// </summary>
|
||||
public Windows.UI.Color Color;
|
||||
public readonly Windows.UI.Color Color;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的渐变锚点
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Win32;
|
||||
@@ -27,15 +28,15 @@ internal static class SoftwareBitmapExtension
|
||||
using (IMemoryBufferReference reference = buffer.CreateReference())
|
||||
{
|
||||
reference.As<IMemoryBufferByteAccess>().GetBuffer(out byte* data, out uint length);
|
||||
|
||||
for (int i = 0; i < length; i += 4)
|
||||
Span<Bgra8> bytes = new(data, unchecked((int)length / (sizeof(Bgra8) / sizeof(uint))));
|
||||
foreach (ref Bgra8 pixel in bytes)
|
||||
{
|
||||
Bgra8* pixel = (Bgra8*)(data + i);
|
||||
byte baseAlpha = pixel->A;
|
||||
pixel->B = (byte)(((pixel->B * baseAlpha) + (tint.B * (0xFF - baseAlpha))) / 0xFF);
|
||||
pixel->G = (byte)(((pixel->G * baseAlpha) + (tint.G * (0xFF - baseAlpha))) / 0xFF);
|
||||
pixel->R = (byte)(((pixel->R * baseAlpha) + (tint.R * (0xFF - baseAlpha))) / 0xFF);
|
||||
pixel->A = 0xFF;
|
||||
byte baseAlpha = pixel.A;
|
||||
int opposite = 0xFF - baseAlpha;
|
||||
pixel.B = (byte)(((pixel.B * baseAlpha) + (tint.B * opposite)) / 0xFF);
|
||||
pixel.G = (byte)(((pixel.G * baseAlpha) + (tint.G * opposite)) / 0xFF);
|
||||
pixel.R = (byte)(((pixel.R * baseAlpha) + (tint.R * opposite)) / 0xFF);
|
||||
pixel.A = 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ internal sealed partial class PanelSelector : SplitButton
|
||||
set => SetValue(CurrentProperty, value);
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(PanelSelector sender, string current)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout;
|
||||
@@ -43,21 +48,16 @@ internal sealed partial class PanelSelector : SplitButton
|
||||
sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph;
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
|
||||
}
|
||||
|
||||
private void OnRootControlLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// because the GroupName shares in global
|
||||
// we have to impl a control scoped GroupName.
|
||||
// we have to implement a control scoped GroupName.
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout;
|
||||
int hash = GetHashCode();
|
||||
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
|
||||
{
|
||||
item.GroupName = $"PanelSelector{hash}Group";
|
||||
item.GroupName = $"{nameof(PanelSelector)}GroupOf@{hash}";
|
||||
}
|
||||
|
||||
OnCurrentChanged(selector, Current);
|
||||
|
||||
@@ -153,6 +153,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
// Simply re-apply texts
|
||||
ApplyDescription((TextBlock)Content, Description);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
/// <summary>
|
||||
@@ -21,7 +23,13 @@ internal interface IImageCache : ICastableService
|
||||
/// Removed items based on uri list passed
|
||||
/// </summary>
|
||||
/// <param name="uriForCachedItems">Enumerable uri list</param>
|
||||
void Remove(IEnumerable<Uri> uriForCachedItems);
|
||||
void Remove(in ReadOnlySpan<Uri> uriForCachedItems);
|
||||
|
||||
/// <summary>
|
||||
/// Removed item based on uri passed
|
||||
/// </summary>
|
||||
/// <param name="uriForCachedItem">uri</param>
|
||||
void Remove(Uri uriForCachedItem);
|
||||
|
||||
/// <summary>
|
||||
/// Removes invalid cached files
|
||||
|
||||
@@ -36,6 +36,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
@@ -45,12 +46,15 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageCache"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="httpClientFactory">http客户端工厂</param>
|
||||
public ImageCache(ILogger<ImageCache> logger, IHttpClientFactory httpClientFactory)
|
||||
public ImageCache(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.logger = logger;
|
||||
httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
logger = serviceProvider.GetRequiredService<ILogger<ImageCache>>();
|
||||
httpClient = serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ImageCache));
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -73,9 +77,15 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(IEnumerable<Uri> uriForCachedItems)
|
||||
public void Remove(Uri uriForCachedItem)
|
||||
{
|
||||
if (uriForCachedItems == null || !uriForCachedItems.Any())
|
||||
Remove(new ReadOnlySpan<Uri>(uriForCachedItem));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(in ReadOnlySpan<Uri> uriForCachedItems)
|
||||
{
|
||||
if (uriForCachedItems == null || uriForCachedItems.Length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -131,24 +141,10 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public string GetFilePathFromCategoryAndFileName(string category, string fileName)
|
||||
{
|
||||
Uri dummyUri = new(Web.HutaoEndpoints.StaticFile(category, fileName));
|
||||
Uri dummyUri = Web.HutaoEndpoints.StaticFile(category, fileName).ToUri();
|
||||
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
|
||||
}
|
||||
|
||||
private static void RemoveInternal(IEnumerable<string> filePaths)
|
||||
{
|
||||
foreach (string filePath in filePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCacheFileName(Uri uri)
|
||||
{
|
||||
string url = uri.ToString();
|
||||
@@ -164,11 +160,25 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
return treatNullFileAsInvalid;
|
||||
}
|
||||
|
||||
// Get extended properties.
|
||||
FileInfo fileInfo = new(file);
|
||||
return fileInfo.Length == 0;
|
||||
}
|
||||
|
||||
private void RemoveInternal(IEnumerable<string> filePaths)
|
||||
{
|
||||
foreach (string filePath in filePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Remove Cache Image Failed:{file}", filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadFileAsync(Uri uri, string baseFile)
|
||||
{
|
||||
logger.LogInformation("Begin downloading for {uri}", uri);
|
||||
@@ -218,7 +228,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
if (cacheFolder == null)
|
||||
{
|
||||
baseFolder ??= ApplicationData.Current.LocalCacheFolder.Path;
|
||||
baseFolder ??= serviceProvider.GetRequiredService<HutaoOptions>().LocalCache;
|
||||
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
|
||||
cacheFolder = info.FullName;
|
||||
}
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.Json;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 核心环境参数
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class CoreEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// 米游社请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabOsUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社移动端请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab 移动端请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabOsMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabXrpcVersion = "2.49.1";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabOsXrpcVersion = "2.30.0";
|
||||
|
||||
/// <summary>
|
||||
/// 盐
|
||||
/// </summary>
|
||||
// https://github.com/UIGF-org/Hoyolab.Salt
|
||||
public static readonly ImmutableDictionary<SaltType, string> DynamicSecretSalts = new Dictionary<SaltType, string>()
|
||||
{
|
||||
[SaltType.K2] = "egBrFMO1BPBG0UX5XOuuwMRLZKwTVKRV",
|
||||
[SaltType.LK2] = "DG8lqMyc9gquwAUFc7zBS62ijQRX9XF7",
|
||||
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
|
||||
// This SALT is not reliable
|
||||
[SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
/// <summary>
|
||||
/// 默认的Json序列化选项
|
||||
/// </summary>
|
||||
public static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
|
||||
{
|
||||
Modifiers =
|
||||
{
|
||||
JsonTypeInfoResolvers.ResolveEnumType,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 当前版本
|
||||
/// </summary>
|
||||
public static readonly Version Version;
|
||||
|
||||
/// <summary>
|
||||
/// 标准UA
|
||||
/// </summary>
|
||||
public static readonly string CommonUA;
|
||||
|
||||
/// <summary>
|
||||
/// 数据文件夹
|
||||
/// </summary>
|
||||
public static readonly string DataFolder;
|
||||
|
||||
/// <summary>
|
||||
/// 包家族名称
|
||||
/// </summary>
|
||||
public static readonly string FamilyName;
|
||||
|
||||
/// <summary>
|
||||
/// 米游社设备Id
|
||||
/// </summary>
|
||||
public static readonly string HoyolabDeviceId;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃设备Id
|
||||
/// </summary>
|
||||
public static readonly string HutaoDeviceId;
|
||||
|
||||
/// <summary>
|
||||
/// 安装位置
|
||||
/// </summary>
|
||||
public static readonly string InstalledLocation;
|
||||
|
||||
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
|
||||
private const string MachineGuidValue = "MachineGuid";
|
||||
|
||||
static CoreEnvironment()
|
||||
{
|
||||
DataFolder = GetDatafolderPath();
|
||||
Version = Package.Current.Id.Version.ToVersion();
|
||||
FamilyName = Package.Current.Id.FamilyName;
|
||||
InstalledLocation = Package.Current.InstalledLocation.Path;
|
||||
CommonUA = $"Snap Hutao/{Version}";
|
||||
|
||||
// simply assign a random guid
|
||||
HoyolabDeviceId = Guid.NewGuid().ToString();
|
||||
HutaoDeviceId = GetUniqueUserID();
|
||||
}
|
||||
|
||||
private static string GetUniqueUserID()
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
}
|
||||
|
||||
private static string GetDatafolderPath()
|
||||
{
|
||||
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
|
||||
|
||||
if (string.IsNullOrEmpty(preferredPath))
|
||||
{
|
||||
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
else
|
||||
{
|
||||
return preferredPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace Snap.Hutao.Core.Database;
|
||||
/// <typeparam name="TEntity">实体的类型</typeparam>
|
||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
||||
[HighQuality]
|
||||
[Obsolete("Use ScopedDbCurrent instead")]
|
||||
internal sealed class DbCurrent<TEntity, TMessage>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntity>, new()
|
||||
|
||||
@@ -24,10 +24,7 @@ internal static class DbSetExtension
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = dbContext.SaveChanges();
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,14 +34,11 @@ internal static class DbSetExtension
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static async ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -58,10 +52,7 @@ internal static class DbSetExtension
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.AddRange(entities);
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = dbSet.Context().SaveChanges();
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -71,14 +62,11 @@ internal static class DbSetExtension
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entities">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static async ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.AddRange(entities);
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,10 +80,7 @@ internal static class DbSetExtension
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = dbContext.SaveChanges();
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -105,14 +90,11 @@ internal static class DbSetExtension
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static async ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,10 +108,7 @@ internal static class DbSetExtension
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = dbContext.SaveChanges();
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -139,11 +118,31 @@ internal static class DbSetExtension
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static async ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int SaveChangesAndClearChangeTracker<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
{
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = dbContext.SaveChanges();
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
{
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
@@ -16,8 +17,7 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntity>, new()
|
||||
{
|
||||
private readonly IServiceScopeFactory scopeFactory;
|
||||
private readonly Func<IServiceProvider, DbSet<TEntity>> dbSetSelector;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
private TEntity? current;
|
||||
@@ -25,14 +25,11 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
/// <summary>
|
||||
/// 构造一个新的数据库当前项
|
||||
/// </summary>
|
||||
/// <param name="scopeFactory">范围工厂</param>
|
||||
/// <param name="dbSetSelector">数据集选择器</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public ScopedDbCurrent(IServiceScopeFactory scopeFactory, Func<IServiceProvider, DbSet<TEntity>> dbSetSelector, IMessenger messenger)
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public ScopedDbCurrent(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.scopeFactory = scopeFactory;
|
||||
this.dbSetSelector = dbSetSelector;
|
||||
this.messenger = messenger;
|
||||
messenger = serviceProvider.GetRequiredService<IMessenger>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,9 +46,10 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
DbSet<TEntity> dbSet = dbSetSelector(scope.ServiceProvider);
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value != null)
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Database;
|
||||
/// 可枚举扩展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class EnumerableExtension
|
||||
internal static class SelectableExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取选中的值或默认值
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 可转换类型服务
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 有名称的对象
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 海外服/Hoyolab 可区分
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 依赖注入
|
||||
/// </summary>
|
||||
internal static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化依赖注入
|
||||
/// </summary>
|
||||
/// <returns>服务提供器</returns>
|
||||
public static ServiceProvider Initialize()
|
||||
{
|
||||
ServiceProvider serviceProvider = new ServiceCollection()
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder.AddDebug())
|
||||
.AddMemoryCache()
|
||||
|
||||
// Hutao extensions
|
||||
.AddJsonOptions()
|
||||
.AddDatabase()
|
||||
.AddInjections()
|
||||
.AddHttpClients()
|
||||
|
||||
// Discrete services
|
||||
.AddSingleton<IMessenger>(WeakReferenceMessenger.Default)
|
||||
|
||||
.BuildServiceProvider(true);
|
||||
|
||||
Ioc.Default.ConfigureServices(serviceProvider);
|
||||
return serviceProvider;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Json;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Service;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Windows.Globalization;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
@@ -20,7 +24,7 @@ internal static class IocConfiguration
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddJsonOptions(this IServiceCollection services)
|
||||
{
|
||||
return services.AddSingleton(CoreEnvironment.JsonOptions);
|
||||
return services.AddSingleton(JsonOptions.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -30,28 +34,53 @@ internal static class IocConfiguration
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddDatabase(this IServiceCollection services)
|
||||
{
|
||||
string dbFile = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Userdata.db");
|
||||
return services
|
||||
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
|
||||
.AddDbContext<AppDbContext>(AddDbContextCore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化语言
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <returns>服务提供器,用于链式调用</returns>
|
||||
public static IServiceProvider InitializeCulture(this IServiceProvider serviceProvider)
|
||||
{
|
||||
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||
appOptions.PreviousCulture = CultureInfo.CurrentCulture;
|
||||
|
||||
CultureInfo cultureInfo = appOptions.CurrentCulture;
|
||||
|
||||
CultureInfo.CurrentCulture = cultureInfo;
|
||||
CultureInfo.CurrentUICulture = cultureInfo;
|
||||
ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name;
|
||||
|
||||
return serviceProvider;
|
||||
}
|
||||
|
||||
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
|
||||
{
|
||||
HutaoOptions hutaoOptions = provider.GetRequiredService<HutaoOptions>();
|
||||
string dbFile = System.IO.Path.Combine(hutaoOptions.DataFolder, "Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
// temporarily create a context
|
||||
// Temporarily create a context
|
||||
using (AppDbContext context = AppDbContext.Create(sqlConnectionString))
|
||||
{
|
||||
if (context.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[Debug] Performing AppDbContext Migrations");
|
||||
Debug.WriteLine("[Database] Performing AppDbContext Migrations");
|
||||
#endif
|
||||
context.Database.Migrate();
|
||||
}
|
||||
}
|
||||
|
||||
return services.AddDbContext<AppDbContext>(builder =>
|
||||
{
|
||||
builder
|
||||
builder
|
||||
#if DEBUG
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableSensitiveDataLogging()
|
||||
#endif
|
||||
.UseSqlite(sqlConnectionString);
|
||||
});
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
|
||||
.UseSqlite(sqlConnectionString);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
|
||||
/// <summary>
|
||||
/// 添加 <see cref="HttpClient"/>
|
||||
/// 此方法将会自动生成
|
||||
/// </summary>
|
||||
/// <param name="services">集合</param>
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
@@ -24,10 +25,12 @@ internal static partial class IocHttpClientConfiguration
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void DefaultConfiguration(HttpClient client)
|
||||
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
|
||||
{
|
||||
HutaoOptions hutaoOptions = serviceProvider.GetRequiredService<HutaoOptions>();
|
||||
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.CommonUA);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,11 +40,11 @@ internal static partial class IocHttpClientConfiguration
|
||||
private static void XRpcConfiguration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgent);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", HoyolabOptions.XrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,13 +54,13 @@ internal static partial class IocHttpClientConfiguration
|
||||
private static void XRpc2Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgent);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-aigis", string.Empty);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", HoyolabOptions.XrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
|
||||
}
|
||||
@@ -70,7 +73,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
private static void XRpc3Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabOsUA);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgentOversea);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", "1.5.0");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "4");
|
||||
|
||||
@@ -12,6 +12,7 @@ internal static partial class ServiceCollectionExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 向容器注册服务
|
||||
/// 此方法将会自动生成
|
||||
/// </summary>
|
||||
/// <param name="services">容器</param>
|
||||
/// <returns>可继续操作的服务集合</returns>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供器扩展
|
||||
/// </summary>
|
||||
internal static class ServiceProviderExtension
|
||||
{
|
||||
/// <inheritdoc cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T CreateInstance<T>(this IServiceProvider serviceProvider, params object[] parameters)
|
||||
{
|
||||
return ActivatorUtilities.CreateInstance<T>(serviceProvider, parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 异常格式化
|
||||
/// </summary>
|
||||
internal sealed class ExceptionFormat
|
||||
{
|
||||
private static readonly string SectionSeparator = new('-', 40);
|
||||
|
||||
/// <summary>
|
||||
/// 格式化异常
|
||||
/// </summary>
|
||||
/// <param name="exception">异常</param>
|
||||
/// <returns>格式化后的异常</returns>
|
||||
public static string Format(Exception exception)
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.AppendLine("Exception Data:");
|
||||
|
||||
foreach (DictionaryEntry entry in exception.Data)
|
||||
{
|
||||
builder
|
||||
.Append(entry.Key)
|
||||
.Append(':')
|
||||
.Append(entry.Value)
|
||||
.Append(StringLiterals.CRLF);
|
||||
}
|
||||
|
||||
builder.AppendLine(SectionSeparator);
|
||||
builder.Append(exception.ToString());
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
@@ -11,50 +9,56 @@ namespace Snap.Hutao.Core.ExceptionService;
|
||||
/// 异常记录器
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class ExceptionRecorder
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly ILogger<ExceptionRecorder> logger;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的异常记录器
|
||||
/// </summary>
|
||||
/// <param name="application">应用程序</param>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public ExceptionRecorder(Application application, ILogger logger)
|
||||
public ExceptionRecorder(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.logger = logger;
|
||||
logger = serviceProvider.GetRequiredService<ILogger<ExceptionRecorder>>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
application.UnhandledException += OnAppUnhandledException;
|
||||
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
|
||||
application.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed;
|
||||
/// <summary>
|
||||
/// 记录应用程序异常
|
||||
/// </summary>
|
||||
/// <param name="app">应用程序</param>
|
||||
public void Record(Application app)
|
||||
{
|
||||
app.UnhandledException += OnAppUnhandledException;
|
||||
app.DebugSettings.BindingFailed += OnXamlBindingFailed;
|
||||
app.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed;
|
||||
}
|
||||
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
#if RELEASE
|
||||
#pragma warning disable VSTHRD002
|
||||
Ioc.Default.GetRequiredService<Web.Hutao.HomaLogUploadClient>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
|
||||
serviceProvider
|
||||
.GetRequiredService<Web.Hutao.HomaLogUploadClient>()
|
||||
.UploadLogAsync(serviceProvider, e.Exception)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
#pragma warning restore VSTHRD002
|
||||
#endif
|
||||
StringBuilder dataDetailBuilder = new();
|
||||
foreach (DictionaryEntry entry in e.Exception.Data)
|
||||
{
|
||||
string key = $"{entry.Key}";
|
||||
string value = $"{entry.Value}";
|
||||
|
||||
dataDetailBuilder.Append(key).Append(':').Append(value).Append("\r\n");
|
||||
}
|
||||
|
||||
logger.LogError(e.Exception, "未经处理的异常\r\n{detail}", dataDetailBuilder.ToString());
|
||||
logger.LogError("未经处理的全局异常:\r\n{detail}", ExceptionFormat.Format(e.Exception));
|
||||
}
|
||||
|
||||
private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)
|
||||
{
|
||||
logger.LogCritical("XAML绑定失败: {message}", e.Message);
|
||||
logger.LogCritical("XAML 绑定失败:{message}", e.Message);
|
||||
}
|
||||
|
||||
private void OnXamlResourceReferenceFailed(DebugSettings sender, XamlResourceReferenceFailedEventArgs e)
|
||||
{
|
||||
logger.LogCritical("XAML资源引用失败: {message}", e.Message);
|
||||
logger.LogCritical("XAML 资源引用失败:{message}", e.Message);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal sealed class UserdataCorruptedException : Exception
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public UserdataCorruptedException(string message, Exception innerException)
|
||||
: base(string.Format(SH.CoreExceptionServiceUserdataCorruptedMessage, message), innerException)
|
||||
: base(string.Format(SH.CoreExceptionServiceUserdataCorruptedMessage, $"{message}\n{innerException.Message}"), innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Snap.Hutao.Core.ExpressionService;
|
||||
|
||||
/// <summary>
|
||||
/// Class to cast to type <see cref="TTo"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="TTo">Target type</typeparam>
|
||||
[HighQuality]
|
||||
internal static class CastTo<TTo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Casts <see cref="s"/> to <see cref="TTo"/>.
|
||||
/// This does not cause boxing for value types.
|
||||
/// Useful in generic methods.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">Source type to cast from. Usually a generic type.</typeparam>
|
||||
/// <param name="from">from value</param>
|
||||
/// <returns>target value</returns>
|
||||
public static TTo From<TFrom>(TFrom from)
|
||||
{
|
||||
return Cache<TFrom>.Caster(from);
|
||||
}
|
||||
|
||||
private static class Cache<TCachedFrom>
|
||||
{
|
||||
public static readonly Func<TCachedFrom, TTo> Caster = Get();
|
||||
|
||||
private static Func<TCachedFrom, TTo> Get()
|
||||
{
|
||||
// 参数表达式,表示 传入源类型
|
||||
ParameterExpression param = Expression.Parameter(typeof(TCachedFrom));
|
||||
|
||||
// 一元转换 调用 相关类的显式或隐式转换运算符
|
||||
UnaryExpression convert = Expression.ConvertChecked(param, typeof(TTo));
|
||||
|
||||
// 生成一个源类型入,目标类型出的 lamdba
|
||||
return Expression.Lambda<Func<TCachedFrom, TTo>>(convert, param).Compile();
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/Snap.Hutao/Snap.Hutao/Core/HoyolabOptions.cs
Normal file
73
src/Snap.Hutao/Snap.Hutao/Core/HoyolabOptions.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 米游社选项
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class HoyolabOptions : IOptions<HoyolabOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// 米游社请求UA
|
||||
/// </summary>
|
||||
public const string UserAgent = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{XrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab 请求UA
|
||||
/// </summary>
|
||||
public const string UserAgentOversea = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBSOversea/{XrpcVersionOversea}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社移动端请求UA
|
||||
/// </summary>
|
||||
public const string MobileUserAgent = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{XrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab 移动端请求UA
|
||||
/// </summary>
|
||||
public const string MobileUserAgentOversea = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{XrpcVersionOversea}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
/// </summary>
|
||||
public const string XrpcVersion = "2.49.1";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab Rpc 版本
|
||||
/// </summary>
|
||||
public const string XrpcVersionOversea = "2.30.0";
|
||||
|
||||
// https://github.com/UIGF-org/Hoyolab.Salt
|
||||
private static readonly ImmutableDictionary<SaltType, string> SaltsInner = new Dictionary<SaltType, string>()
|
||||
{
|
||||
[SaltType.K2] = "egBrFMO1BPBG0UX5XOuuwMRLZKwTVKRV",
|
||||
[SaltType.LK2] = "DG8lqMyc9gquwAUFc7zBS62ijQRX9XF7",
|
||||
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
|
||||
// This SALT is not reliable
|
||||
[SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
private static string? deviceId;
|
||||
|
||||
/// <summary>
|
||||
/// 米游社设备Id
|
||||
/// </summary>
|
||||
public static string DeviceId { get => deviceId ??= Guid.NewGuid().ToString(); }
|
||||
|
||||
/// <summary>
|
||||
/// 盐
|
||||
/// </summary>
|
||||
public static ImmutableDictionary<SaltType, string> Salts { get => SaltsInner; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HoyolabOptions Value { get => this; }
|
||||
}
|
||||
135
src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs
Normal file
135
src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using System.IO;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃选项
|
||||
/// 存储环境相关的选项
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class HutaoOptions : IOptions<HutaoOptions>
|
||||
{
|
||||
private readonly ILogger<HutaoOptions> logger;
|
||||
|
||||
private readonly bool isWebView2Supported;
|
||||
private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃选项
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
public HutaoOptions(ILogger<HutaoOptions> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
|
||||
DataFolder = GetDataFolderPath();
|
||||
LocalCache = ApplicationData.Current.LocalCacheFolder.Path;
|
||||
InstalledLocation = Package.Current.InstalledLocation.Path;
|
||||
FamilyName = Package.Current.Id.FamilyName;
|
||||
|
||||
Version = Package.Current.Id.Version.ToVersion();
|
||||
UserAgent = $"Snap Hutao/{Version}";
|
||||
|
||||
DeviceId = GetUniqueUserId();
|
||||
DetectWebView2Environment(ref webView2Version, ref isWebView2Supported);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前版本
|
||||
/// </summary>
|
||||
public Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准UA
|
||||
/// </summary>
|
||||
public string UserAgent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据文件夹路径
|
||||
/// </summary>
|
||||
public string DataFolder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 安装位置
|
||||
/// </summary>
|
||||
public string InstalledLocation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 本地缓存
|
||||
/// </summary>
|
||||
public string LocalCache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 包家族名称
|
||||
/// </summary>
|
||||
public string FamilyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 设备Id
|
||||
/// </summary>
|
||||
public string DeviceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// WebView2 版本
|
||||
/// </summary>
|
||||
public string WebView2Version { get => webView2Version; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持 WebView2
|
||||
/// </summary>
|
||||
public bool IsWebView2Supported { get => isWebView2Supported; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HutaoOptions Value { get => this; }
|
||||
|
||||
private static string GetDataFolderPath()
|
||||
{
|
||||
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(preferredPath) && Directory.Exists(preferredPath))
|
||||
{
|
||||
return preferredPath;
|
||||
}
|
||||
|
||||
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static string GetUniqueUserId()
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
}
|
||||
|
||||
private void DetectWebView2Environment(ref string webView2Version, ref bool isWebView2Supported)
|
||||
{
|
||||
try
|
||||
{
|
||||
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
|
||||
isWebView2Supported = true;
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
logger.LogError(ex, "WebView2 Runtime not installed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,20 +17,21 @@ internal static class Clipboard
|
||||
/// 从剪贴板文本中反序列化
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标类型</typeparam>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <returns>实例</returns>
|
||||
public static async Task<T?> DeserializeTextAsync<T>(JsonSerializerOptions options)
|
||||
public static async Task<T?> DeserializeTextAsync<T>(IServiceProvider serviceProvider)
|
||||
where T : class
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
|
||||
|
||||
if (view.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
string json = await view.GetTextAsync();
|
||||
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
return JsonSerializer.Deserialize<T>(json, options);
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
return JsonSerializer.Deserialize<T>(json, serviceProvider.GetRequiredService<JsonSerializerOptions>());
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -72,4 +73,4 @@ internal static class Clipboard
|
||||
SetBitmap(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.DataTransfer;
|
||||
|
||||
/// <summary>
|
||||
/// 剪贴板互操作
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IClipboardInterop))]
|
||||
internal sealed class ClipboardInterop : IClipboardInterop
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的剪贴板互操作对象
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public ClipboardInterop(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T?> DeserializeTextAsync<T>()
|
||||
where T : class
|
||||
{
|
||||
return Clipboard.DeserializeTextAsync<T>(serviceProvider);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetBitmap(IRandomAccessStream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetBitmap(stream);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.DataTransfer;
|
||||
|
||||
/// <summary>
|
||||
/// 剪贴板互操作
|
||||
/// </summary>
|
||||
internal interface IClipboardInterop
|
||||
{
|
||||
/// <summary>
|
||||
/// 从剪贴板文本中反序列化
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标类型</typeparam>
|
||||
/// <returns>实例</returns>
|
||||
Task<T?> DeserializeTextAsync<T>()
|
||||
where T : class;
|
||||
|
||||
/// <summary>
|
||||
/// 设置位图
|
||||
/// </summary>
|
||||
/// <param name="stream">图片流</param>
|
||||
/// <returns>是否设置成功</returns>
|
||||
bool SetBitmap(IRandomAccessStream stream);
|
||||
}
|
||||
@@ -54,4 +54,58 @@ internal sealed class StreamCopyWorker
|
||||
}
|
||||
while (bytesRead > 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 针对特定类型的流复制器
|
||||
/// </summary>
|
||||
/// <typeparam name="TStatus">进度类型</typeparam>
|
||||
[SuppressMessage("", "SA1402")]
|
||||
internal sealed class StreamCopyWorker<TStatus>
|
||||
{
|
||||
private readonly Stream source;
|
||||
private readonly Stream destination;
|
||||
private readonly int bufferSize;
|
||||
private readonly Func<long, TStatus> statusFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的流复制器
|
||||
/// </summary>
|
||||
/// <param name="source">源</param>
|
||||
/// <param name="destination">目标</param>
|
||||
/// <param name="statusFactory">状态工厂</param>
|
||||
/// <param name="totalBytes">总字节</param>
|
||||
/// <param name="bufferSize">字节尺寸</param>
|
||||
public StreamCopyWorker(Stream source, Stream destination, Func<long, TStatus> statusFactory, int bufferSize = 81920)
|
||||
{
|
||||
Verify.Operation(source.CanRead, "Source Stream can't read");
|
||||
Verify.Operation(destination.CanWrite, "Destination Stream can't write");
|
||||
|
||||
this.source = source;
|
||||
this.destination = destination;
|
||||
this.statusFactory = statusFactory;
|
||||
this.bufferSize = bufferSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步复制
|
||||
/// </summary>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <returns>任务</returns>
|
||||
public async Task CopyAsync(IProgress<TStatus> progress)
|
||||
{
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
Memory<byte> buffer = new byte[bufferSize];
|
||||
|
||||
do
|
||||
{
|
||||
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
}
|
||||
while (bytesRead > 0);
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ internal sealed class TempFile : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="file">源文件</param>
|
||||
/// <returns>临时文件</returns>
|
||||
public static TempFile? CreateCopyFrom(string file)
|
||||
public static TempFile? CopyFrom(string file)
|
||||
{
|
||||
TempFile temporaryFile = new();
|
||||
try
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Core.Json.Annotation;
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
internal class JsonEnumAttribute : Attribute
|
||||
{
|
||||
private static readonly Type ConfigurableEnumConverterType = typeof(ConfigurableEnumConverter<>);
|
||||
private static readonly Type UnsafeEnumConverterType = typeof(UnsafeEnumConverter<>);
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的Json枚举声明
|
||||
@@ -52,7 +52,7 @@ internal class JsonEnumAttribute : Attribute
|
||||
/// <returns>Json转换器</returns>
|
||||
internal JsonConverter CreateConverter(JsonPropertyInfo info)
|
||||
{
|
||||
Type converterType = ConfigurableEnumConverterType.MakeGenericType(info.PropertyType);
|
||||
Type converterType = UnsafeEnumConverterType.MakeGenericType(info.PropertyType);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType, ReadAs, WriteAs)!;
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,14 @@ namespace Snap.Hutao.Core.Json.Annotation;
|
||||
internal enum JsonSerializeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Int32
|
||||
/// 数字
|
||||
/// </summary>
|
||||
Int32,
|
||||
Number,
|
||||
|
||||
/// <summary>
|
||||
/// 字符串包裹的数字
|
||||
/// </summary>
|
||||
Int32AsString,
|
||||
NumberString,
|
||||
|
||||
/// <summary>
|
||||
/// 名称字符串
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExpressionService;
|
||||
using Snap.Hutao.Core.Json.Annotation;
|
||||
|
||||
namespace Snap.Hutao.Core.Json.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举转换器
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">枚举的类型</typeparam>
|
||||
[HighQuality]
|
||||
internal sealed class ConfigurableEnumConverter<TEnum> : JsonConverter<TEnum>
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
private readonly JsonSerializeType readAs;
|
||||
private readonly JsonSerializeType writeAs;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的枚举转换器
|
||||
/// </summary>
|
||||
/// <param name="readAs">读取</param>
|
||||
/// <param name="writeAs">写入</param>
|
||||
public ConfigurableEnumConverter(JsonSerializeType readAs, JsonSerializeType writeAs)
|
||||
{
|
||||
this.readAs = readAs;
|
||||
this.writeAs = writeAs;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (readAs == JsonSerializeType.Int32)
|
||||
{
|
||||
return CastTo<TEnum>.From(reader.GetInt32());
|
||||
}
|
||||
|
||||
if (reader.GetString() is string str)
|
||||
{
|
||||
return Enum.Parse<TEnum>(str);
|
||||
}
|
||||
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
|
||||
{
|
||||
switch (writeAs)
|
||||
{
|
||||
case JsonSerializeType.Int32:
|
||||
writer.WriteNumberValue(CastTo<int>.From(value));
|
||||
break;
|
||||
case JsonSerializeType.Int32AsString:
|
||||
writer.WriteStringValue(value.ToString("D"));
|
||||
break;
|
||||
default:
|
||||
writer.WriteStringValue(value.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Json.Annotation;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Json.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举转换器
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">枚举的类型</typeparam>
|
||||
[HighQuality]
|
||||
internal sealed class UnsafeEnumConverter<TEnum> : JsonConverter<TEnum>
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
private readonly TypeCode enumTypeCode = Type.GetTypeCode(typeof(TEnum));
|
||||
|
||||
private readonly JsonSerializeType readAs;
|
||||
private readonly JsonSerializeType writeAs;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的枚举转换器
|
||||
/// </summary>
|
||||
/// <param name="readAs">读取</param>
|
||||
/// <param name="writeAs">写入</param>
|
||||
public UnsafeEnumConverter(JsonSerializeType readAs, JsonSerializeType writeAs)
|
||||
{
|
||||
this.readAs = readAs;
|
||||
this.writeAs = writeAs;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConverTEnum, JsonSerializerOptions options)
|
||||
{
|
||||
if (readAs == JsonSerializeType.Number)
|
||||
{
|
||||
return GetEnum(ref reader, enumTypeCode);
|
||||
}
|
||||
|
||||
if (reader.GetString() is string str)
|
||||
{
|
||||
return Enum.Parse<TEnum>(str);
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
|
||||
{
|
||||
switch (writeAs)
|
||||
{
|
||||
case JsonSerializeType.Number:
|
||||
WriteEnumValue(writer, value, enumTypeCode);
|
||||
break;
|
||||
case JsonSerializeType.NumberString:
|
||||
writer.WriteStringValue(value.ToString("D"));
|
||||
break;
|
||||
default:
|
||||
writer.WriteStringValue(value.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static TEnum GetEnum(ref Utf8JsonReader reader, TypeCode typeCode)
|
||||
{
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Int32:
|
||||
if (reader.TryGetInt32(out int int32))
|
||||
{
|
||||
return Unsafe.As<int, TEnum>(ref int32);
|
||||
}
|
||||
|
||||
break;
|
||||
case TypeCode.UInt32:
|
||||
if (reader.TryGetUInt32(out uint uint32))
|
||||
{
|
||||
return Unsafe.As<uint, TEnum>(ref uint32);
|
||||
}
|
||||
|
||||
break;
|
||||
case TypeCode.UInt64:
|
||||
if (reader.TryGetUInt64(out ulong uint64))
|
||||
{
|
||||
return Unsafe.As<ulong, TEnum>(ref uint64);
|
||||
}
|
||||
|
||||
break;
|
||||
case TypeCode.Int64:
|
||||
if (reader.TryGetInt64(out long int64))
|
||||
{
|
||||
return Unsafe.As<long, TEnum>(ref int64);
|
||||
}
|
||||
|
||||
break;
|
||||
case TypeCode.Byte:
|
||||
if (reader.TryGetByte(out byte byte8))
|
||||
{
|
||||
return Unsafe.As<byte, TEnum>(ref byte8);
|
||||
}
|
||||
|
||||
break;
|
||||
case TypeCode.Int16:
|
||||
if (reader.TryGetInt16(out short int16))
|
||||
{
|
||||
return Unsafe.As<short, TEnum>(ref int16);
|
||||
}
|
||||
|
||||
break;
|
||||
case TypeCode.UInt16:
|
||||
if (reader.TryGetUInt16(out ushort uint16))
|
||||
{
|
||||
return Unsafe.As<ushort, TEnum>(ref uint16);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
private static void WriteEnumValue(Utf8JsonWriter writer, TEnum value, TypeCode typeCode)
|
||||
{
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Int32:
|
||||
writer.WriteNumberValue(Unsafe.As<TEnum, int>(ref value));
|
||||
break;
|
||||
case TypeCode.UInt32:
|
||||
writer.WriteNumberValue(Unsafe.As<TEnum, uint>(ref value));
|
||||
break;
|
||||
case TypeCode.UInt64:
|
||||
writer.WriteNumberValue(Unsafe.As<TEnum, ulong>(ref value));
|
||||
break;
|
||||
case TypeCode.Int64:
|
||||
writer.WriteNumberValue(Unsafe.As<TEnum, long>(ref value));
|
||||
break;
|
||||
case TypeCode.Int16:
|
||||
writer.WriteNumberValue(Unsafe.As<TEnum, short>(ref value));
|
||||
break;
|
||||
case TypeCode.UInt16:
|
||||
writer.WriteNumberValue(Unsafe.As<TEnum, ushort>(ref value));
|
||||
break;
|
||||
case TypeCode.Byte:
|
||||
writer.WriteNumberValue(Unsafe.As<TEnum, byte>(ref value));
|
||||
break;
|
||||
case TypeCode.SByte:
|
||||
writer.WriteNumberValue(Unsafe.As<TEnum, sbyte>(ref value));
|
||||
break;
|
||||
default:
|
||||
throw new JsonException();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/Snap.Hutao/Snap.Hutao/Core/Json/JsonOptions.cs
Normal file
33
src/Snap.Hutao/Snap.Hutao/Core/Json/JsonOptions.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Snap.Hutao.Core.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Json 选项
|
||||
/// </summary>
|
||||
internal static class JsonOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认的Json序列化选项
|
||||
/// </summary>
|
||||
public static readonly JsonSerializerOptions Default = new()
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
|
||||
{
|
||||
Modifiers =
|
||||
{
|
||||
JsonTypeInfoResolvers.ResolveEnumType,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -32,9 +32,8 @@ internal static class JsonTypeInfoResolvers
|
||||
if (property.AttributeProvider is System.Reflection.ICustomAttributeProvider provider)
|
||||
{
|
||||
object[] attributes = provider.GetCustomAttributes(JsonEnumAttributeType, false);
|
||||
if (attributes.Length == 1)
|
||||
if (attributes.SingleOrDefault() is JsonEnumAttribute attr)
|
||||
{
|
||||
JsonEnumAttribute attr = (JsonEnumAttribute)attributes[0];
|
||||
property.CustomConverter = attr.CreateConverter(property);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
[HighQuality]
|
||||
internal static class Activation
|
||||
{
|
||||
// TODO: make this class a dependency
|
||||
|
||||
/// <summary>
|
||||
/// 操作
|
||||
/// </summary>
|
||||
@@ -40,12 +42,14 @@ internal static class Activation
|
||||
/// <summary>
|
||||
/// 从剪贴板导入成就
|
||||
/// </summary>
|
||||
public const string ImportUIAFFromClipBoard = nameof(ImportUIAFFromClipBoard);
|
||||
public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard);
|
||||
|
||||
private const string CategoryAchievement = "achievement";
|
||||
private const string CategoryDailyNote = "dailynote";
|
||||
private const string UrlActionImport = "/import";
|
||||
private const string UrlActionRefresh = "/refresh";
|
||||
|
||||
private static readonly WeakReference<MainWindow> MainWindowReference = new(default!);
|
||||
private static readonly SemaphoreSlim ActivateSemaphore = new(1);
|
||||
|
||||
/// <summary>
|
||||
@@ -72,7 +76,7 @@ internal static class Activation
|
||||
/// <returns>任务</returns>
|
||||
public static async ValueTask RestartAsElevatedAsync()
|
||||
{
|
||||
if (GetElevated())
|
||||
if (!GetElevated())
|
||||
{
|
||||
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
|
||||
Process.GetCurrentProcess().Kill();
|
||||
@@ -182,12 +186,16 @@ internal static class Activation
|
||||
|
||||
private static async Task WaitMainWindowAsync()
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
IServiceProvider serviceProvider = Ioc.Default;
|
||||
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
serviceProvider.GetRequiredService<MainWindow>().Activate();
|
||||
MainWindowReference.SetTarget(serviceProvider.GetRequiredService<MainWindow>());
|
||||
|
||||
await serviceProvider.GetRequiredService<IInfoBarService>().WaitInitializationAsync().ConfigureAwait(false);
|
||||
await serviceProvider
|
||||
.GetRequiredService<IInfoBarService>()
|
||||
.WaitInitializationAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<IMetadataService>()
|
||||
@@ -241,9 +249,10 @@ internal static class Activation
|
||||
{
|
||||
case UrlActionImport:
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ITaskContext taskContext = Ioc.Default.GetRequiredService<ITaskContext>();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipBoard);
|
||||
INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipboard);
|
||||
await Ioc.Default
|
||||
.GetRequiredService<INavigationService>()
|
||||
.NavigateAsync<View.Page.AchievementPage>(navigationAwaiter, true)
|
||||
@@ -262,7 +271,7 @@ internal static class Activation
|
||||
{
|
||||
await Ioc.Default
|
||||
.GetRequiredService<IDailyNoteService>()
|
||||
.RefreshDailyNotesAsync(true)
|
||||
.RefreshDailyNotesAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Check if it's redirected.
|
||||
@@ -279,16 +288,19 @@ internal static class Activation
|
||||
|
||||
private static async Task HandleLaunchGameActionAsync(string? uid = null)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IMemoryCache>().Set(ViewModel.Game.LaunchGameViewModel.DesiredUid, uid);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
IServiceProvider serviceProvider = Ioc.Default;
|
||||
IMemoryCache memoryCache = serviceProvider.GetRequiredService<IMemoryCache>();
|
||||
memoryCache.Set(ViewModel.Game.LaunchGameViewModel.DesiredUid, uid);
|
||||
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
if (!MainWindow.IsPresent)
|
||||
if (!MainWindowReference.TryGetTarget(out _))
|
||||
{
|
||||
_ = Ioc.Default.GetRequiredService<LaunchGameWindow>();
|
||||
_ = serviceProvider.GetRequiredService<LaunchGameWindow>();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Ioc.Default
|
||||
await serviceProvider
|
||||
.GetRequiredService<INavigationService>()
|
||||
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -39,6 +39,8 @@ internal static class SettingKeys
|
||||
/// </summary>
|
||||
public const string PassportPassword = "PassportPassword";
|
||||
|
||||
#region StaticResource
|
||||
|
||||
/// <summary>
|
||||
/// 静态资源合约
|
||||
/// 新增合约时 请注意
|
||||
@@ -66,4 +68,5 @@ internal static class SettingKeys
|
||||
/// 静态资源合约V5 刷新 AvatarIcon
|
||||
/// </summary>
|
||||
public const string StaticResourceV5Contract = "StaticResourceV5Contract";
|
||||
#endregion
|
||||
}
|
||||
@@ -28,4 +28,6 @@ internal static class StringLiterals
|
||||
/// False
|
||||
/// </summary>
|
||||
public const string False = "False";
|
||||
|
||||
public const string CRLF = "\r\n";
|
||||
}
|
||||
@@ -18,7 +18,7 @@ internal class ConcurrentCancellationTokenSource
|
||||
/// 注册取消令牌
|
||||
/// </summary>
|
||||
/// <returns>取消令牌</returns>
|
||||
public CancellationToken Register()
|
||||
public CancellationToken CancelPreviousOne()
|
||||
{
|
||||
source.Cancel();
|
||||
source = new();
|
||||
|
||||
30
src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs
Normal file
30
src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 任务上下文
|
||||
/// </summary>
|
||||
internal interface ITaskContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 在主线程上同步等待执行操作
|
||||
/// </summary>
|
||||
/// <param name="action">操作</param>
|
||||
void InvokeOnMainThread(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// 异步切换到 后台线程
|
||||
/// </summary>
|
||||
/// <remarks>使用 <see cref="SwitchToMainThreadAsync"/> 异步切换到 主线程</remarks>
|
||||
/// <returns>等待体</returns>
|
||||
ThreadPoolSwitchOperation SwitchToBackgroundAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步切换到 主线程
|
||||
/// </summary>
|
||||
/// <remarks>使用 <see cref="SwitchToBackgroundAsync"/> 异步切换到 后台线程</remarks>
|
||||
/// <returns>等待体</returns>
|
||||
DispatherQueueSwitchOperation SwitchToMainThreadAsync();
|
||||
}
|
||||
@@ -29,19 +29,22 @@ internal static class SemaphoreSlimExtension
|
||||
|
||||
return new SemaphoreSlimReleaser(semaphoreSlim);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct SemaphoreSlimReleaser : IDisposable
|
||||
[SuppressMessage("", "SA1201")]
|
||||
[SuppressMessage("", "SA1400")]
|
||||
[SuppressMessage("", "SA1600")]
|
||||
file readonly struct SemaphoreSlimReleaser : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim semaphoreSlim;
|
||||
|
||||
public SemaphoreSlimReleaser(SemaphoreSlim semaphoreSlim)
|
||||
{
|
||||
private readonly SemaphoreSlim semaphoreSlim;
|
||||
this.semaphoreSlim = semaphoreSlim;
|
||||
}
|
||||
|
||||
public SemaphoreSlimReleaser(SemaphoreSlim semaphoreSlim)
|
||||
{
|
||||
this.semaphoreSlim = semaphoreSlim;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
51
src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs
Normal file
51
src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 任务上下文
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton, typeof(ITaskContext))]
|
||||
internal sealed class TaskContext : ITaskContext
|
||||
{
|
||||
private readonly DispatcherQueue dispatcherQueue;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的任务上下文
|
||||
/// </summary>
|
||||
public TaskContext()
|
||||
{
|
||||
dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
DispatcherQueueSynchronizationContext context = new(dispatcherQueue);
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ThreadPoolSwitchOperation SwitchToBackgroundAsync()
|
||||
{
|
||||
return new(dispatcherQueue);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DispatherQueueSwitchOperation SwitchToMainThreadAsync()
|
||||
{
|
||||
return new(dispatcherQueue);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void InvokeOnMainThread(Action action)
|
||||
{
|
||||
if (dispatcherQueue!.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatcherQueue.Invoke(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Threading;
|
||||
[HighQuality]
|
||||
[SuppressMessage("", "VSTHRD003")]
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
internal static class TaskExtensions
|
||||
internal static class TaskExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 安全的触发任务
|
||||
@@ -21,6 +21,10 @@ internal static class TaskExtensions
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 线程帮助类
|
||||
/// </summary>
|
||||
internal static class ThreadHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 主线程队列
|
||||
/// </summary>
|
||||
private static volatile DispatcherQueue? dispatcherQueue;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
DispatcherQueueSynchronizationContext context = new(dispatcherQueue);
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用此静态方法以 异步切换到 后台线程
|
||||
/// </summary>
|
||||
/// <remarks>使用 <see cref="SwitchToMainThreadAsync"/> 异步切换到 主线程</remarks>
|
||||
/// <returns>等待体</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ThreadPoolSwitchOperation SwitchToBackgroundAsync()
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用此静态方法以 异步切换到 主线程
|
||||
/// </summary>
|
||||
/// <remarks>使用 <see cref="SwitchToBackgroundAsync"/> 异步切换到 后台线程</remarks>
|
||||
/// <returns>等待体</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static DispatherQueueSwitchOperation SwitchToMainThreadAsync()
|
||||
{
|
||||
return new(dispatcherQueue!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在主线程上同步等待执行操作
|
||||
/// </summary>
|
||||
/// <param name="action">操作</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeOnMainThread(Action action)
|
||||
{
|
||||
if (dispatcherQueue!.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatcherQueue.Invoke(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Snap.Hutao.Core.Threading.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
@@ -12,9 +13,26 @@ namespace Snap.Hutao.Core.Threading;
|
||||
internal readonly struct ThreadPoolSwitchOperation : IAwaitable<ThreadPoolSwitchOperation>, IAwaiter, ICriticalAwaiter
|
||||
{
|
||||
private static readonly WaitCallback WaitCallbackRunAction = RunAction;
|
||||
private readonly DispatcherQueue dispatherQueue;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的线程池切换操作
|
||||
/// </summary>
|
||||
/// <param name="dispatherQueue">主线程队列</param>
|
||||
public ThreadPoolSwitchOperation(DispatcherQueue dispatherQueue)
|
||||
{
|
||||
this.dispatherQueue = dispatherQueue;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCompleted { get => false; }
|
||||
public bool IsCompleted
|
||||
{
|
||||
get
|
||||
{
|
||||
// 如果已经处于后台就不再切换新的线程
|
||||
return !dispatherQueue.HasThreadAccess;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ThreadPoolSwitchOperation GetAwaiter()
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 用于包装异步操作的结果
|
||||
/// 结构类型,在栈上分配
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">结果类型</typeparam>
|
||||
/// <typeparam name="TValue">值类型</typeparam>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 结果扩展
|
||||
/// </summary>
|
||||
internal static class ValueResultExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试获取结果的值
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">值的类型</typeparam>
|
||||
/// <param name="valueResult">结果</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>是否获取成功</returns>
|
||||
public static bool TryGetValue<TValue>(this in ValueResult<bool, TValue> valueResult,[NotNullWhen(true)] out TValue value)
|
||||
{
|
||||
value = valueResult.Value;
|
||||
return valueResult.IsOk;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace Snap.Hutao.Core;
|
||||
/// 必须为抽象类才能使用泛型日志器
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Obsolete("Use HutaoOptions instead")]
|
||||
internal abstract class WebView2Helper
|
||||
{
|
||||
private static bool hasEverDetected;
|
||||
|
||||
@@ -11,9 +11,9 @@ using Snap.Hutao.Message;
|
||||
using Snap.Hutao.Service;
|
||||
using Snap.Hutao.Win32;
|
||||
using System.IO;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
@@ -25,22 +25,19 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessage>
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
where TWindow : Window, IWindowOptionsSource
|
||||
{
|
||||
private readonly WindowOptions<TWindow> options;
|
||||
|
||||
private readonly TWindow window;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<ExtendedWindow<TWindow>> logger;
|
||||
private readonly WindowSubclass<TWindow> subclass;
|
||||
|
||||
private ExtendedWindow(TWindow window, FrameworkElement titleBar, IServiceProvider serviceProvider)
|
||||
private ExtendedWindow(TWindow window, IServiceProvider serviceProvider)
|
||||
{
|
||||
options = new(window, titleBar);
|
||||
subclass = new(options);
|
||||
|
||||
logger = serviceProvider.GetRequiredService<ILogger<ExtendedWindow<TWindow>>>();
|
||||
this.window = window;
|
||||
this.serviceProvider = serviceProvider;
|
||||
|
||||
subclass = new(window);
|
||||
|
||||
InitializeWindow();
|
||||
}
|
||||
|
||||
@@ -52,7 +49,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
/// <returns>实例</returns>
|
||||
public static ExtendedWindow<TWindow> Initialize(TWindow window, IServiceProvider serviceProvider)
|
||||
{
|
||||
return new(window, window.TitleBar, serviceProvider);
|
||||
return new(window, serviceProvider);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -63,16 +60,19 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
|
||||
private void InitializeWindow()
|
||||
{
|
||||
options.AppWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version);
|
||||
options.AppWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico"));
|
||||
HutaoOptions hutaoOptions = serviceProvider.GetRequiredService<HutaoOptions>();
|
||||
|
||||
WindowOptions options = window.WindowOptions;
|
||||
window.AppWindow.Title = string.Format(SH.AppNameAndVersion, hutaoOptions.Version);
|
||||
window.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico"));
|
||||
ExtendsContentIntoTitleBar();
|
||||
|
||||
Persistence.RecoverOrInit(options);
|
||||
Persistence.RecoverOrInit(window);
|
||||
UpdateImmersiveDarkMode(options.TitleBar, default!);
|
||||
|
||||
// appWindow.Show(true);
|
||||
// appWindow.Show can't bring window to top.
|
||||
// options.Window.Activate();
|
||||
window.Activate();
|
||||
Persistence.BringToForeground(options.Hwnd);
|
||||
|
||||
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||
@@ -83,7 +83,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
|
||||
serviceProvider.GetRequiredService<IMessenger>().Register(this);
|
||||
|
||||
options.Window.Closed += OnWindowClosed;
|
||||
window.Closed += OnWindowClosed;
|
||||
options.TitleBar.ActualThemeChanged += UpdateImmersiveDarkMode;
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
|
||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
if (options.Window.PersistSize)
|
||||
if (window.WindowOptions.PersistSize)
|
||||
{
|
||||
Persistence.Save(options);
|
||||
Persistence.Save(window);
|
||||
}
|
||||
|
||||
subclass?.Dispose();
|
||||
@@ -107,15 +107,16 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
|
||||
private void ExtendsContentIntoTitleBar()
|
||||
{
|
||||
WindowOptions options = window.WindowOptions;
|
||||
if (options.UseLegacyDragBarImplementation)
|
||||
{
|
||||
// use normal Window method to extend.
|
||||
options.Window.ExtendsContentIntoTitleBar = true;
|
||||
options.Window.SetTitleBar(options.TitleBar);
|
||||
window.ExtendsContentIntoTitleBar = true;
|
||||
window.SetTitleBar(options.TitleBar);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppWindowTitleBar appTitleBar = options.AppWindow.TitleBar;
|
||||
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
|
||||
appTitleBar.IconShowOptions = IconShowOptions.HideIconAndSystemMenu;
|
||||
appTitleBar.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
@@ -128,7 +129,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
|
||||
private void UpdateSystemBackdrop(BackdropType backdropType)
|
||||
{
|
||||
options.Window.SystemBackdrop = backdropType switch
|
||||
window.SystemBackdrop = backdropType switch
|
||||
{
|
||||
BackdropType.MicaAlt => new MicaBackdrop() { Kind = MicaKind.BaseAlt },
|
||||
BackdropType.Mica => new MicaBackdrop() { Kind = MicaKind.Base },
|
||||
@@ -139,17 +140,17 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
|
||||
private void UpdateTitleButtonColor()
|
||||
{
|
||||
AppWindowTitleBar appTitleBar = options.AppWindow.TitleBar;
|
||||
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
|
||||
|
||||
appTitleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
appTitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
|
||||
|
||||
App app = serviceProvider.GetRequiredService<App>();
|
||||
IAppResourceProvider resourceProvider = serviceProvider.GetRequiredService<IAppResourceProvider>();
|
||||
|
||||
Color systemBaseLowColor = (Color)app.Resources["SystemBaseLowColor"];
|
||||
Color systemBaseLowColor = resourceProvider.GetResource<Color>("SystemBaseLowColor");
|
||||
appTitleBar.ButtonHoverBackgroundColor = systemBaseLowColor;
|
||||
|
||||
Color systemBaseMediumLowColor = (Color)app.Resources["SystemBaseMediumLowColor"];
|
||||
Color systemBaseMediumLowColor = resourceProvider.GetResource<Color>("SystemBaseMediumLowColor");
|
||||
appTitleBar.ButtonPressedBackgroundColor = systemBaseMediumLowColor;
|
||||
|
||||
// The Foreground doesn't accept Alpha channel. So we translate it to gray.
|
||||
@@ -157,7 +158,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
byte result = (byte)((systemBaseMediumLowColor.A / 255.0) * light);
|
||||
appTitleBar.ButtonInactiveForegroundColor = Color.FromArgb(0xFF, result, result, result);
|
||||
|
||||
Color systemBaseHighColor = (Color)app.Resources["SystemBaseHighColor"];
|
||||
Color systemBaseHighColor = resourceProvider.GetResource<Color>("SystemBaseHighColor");
|
||||
appTitleBar.ButtonForegroundColor = systemBaseHighColor;
|
||||
appTitleBar.ButtonHoverForegroundColor = systemBaseHighColor;
|
||||
appTitleBar.ButtonPressedForegroundColor = systemBaseHighColor;
|
||||
@@ -166,12 +167,12 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
private unsafe void UpdateImmersiveDarkMode(FrameworkElement titleBar, object discard)
|
||||
{
|
||||
BOOL isDarkMode = Control.Theme.ThemeHelper.IsDarkMode(titleBar.ActualTheme);
|
||||
DwmSetWindowAttribute(options.Hwnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, &isDarkMode, unchecked((uint)sizeof(BOOL)));
|
||||
DwmSetWindowAttribute(window.WindowOptions.Hwnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, &isDarkMode, unchecked((uint)sizeof(BOOL)));
|
||||
}
|
||||
|
||||
private void UpdateDragRectangles(bool isFlyoutOpened = false)
|
||||
{
|
||||
AppWindowTitleBar appTitleBar = options.AppWindow.TitleBar;
|
||||
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
|
||||
|
||||
if (isFlyoutOpened)
|
||||
{
|
||||
@@ -180,6 +181,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowOptions options = window.WindowOptions;
|
||||
double scale = Persistence.GetScaleForWindowHandle(options.Hwnd);
|
||||
|
||||
// 48 is the navigation button leftInset
|
||||
@@ -187,9 +189,9 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
appTitleBar.SetDragRectangles(dragRect.ToArray());
|
||||
|
||||
// workaround for https://github.com/microsoft/WindowsAppSDK/issues/2976
|
||||
SizeInt32 size = options.AppWindow.ClientSize;
|
||||
SizeInt32 size = window.AppWindow.ClientSize;
|
||||
size.Height -= (int)(31 * scale);
|
||||
options.AppWindow.ResizeClient(size);
|
||||
window.AppWindow.ResizeClient(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
@@ -11,22 +9,12 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// 为扩展窗体提供必要的选项
|
||||
/// </summary>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal interface IExtendedWindowSource
|
||||
internal interface IWindowOptionsSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 提供的标题栏
|
||||
/// 窗体选项
|
||||
/// </summary>
|
||||
FrameworkElement TitleBar { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否持久化尺寸
|
||||
/// </summary>
|
||||
bool PersistSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始大小
|
||||
/// </summary>
|
||||
SizeInt32 InitSize { get; }
|
||||
WindowOptions WindowOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理最大最小信息
|
||||
@@ -22,44 +22,45 @@ internal static class Persistence
|
||||
/// <summary>
|
||||
/// 设置窗体位置
|
||||
/// </summary>
|
||||
/// <param name="options">选项</param>
|
||||
/// <param name="window">选项窗口param>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
public static void RecoverOrInit<TWindow>(in WindowOptions<TWindow> options)
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
public static void RecoverOrInit<TWindow>(TWindow window)
|
||||
where TWindow : Window, IWindowOptionsSource
|
||||
{
|
||||
WindowOptions options = window.WindowOptions;
|
||||
// Set first launch size.
|
||||
double scale = GetScaleForWindowHandle(options.Hwnd);
|
||||
SizeInt32 transformedSize = options.Window.InitSize.Scale(scale);
|
||||
SizeInt32 transformedSize = options.InitSize.Scale(scale);
|
||||
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
||||
|
||||
if (options.Window.PersistSize)
|
||||
if (options.PersistSize)
|
||||
{
|
||||
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedRect.Size() >= options.Window.InitSize.Size())
|
||||
if (persistedRect.Size() >= options.InitSize.Size())
|
||||
{
|
||||
rect = persistedRect;
|
||||
}
|
||||
}
|
||||
|
||||
TransformToCenterScreen(ref rect);
|
||||
options.AppWindow.MoveAndResize(rect);
|
||||
window.AppWindow.MoveAndResize(rect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存窗体的位置
|
||||
/// </summary>
|
||||
/// <param name="options">选项</param>
|
||||
/// <param name="window">窗口</param>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
public static void Save<TWindow>(in WindowOptions<TWindow> options)
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
public static void Save<TWindow>(TWindow window)
|
||||
where TWindow : Window, IWindowOptionsSource
|
||||
{
|
||||
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
|
||||
GetWindowPlacement(options.Hwnd, ref windowPlacement);
|
||||
GetWindowPlacement(window.WindowOptions.Hwnd, ref windowPlacement);
|
||||
|
||||
// prevent save value when we are maximized.
|
||||
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)options.AppWindow.GetRect());
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.Foundation;
|
||||
using WinRT.Interop;
|
||||
|
||||
@@ -13,29 +13,28 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// Window 选项
|
||||
/// </summary>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal readonly struct WindowOptions<TWindow>
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
internal readonly struct WindowOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 窗体句柄
|
||||
/// </summary>
|
||||
public readonly HWND Hwnd;
|
||||
|
||||
/// <summary>
|
||||
/// AppWindow
|
||||
/// </summary>
|
||||
public readonly AppWindow AppWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 窗体
|
||||
/// </summary>
|
||||
public readonly TWindow Window;
|
||||
|
||||
/// <summary>
|
||||
/// 标题栏元素
|
||||
/// </summary>
|
||||
public readonly FrameworkElement TitleBar;
|
||||
|
||||
/// <summary>
|
||||
/// 初始大小
|
||||
/// </summary>
|
||||
public readonly SizeInt32 InitSize;
|
||||
|
||||
/// <summary>
|
||||
/// 是否持久化尺寸
|
||||
/// </summary>
|
||||
public readonly bool PersistSize;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用 Win UI 3 自带的拓展标题栏实现
|
||||
/// </summary>
|
||||
@@ -46,11 +45,13 @@ internal readonly struct WindowOptions<TWindow>
|
||||
/// </summary>
|
||||
/// <param name="window">窗体</param>
|
||||
/// <param name="titleBar">标题栏</param>
|
||||
public WindowOptions(TWindow window, FrameworkElement titleBar)
|
||||
/// <param name="initSize">初始尺寸</param>
|
||||
/// <param name="persistSize">持久化尺寸</param>
|
||||
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false)
|
||||
{
|
||||
Window = window;
|
||||
Hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
||||
AppWindow = window.AppWindow;
|
||||
TitleBar = titleBar;
|
||||
InitSize = initSize;
|
||||
PersistSize = persistSize;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
@@ -15,12 +16,12 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
[HighQuality]
|
||||
internal sealed class WindowSubclass<TWindow> : IDisposable
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
where TWindow : Window, IWindowOptionsSource
|
||||
{
|
||||
private const int WindowSubclassId = 101;
|
||||
private const int DragBarSubclassId = 102;
|
||||
|
||||
private readonly WindowOptions<TWindow> options;
|
||||
private readonly TWindow window;
|
||||
|
||||
// We have to explicitly hold a reference to SUBCLASSPROC
|
||||
private SUBCLASSPROC? windowProc;
|
||||
@@ -30,9 +31,9 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
|
||||
/// 构造一个新的窗体子类管理器
|
||||
/// </summary>
|
||||
/// <param name="options">选项</param>
|
||||
public WindowSubclass(in WindowOptions<TWindow> options)
|
||||
public WindowSubclass(TWindow window)
|
||||
{
|
||||
this.options = options;
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,6 +42,8 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
|
||||
/// <returns>是否设置成功</returns>
|
||||
public bool Initialize()
|
||||
{
|
||||
WindowOptions options = window.WindowOptions;
|
||||
|
||||
windowProc = new(OnSubclassProcedure);
|
||||
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0);
|
||||
|
||||
@@ -65,6 +68,8 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
WindowOptions options = window.WindowOptions;
|
||||
|
||||
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
|
||||
windowProc = null;
|
||||
|
||||
@@ -83,14 +88,14 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
|
||||
case WM_GETMINMAXINFO:
|
||||
{
|
||||
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
|
||||
options.Window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_NCRBUTTONDOWN:
|
||||
case WM_NCRBUTTONUP:
|
||||
{
|
||||
return (LRESULT)0; // WM_NULL
|
||||
return (LRESULT)(nint)WM_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +110,7 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
|
||||
case WM_NCRBUTTONDOWN:
|
||||
case WM_NCRBUTTONUP:
|
||||
{
|
||||
return (LRESULT)0; // WM_NULL
|
||||
return (LRESULT)(nint)WM_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举拓展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class EnumExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取枚举的描述
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">枚举的类型</typeparam>
|
||||
/// <param name="enum">枚举值</param>
|
||||
/// <returns>描述</returns>
|
||||
[Obsolete]
|
||||
public static string GetDescription<TEnum>(this TEnum @enum)
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
string enumName = Enum.GetName(@enum)!;
|
||||
FieldInfo? field = @enum.GetType().GetField(enumName);
|
||||
DescriptionAttribute? attr = field?.GetCustomAttribute<DescriptionAttribute>();
|
||||
return attr?.Description ?? enumName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取枚举的描述
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">枚举的类型</typeparam>
|
||||
/// <param name="enum">枚举值</param>
|
||||
/// <returns>描述</returns>
|
||||
[Obsolete]
|
||||
public static string? GetDescriptionOrNull<TEnum>(this TEnum @enum)
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
string enumName = Enum.GetName(@enum)!;
|
||||
FieldInfo? field = @enum.GetType().GetField(enumName);
|
||||
DescriptionAttribute? attr = field?.GetCustomAttribute<DescriptionAttribute>();
|
||||
return attr?.Description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">枚举的类型</typeparam>
|
||||
/// <param name="enum">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
public static string GetLocalizedDescription<TEnum>(this TEnum @enum)
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
string enumName = Enum.GetName(@enum)!;
|
||||
FieldInfo? field = @enum.GetType().GetField(enumName);
|
||||
LocalizationKeyAttribute? attr = field?.GetCustomAttribute<LocalizationKeyAttribute>();
|
||||
string? result = null;
|
||||
if (attr != null)
|
||||
{
|
||||
result = SH.ResourceManager.GetString(attr.Key);
|
||||
}
|
||||
|
||||
return result ?? enumName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">枚举的类型</typeparam>
|
||||
/// <param name="enum">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
public static string? GetLocalizedDescriptionOrDefault<TEnum>(this TEnum @enum)
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
string enumName = Enum.GetName(@enum)!;
|
||||
FieldInfo? field = @enum.GetType().GetField(enumName);
|
||||
LocalizationKeyAttribute? attr = field?.GetCustomAttribute<LocalizationKeyAttribute>();
|
||||
string? result = null;
|
||||
if (attr != null)
|
||||
{
|
||||
result = SH.ResourceManager.GetString(attr.Key);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,6 +16,6 @@ internal static class ObjectExtension
|
||||
/// <returns>数组</returns>
|
||||
public static T[] ToArray<T>(this T source)
|
||||
{
|
||||
return new[] { source };
|
||||
return new T[] { source };
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,19 @@ internal interface IPickerFactory
|
||||
/// 获取 经过初始化的 <see cref="FileSavePicker"/>
|
||||
/// </summary>
|
||||
/// <returns>经过初始化的 <see cref="FileSavePicker"/></returns>
|
||||
[Obsolete]
|
||||
FileSavePicker GetFileSavePicker();
|
||||
|
||||
/// <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>
|
||||
|
||||
@@ -12,13 +12,16 @@ namespace Snap.Hutao.Factory;
|
||||
internal sealed class ContentDialogFactory : IContentDialogFactory
|
||||
{
|
||||
private readonly MainWindow mainWindow;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的内容对话框工厂
|
||||
/// </summary>
|
||||
/// <param name="taskContext">任务上下文</param>
|
||||
/// <param name="mainWindow">主窗体</param>
|
||||
public ContentDialogFactory(MainWindow mainWindow)
|
||||
public ContentDialogFactory(ITaskContext taskContext, MainWindow mainWindow)
|
||||
{
|
||||
this.taskContext = taskContext;
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
@@ -26,7 +29,7 @@ internal sealed class ContentDialogFactory : IContentDialogFactory
|
||||
public async ValueTask<ContentDialogResult> ConfirmAsync(string title, string content)
|
||||
{
|
||||
ContentDialog dialog = await CreateForConfirmAsync(title, content).ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
return await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
@@ -34,14 +37,14 @@ internal sealed class ContentDialogFactory : IContentDialogFactory
|
||||
public async ValueTask<ContentDialogResult> ConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
|
||||
{
|
||||
ContentDialog dialog = await CreateForConfirmCancelAsync(title, content, defaultButton).ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
return await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
@@ -54,7 +57,7 @@ internal sealed class ContentDialogFactory : IContentDialogFactory
|
||||
|
||||
private async ValueTask<ContentDialog> CreateForConfirmAsync(string title, string content)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
@@ -69,7 +72,7 @@ internal sealed class ContentDialogFactory : IContentDialogFactory
|
||||
|
||||
private async ValueTask<ContentDialog> CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
|
||||
@@ -55,6 +55,23 @@ internal class PickerFactory : IPickerFactory
|
||||
return GetInitializedPicker<FileSavePicker>();
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
@@ -75,9 +92,7 @@ internal class PickerFactory : IPickerFactory
|
||||
{
|
||||
// Create a folder picker.
|
||||
T picker = new();
|
||||
|
||||
IntPtr hWnd = WindowNative.GetWindowHandle(mainWindow);
|
||||
InitializeWithWindow.Initialize(picker, hWnd);
|
||||
InitializeWithWindow.Initialize(picker, mainWindow.WindowOptions.Hwnd);
|
||||
|
||||
return picker;
|
||||
}
|
||||
|
||||
18
src/Snap.Hutao/Snap.Hutao/IAppResourceProvider.cs
Normal file
18
src/Snap.Hutao/Snap.Hutao/IAppResourceProvider.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序资源提供器
|
||||
/// </summary>
|
||||
internal interface IAppResourceProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取资源
|
||||
/// </summary>
|
||||
/// <typeparam name="T">资源的类型</typeparam>
|
||||
/// <param name="name">资源的名称</param>
|
||||
/// <returns>资源</returns>
|
||||
T GetResource<T>(string name);
|
||||
}
|
||||
@@ -1,9 +1,24 @@
|
||||
[
|
||||
{
|
||||
"Name": "AchievementGoalId",
|
||||
"Type": "int",
|
||||
"Documentation": "1-2位 成就分类Id"
|
||||
},
|
||||
{
|
||||
"Name": "AchievementId",
|
||||
"Type": "int",
|
||||
"Documentation": "5位 成就Id"
|
||||
},
|
||||
{
|
||||
"Name": "AvatarId",
|
||||
"Type": "int",
|
||||
"Documentation": "8位 角色Id"
|
||||
},
|
||||
{
|
||||
"Name": "CostumeId",
|
||||
"Type": "int",
|
||||
"Documentation": "6位 角色装扮Id"
|
||||
},
|
||||
{
|
||||
"Name": "EquipAffixId",
|
||||
"Type": "int",
|
||||
@@ -34,19 +49,34 @@
|
||||
"Type": "int",
|
||||
"Documentation": "6位 圣遗物副词条Id"
|
||||
},
|
||||
{
|
||||
"Name": "ReliquaryId",
|
||||
"Type": "int",
|
||||
"Documentation": "5位 圣遗物Id"
|
||||
},
|
||||
{
|
||||
"Name": "ReliquaryMainAffixId",
|
||||
"Type": "int",
|
||||
"Documentation": "5位 圣遗物主属性Id"
|
||||
},
|
||||
{
|
||||
"Name": "ReliquarySetId",
|
||||
"Type": "int",
|
||||
"Documentation": "5位 圣遗物套装Id"
|
||||
},
|
||||
{
|
||||
"Name": "SkillGroupId",
|
||||
"Type": "int",
|
||||
"Documentation": "3-4位 技能组Id"
|
||||
},
|
||||
{
|
||||
"Name": "SkillId",
|
||||
"Type": "int",
|
||||
"Documentation": "5-6位 技能Id"
|
||||
},
|
||||
{
|
||||
"Name": "WeaponId",
|
||||
"Type": "int",
|
||||
"Documentation": "5位 武器Id"
|
||||
},
|
||||
{
|
||||
"Name": "AchievementId",
|
||||
"Type": "int",
|
||||
"Documentation": "5位 成就Id"
|
||||
}
|
||||
]
|
||||
@@ -4,7 +4,6 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.ViewModel.Game;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
@@ -14,7 +13,7 @@ namespace Snap.Hutao;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedWindowSource
|
||||
internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOptionsSource
|
||||
{
|
||||
private const int MinWidth = 240;
|
||||
private const int MinHeight = 240;
|
||||
@@ -22,6 +21,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedW
|
||||
private const int MaxWidth = 320;
|
||||
private const int MaxHeight = 320;
|
||||
|
||||
private readonly WindowOptions windowOptions;
|
||||
private readonly IServiceScope scope;
|
||||
|
||||
/// <summary>
|
||||
@@ -33,19 +33,13 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedW
|
||||
InitializeComponent();
|
||||
|
||||
scope = scopeFactory.CreateScope();
|
||||
windowOptions = new(this, DragableGrid, new(320, 320));
|
||||
ExtendedWindow<LaunchGameWindow>.Initialize(this, scope.ServiceProvider);
|
||||
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
|
||||
Closed += (s, e) => Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FrameworkElement TitleBar { get => DragableGrid; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PersistSize { get => false; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SizeInt32 InitSize { get => new(320, 320); }
|
||||
public WindowOptions WindowOptions { get => throw new NotImplementedException(); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
|
||||
@@ -17,11 +17,13 @@ namespace Snap.Hutao;
|
||||
[HighQuality]
|
||||
[Injection(InjectAs.Singleton)]
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal sealed partial class MainWindow : Window, IExtendedWindowSource, IRecipient<WelcomeStateCompleteMessage>
|
||||
internal sealed partial class MainWindow : Window, IWindowOptionsSource, IRecipient<WelcomeStateCompleteMessage>
|
||||
{
|
||||
private const int MinWidth = 848;
|
||||
private const int MinHeight = 524;
|
||||
|
||||
private readonly WindowOptions windowOptions;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的主窗体
|
||||
/// </summary>
|
||||
@@ -29,29 +31,16 @@ internal sealed partial class MainWindow : Window, IExtendedWindowSource, IRecip
|
||||
public MainWindow(IServiceProvider serviceProvider)
|
||||
{
|
||||
InitializeComponent();
|
||||
windowOptions = new(this, TitleBarView.DragArea, new(1200, 741), true);
|
||||
ExtendedWindow<MainWindow>.Initialize(this, serviceProvider);
|
||||
IsPresent = true;
|
||||
Closed += (s, e) => IsPresent = false;
|
||||
|
||||
Ioc.Default.GetRequiredService<IMessenger>().Register(this);
|
||||
serviceProvider.GetRequiredService<IMessenger>().Register(this);
|
||||
|
||||
// If not complete we should present the welcome view.
|
||||
ContentSwitchPresenter.Value = StaticResource.IsAnyUnfulfilledContractPresent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否打开
|
||||
/// </summary>
|
||||
public static bool IsPresent { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FrameworkElement TitleBar { get => TitleBarView.DragArea; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PersistSize { get => true; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SizeInt32 InitSize { get => new(1200, 741); }
|
||||
public WindowOptions WindowOptions { get => windowOptions; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
|
||||
|
||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||
|
||||
namespace Snap.Hutao.Model.Calculable;
|
||||
|
||||
@@ -26,7 +27,7 @@ internal sealed class CalculableAvatar : ObservableObject, ICalculableAvatar
|
||||
AvatarId = avatar.Id;
|
||||
LevelMin = 1;
|
||||
LevelMax = 90;
|
||||
Skills = avatar.SkillDepot.EnumerateCompositeSkillsNoInherents().Select(p => p.ToCalculable()).ToList();
|
||||
Skills = avatar.SkillDepot.CompositeSkillsNoInherents().SelectList(p => p.ToCalculable());
|
||||
Name = avatar.Name;
|
||||
Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
|
||||
Quality = avatar.Quality;
|
||||
@@ -39,12 +40,12 @@ internal sealed class CalculableAvatar : ObservableObject, ICalculableAvatar
|
||||
/// 构造一个新的可计算角色
|
||||
/// </summary>
|
||||
/// <param name="avatar">角色</param>
|
||||
public CalculableAvatar(Binding.AvatarProperty.AvatarView avatar)
|
||||
public CalculableAvatar(AvatarView avatar)
|
||||
{
|
||||
AvatarId = avatar.Id;
|
||||
LevelMin = avatar.LevelNumber;
|
||||
LevelMax = 90; // hard coded 90
|
||||
Skills = avatar.Skills.Select(s => s.ToCalculable()).ToList();
|
||||
Skills = avatar.Skills.SelectList(s => s.ToCalculable());
|
||||
Name = avatar.Name;
|
||||
Icon = avatar.Icon;
|
||||
Quality = avatar.Quality;
|
||||
@@ -63,7 +64,7 @@ internal sealed class CalculableAvatar : ObservableObject, ICalculableAvatar
|
||||
public int LevelMax { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IList<ICalculableSkill> Skills { get; }
|
||||
public List<ICalculableSkill> Skills { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; }
|
||||
|
||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||
|
||||
namespace Snap.Hutao.Model.Calculable;
|
||||
|
||||
@@ -38,7 +39,7 @@ internal sealed class CalculableSkill : ObservableObject, ICalculableSkill
|
||||
/// 构造一个新的可计算的技能
|
||||
/// </summary>
|
||||
/// <param name="skill">技能</param>
|
||||
public CalculableSkill(Binding.AvatarProperty.SkillView skill)
|
||||
public CalculableSkill(SkillView skill)
|
||||
{
|
||||
GruopId = skill.GroupId;
|
||||
LevelMin = skill.LevelNumber;
|
||||
|
||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||
|
||||
namespace Snap.Hutao.Model.Calculable;
|
||||
|
||||
@@ -25,7 +26,7 @@ internal class CalculableWeapon : ObservableObject, ICalculableWeapon
|
||||
{
|
||||
WeaponId = weapon.Id;
|
||||
LevelMin = 1;
|
||||
LevelMax = (int)weapon.Quality >= 3 ? 90 : 70;
|
||||
LevelMax = weapon.MaxLevel;
|
||||
Name = weapon.Name;
|
||||
Icon = EquipIconConverter.IconNameToUri(weapon.Icon);
|
||||
Quality = weapon.RankLevel;
|
||||
@@ -38,11 +39,11 @@ internal class CalculableWeapon : ObservableObject, ICalculableWeapon
|
||||
/// 构造一个新的可计算武器
|
||||
/// </summary>
|
||||
/// <param name="weapon">武器</param>
|
||||
public CalculableWeapon(Binding.AvatarProperty.WeaponView weapon)
|
||||
public CalculableWeapon(WeaponView weapon)
|
||||
{
|
||||
WeaponId = weapon.Id;
|
||||
LevelMin = weapon.LevelNumber;
|
||||
LevelMax = (int)weapon.Quality >= 3 ? 90 : 70;
|
||||
LevelMax = weapon.MaxLevel;
|
||||
Name = weapon.Name;
|
||||
Icon = weapon.Icon;
|
||||
Quality = weapon.Quality;
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Calculable;
|
||||
/// 可计算源
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal interface ICalculable : Binding.INameIcon
|
||||
internal interface ICalculable : INameIcon
|
||||
{
|
||||
/// <summary>
|
||||
/// 星级
|
||||
|
||||
@@ -29,5 +29,5 @@ internal interface ICalculableAvatar : ICalculable
|
||||
/// <summary>
|
||||
/// 技能组
|
||||
/// </summary>
|
||||
IList<ICalculableSkill> Skills { get; }
|
||||
List<ICalculableSkill> Skills { get; }
|
||||
}
|
||||
@@ -23,17 +23,17 @@ internal sealed class Achievement : IEquatable<Achievement>
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid InnerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存档 Id
|
||||
/// </summary>
|
||||
public Guid ArchiveId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存档
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(ArchiveId))]
|
||||
public AchievementArchive Archive { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 存档Id
|
||||
/// </summary>
|
||||
public Guid ArchiveId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
@@ -100,7 +100,7 @@ internal sealed class Achievement : IEquatable<Achievement>
|
||||
Id = Id,
|
||||
Current = Current,
|
||||
Status = Status,
|
||||
Timestamp = Time.ToUniversalTime().ToUnixTimeSeconds(),
|
||||
Timestamp = Time.ToUnixTimeSeconds(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Json;
|
||||
|
||||
namespace Snap.Hutao.Model.Entity.Configuration;
|
||||
|
||||
@@ -18,8 +18,8 @@ internal sealed class JsonTextValueConverter<TProperty> : ValueConverter<TProper
|
||||
/// </summary>
|
||||
public JsonTextValueConverter()
|
||||
: base(
|
||||
obj => JsonSerializer.Serialize(obj, CoreEnvironment.JsonOptions),
|
||||
str => JsonSerializer.Deserialize<TProperty>(str, CoreEnvironment.JsonOptions)!)
|
||||
obj => JsonSerializer.Serialize(obj, JsonOptions.Default),
|
||||
str => JsonSerializer.Deserialize<TProperty>(str, JsonOptions.Default)!)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -101,11 +101,6 @@ internal sealed class DailyNoteEntry : ObservableObject
|
||||
/// </summary>
|
||||
public bool ExpeditionNotifySuppressed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否在主页显示小组件
|
||||
/// </summary>
|
||||
public bool ShowInHomeWidget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的实时便笺
|
||||
/// </summary>
|
||||
@@ -117,8 +112,8 @@ internal sealed class DailyNoteEntry : ObservableObject
|
||||
{
|
||||
UserId = userAndUid.User.InnerId,
|
||||
Uid = userAndUid.Uid.Value,
|
||||
ResinNotifyThreshold = 160,
|
||||
HomeCoinNotifyThreshold = 2400,
|
||||
ResinNotifyThreshold = 120,
|
||||
HomeCoinNotifyThreshold = 1800,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ internal sealed class AppDbContext : DbContext
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,8 +5,8 @@ using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Service.GachaLog;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
@@ -47,34 +47,31 @@ internal sealed class GachaArchive : ISelectable
|
||||
/// <summary>
|
||||
/// 初始化或跳过
|
||||
/// </summary>
|
||||
/// <param name="context">上下文</param>
|
||||
/// <param name="archive">存档</param>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="gachaArchives">数据库集</param>
|
||||
/// <param name="collection">集合</param>
|
||||
public static void SkipOrInit([NotNull] ref GachaArchive? archive, string uid, DbSet<GachaArchive> gachaArchives, ObservableCollection<GachaArchive> collection)
|
||||
public static void SkipOrInit(in GachaArchiveInitializationContext context, [NotNull] ref GachaArchive? archive)
|
||||
{
|
||||
if (archive == null)
|
||||
{
|
||||
Init(out archive, uid, gachaArchives, collection);
|
||||
Init(context, out archive);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
/// <param name="context">上下文</param>
|
||||
/// <param name="archive">存档</param>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="gachaArchives">数据库集</param>
|
||||
/// <param name="collection">集合</param>
|
||||
public static void Init([NotNull] out GachaArchive? archive, string uid, DbSet<GachaArchive> gachaArchives, ObservableCollection<GachaArchive> collection)
|
||||
[SuppressMessage("", "SH002")]
|
||||
public static void Init(GachaArchiveInitializationContext context, [NotNull] out GachaArchive? archive)
|
||||
{
|
||||
archive = collection.SingleOrDefault(a => a.Uid == uid);
|
||||
archive = context.ArchiveCollection.SingleOrDefault(a => a.Uid == context.Uid);
|
||||
|
||||
if (archive == null)
|
||||
{
|
||||
GachaArchive created = Create(uid);
|
||||
gachaArchives.AddAndSave(created);
|
||||
ThreadHelper.InvokeOnMainThread(() => collection!.Add(created));
|
||||
GachaArchive created = Create(context.Uid);
|
||||
context.GachaArchives.AddAndSave(created);
|
||||
context.TaskContext.InvokeOnMainThread(() => context.ArchiveCollection.Add(created));
|
||||
archive = created;
|
||||
}
|
||||
}
|
||||
@@ -82,24 +79,22 @@ internal sealed class GachaArchive : ISelectable
|
||||
/// <summary>
|
||||
/// 保存祈愿物品
|
||||
/// </summary>
|
||||
/// <param name="itemsToAdd">待添加物品</param>
|
||||
/// <param name="isLazy">是否懒惰</param>
|
||||
/// <param name="endId">结尾Id</param>
|
||||
/// <param name="gachaItems">数据集</param>
|
||||
public void SaveItems(List<GachaItem> itemsToAdd, bool isLazy, long endId, DbSet<GachaItem> gachaItems)
|
||||
/// <param name="context">上下文</param>
|
||||
[SuppressMessage("", "SH002")]
|
||||
public void SaveItems(GachaItemSaveContext context)
|
||||
{
|
||||
if (itemsToAdd.Count > 0)
|
||||
if (context.ItemsToAdd.Count > 0)
|
||||
{
|
||||
// 全量刷新
|
||||
if (!isLazy)
|
||||
if (!context.IsLazy)
|
||||
{
|
||||
gachaItems
|
||||
context.GachaItems
|
||||
.Where(i => i.ArchiveId == InnerId)
|
||||
.Where(i => i.Id >= endId)
|
||||
.Where(i => i.Id >= context.EndId)
|
||||
.ExecuteDelete();
|
||||
}
|
||||
|
||||
gachaItems.AddRangeAndSave(itemsToAdd);
|
||||
context.GachaItems.AddRangeAndSave(context.ItemsToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,12 @@ internal sealed class GachaItem
|
||||
/// <returns>物品类型字符串</returns>
|
||||
public static string GetItemTypeStringByItemId(int itemId)
|
||||
{
|
||||
int idLength = itemId.Place();
|
||||
return idLength == 8 ? "角色" : idLength == 5 ? "武器" : "未知";
|
||||
return itemId.Place() switch
|
||||
{
|
||||
8 => "角色",
|
||||
5 => "武器",
|
||||
_ => "未知",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
@@ -12,11 +13,8 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Table("game_accounts")]
|
||||
internal sealed class GameAccount : INotifyPropertyChanged
|
||||
internal sealed class GameAccount : ObservableObject
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
/// </summary>
|
||||
@@ -66,7 +64,7 @@ internal sealed class GameAccount : INotifyPropertyChanged
|
||||
public void UpdateAttachUid(string? uid)
|
||||
{
|
||||
AttachUid = uid;
|
||||
PropertyChanged?.Invoke(this, new(nameof(AttachUid)));
|
||||
OnPropertyChanged(nameof(AttachUid));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,6 +74,6 @@ internal sealed class GameAccount : INotifyPropertyChanged
|
||||
public void UpdateName(string name)
|
||||
{
|
||||
Name = name;
|
||||
PropertyChanged?.Invoke(this, new(nameof(Name)));
|
||||
OnPropertyChanged($"{nameof(Name)}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Entity;
|
||||
|
||||
/// <summary>
|
||||
/// 键名
|
||||
/// </summary>
|
||||
internal sealed partial class SettingEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public const string GamePath = "GamePath";
|
||||
|
||||
/// <summary>
|
||||
/// 空的历史记录卡池是否可见
|
||||
/// </summary>
|
||||
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
|
||||
|
||||
/// <summary>
|
||||
/// 窗口背景类型
|
||||
/// </summary>
|
||||
public const string SystemBackdropType = "SystemBackdropType";
|
||||
|
||||
/// <summary>
|
||||
/// 启用高级功能
|
||||
/// </summary>
|
||||
public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺刷新时间
|
||||
/// </summary>
|
||||
public const string DailyNoteRefreshSeconds = "DailyNote.RefreshSeconds";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺提醒式通知
|
||||
/// </summary>
|
||||
public const string DailyNoteReminderNotify = "DailyNote.ReminderNotify";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺免打扰模式
|
||||
/// </summary>
|
||||
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 独占全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsExclusive = "Launch.IsExclusive";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsFullScreen = "Launch.IsFullScreen";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 无边框
|
||||
/// </summary>
|
||||
public const string LaunchIsBorderless = "Launch.IsBorderless";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 宽度
|
||||
/// </summary>
|
||||
public const string LaunchScreenWidth = "Launch.ScreenWidth";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 高度
|
||||
/// </summary>
|
||||
public const string LaunchScreenHeight = "Launch.ScreenHeight";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 解锁帧率
|
||||
/// </summary>
|
||||
public const string LaunchUnlockFps = "Launch.UnlockFps";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 目标帧率
|
||||
/// </summary>
|
||||
public const string LaunchTargetFps = "Launch.TargetFps";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 显示器编号
|
||||
/// </summary>
|
||||
public const string LaunchMonitor = "Launch.Monitor";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 多倍启动
|
||||
/// </summary>
|
||||
public const string MultipleInstances = "Launch.MultipleInstances";
|
||||
|
||||
/// <summary>
|
||||
/// 语言
|
||||
/// </summary>
|
||||
public const string Culture = "Culture";
|
||||
}
|
||||
@@ -12,96 +12,8 @@ namespace Snap.Hutao.Model.Entity;
|
||||
[HighQuality]
|
||||
[Table("settings")]
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal sealed class SettingEntry
|
||||
internal sealed partial class SettingEntry
|
||||
{
|
||||
#region EntryNames
|
||||
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public const string GamePath = "GamePath";
|
||||
|
||||
/// <summary>
|
||||
/// 空的历史记录卡池是否可见
|
||||
/// </summary>
|
||||
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
|
||||
|
||||
/// <summary>
|
||||
/// 窗口背景类型
|
||||
/// </summary>
|
||||
public const string SystemBackdropType = "SystemBackdropType";
|
||||
|
||||
/// <summary>
|
||||
/// 启用高级功能
|
||||
/// </summary>
|
||||
public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺刷新时间
|
||||
/// </summary>
|
||||
public const string DailyNoteRefreshSeconds = "DailyNote.RefreshSeconds";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺提醒式通知
|
||||
/// </summary>
|
||||
public const string DailyNoteReminderNotify = "DailyNote.ReminderNotify";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺免打扰模式
|
||||
/// </summary>
|
||||
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 独占全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsExclusive = "Launch.IsExclusive";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsFullScreen = "Launch.IsFullScreen";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 无边框
|
||||
/// </summary>
|
||||
public const string LaunchIsBorderless = "Launch.IsBorderless";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 宽度
|
||||
/// </summary>
|
||||
public const string LaunchScreenWidth = "Launch.ScreenWidth";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 高度
|
||||
/// </summary>
|
||||
public const string LaunchScreenHeight = "Launch.ScreenHeight";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 解锁帧率
|
||||
/// </summary>
|
||||
public const string LaunchUnlockFps = "Launch.UnlockFps";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 目标帧率
|
||||
/// </summary>
|
||||
public const string LaunchTargetFps = "Launch.TargetFps";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 显示器编号
|
||||
/// </summary>
|
||||
public const string LaunchMonitor = "Launch.Monitor";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 多倍启动
|
||||
/// </summary>
|
||||
public const string MultipleInstances = "Launch.MultipleInstances";
|
||||
|
||||
/// <summary>
|
||||
/// 语言
|
||||
/// </summary>
|
||||
public const string Culture = "Culture";
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的设置入口
|
||||
/// </summary>
|
||||
|
||||
@@ -27,7 +27,7 @@ internal sealed class SpiralAbyssEntry : ObservableObject
|
||||
public int ScheduleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 视图中使用的计划Id字符串
|
||||
/// 视图 中使用的计划 Id 字符串
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public string Schedule { get => string.Format(SH.ModelEntitySpiralAbyssScheduleFormat, ScheduleId); }
|
||||
|
||||
@@ -67,9 +67,9 @@ internal sealed class User : ISelectable
|
||||
/// <returns>新创建的用户</returns>
|
||||
public static User Create(Cookie cookie, bool isOversea)
|
||||
{
|
||||
_ = cookie.TryGetAsSToken(isOversea, out Cookie? stoken);
|
||||
_ = cookie.TryGetAsLToken(out Cookie? ltoken);
|
||||
_ = cookie.TryGetAsCookieToken(out Cookie? cookieToken);
|
||||
_ = cookie.TryGetSToken(isOversea, out Cookie? stoken);
|
||||
_ = cookie.TryGetLToken(out Cookie? ltoken);
|
||||
_ = cookie.TryGetCookieToken(out Cookie? cookieToken);
|
||||
|
||||
return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding;
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 实体与元数据
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding;
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 名称与图标
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding;
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 包括侧面图标的名称与图标
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user