Merge pull request #700 from DGP-Studio/refactor/application-model

Refactor Application Model
This commit is contained in:
DismissedLight
2023-04-30 20:28:10 +08:00
committed by GitHub
307 changed files with 4927 additions and 3283 deletions

View File

@@ -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);

View File

@@ -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));

View File

@@ -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($"""
""");
}
}

View File

@@ -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);
}
}
""";

View File

@@ -205,7 +205,7 @@ public static class JsonParser
try
{
return Enum.Parse(type, json, false);
return System.Enum.Parse(type, json, false);
}
catch
{

View File

@@ -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(); }
}

View File

@@ -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)
{
}
}
}

View File

@@ -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

View File

@@ -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"/>

View File

@@ -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);
}
}

View 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];
}
}

View File

@@ -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>
/// 目标宽度

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.Control;

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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
{

View File

@@ -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>
/// 构造一个新的渐变锚点

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -153,6 +153,7 @@ internal sealed class DescriptionTextBlock : ContentControl
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
// Simply re-apply texts
ApplyDescription((TextBlock)Content, Description);
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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()

View File

@@ -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)]

View File

@@ -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)

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Database;
/// 可枚举扩展
/// </summary>
[HighQuality]
internal static class EnumerableExtension
internal static class SelectableExtension
{
/// <summary>
/// 获取选中的值或默认值

View File

@@ -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>
/// 可转换类型服务

View File

@@ -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>
/// 有名称的对象

View File

@@ -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 可区分

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
namespace Snap.Hutao.Core.DependencyInjection;

View File

@@ -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);
}
}

View File

@@ -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");

View File

@@ -12,6 +12,7 @@ internal static partial class ServiceCollectionExtension
{
/// <summary>
/// 向容器注册服务
/// 此方法将会自动生成
/// </summary>
/// <param name="services">容器</param>
/// <returns>可继续操作的服务集合</returns>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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();
}
}
}

View 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; }
}

View 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.");
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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)!;
}
}

View File

@@ -10,14 +10,14 @@ namespace Snap.Hutao.Core.Json.Annotation;
internal enum JsonSerializeType
{
/// <summary>
/// Int32
/// 数字
/// </summary>
Int32,
Number,
/// <summary>
/// 字符串包裹的数字
/// </summary>
Int32AsString,
NumberString,
/// <summary>
/// 名称字符串

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View 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,
},
},
};
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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
}

View File

@@ -28,4 +28,6 @@ internal static class StringLiterals
/// False
/// </summary>
public const string False = "False";
public const string CRLF = "\r\n";
}

View File

@@ -18,7 +18,7 @@ internal class ConcurrentCancellationTokenSource
/// 注册取消令牌
/// </summary>
/// <returns>取消令牌</returns>
public CancellationToken Register()
public CancellationToken CancelPreviousOne()
{
source.Cancel();
source = new();

View 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();
}

View File

@@ -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();
}
}

View 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);
}
}
}

View File

@@ -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)
{

View File

@@ -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);
}
}
}

View File

@@ -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()

View File

@@ -7,7 +7,6 @@ namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 用于包装异步操作的结果
/// 结构类型,在栈上分配
/// </summary>
/// <typeparam name="TResult">结果类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>

View File

@@ -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;
}
}

View File

@@ -12,6 +12,7 @@ namespace Snap.Hutao.Core;
/// 必须为抽象类才能使用泛型日志器
/// </summary>
[HighQuality]
[Obsolete("Use HutaoOptions instead")]
internal abstract class WebView2Helper
{
private static bool hasEverDetected;

View File

@@ -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);
}
}
}

View File

@@ -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>
/// 处理最大最小信息

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 };
}
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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;
}

View 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);
}

View File

@@ -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"
}
]

View File

@@ -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()

View File

@@ -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)

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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;

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Calculable;
/// 可计算源
/// </summary>
[HighQuality]
internal interface ICalculable : Binding.INameIcon
internal interface ICalculable : INameIcon
{
/// <summary>
/// 星级

View File

@@ -29,5 +29,5 @@ internal interface ICalculableAvatar : ICalculable
/// <summary>
/// 技能组
/// </summary>
IList<ICalculableSkill> Skills { get; }
List<ICalculableSkill> Skills { get; }
}

View File

@@ -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(),
};
}

View File

@@ -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)!)
{
}
}

View File

@@ -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,
};
}

View File

@@ -23,7 +23,6 @@ internal sealed class AppDbContext : DbContext
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
/// <summary>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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)}");
}
}

View File

@@ -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";
}

View File

@@ -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>

View File

@@ -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); }

View File

@@ -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 };
}

View File

@@ -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>
/// 实体与元数据

View File

@@ -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>
/// 名称与图标

View File

@@ -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