Generate Enum Localization

This commit is contained in:
Lightczx
2023-04-26 21:43:21 +08:00
parent 9a54df4163
commit 9657df36c2
157 changed files with 1211 additions and 800 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

@@ -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;
@@ -63,9 +64,27 @@ public class CSharpLanguageFeatureTest
}
[TestMethod]
public void GetTwiceOnPropertyResultsSame()
public void GetTwiceOnPropertyResultsNotSame()
{
Assert.AreEqual(UUID, UUID);
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

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

@@ -36,6 +36,7 @@ public sealed partial class App : Application
logger = serviceProvider.GetRequiredService<ILogger<App>>();
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
this.serviceProvider = serviceProvider;
}

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

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

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

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

@@ -15,6 +15,7 @@ internal static partial class IocHttpClientConfiguration
/// <summary>
/// 添加 <see cref="HttpClient"/>
/// 此方法将会自动生成
/// </summary>
/// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns>

View File

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

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.ExceptionService;
/// </summary>
internal sealed class ExceptionFormat
{
private const string SectionSeparator = "----------------------------------------";
private static readonly string SectionSeparator = new('-', 40);
/// <summary>
/// 格式化异常

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

@@ -2,6 +2,7 @@
// 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;
@@ -17,6 +18,9 @@ namespace Snap.Hutao.Core;
[Injection(InjectAs.Singleton)]
internal sealed class HutaoOptions : IOptions<HutaoOptions>
{
private readonly bool isWebView2Supported;
private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected;
/// <summary>
/// 构造一个新的胡桃选项
/// </summary>
@@ -31,6 +35,7 @@ internal sealed class HutaoOptions : IOptions<HutaoOptions>
UserAgent = $"Snap Hutao/{Version}";
DeviceId = GetUniqueUserId();
DetectWebView2Environment(ref webView2Version, ref isWebView2Supported);
}
/// <summary>
@@ -68,6 +73,16 @@ internal sealed class HutaoOptions : IOptions<HutaoOptions>
/// </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; }
@@ -99,4 +114,18 @@ internal sealed class HutaoOptions : IOptions<HutaoOptions>
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
return Convert.ToMd5HexString($"{userName}{machineGuid}");
}
private static void DetectWebView2Environment(ref string webView2Version, ref bool isWebView2Supported)
{
try
{
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
isWebView2Supported = true;
}
catch (FileNotFoundException ex)
{
ILogger<WebView2Helper> logger = Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>();
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;

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

@@ -46,6 +46,8 @@ internal static class Activation
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 +74,7 @@ internal static class Activation
/// <returns>任务</returns>
public static async ValueTask RestartAsElevatedAsync()
{
if (GetElevated())
if (!GetElevated())
{
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
Process.GetCurrentProcess().Kill();
@@ -182,12 +184,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>()
@@ -279,16 +285,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

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

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

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

View File

@@ -25,20 +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 WindowSubclass<TWindow> subclass;
private ExtendedWindow(TWindow window, FrameworkElement titleBar, IServiceProvider serviceProvider)
private ExtendedWindow(TWindow window, IServiceProvider serviceProvider)
{
options = new(window, titleBar);
subclass = new(options);
this.window = window;
this.serviceProvider = serviceProvider;
subclass = new(window.WindowOptions);
InitializeWindow();
}
@@ -50,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 +62,17 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
{
HutaoOptions hutaoOptions = serviceProvider.GetRequiredService<HutaoOptions>();
options.AppWindow.Title = string.Format(SH.AppNameAndVersion, hutaoOptions.Version);
options.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico"));
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,7 +140,7 @@ 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;
@@ -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

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

@@ -75,9 +75,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

@@ -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",
@@ -39,14 +54,19 @@
"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

@@ -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>
/// 包括侧面图标的名称与图标

View File

@@ -16,6 +16,6 @@ internal sealed class UIGFItem : GachaLogItem
/// 额外祈愿映射
/// </summary>
[JsonPropertyName("uigf_gacha_type")]
[JsonEnum(JsonSerializeType.Int32AsString)]
[JsonEnum(JsonSerializeType.NumberString)]
public GachaConfigType UIGFGachaType { get; set; } = default!;
}

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic;
/// 从属地区
/// </summary>
[HighQuality]
[Localization]
internal enum AssociationType
{
/// <summary>

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic;
/// 体型
/// </summary>
[HighQuality]
[Localization]
internal enum BodyType
{
/// <summary>

View File

@@ -60,7 +60,27 @@ internal enum ElementType
AntiFire = 9,
/// <summary>
/// 默认
/// 枫丹玩法
/// </summary>
Default = 255,
VehicleMuteIce = 10,
/// <summary>
/// 弹弹菇
/// </summary>
Mushroom = 11,
/// <summary>
/// 激元素
/// </summary>
Overdose = 12,
/// <summary>
/// 木元素
/// </summary>
Wood = 13,
/// <summary>
/// 个数
/// </summary>
Count = 14,
}

View File

@@ -9,6 +9,7 @@ namespace Snap.Hutao.Model.Intrinsic;
/// 战斗属性
/// </summary>
[HighQuality]
[Localization]
internal enum FightProperty
{
/// <summary>

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic;
/// 稀有度
/// </summary>
[HighQuality]
[Localization]
internal enum ItemQuality
{
/// <summary>

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic;
/// 武器类型
/// </summary>
[HighQuality]
[Localization]
[SuppressMessage("", "SA1124")]
internal enum WeaponType
{

View File

@@ -3,7 +3,7 @@
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Binding;
namespace Snap.Hutao.Model;
/// <summary>
/// 物品基类

View File

@@ -19,7 +19,7 @@ internal sealed class Achievement
/// <summary>
/// 分类Id
/// </summary>
public int Goal { get; set; }
public AchievementGoalId Goal { get; set; }
/// <summary>
/// 排序顺序

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Achievement;
/// <summary>
@@ -12,7 +14,7 @@ internal sealed class AchievementGoal
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
public AchievementGoalId Id { get; set; }
/// <summary>
/// 排序顺序

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Achievement;
/// <summary>
@@ -11,7 +13,7 @@ internal sealed class Reward
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
public MaterialId Id { get; set; }
/// <summary>
/// 数量

View File

@@ -20,6 +20,7 @@ internal static class EnumExtension
internal static FormatMethod GetFormatMethod<TEnum>(this TEnum @enum)
where TEnum : struct, Enum
{
// TODO: Not use Reflection
string enumName = Must.NotNull(Enum.GetName(@enum)!);
FieldInfo? field = @enum.GetType().GetField(enumName);
FormatAttribute? attr = field?.GetCustomAttribute<FormatAttribute>();

View File

@@ -49,7 +49,7 @@ internal partial class Avatar : IStatisticsItemSource, ISummaryItemSource, IName
/// 转换为基础物品
/// </summary>
/// <returns>基础物品</returns>
public Binding.Item ToItemBase()
public Model.Item ToItemBase()
{
return new()
{

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Avatar;
/// <summary>
@@ -12,7 +14,7 @@ internal sealed class Costume
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
public CostumeId Id { get; set; }
/// <summary>
/// 名称

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Avatar;
/// <summary>
@@ -12,7 +14,7 @@ internal sealed partial class ProudableSkill : Skill
/// <summary>
/// 组Id
/// </summary>
public int GroupId { get; set; }
public SkillGroupId GroupId { get; set; }
/// <summary>
/// 提升属性

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Avatar;
/// <summary>
@@ -13,7 +15,7 @@ internal class Skill
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
public SkillId Id { get; set; }
/// <summary>
/// 名称

View File

@@ -9,6 +9,9 @@ namespace Snap.Hutao.Model.Metadata.Avatar;
[HighQuality]
internal sealed class SkillDepot
{
private List<ProudableSkill>? compositeSkills;
private List<ProudableSkill>? compositeSkillsNoInherents;
/// <summary>
/// 技能天赋
/// </summary>
@@ -30,7 +33,18 @@ internal sealed class SkillDepot
/// </summary>
public List<ProudableSkill> CompositeSkills
{
get => EnumerateCompositeSkills().ToList();
get
{
if (compositeSkills == null)
{
compositeSkills = new(Skills.Count + 1 + Inherents.Count);
compositeSkills.AddRange(Skills);
compositeSkills.Add(EnergySkill);
compositeSkills.AddRange(Inherents);
}
return compositeSkills;
}
}
/// <summary>
@@ -42,32 +56,16 @@ internal sealed class SkillDepot
/// 获取无固有天赋的技能列表
/// </summary>
/// <returns>天赋列表</returns>
public IEnumerable<ProudableSkill> EnumerateCompositeSkillsNoInherents()
public List<ProudableSkill> CompositeSkillsNoInherents()
{
foreach (ProudableSkill skill in Skills)
if (compositeSkillsNoInherents == null)
{
// skip skills like Mona's & Ayaka's shift
if (skill.Proud.Parameters.Count > 1)
{
yield return skill;
}
compositeSkillsNoInherents = new(Skills.Count + 1);
compositeSkillsNoInherents.AddRange(Skills);
compositeSkillsNoInherents.Add(EnergySkill);
}
yield return EnergySkill;
}
private IEnumerable<ProudableSkill> EnumerateCompositeSkills()
{
foreach (ProudableSkill skill in Skills)
{
yield return skill;
}
yield return EnergySkill;
foreach (ProudableSkill skill in Inherents)
{
yield return skill;
}
// No Inherents
return compositeSkillsNoInherents;
}
}

View File

@@ -5,6 +5,7 @@ using Snap.Hutao.Control;
using Snap.Hutao.Model.Metadata.Avatar;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Snap.Hutao.Model.Metadata.Converter;
@@ -13,7 +14,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// 描述参数解析器
/// </summary>
[HighQuality]
internal sealed partial class ParameterDescriptor : ValueConverter<DescriptionsParameters, IList<LevelParameters<string, ParameterDescription>>>
internal sealed partial class DescriptionsParametersDescriptor : ValueConverter<DescriptionsParameters, IList<LevelParameters<string, ParameterDescription>>>
{
/// <summary>
/// 获取特定等级的解释
@@ -23,58 +24,48 @@ internal sealed partial class ParameterDescriptor : ValueConverter<DescriptionsP
/// <returns>特定等级的解释</returns>
public static LevelParameters<string, ParameterDescription> Convert(DescriptionsParameters from, int level)
{
// DO NOT INLINE!
// Cache the formats
List<DescFormat> formats = from.Descriptions
.Select(desc => new DescFormat(desc))
.ToList();
LevelParameters<int, double> param = from.Parameters.Single(param => param.Level == level);
return new LevelParameters<string, ParameterDescription>($"Lv.{param.Level}", GetParameterInfos(formats, param.Parameters));
return new LevelParameters<string, ParameterDescription>($"Lv.{param.Level}", GetParameterDescription(from.Descriptions, param.Parameters));
}
/// <inheritdoc/>
public override List<LevelParameters<string, ParameterDescription>> Convert(DescriptionsParameters from)
{
// DO NOT INLINE!
// Cache the formats
List<DescFormat> formats = from.Descriptions
.SelectList(desc => new DescFormat(desc));
List<LevelParameters<string, ParameterDescription>> parameters = from.Parameters
.SelectList(param => new LevelParameters<string, ParameterDescription>(param.Level.ToString(), GetParameterInfos(formats, param.Parameters)));
.SelectList(param => new LevelParameters<string, ParameterDescription>($"Lv.{param.Level}", GetParameterDescription(from.Descriptions, param.Parameters)));
return parameters;
}
private static List<ParameterDescription> GetParameterInfos(List<DescFormat> formats, List<double> param)
{
Span<DescFormat> span = CollectionsMarshal.AsSpan(formats);
List<ParameterDescription> results = new(span.Length);
foreach (DescFormat descFormat in span)
{
string format = descFormat.Format;
string resultFormatted = ParamRegex().Replace(format, match => EvaluateMatch(match, param));
results.Add(new ParameterDescription { Description = descFormat.Description, Parameter = resultFormatted });
}
return results;
}
[GeneratedRegex("{param[0-9]+.*?}")]
private static partial Regex ParamRegex();
private static string EvaluateMatch(Match match, IList<double> param)
private static List<ParameterDescription> GetParameterDescription(List<string> descriptions, List<double> param)
{
Span<string> span = CollectionsMarshal.AsSpan(descriptions);
List<ParameterDescription> results = new(span.Length);
foreach (ReadOnlySpan<char> desc in span)
{
int indexOfSeparator = desc.IndexOf('|');
ReadOnlySpan<char> description = desc[..indexOfSeparator];
ReadOnlySpan<char> format = desc[(indexOfSeparator + 1)..];
string resultFormatted = ParamRegex().Replace(format.ToString(), match => ReplaceParamInMatch(match, param));
results.Add(new ParameterDescription { Description = description.ToString(), Parameter = resultFormatted });
}
return results;
}
private static string ReplaceParamInMatch(Match match, List<double> param)
{
if (match.Success)
{
// remove parentheses and split by {value:format}
// remove parentheses and split by {value:format} like {param1:F}
string[] parts = match.Value[1..^1].Split(':', 2);
int index = int.Parse(parts[0]["param".Length..]) - 1;
return ParameterFormat.Format($"{{0:{parts[1]}}}", param[index]);
}
else
@@ -82,20 +73,4 @@ internal sealed partial class ParameterDescriptor : ValueConverter<DescriptionsP
return string.Empty;
}
}
private sealed class DescFormat
{
public DescFormat(string desc)
{
// Spilt rawDesc into two parts: desc and format
string[] parts = desc.Split('|', 2);
Description = parts[0];
Format = parts[1];
}
public string Description { get; set; }
public string Format { get; set; }
}
}

View File

@@ -12,6 +12,28 @@ namespace Snap.Hutao.Model.Metadata.Converter;
[HighQuality]
internal sealed class ElementNameIconConverter : ValueConverter<string, Uri>
{
private static readonly Dictionary<string, string> LocalizedNameToElementIconName = new()
{
[SH.ModelIntrinsicElementNameElec] = "Electric",
[SH.ModelIntrinsicElementNameFire] = "Fire",
[SH.ModelIntrinsicElementNameGrass] = "Grass",
[SH.ModelIntrinsicElementNameIce] = "Ice",
[SH.ModelIntrinsicElementNameRock] = "Rock",
[SH.ModelIntrinsicElementNameWater] = "Water",
[SH.ModelIntrinsicElementNameWind] = "Wind",
};
private static readonly Dictionary<string, ElementType> LocalizedNameToElementType = new()
{
[SH.ModelIntrinsicElementNameElec] = ElementType.Electric,
[SH.ModelIntrinsicElementNameFire] = ElementType.Fire,
[SH.ModelIntrinsicElementNameGrass] = ElementType.Grass,
[SH.ModelIntrinsicElementNameIce] = ElementType.Ice,
[SH.ModelIntrinsicElementNameRock] = ElementType.Rock,
[SH.ModelIntrinsicElementNameWater] = ElementType.Water,
[SH.ModelIntrinsicElementNameWind] = ElementType.Wind,
};
/// <summary>
/// 将中文元素名称转换为图标链接
/// </summary>
@@ -19,17 +41,7 @@ internal sealed class ElementNameIconConverter : ValueConverter<string, Uri>
/// <returns>图标链接</returns>
public static Uri ElementNameToIconUri(string elementName)
{
string element = elementName switch
{
"雷" => "Electric",
"火" => "Fire",
"草" => "Grass",
"冰" => "Ice",
"岩" => "Rock",
"水" => "Water",
"风" => "Wind",
_ => string.Empty,
};
string? element = LocalizedNameToElementIconName.GetValueOrDefault(elementName);
return string.IsNullOrEmpty(element)
? Web.HutaoEndpoints.UIIconNone
@@ -45,17 +57,7 @@ internal sealed class ElementNameIconConverter : ValueConverter<string, Uri>
/// <returns>元素类型</returns>
public static ElementType ElementNameToElementType(string elementName)
{
return elementName switch
{
"雷" => ElementType.Electric,
"火" => ElementType.Fire,
"草" => ElementType.Grass,
"冰" => ElementType.Ice,
"岩" => ElementType.Rock,
"水" => ElementType.Water,
"风" => ElementType.Wind,
_ => ElementType.None,
};
return LocalizedNameToElementType.GetValueOrDefault(elementName);
}
/// <inheritdoc/>

View File

@@ -2,9 +2,9 @@
// Licensed under the MIT license.
using Snap.Hutao.Control;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Annotation;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Model.Metadata.Converter;
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// 基础属性翻译器
/// </summary>
[HighQuality]
internal sealed class PropertyDescriptor : ValueConverter<PropertiesParameters, List<LevelParameters<string, ParameterDescription>>?>
internal sealed class PropertiesParametersDescriptor : ValueConverter<PropertiesParameters, List<LevelParameters<string, ParameterDescription>>?>
{
/// <summary>
/// 格式化名称与值

View File

@@ -57,14 +57,14 @@ internal sealed class MonsterBaseValue : BaseValue
{
return new()
{
Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_FIRE_SUB_HURT, FireSubHurt),
Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WATER_SUB_HURT, WaterSubHurt),
Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_GRASS_SUB_HURT, GrassSubHurt),
Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ELEC_SUB_HURT, ElecSubHurt),
Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WIND_SUB_HURT, WindSubHurt),
Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ICE_SUB_HURT, IceSubHurt),
Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ROCK_SUB_HURT, RockSubHurt),
Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, PhysicalSubHurt),
Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_FIRE_SUB_HURT, FireSubHurt),
Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WATER_SUB_HURT, WaterSubHurt),
Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_GRASS_SUB_HURT, GrassSubHurt),
Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ELEC_SUB_HURT, ElecSubHurt),
Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WIND_SUB_HURT, WindSubHurt),
Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ICE_SUB_HURT, IceSubHurt),
Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ROCK_SUB_HURT, RockSubHurt),
Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, PhysicalSubHurt),
};
}
}

View File

@@ -31,7 +31,7 @@ internal sealed partial class Weapon : IStatisticsItemSource, ISummaryItemSource
/// <summary>
/// 最大等级
/// </summary>
public int MaxLevel { get => ((int)Quality) >= 3 ? 90 : 70; }
internal int MaxLevel { get => ((int)Quality) >= 3 ? 90 : 70; }
/// <inheritdoc/>
public ICalculableWeapon ToCalculable()
@@ -43,7 +43,7 @@ internal sealed partial class Weapon : IStatisticsItemSource, ISummaryItemSource
/// 转换为基础物品
/// </summary>
/// <returns>基础物品</returns>
public Binding.Item ToItemBase()
public Model.Item ToItemBase()
{
return new()
{

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Resource.Localization;
/// <summary>
/// 指示此枚举支持本地化
/// </summary>
[AttributeUsage(AttributeTargets.Enum)]
internal sealed class LocalizationAttribute : Attribute
{
}

View File

@@ -1,14 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Annotation;
namespace Snap.Hutao.Resource.Localization;
/// <summary>
/// 本地化键
/// </summary>
[HighQuality]
[AttributeUsage(AttributeTargets.Field)]
internal class LocalizationKeyAttribute : Attribute
internal sealed class LocalizationKeyAttribute : Attribute
{
/// <summary>
/// 指定本地化键

View File

@@ -1536,6 +1536,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 同步角色信息 的本地化字符串。
/// </summary>
internal static string ViewAvatarPropertySyncDataButtonLabel {
get {
return ResourceManager.GetString("ViewAvatarPropertySyncDataButtonLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 成就统计 的本地化字符串。
/// </summary>

View File

@@ -2,10 +2,10 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.AvatarInfo.Factory;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.ViewModel.AvatarProperty;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Enka;
using Snap.Hutao.Web.Enka.Model;

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory;

View File

@@ -1,16 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
using Snap.Hutao.Web.Enka.Model;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using PropertyAvatar = Snap.Hutao.Model.Binding.AvatarProperty.AvatarView;
using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.ReliquaryView;
using PropertyWeapon = Snap.Hutao.Model.Binding.AvatarProperty.WeaponView;
using PropertyAvatar = Snap.Hutao.ViewModel.AvatarProperty.AvatarView;
using PropertyReliquary = Snap.Hutao.ViewModel.AvatarProperty.ReliquaryView;
using PropertyWeapon = Snap.Hutao.ViewModel.AvatarProperty.WeaponView;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
@@ -54,7 +55,7 @@ internal sealed class SummaryAvatarFactory
// webinfo & metadata mixed part
Constellations = SummaryHelper.CreateConstellations(avatar.SkillDepot.Talents, avatarInfo.TalentIdList),
Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.EnumerateCompositeSkillsNoInherents()),
Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.CompositeSkillsNoInherents()),
// webinfo part
FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0,
@@ -76,7 +77,7 @@ internal sealed class SummaryAvatarFactory
{
if (avatarInfo.CostumeId.HasValue)
{
int costumeId = avatarInfo.CostumeId.Value;
CostumeId costumeId = avatarInfo.CostumeId.Value;
Model.Metadata.Avatar.Costume costume = avatar.Costumes.Single(c => c.Id == costumeId);
// Set to costume icon
@@ -132,7 +133,7 @@ internal sealed class SummaryAvatarFactory
else
{
subStat.StatValue = subStat.StatValue - Math.Truncate(subStat.StatValue) > 0 ? subStat.StatValue / 100D : subStat.StatValue;
subProperty = Model.Metadata.Converter.PropertyDescriptor.FormatNameDescription(subStat.AppendPropId, subStat.StatValue);
subProperty = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatNameDescription(subStat.AppendPropId, subStat.StatValue);
}
return new()

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