refactor viewmodels

This commit is contained in:
Lightczx
2023-08-07 17:28:39 +08:00
parent 4dca174019
commit 3cf0cd1c9a
68 changed files with 325 additions and 320 deletions

View File

@@ -79,7 +79,6 @@ internal sealed class CommandGenerator : IIncrementalGenerator
string className = classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); string className = classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
// TODO: 支持嵌套类
string code = $$""" string code = $$"""
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;

View File

@@ -18,7 +18,7 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
{ {
private const string AttributeName = "Snap.Hutao.Core.Annotation.ConstructorGeneratedAttribute"; private const string AttributeName = "Snap.Hutao.Core.Annotation.ConstructorGeneratedAttribute";
private static readonly DiagnosticDescriptor genericTypeNotSupportedDescriptor = new("SH102", "Generic type is not supported to generate .ctor", "Type [{0}] is not supported", "Quality", DiagnosticSeverity.Error, true); //private static readonly DiagnosticDescriptor genericTypeNotSupportedDescriptor = new("SH102", "Generic type is not supported to generate .ctor", "Type [{0}] is not supported", "Quality", DiagnosticSeverity.Error, true);
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)
{ {
@@ -61,12 +61,6 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
private static void GenerateConstructorImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2) private static void GenerateConstructorImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
{ {
if (context2.Symbol.IsGenericType)
{
production.ReportDiagnostic(Diagnostic.Create(genericTypeNotSupportedDescriptor, context2.Context.Node.GetLocation(), context2.Symbol));
return;
}
AttributeData constructorInfo = context2.SingleAttribute(AttributeName); AttributeData constructorInfo = context2.SingleAttribute(AttributeName);
bool resolveHttpClient = constructorInfo.HasNamedArgumentWith<bool>("ResolveHttpClient", value => value); bool resolveHttpClient = constructorInfo.HasNamedArgumentWith<bool>("ResolveHttpClient", value => value);
@@ -79,10 +73,9 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
namespace {{context2.Symbol.ContainingNamespace}}; namespace {{context2.Symbol.ContainingNamespace}};
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")]
partial class {{context2.Symbol.Name}} partial class {{context2.Symbol.ToDisplayString(SymbolDisplayFormats.QualifiedNonNullableFormat)}}
{ {
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}}) public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}}){{(options.CallBaseConstructor? " : base(serviceProvider)" : string.Empty)}}
{{(options.CallBaseConstructor? ": base(serviceProvider)" : string.Empty)}}
{ {
"""); """);
@@ -93,8 +86,9 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
} }
} }
"""); """);
production.AddSource($"{context2.Symbol.ToDisplayString()}.ctor.g.cs", sourceBuilder.ToString()); string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
production.AddSource($"{normalizedClassName}.ctor.g.cs", sourceBuilder.ToString());
} }
private static void FillUpWithFieldValueAssignment(StringBuilder builder, GeneratorSyntaxContext2 context2, FieldValueAssignmentOptions options) private static void FillUpWithFieldValueAssignment(StringBuilder builder, GeneratorSyntaxContext2 context2, FieldValueAssignmentOptions options)

View File

@@ -12,4 +12,10 @@ internal static class SymbolDisplayFormats
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes); miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
public static SymbolDisplayFormat QualifiedNonNullableFormat { get; } = new(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
} }

View File

@@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Snap.Hutao.SourceGeneration.Primitive; using Snap.Hutao.SourceGeneration.Primitive;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
@@ -18,6 +19,10 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true); private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true); private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true); private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useIsNotNullPatternMatchingDescriptor = new("SH004", "Use \"is not null\" instead of \"!= null\" whenever possible", "Use \"is not null\" instead of \"!= null\"", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useIsNullPatternMatchingDescriptor = new("SH005", "Use \"is null\" instead of \"== null\" whenever possible", "Use \"is null\" instead of \"== null\"", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useIsPatternRecursiveMatchingDescriptor = new("SH006", "Use \"is { } obj\" whenever possible", "Use \"is {{ }} {0}\"", "Quality", DiagnosticSeverity.Info, true);
private static readonly ImmutableHashSet<string> RefLikeKeySkipTypes = new HashSet<string>() private static readonly ImmutableHashSet<string> RefLikeKeySkipTypes = new HashSet<string>()
{ {
@@ -34,6 +39,9 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
typeInternalDescriptor, typeInternalDescriptor,
readOnlyStructRefDescriptor, readOnlyStructRefDescriptor,
useValueTaskIfPossibleDescriptor, useValueTaskIfPossibleDescriptor,
useIsNotNullPatternMatchingDescriptor,
useIsNullPatternMatchingDescriptor,
useIsPatternRecursiveMatchingDescriptor
}.ToImmutableArray(); }.ToImmutableArray();
} }
} }
@@ -48,18 +56,25 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
private static void CompilationStart(CompilationStartAnalysisContext context) private static void CompilationStart(CompilationStartAnalysisContext context)
{ {
SyntaxKind[] types = new SyntaxKind[] SyntaxKind[] types =
{ {
SyntaxKind.ClassDeclaration, SyntaxKind.ClassDeclaration,
SyntaxKind.InterfaceDeclaration, SyntaxKind.InterfaceDeclaration,
SyntaxKind.StructDeclaration, SyntaxKind.StructDeclaration,
SyntaxKind.EnumDeclaration SyntaxKind.EnumDeclaration,
}; };
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types); context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration); context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration); context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration); context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
SyntaxKind[] expressions =
{
SyntaxKind.EqualsExpression,
SyntaxKind.NotEqualsExpression,
};
context.RegisterSyntaxNodeAction(HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching, expressions);
context.RegisterSyntaxNodeAction(HandleIsPatternShouldUseRecursivePattern, SyntaxKind.IsPatternExpression);
} }
private static void HandleTypeShouldBeInternal(SyntaxNodeAnalysisContext context) private static void HandleTypeShouldBeInternal(SyntaxNodeAnalysisContext context)
@@ -211,6 +226,39 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
} }
} }
public static void HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching(SyntaxNodeAnalysisContext context)
{
BinaryExpressionSyntax syntax = (BinaryExpressionSyntax)context.Node;
if (syntax.IsKind(SyntaxKind.NotEqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
{
Location location = syntax.OperatorToken.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useIsNotNullPatternMatchingDescriptor, location);
context.ReportDiagnostic(diagnostic);
}
else if(syntax.IsKind(SyntaxKind.EqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
{
Location location = syntax.OperatorToken.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useIsNullPatternMatchingDescriptor, location);
context.ReportDiagnostic(diagnostic);
}
}
private static void HandleIsPatternShouldUseRecursivePattern(SyntaxNodeAnalysisContext context)
{
IsPatternExpressionSyntax syntax = (IsPatternExpressionSyntax)context.Node;
if (syntax.Pattern is DeclarationPatternSyntax declaration)
{
ITypeSymbol? leftType = context.SemanticModel.GetTypeInfo(syntax.Expression).ConvertedType;
ITypeSymbol? rightType = context.SemanticModel.GetTypeInfo(declaration).ConvertedType;
if (SymbolEqualityComparer.Default.Equals(leftType, rightType))
{
Location location = declaration.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useIsPatternRecursiveMatchingDescriptor, location, declaration.Designation);
context.ReportDiagnostic(diagnostic);
}
}
}
private static bool IsBuiltInType(ITypeSymbol symbol) private static bool IsBuiltInType(ITypeSymbol symbol)
{ {
return symbol.SpecialType switch return symbol.SpecialType switch

View File

@@ -17,7 +17,7 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnAssociatedObjectLoaded() protected override void OnAssociatedObjectLoaded()
{ {
if (Command != null && Command.CanExecute(CommandParameter)) if (Command is not null && Command.CanExecute(CommandParameter))
{ {
Command.Execute(CommandParameter); Command.Execute(CommandParameter);
} }

View File

@@ -126,7 +126,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{ {
await HideAsync(token).ConfigureAwait(true); await HideAsync(token).ConfigureAwait(true);
if (uri != null) if (uri is not null)
{ {
LoadedImageSurface? imageSurface = null; LoadedImageSurface? imageSurface = null;
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
@@ -147,7 +147,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
imageCache.Remove(uri); imageCache.Remove(uri);
} }
if (imageSurface != null) if (imageSurface is not null)
{ {
using (imageSurface) using (imageSurface)
{ {
@@ -221,7 +221,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
private void OnSizeChanged(object sender, SizeChangedEventArgs e) private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{ {
if (e.NewSize != e.PreviousSize && spriteVisual != null) if (e.NewSize != e.PreviousSize && spriteVisual is not null)
{ {
UpdateVisual(spriteVisual); UpdateVisual(spriteVisual);
} }

View File

@@ -57,7 +57,7 @@ internal sealed class MonoChrome : CompositionImage
private void OnActualThemeChanged(FrameworkElement sender, object args) private void OnActualThemeChanged(FrameworkElement sender, object args)
{ {
if (backgroundBrush != null) if (backgroundBrush is not null)
{ {
SetBackgroundColor(backgroundBrush); SetBackgroundColor(backgroundBrush);
} }

View File

@@ -56,7 +56,7 @@ internal class ScopedPage : Page
/// <returns>任务</returns> /// <returns>任务</returns>
public async ValueTask NotifyRecipientAsync(INavigationData extra) public async ValueTask NotifyRecipientAsync(INavigationData extra)
{ {
if (extra.Data != null && DataContext is INavigationRecipient recipient) if (extra.Data is not null && DataContext is INavigationRecipient recipient)
{ {
await recipient.ReceiveAsync(extra).ConfigureAwait(false); await recipient.ReceiveAsync(extra).ConfigureAwait(false);
} }

View File

@@ -34,7 +34,7 @@ internal sealed class CommandLineBuilder
/// <returns>命令行建造器</returns> /// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIfNotNull(string name, object? value = null) public CommandLineBuilder AppendIfNotNull(string name, object? value = null)
{ {
return AppendIf(name, value != null, value); return AppendIf(name, value is not null, value);
} }
/// <summary> /// <summary>

View File

@@ -14,7 +14,8 @@ namespace Snap.Hutao.Core.Database;
/// </summary> /// </summary>
/// <typeparam name="TEntity">实体的类型</typeparam> /// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam> /// <typeparam name="TMessage">消息的类型</typeparam>
internal sealed class ScopedDbCurrent<TEntity, TMessage> [ConstructorGenerated]
internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>, new() where TMessage : Message.ValueChangedMessage<TEntity>, new()
{ {
@@ -23,16 +24,6 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
private TEntity? current; private TEntity? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public ScopedDbCurrent(IServiceProvider serviceProvider)
{
messenger = serviceProvider.GetRequiredService<IMessenger>();
this.serviceProvider = serviceProvider;
}
/// <summary> /// <summary>
/// 当前选中的项 /// 当前选中的项
/// </summary> /// </summary>
@@ -54,9 +45,9 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>(); DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
// only update when not processing a deletion // only update when not processing a deletion
if (value != null) if (value is not null)
{ {
if (current != null) if (current is not null)
{ {
current.IsSelected = false; current.IsSelected = false;
dbSet.UpdateAndSave(current); dbSet.UpdateAndSave(current);
@@ -67,7 +58,7 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
current = value; current = value;
if (current != null) if (current is not null)
{ {
current.IsSelected = true; current.IsSelected = true;
dbSet.UpdateAndSave(current); dbSet.UpdateAndSave(current);
@@ -79,8 +70,8 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
} }
} }
[SuppressMessage("", "SA1402")] [ConstructorGenerated]
internal sealed class ScopedDbCurrent<TEntityOnly, TEntity, TMessage> internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
where TEntityOnly : class, IEntityOnly<TEntity> where TEntityOnly : class, IEntityOnly<TEntity>
where TEntity : class, ISelectable where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntityOnly>, new() where TMessage : Message.ValueChangedMessage<TEntityOnly>, new()
@@ -90,16 +81,6 @@ internal sealed class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
private TEntityOnly? current; private TEntityOnly? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public ScopedDbCurrent(IServiceProvider serviceProvider)
{
messenger = serviceProvider.GetRequiredService<IMessenger>();
this.serviceProvider = serviceProvider;
}
/// <summary> /// <summary>
/// 当前选中的项 /// 当前选中的项
/// </summary> /// </summary>
@@ -121,9 +102,9 @@ internal sealed class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>(); DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
// only update when not processing a deletion // only update when not processing a deletion
if (value != null) if (value is not null)
{ {
if (current != null) if (current is not null)
{ {
current.Entity.IsSelected = false; current.Entity.IsSelected = false;
dbSet.UpdateAndSave(current.Entity); dbSet.UpdateAndSave(current.Entity);
@@ -134,7 +115,7 @@ internal sealed class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
current = value; current = value;
if (current != null) if (current is not null)
{ {
current.Entity.IsSelected = true; current.Entity.IsSelected = true;
dbSet.UpdateAndSave(current.Entity); dbSet.UpdateAndSave(current.Entity);

View File

@@ -27,7 +27,7 @@ internal static class PickerExtension
file = null; file = null;
} }
if (file != null) if (file is not null)
{ {
return new(true, file.Path); return new(true, file.Path);
} }
@@ -53,7 +53,7 @@ internal static class PickerExtension
file = null; file = null;
} }
if (file != null) if (file is not null)
{ {
return new(true, file.Path); return new(true, file.Path);
} }
@@ -79,7 +79,7 @@ internal static class PickerExtension
folder = null; folder = null;
} }
if (folder != null) if (folder is not null)
{ {
return new(true, folder.Path); return new(true, folder.Path);
} }
@@ -92,7 +92,7 @@ internal static class PickerExtension
private static void InfoBarWaringPickerException(Exception? exception) private static void InfoBarWaringPickerException(Exception? exception)
{ {
if (exception != null) if (exception is not null)
{ {
Ioc.Default Ioc.Default
.GetRequiredService<IInfoBarService>() .GetRequiredService<IInfoBarService>()

View File

@@ -55,7 +55,7 @@ internal sealed class DebugLogger : ILogger
message = $"{logLevel}: {message}"; message = $"{logLevel}: {message}";
if (exception != null) if (exception is not null)
{ {
message += Environment.NewLine + Environment.NewLine + exception; message += Environment.NewLine + Environment.NewLine + exception;
} }

View File

@@ -42,7 +42,7 @@ internal static class TypeNameHelper
[return: NotNullIfNotNull(nameof(item))] [return: NotNullIfNotNull(nameof(item))]
public static string? GetTypeDisplayName(object? item, bool fullName = true) public static string? GetTypeDisplayName(object? item, bool fullName = true)
{ {
return item == null ? null : GetTypeDisplayName(item.GetType(), fullName); return item is null ? null : GetTypeDisplayName(item.GetType(), fullName);
} }
/// <summary> /// <summary>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -92,6 +93,7 @@ internal static partial class EnumerableExtension
/// <param name="list">列表</param> /// <param name="list">列表</param>
/// <param name="selector">选择器</param> /// <param name="selector">选择器</param>
/// <returns>新类型的列表</returns> /// <returns>新类型的列表</returns>
[Pure]
public static List<TResult> SelectList<TSource, TResult>(this List<TSource> list, Func<TSource, TResult> selector) public static List<TResult> SelectList<TSource, TResult>(this List<TSource> list, Func<TSource, TResult> selector)
{ {
Span<TSource> span = CollectionsMarshal.AsSpan(list); Span<TSource> span = CollectionsMarshal.AsSpan(list);

View File

@@ -97,7 +97,7 @@ internal sealed class Achievement
/// <inheritdoc/> /// <inheritdoc/>
public bool Equals(Achievement? other) public bool Equals(Achievement? other)
{ {
if (other == null) if (other is null)
{ {
return false; return false;
} }

View File

@@ -15,7 +15,8 @@ internal sealed class UIAF
/// </summary> /// </summary>
public const string CurrentVersion = "v1.1"; public const string CurrentVersion = "v1.1";
private static readonly List<string> SupportedVersion = new() // TODO use FrozenSet
private static readonly HashSet<string> SupportedVersion = new()
{ {
CurrentVersion, CurrentVersion,
}; };

View File

@@ -53,7 +53,7 @@ internal sealed class UIGF
{ {
foreach (UIGFItem item in List) foreach (UIGFItem item in List)
{ {
if (item.ItemType != SH.ModelInterchangeUIGFItemTypeAvatar && item.ItemType != SH.ModelInterchangeUIGFItemTypeWeapon) if (item.ItemType != SH.ModelInterchangeUIGFItemTypeAvatar || item.ItemType != SH.ModelInterchangeUIGFItemTypeWeapon)
{ {
return false; return false;
} }

View File

@@ -37,7 +37,7 @@ internal sealed class SkillDepot
{ {
get get
{ {
if (compositeSkills == null) if (compositeSkills is null)
{ {
compositeSkills = new(Skills.Count + 1 + Inherents.Count); compositeSkills = new(Skills.Count + 1 + Inherents.Count);
compositeSkills.AddRange(Skills); compositeSkills.AddRange(Skills);
@@ -60,7 +60,7 @@ internal sealed class SkillDepot
/// <returns>天赋列表</returns> /// <returns>天赋列表</returns>
public List<ProudableSkill> CompositeSkillsNoInherents() public List<ProudableSkill> CompositeSkillsNoInherents()
{ {
if (compositeSkillsNoInherents == null) if (compositeSkillsNoInherents is null)
{ {
compositeSkillsNoInherents = new(Skills.Count + 1); compositeSkillsNoInherents = new(Skills.Count + 1);

View File

@@ -18,7 +18,7 @@ internal sealed class AvatarNameCardPicConverter : ValueConverter<Avatar.Avatar?
/// <returns>名片</returns> /// <returns>名片</returns>
public static Uri AvatarToUri(Avatar.Avatar? avatar) public static Uri AvatarToUri(Avatar.Avatar? avatar)
{ {
if (avatar == null) if (avatar is null)
{ {
return null!; return null!;
} }

View File

@@ -38,7 +38,7 @@ internal sealed class ParameterFormat : IFormatProvider, ICustomFormatter
case 'P': case 'P':
return string.Format($"{{0:P0}}", arg); return string.Format($"{{0:P0}}", arg);
case 'I': case 'I':
return arg == null ? "0" : ((IConvertible)arg).ToInt32(default).ToString(); return arg is null ? "0" : ((IConvertible)arg).ToInt32(default).ToString();
} }
break; break;

View File

@@ -21,6 +21,11 @@ internal sealed class ReliquarySet
/// </summary> /// </summary>
public EquipAffixId EquipAffixId { get; set; } public EquipAffixId EquipAffixId { get; set; }
/// <summary>
/// 装备被动的被动Id
/// </summary>
public HashSet<ExtendedEquipAffixId> EquipAffixIds { get; set; } = default!;
/// <summary> /// <summary>
/// 套装名称 /// 套装名称
/// </summary> /// </summary>

View File

@@ -32,7 +32,12 @@ internal sealed partial class Weapon : IStatisticsItemSource, ISummaryItemSource
/// <summary> /// <summary>
/// 最大等级 /// 最大等级
/// </summary> /// </summary>
internal uint MaxLevel { get => ((int)Quality) >= 3 ? 90U : 70U; } internal uint MaxLevel { get => GetMaxLevelByQuality(Quality); }
public static uint GetMaxLevelByQuality(QualityType quality)
{
return quality >= QualityType.QUALITY_BLUE ? 90U : 70U;
}
/// <inheritdoc/> /// <inheritdoc/>
public ICalculableWeapon ToCalculable() public ICalculableWeapon ToCalculable()

View File

@@ -61,7 +61,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
{ {
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value; string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value;
storage = value == null ? defaultValue : bool.Parse(value); storage = value is null ? defaultValue : bool.Parse(value);
} }
return storage.Value; return storage.Value;
@@ -85,7 +85,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
{ {
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value; string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value;
storage = value == null ? defaultValue : int.Parse(value); storage = value is null ? defaultValue : int.Parse(value);
} }
return storage.Value; return storage.Value;
@@ -103,7 +103,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, T defaultValue) protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, T defaultValue)
where T : class where T : class
{ {
if (storage != null) if (storage is not null)
{ {
return storage; return storage;
} }
@@ -112,7 +112,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
{ {
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value; string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value;
storage = value == null ? defaultValue : deserializer(value); storage = value is null ? defaultValue : deserializer(value);
} }
return storage; return storage;
@@ -139,7 +139,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
{ {
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value; string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value;
storage = value == null ? defaultValue : deserializer(value); storage = value is null ? defaultValue : deserializer(value);
} }
return storage.Value; return storage.Value;

View File

@@ -70,7 +70,7 @@ internal sealed partial class AchievementDbBulkOperation
add++; add++;
continue; continue;
} }
else if (entity != null && uiaf == null) else if (entity is not null && uiaf is null)
{ {
// skip // skip
continue; continue;
@@ -157,7 +157,7 @@ internal sealed partial class AchievementDbBulkOperation
add++; add++;
continue; continue;
} }
else if (oldEntity != null && newEntity == null) else if (oldEntity is not null && newEntity is null)
{ {
appDbContext.Achievements.RemoveAndSave(oldEntity); appDbContext.Achievements.RemoveAndSave(oldEntity);
remove++; remove++;

View File

@@ -27,7 +27,7 @@ internal sealed partial class AchievementService
{ {
get get
{ {
if (archiveCollection == null) if (archiveCollection is null)
{ {
archiveCollection = achievementDbService.GetAchievementArchiveCollection(); archiveCollection = achievementDbService.GetAchievementArchiveCollection();
CurrentArchive = archiveCollection.SelectedOrDefault(); CurrentArchive = archiveCollection.SelectedOrDefault();

View File

@@ -27,7 +27,7 @@ internal sealed partial class AchievementService : IAchievementService
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
/// <inheritdoc/> /// <inheritdoc/>
public List<AchievementView> GetAchievementViews(AchievementArchive archive, List<MetadataAchievement> metadata) public List<AchievementView> GetAchievementViewList(AchievementArchive archive, List<MetadataAchievement> metadata)
{ {
Dictionary<AchievementId, EntityAchievement> entities = achievementDbService.GetAchievementMapByArchiveId(archive.InnerId); Dictionary<AchievementId, EntityAchievement> entities = achievementDbService.GetAchievementMapByArchiveId(archive.InnerId);

View File

@@ -38,7 +38,7 @@ internal interface IAchievementService
/// <param name="archive">用户</param> /// <param name="archive">用户</param>
/// <param name="metadata">元数据</param> /// <param name="metadata">元数据</param>
/// <returns>整合的成就</returns> /// <returns>整合的成就</returns>
List<AchievementView> GetAchievementViews(EntityArchive archive, List<MetadataAchievement> metadata); List<AchievementView> GetAchievementViewList(EntityArchive archive, List<MetadataAchievement> metadata);
/// <summary> /// <summary>
/// 异步导入UIAF数据 /// 异步导入UIAF数据

View File

@@ -13,17 +13,18 @@ namespace Snap.Hutao.Service;
/// 应用程序选项 /// 应用程序选项
/// 存储服务相关的选项 /// 存储服务相关的选项
/// </summary> /// </summary>
[ConstructorGenerated(CallBaseConstructor = true)]
[Injection(InjectAs.Singleton)] [Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions internal sealed partial class AppOptions : DbStoreOptions
{ {
private readonly List<NameValue<BackdropType>> supportedBackdropTypes = new() private static readonly List<NameValue<BackdropType>> SupportedBackdropTypesInner = new()
{ {
new("Acrylic", BackdropType.Acrylic), new("Acrylic", BackdropType.Acrylic),
new("Mica", BackdropType.Mica), new("Mica", BackdropType.Mica),
new("MicaAlt", BackdropType.MicaAlt), new("MicaAlt", BackdropType.MicaAlt),
}; };
private readonly List<NameValue<string>> supportedCultures = new() private static readonly List<NameValue<string>> SupportedCulturesInner = new()
{ {
ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")), ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")),
ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")), ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")),
@@ -38,15 +39,6 @@ internal sealed partial class AppOptions : DbStoreOptions
private CultureInfo? currentCulture; private CultureInfo? currentCulture;
private bool? isAdvancedLaunchOptionsEnabled; private bool? isAdvancedLaunchOptionsEnabled;
/// <summary>
/// 构造一个新的应用程序选项
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public AppOptions(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
/// <summary> /// <summary>
/// 游戏路径 /// 游戏路径
/// </summary> /// </summary>
@@ -68,7 +60,7 @@ internal sealed partial class AppOptions : DbStoreOptions
/// <summary> /// <summary>
/// 所有支持的背景样式 /// 所有支持的背景样式
/// </summary> /// </summary>
public List<NameValue<BackdropType>> BackdropTypes { get => supportedBackdropTypes; } public List<NameValue<BackdropType>> BackdropTypes { get => SupportedBackdropTypesInner; }
/// <summary> /// <summary>
/// 背景类型 默认 Mica /// 背景类型 默认 Mica
@@ -82,7 +74,7 @@ internal sealed partial class AppOptions : DbStoreOptions
/// <summary> /// <summary>
/// 所有支持的语言 /// 所有支持的语言
/// </summary> /// </summary>
public List<NameValue<string>> Cultures { get => supportedCultures; } public List<NameValue<string>> Cultures { get => SupportedCulturesInner; }
/// <summary> /// <summary>
/// 初始化前的语言 /// 初始化前的语言

View File

@@ -178,7 +178,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddOrUpdateAvatarInfo<TSource>(ModelAvatarInfo? entity, in AvatarId avatarId, string uid, AppDbContext appDbContext, IAvatarInfoTransformer<TSource> transformer, TSource source) private static void AddOrUpdateAvatarInfo<TSource>(ModelAvatarInfo? entity, in AvatarId avatarId, string uid, AppDbContext appDbContext, IAvatarInfoTransformer<TSource> transformer, TSource source)
{ {
if (entity == null) if (entity is null)
{ {
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId }; EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId };
transformer.Transform(ref avatarInfo, source); transformer.Transform(ref avatarInfo, source);
@@ -197,7 +197,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddOrUpdateAvatarInfo(ModelAvatarInfo? entity, string uid, AppDbContext appDbContext, EnkaAvatarInfo webInfo) private static void AddOrUpdateAvatarInfo(ModelAvatarInfo? entity, string uid, AppDbContext appDbContext, EnkaAvatarInfo webInfo)
{ {
if (entity == null) if (entity is null)
{ {
entity = ModelAvatarInfo.From(uid, webInfo); entity = ModelAvatarInfo.From(uid, webInfo);
appDbContext.AvatarInfos.AddAndSave(entity); appDbContext.AvatarInfos.AddAndSave(entity);

View File

@@ -41,7 +41,7 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
{ {
EnkaResponse? resp = await GetEnkaResponseAsync(userAndUid.Uid, token).ConfigureAwait(false); EnkaResponse? resp = await GetEnkaResponseAsync(userAndUid.Uid, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
if (resp == null) if (resp is null)
{ {
return new(RefreshResult.APIUnavailable, default); return new(RefreshResult.APIUnavailable, default);
} }

View File

@@ -123,7 +123,7 @@ internal sealed class SummaryAvatarFactory
WeaponStat? subStat = equip.Flat.WeaponStats?.ElementAtOrDefault(1); WeaponStat? subStat = equip.Flat.WeaponStats?.ElementAtOrDefault(1);
NameDescription subProperty; NameDescription subProperty;
if (subStat == null) if (subStat is null)
{ {
subProperty = new(string.Empty, string.Empty); subProperty = new(string.Empty, string.Empty);
} }
@@ -148,7 +148,7 @@ internal sealed class SummaryAvatarFactory
// EquipBase // EquipBase
Level = $"Lv.{equip.Weapon.Level.Value}", Level = $"Lv.{equip.Weapon.Level.Value}",
Quality = weapon.Quality, Quality = weapon.Quality,
MainProperty = mainStat != null ? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue) : default!, MainProperty = mainStat is not null ? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue) : default!,
// Weapon // Weapon
Id = weapon.Id, Id = weapon.Id,

View File

@@ -53,7 +53,7 @@ internal static class SummaryHelper
Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap); Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap);
if (proudSkillExtraLevelMap != null) if (proudSkillExtraLevelMap is not null)
{ {
foreach ((SkillGroupId groupId, SkillLevel extraLevel) in proudSkillExtraLevelMap) foreach ((SkillGroupId groupId, SkillLevel extraLevel) in proudSkillExtraLevelMap)
{ {

View File

@@ -27,7 +27,7 @@ internal sealed partial class CultivationService
{ {
get get
{ {
if (projects == null) if (projects is null)
{ {
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())
{ {

View File

@@ -177,7 +177,7 @@ internal sealed partial class CultivationService : ICultivationService
.GetCultivateEntryByProjectIdAndItemIdAsync(Current.InnerId, itemId) .GetCultivateEntryByProjectIdAndItemIdAsync(Current.InnerId, itemId)
.ConfigureAwait(false); .ConfigureAwait(false);
if (entry == null) if (entry is null)
{ {
entry = CultivateEntry.From(Current.InnerId, type, itemId); entry = CultivateEntry.From(Current.InnerId, type, itemId);
await cultivationDbService.InsertCultivateEntryAsync(entry).ConfigureAwait(false); await cultivationDbService.InsertCultivateEntryAsync(entry).ConfigureAwait(false);

View File

@@ -44,7 +44,7 @@ internal sealed class DailyNoteNotificationOperation
/// <returns>任务</returns> /// <returns>任务</returns>
public async ValueTask SendAsync() public async ValueTask SendAsync()
{ {
if (entry.DailyNote == null) if (entry.DailyNote is null)
{ {
return; return;
} }

View File

@@ -55,7 +55,7 @@ internal sealed class DailyNoteOptions : DbStoreOptions
get => GetOption(ref selectedRefreshTime, SettingEntry.DailyNoteRefreshSeconds, time => RefreshTimes.Single(t => t.Value == int.Parse(time)), RefreshTimes[1]); get => GetOption(ref selectedRefreshTime, SettingEntry.DailyNoteRefreshSeconds, time => RefreshTimes.Single(t => t.Value == int.Parse(time)), RefreshTimes[1]);
set set
{ {
if (value != null) if (value is not null)
{ {
if (scheduleTaskInterop.RegisterForDailyNoteRefresh(value.Value)) if (scheduleTaskInterop.RegisterForDailyNoteRefresh(value.Value))
{ {

View File

@@ -71,7 +71,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<ObservableCollection<DailyNoteEntry>> GetDailyNoteEntryCollectionAsync() public async ValueTask<ObservableCollection<DailyNoteEntry>> GetDailyNoteEntryCollectionAsync()
{ {
if (entries == null) if (entries is null)
{ {
await RefreshDailyNotesAsync().ConfigureAwait(false); await RefreshDailyNotesAsync().ConfigureAwait(false);

View File

@@ -15,7 +15,7 @@ internal static class GachaArchiveOperation
{ {
archive = archives.SingleOrDefault(a => a.Uid == uid); archive = archives.SingleOrDefault(a => a.Uid == uid);
if (archive == null) if (archive is null)
{ {
GachaArchive created = GachaArchive.From(uid); GachaArchive created = GachaArchive.From(uid);
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())

View File

@@ -97,7 +97,7 @@ internal struct GachaLogFetchContext
/// <param name="gachaLogDbService">祈愿记录数据库服务</param> /// <param name="gachaLogDbService">祈愿记录数据库服务</param>
public void EnsureArchiveAndEndId(GachaLogItem item, ObservableCollection<GachaArchive> archives, IGachaLogDbService gachaLogDbService) public void EnsureArchiveAndEndId(GachaLogItem item, ObservableCollection<GachaArchive> archives, IGachaLogDbService gachaLogDbService)
{ {
if (TargetArchive == null) if (TargetArchive is null)
{ {
GachaArchiveOperation.GetOrAdd(serviceProvider, item.Uid, archives, out TargetArchive); GachaArchiveOperation.GetOrAdd(serviceProvider, item.Uid, archives, out TargetArchive);
} }
@@ -146,7 +146,7 @@ internal struct GachaLogFetchContext
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// While no item is fetched, archive can be null. // While no item is fetched, archive can be null.
if (TargetArchive != null) if (TargetArchive is not null)
{ {
GachaItemSaveContext saveContext = new(ItemsToAdd, isLazy, QueryOptions.EndId, appDbContext.GachaItems); GachaItemSaveContext saveContext = new(ItemsToAdd, isLazy, QueryOptions.EndId, appDbContext.GachaItems);
TargetArchive.SaveItems(saveContext); TargetArchive.SaveItems(saveContext);

View File

@@ -69,7 +69,7 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
return new(false, null); return new(false, null);
} }
if (archive == null) if (archive is null)
{ {
archive = GachaArchive.From(uid); archive = GachaArchive.From(uid);
await gachaLogDbService.AddGachaArchiveAsync(archive).ConfigureAwait(false); await gachaLogDbService.AddGachaArchiveAsync(archive).ConfigureAwait(false);
@@ -121,7 +121,7 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
EndIds endIds = new(); EndIds endIds = new();
foreach (GachaConfigType type in GachaLog.QueryTypes) foreach (GachaConfigType type in GachaLog.QueryTypes)
{ {
if (archive != null) if (archive is not null)
{ {
endIds[type] = await gachaLogDbService endIds[type] = await gachaLogDbService
.GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(archive.InnerId, type, token) .GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(archive.InnerId, type, token)

View File

@@ -135,7 +135,7 @@ internal sealed partial class GachaLogService : IGachaLogService
(bool authkeyValid, GachaArchive? result) = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false); (bool authkeyValid, GachaArchive? result) = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false);
if (result != null) if (result is not null)
{ {
CurrentArchive = result; CurrentArchive = result;
} }

View File

@@ -294,7 +294,7 @@ internal sealed partial class GameService : IGameService
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
} }
if (account == null) if (account is null)
{ {
// ContentDialog must be created by main thread. // ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();

View File

@@ -42,7 +42,7 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
.FirstOrDefault(p => p.Key == "game_install_path")?.Value; .FirstOrDefault(p => p.Key == "game_install_path")?.Value;
} }
if (escapedPath != null) if (escapedPath is not null)
{ {
string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName); string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName);
return new(true, gamePath); return new(true, gamePath);
@@ -56,7 +56,7 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
{ {
using (RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神")) using (RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神"))
{ {
if (uninstallKey != null) if (uninstallKey is not null)
{ {
if (uninstallKey.GetValue(key) is string path) if (uninstallKey.GetValue(key) is string path)
{ {

View File

@@ -24,7 +24,6 @@ namespace Snap.Hutao.Service.Game.Package;
internal sealed partial class PackageConverter internal sealed partial class PackageConverter
{ {
private const string PackageVersion = "pkg_version"; private const string PackageVersion = "pkg_version";
private readonly IServiceProvider serviceProvider;
private readonly JsonSerializerOptions options; private readonly JsonSerializerOptions options;
private readonly RuntimeOptions runtimeOptions; private readonly RuntimeOptions runtimeOptions;
private readonly HttpClient httpClient; private readonly HttpClient httpClient;
@@ -97,7 +96,7 @@ internal sealed partial class PackageConverter
string sdkVersion = Path.Combine(gameFolder, "sdk_pkg_version"); string sdkVersion = Path.Combine(gameFolder, "sdk_pkg_version");
// Only bilibili's sdk is not null // Only bilibili's sdk is not null
if (resource.Sdk != null) if (resource.Sdk is not null)
{ {
// TODO: verify sdk md5 // TODO: verify sdk md5
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup)) if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
@@ -132,7 +131,7 @@ internal sealed partial class PackageConverter
FileOperation.Move(sdkVersion, sdkVersionBackup, true); FileOperation.Move(sdkVersion, sdkVersionBackup, true);
} }
if (resource.DeprecatedFiles != null) if (resource.DeprecatedFiles is not null)
{ {
foreach (NameMd5 file in resource.DeprecatedFiles) foreach (NameMd5 file in resource.DeprecatedFiles)
{ {
@@ -170,12 +169,6 @@ internal sealed partial class PackageConverter
} }
} }
private static void MoveToCache(string cacheFilePath, string targetFullPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
File.Move(targetFullPath, cacheFilePath, true);
}
[GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")] [GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")]
private static partial Regex DataFolderRegex(); private static partial Regex DataFolderRegex();

View File

@@ -35,7 +35,7 @@ internal static class RegistryInterop
/// <returns>账号是否设置</returns> /// <returns>账号是否设置</returns>
public static bool Set(GameAccount? account) public static bool Set(GameAccount? account)
{ {
if (account != null) if (account is not null)
{ {
string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(account.MihoyoSDK)); string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(account.MihoyoSDK));
string path = $"HKCU:{GenshinKey[@"HKEY_CURRENT_USER\".Length..]}"; string path = $"HKCU:{GenshinKey[@"HKEY_CURRENT_USER\".Length..]}";

View File

@@ -53,7 +53,7 @@ internal sealed partial class HutaoCache : IHutaoCache
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<bool> InitializeForDatabaseViewModelAsync() public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
{ {
if (databaseViewModelTaskSource != null) if (databaseViewModelTaskSource is not null)
{ {
return await databaseViewModelTaskSource.Task.ConfigureAwait(false); return await databaseViewModelTaskSource.Task.ConfigureAwait(false);
} }
@@ -84,7 +84,7 @@ internal sealed partial class HutaoCache : IHutaoCache
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync() public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync()
{ {
if (wikiAvatarViewModelTaskSource != null) if (wikiAvatarViewModelTaskSource is not null)
{ {
return await wikiAvatarViewModelTaskSource.Task.ConfigureAwait(false); return await wikiAvatarViewModelTaskSource.Task.ConfigureAwait(false);
} }
@@ -108,7 +108,7 @@ internal sealed partial class HutaoCache : IHutaoCache
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<bool> InitializeForWikiWeaponViewModelAsync() public async ValueTask<bool> InitializeForWikiWeaponViewModelAsync()
{ {
if (wikiWeaponViewModelTaskSource != null) if (wikiWeaponViewModelTaskSource is not null)
{ {
return await wikiWeaponViewModelTaskSource.Task.ConfigureAwait(false); return await wikiWeaponViewModelTaskSource.Task.ConfigureAwait(false);
} }
@@ -129,7 +129,7 @@ internal sealed partial class HutaoCache : IHutaoCache
private async ValueTask<Dictionary<AvatarId, Avatar>> GetIdAvatarMapExtendedAsync() private async ValueTask<Dictionary<AvatarId, Avatar>> GetIdAvatarMapExtendedAsync()
{ {
if (idAvatarExtendedMap == null) if (idAvatarExtendedMap is null)
{ {
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idAvatarExtendedMap = AvatarIds.WithPlayers(idAvatarMap); idAvatarExtendedMap = AvatarIds.WithPlayers(idAvatarMap);

View File

@@ -99,7 +99,7 @@ internal sealed partial class HutaoService : IHutaoService
try try
{ {
if (data != null) if (data is not null)
{ {
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())
{ {

View File

@@ -30,7 +30,7 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
{ {
get get
{ {
if (fallbackDataFolder == null) if (fallbackDataFolder is null)
{ {
fallbackDataFolder = Path.Combine(hutaoOptions.DataFolder, "Metadata", "CHS"); fallbackDataFolder = Path.Combine(hutaoOptions.DataFolder, "Metadata", "CHS");
Directory.CreateDirectory(fallbackDataFolder); Directory.CreateDirectory(fallbackDataFolder);
@@ -47,7 +47,7 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
{ {
get get
{ {
if (localizedDataFolder == null) if (localizedDataFolder is null)
{ {
localizedDataFolder = Path.Combine(hutaoOptions.DataFolder, "Metadata", LocaleName); localizedDataFolder = Path.Combine(hutaoOptions.DataFolder, "Metadata", LocaleName);
Directory.CreateDirectory(localizedDataFolder); Directory.CreateDirectory(localizedDataFolder);

View File

@@ -216,7 +216,7 @@ internal sealed class NavigationService : INavigationService, INavigationInitial
: NavHelper.GetNavigateTo(selected); : NavHelper.GetNavigateTo(selected);
// ignore item that doesn't have nav type specified // ignore item that doesn't have nav type specified
if (targetType != null) if (targetType is not null)
{ {
INavigationAwaiter navigationAwaiter = new NavigationExtra(NavHelper.GetExtraData(selected)); INavigationAwaiter navigationAwaiter = new NavigationExtra(NavHelper.GetExtraData(selected));
Navigate(targetType, navigationAwaiter, false); Navigate(targetType, navigationAwaiter, false);

View File

@@ -60,7 +60,7 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
{ {
Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data; Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data;
if (spiralAbysses!.SingleOrDefault(s => s.ScheduleId == webSpiralAbyss.ScheduleId) is SpiralAbyssEntry existEntry) if (spiralAbysses!.SingleOrDefault(s => s.ScheduleId == webSpiralAbyss.ScheduleId) is { } existEntry)
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
existEntry.UpdateSpiralAbyss(webSpiralAbyss); existEntry.UpdateSpiralAbyss(webSpiralAbyss);

View File

@@ -60,7 +60,7 @@ internal sealed partial class UserService : IUserService
public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync() public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
{ {
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
if (userCollection == null) if (userCollection is null)
{ {
List<Model.Entity.User> entities = await userDbService.GetUserListAsync().ConfigureAwait(false); List<Model.Entity.User> entities = await userDbService.GetUserListAsync().ConfigureAwait(false);
List<BindingUser> users = await entities.SelectListAsync(BindingUser.ResumeAsync, default).ConfigureAwait(false); List<BindingUser> users = await entities.SelectListAsync(BindingUser.ResumeAsync, default).ConfigureAwait(false);
@@ -83,7 +83,7 @@ internal sealed partial class UserService : IUserService
public async ValueTask<ObservableCollection<UserAndUid>> GetRoleCollectionAsync() public async ValueTask<ObservableCollection<UserAndUid>> GetRoleCollectionAsync()
{ {
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
if (userAndUidCollection == null) if (userAndUidCollection is null)
{ {
ObservableCollection<BindingUser> users = await GetUserCollectionAsync().ConfigureAwait(false); ObservableCollection<BindingUser> users = await GetUserCollectionAsync().ConfigureAwait(false);
userAndUidCollection = users userAndUidCollection = users
@@ -186,16 +186,16 @@ internal sealed partial class UserService : IUserService
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
BindingUser? newUser = await BindingUser.CreateAsync(cookie, isOversea).ConfigureAwait(false); BindingUser? newUser = await BindingUser.CreateAsync(cookie, isOversea).ConfigureAwait(false);
if (newUser != null) if (newUser is not null)
{ {
// Sync cache // Sync cache
if (userCollection != null) if (userCollection is not null)
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
{ {
userCollection!.Add(newUser); userCollection.Add(newUser);
if (userAndUidCollection != null) if (userAndUidCollection is not null)
{ {
foreach (UserGameRole role in newUser.UserGameRoles) foreach (UserGameRole role in newUser.UserGameRoles)
{ {

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.View.Control;
/// <summary> /// <summary>
/// 胡桃云祈愿统计卡片 /// 胡桃云祈愿统计卡片
/// </summary> /// </summary>
public sealed partial class HutaoStatisticsCard : UserControl internal sealed partial class HutaoStatisticsCard : UserControl
{ {
public HutaoStatisticsCard() public HutaoStatisticsCard()
{ {

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.ViewModel.Abstraction; namespace Snap.Hutao.ViewModel.Abstraction;
@@ -27,19 +28,31 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel
/// <inheritdoc/> /// <inheritdoc/>
public bool IsViewDisposed { get; set; } public bool IsViewDisposed { get; set; }
protected TaskCompletionSource<bool> Initialization { get; } = new();
/// <summary> /// <summary>
/// 异步初始化UI /// 异步初始化UI
/// </summary> /// </summary>
/// <returns>任务</returns> /// <returns>任务</returns>
[Command("OpenUICommand")] [Command("OpenUICommand")]
protected abstract Task OpenUIAsync(); protected virtual async Task OpenUIAsync()
{
// Set value on UI thread
IsInitialized = await InitializeUIAsync().ConfigureAwait(true);
Initialization.TrySetResult(IsInitialized);
}
protected virtual ValueTask<bool> InitializeUIAsync()
{
return ValueTask.FromResult(true);
}
/// <summary> /// <summary>
/// 保证 using scope 内的代码运行完成 /// 保证 using scope 内的代码运行完成
/// 防止 视图资源被回收 /// 防止 视图资源被回收
/// </summary> /// </summary>
/// <returns>解除执行限制</returns> /// <returns>解除执行限制</returns>
protected async Task<IDisposable> EnterCriticalExecutionAsync() protected async ValueTask<IDisposable> EnterCriticalExecutionAsync()
{ {
ThrowIfViewDisposed(); ThrowIfViewDisposed();
IDisposable disposable = await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(false); IDisposable disposable = await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(false);
@@ -55,7 +68,7 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel
{ {
if (IsViewDisposed) if (IsViewDisposed)
{ {
throw new OperationCanceledException(SH.ViewModelViewDisposedOperationCancel); ThrowHelper.OperationCanceled(SH.ViewModelViewDisposedOperationCancel);
} }
} }
} }

View File

@@ -10,19 +10,12 @@ namespace Snap.Hutao.ViewModel.Abstraction;
/// <summary> /// <summary>
/// 简化的视图模型抽象类 /// 简化的视图模型抽象类
/// </summary> /// </summary>
[ConstructorGenerated]
internal abstract partial class ViewModelSlim : ObservableObject internal abstract partial class ViewModelSlim : ObservableObject
{ {
private readonly IServiceProvider serviceProvider;
private bool isInitialized; private bool isInitialized;
/// <summary>
/// 构造一个新的简化的视图模型抽象类
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public ViewModelSlim(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
/// <summary> /// <summary>
/// 是否初始化完成 /// 是否初始化完成
/// </summary> /// </summary>
@@ -31,7 +24,7 @@ internal abstract partial class ViewModelSlim : ObservableObject
/// <summary> /// <summary>
/// 服务提供器 /// 服务提供器
/// </summary> /// </summary>
protected IServiceProvider ServiceProvider { get; } protected IServiceProvider ServiceProvider { get => serviceProvider; }
/// <summary> /// <summary>
/// 打开界面执行 /// 打开界面执行
@@ -48,19 +41,11 @@ internal abstract partial class ViewModelSlim : ObservableObject
/// 简化的视图模型抽象类 /// 简化的视图模型抽象类
/// 可导航 /// 可导航
/// </summary> /// </summary>
/// <typeparam name="TPage">页面类型</typeparam> /// <typeparam name="TPage">要导航到的页面类型</typeparam>
[ConstructorGenerated(CallBaseConstructor = true)]
internal abstract partial class ViewModelSlim<TPage> : ViewModelSlim internal abstract partial class ViewModelSlim<TPage> : ViewModelSlim
where TPage : Page where TPage : Page
{ {
/// <summary>
/// 构造一个新的简化的视图模型抽象类
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public ViewModelSlim(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
/// <summary> /// <summary>
/// 导航到指定的页面类型 /// 导航到指定的页面类型
/// </summary> /// </summary>

View File

@@ -22,28 +22,37 @@ internal static class AchievementFinishPercent
int totalFinished = 0; int totalFinished = 0;
int totalCount = 0; int totalCount = 0;
if (viewModel.Achievements is AdvancedCollectionView achievements) if (viewModel.Achievements is { } achievements)
{ {
if (viewModel.AchievementGoals is List<AchievementGoalView> achievementGoals) if (viewModel.AchievementGoals is { } achievementGoals)
{ {
Dictionary<AchievementGoalId, AchievementGoalStatistics> counter = achievementGoals.ToDictionary(x => x.Id, AchievementGoalStatistics.Create); Dictionary<AchievementGoalId, AchievementGoalStatistics> counter = achievementGoals.ToDictionary(x => x.Id, AchievementGoalStatistics.From);
foreach (AchievementView achievement in achievements.SourceCollection.Cast<AchievementView>())
{
// Make the state update as fast as possible
ref AchievementGoalStatistics stat = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal);
stat.TotalCount += 1; // Fast path
totalCount += 1; if (achievements.SourceCollection is List<AchievementView> list)
if (achievement.IsChecked) {
foreach (ref readonly AchievementView achievement in CollectionsMarshal.AsSpan(list))
{ {
stat.Finished += 1; // Make the state update as fast as possible
totalFinished += 1; ref AchievementGoalStatistics stat = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal);
stat.TotalCount += 1;
totalCount += 1;
if (achievement.IsChecked)
{
stat.Finished += 1;
totalFinished += 1;
}
} }
} }
else
{
Must.NeverHappen("AchievementViewModel.Achievements.SourceCollection 应为 List<AchievementView>");
}
foreach (AchievementGoalStatistics statistics in counter.Values) foreach (AchievementGoalStatistics statistics in counter.Values)
{ {
statistics.AchievementGoal.UpdateFinishPercent(statistics); statistics.AchievementGoal.UpdateFinishDescriptionAndPercent(statistics);
} }
viewModel.FinishDescription = AchievementStatistics.Format(totalFinished, totalCount, out _); viewModel.FinishDescription = AchievementStatistics.Format(totalFinished, totalCount, out _);

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using BindingAchievementGoal = Snap.Hutao.ViewModel.Achievement.AchievementGoalView; using BindingAchievementGoal = Snap.Hutao.ViewModel.Achievement.AchievementGoalView;
namespace Snap.Hutao.ViewModel.Achievement; namespace Snap.Hutao.ViewModel.Achievement;
@@ -9,7 +10,7 @@ namespace Snap.Hutao.ViewModel.Achievement;
/// 成就分类统计 /// 成就分类统计
/// </summary> /// </summary>
[HighQuality] [HighQuality]
internal struct AchievementGoalStatistics internal struct AchievementGoalStatistics : IMappingFrom<AchievementGoalStatistics, BindingAchievementGoal>
{ {
/// <summary> /// <summary>
/// 成就分类 /// 成就分类
@@ -40,7 +41,7 @@ internal struct AchievementGoalStatistics
/// </summary> /// </summary>
/// <param name="goal">分类</param> /// <param name="goal">分类</param>
/// <returns>新的成就分类统计</returns> /// <returns>新的成就分类统计</returns>
public static AchievementGoalStatistics Create(BindingAchievementGoal goal) public static AchievementGoalStatistics From(BindingAchievementGoal goal)
{ {
return new(goal); return new(goal);
} }

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Model; using Snap.Hutao.Model;
using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Metadata.Converter;
@@ -13,7 +14,7 @@ namespace Snap.Hutao.ViewModel.Achievement;
/// 绑定成就分类 /// 绑定成就分类
/// </summary> /// </summary>
[HighQuality] [HighQuality]
internal sealed class AchievementGoalView : ObservableObject, INameIcon internal sealed class AchievementGoalView : ObservableObject, INameIcon, IMappingFrom<AchievementGoalView, AchievementGoal>
{ {
private double finishPercent; private double finishPercent;
private string? finishDescription; private string? finishDescription;
@@ -22,7 +23,7 @@ internal sealed class AchievementGoalView : ObservableObject, INameIcon
/// 构造一个新的成就分类 /// 构造一个新的成就分类
/// </summary> /// </summary>
/// <param name="goal">分类</param> /// <param name="goal">分类</param>
public AchievementGoalView(AchievementGoal goal) private AchievementGoalView(AchievementGoal goal)
{ {
Id = goal.Id; Id = goal.Id;
Order = goal.Order; Order = goal.Order;
@@ -60,26 +61,18 @@ internal sealed class AchievementGoalView : ObservableObject, INameIcon
/// </summary> /// </summary>
public string? FinishDescription { get => finishDescription; set => SetProperty(ref finishDescription, value); } public string? FinishDescription { get => finishDescription; set => SetProperty(ref finishDescription, value); }
/// <summary> public static AchievementGoalView From(AchievementGoal source)
/// 创建新的列表
/// </summary>
/// <param name="goals">目标</param>
/// <returns>列表</returns>
public static List<AchievementGoalView> List(List<AchievementGoal> goals)
{ {
return goals return new(source);
.OrderBy(goal => goal.Order)
.Select(goal => new AchievementGoalView(goal))
.ToList();
} }
/// <summary> /// <summary>
/// 更新进度 /// 更新进度
/// </summary> /// </summary>
/// <param name="statistics">统计</param> /// <param name="statistics">统计</param>
public void UpdateFinishPercent(AchievementGoalStatistics statistics) public void UpdateFinishDescriptionAndPercent(AchievementGoalStatistics statistics)
{ {
UpdateFinishPercent(statistics.Finished, statistics.TotalCount); UpdateFinishDescriptionAndPercent(statistics.Finished, statistics.TotalCount);
} }
/// <summary> /// <summary>
@@ -87,7 +80,7 @@ internal sealed class AchievementGoalView : ObservableObject, INameIcon
/// </summary> /// </summary>
/// <param name="finished">完成项</param> /// <param name="finished">完成项</param>
/// <param name="count">总项</param> /// <param name="count">总项</param>
private void UpdateFinishPercent(int finished, int count) private void UpdateFinishDescriptionAndPercent(int finished, int count)
{ {
FinishDescription = AchievementStatistics.Format(finished, count, out double finishPercent); FinishDescription = AchievementStatistics.Format(finished, count, out double finishPercent);
FinishPercent = finishPercent; FinishPercent = finishPercent;

View File

@@ -33,11 +33,11 @@ internal sealed partial class AchievementImporter
/// 从剪贴板导入 /// 从剪贴板导入
/// </summary> /// </summary>
/// <returns>是否导入成功</returns> /// <returns>是否导入成功</returns>
public async Task<bool> FromClipboardAsync() public async ValueTask<bool> FromClipboardAsync()
{ {
if (achievementService.CurrentArchive is EntityAchievementArchive archive) if (achievementService.CurrentArchive is { } archive)
{ {
if (await TryCatchGetUIAFFromClipboardAsync().ConfigureAwait(false) is UIAF uiaf) if (await TryCatchGetUIAFFromClipboardAsync().ConfigureAwait(false) is { } uiaf)
{ {
return await TryImportAsync(archive, uiaf).ConfigureAwait(false); return await TryImportAsync(archive, uiaf).ConfigureAwait(false);
} }
@@ -58,9 +58,9 @@ internal sealed partial class AchievementImporter
/// 从文件导入 /// 从文件导入
/// </summary> /// </summary>
/// <returns>是否导入成功</returns> /// <returns>是否导入成功</returns>
public async Task<bool> FromFileAsync() public async ValueTask<bool> FromFileAsync()
{ {
if (achievementService.CurrentArchive is EntityAchievementArchive archive) if (achievementService.CurrentArchive is { } archive)
{ {
ValueResult<bool, ValueFile> pickerResult = await serviceProvider ValueResult<bool, ValueFile> pickerResult = await serviceProvider
.GetRequiredService<IPickerFactory>() .GetRequiredService<IPickerFactory>()
@@ -90,7 +90,7 @@ internal sealed partial class AchievementImporter
return false; return false;
} }
private async Task<UIAF?> TryCatchGetUIAFFromClipboardAsync() private async ValueTask<UIAF?> TryCatchGetUIAFFromClipboardAsync()
{ {
try try
{ {
@@ -106,28 +106,30 @@ internal sealed partial class AchievementImporter
} }
} }
private async Task<bool> TryImportAsync(EntityAchievementArchive archive, UIAF uiaf) private async ValueTask<bool> TryImportAsync(EntityAchievementArchive archive, UIAF uiaf)
{ {
if (uiaf.IsCurrentVersionSupported()) if (uiaf.IsCurrentVersionSupported())
{ {
// ContentDialog must be created by main thread. // ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
AchievementImportDialog importDialog = serviceProvider.CreateInstance<AchievementImportDialog>(uiaf); AchievementImportDialog importDialog = serviceProvider.CreateInstance<AchievementImportDialog>(uiaf);
(bool isOk, ImportStrategy strategy) = await importDialog.GetImportStrategyAsync().ConfigureAwait(true); (bool isOk, ImportStrategy strategy) = await importDialog.GetImportStrategyAsync().ConfigureAwait(false);
if (isOk) if (isOk)
{ {
ImportResult result; await taskContext.SwitchToMainThreadAsync();
ContentDialog dialog = await serviceProvider.GetRequiredService<IContentDialogFactory>() ContentDialog dialog = await serviceProvider
.GetRequiredService<IContentDialogFactory>()
.CreateForIndeterminateProgressAsync(SH.ViewModelAchievementImportProgress) .CreateForIndeterminateProgressAsync(SH.ViewModelAchievementImportProgress)
.ConfigureAwait(false); .ConfigureAwait(false);
ImportResult result;
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{ {
result = await achievementService.ImportFromUIAFAsync(archive, uiaf.List, strategy).ConfigureAwait(false); result = await achievementService.ImportFromUIAFAsync(archive, uiaf.List, strategy).ConfigureAwait(false);
} }
infoBarService.Success(result.ToString()); infoBarService.Success($"{result}");
return true; return true;
} }
} }

View File

@@ -25,7 +25,7 @@ internal sealed class AchievementStatistics
/// <summary> /// <summary>
/// 格式化完成进度 /// 格式化完成进度
/// "xxx/yyy - zz.zz%" /// "xxx/yyy - z.zz%"
/// </summary> /// </summary>
/// <param name="finished">完成的成就个数</param> /// <param name="finished">完成的成就个数</param>
/// <param name="totalCount">总个数</param> /// <param name="totalCount">总个数</param>

View File

@@ -53,19 +53,11 @@ internal sealed class AchievementView : ObservableObject, IEntityWithMetadata<Mo
{ {
if (SetProperty(ref isChecked, value)) if (SetProperty(ref isChecked, value))
{ {
// Only update state when checked (Entity.Status, Entity.Time) = value
if (value) ? (AchievementStatus.STATUS_REWARD_TAKEN, DateTimeOffset.Now)
{ : (AchievementStatus.STATUS_FINISHED, default);
Entity.Status = AchievementStatus.STATUS_REWARD_TAKEN;
Entity.Time = DateTimeOffset.Now; OnPropertyChanged(nameof(Time));
OnPropertyChanged(nameof(Time));
}
else
{
Entity.Status = AchievementStatus.STATUS_UNFINISHED;
Entity.Time = default;
OnPropertyChanged(nameof(Time));
}
} }
} }
} }

View File

@@ -31,7 +31,6 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
private static readonly SortDescription UncompletedItemsFirstSortDescription = new(nameof(AchievementView.IsChecked), SortDirection.Ascending); private static readonly SortDescription UncompletedItemsFirstSortDescription = new(nameof(AchievementView.IsChecked), SortDirection.Ascending);
private static readonly SortDescription CompletionTimeSortDescription = new(nameof(AchievementView.Time), SortDirection.Descending); private static readonly SortDescription CompletionTimeSortDescription = new(nameof(AchievementView.Time), SortDirection.Descending);
private readonly TaskCompletionSource<bool> openUITaskCompletionSource;
private readonly IContentDialogFactory contentDialogFactory; private readonly IContentDialogFactory contentDialogFactory;
private readonly AchievementImporter achievementImporter; private readonly AchievementImporter achievementImporter;
private readonly IAchievementService achievementService; private readonly IAchievementService achievementService;
@@ -101,9 +100,11 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
get => selectedAchievementGoal; get => selectedAchievementGoal;
set set
{ {
SetProperty(ref selectedAchievementGoal, value); if (SetProperty(ref selectedAchievementGoal, value))
SearchText = string.Empty; {
UpdateAchievementsFilterByGoal(value); SearchText = string.Empty;
UpdateAchievementsFilterByGoal(value);
}
} }
} }
@@ -137,7 +138,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<bool> ReceiveAsync(INavigationData data) public async ValueTask<bool> ReceiveAsync(INavigationData data)
{ {
if (await openUITaskCompletionSource.Task.ConfigureAwait(false)) if (await Initialization.Task.ConfigureAwait(false))
{ {
if (data.Data is Activation.ImportUIAFFromClipboard) if (data.Data is Activation.ImportUIAFFromClipboard)
{ {
@@ -149,12 +150,9 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
return false; return false;
} }
/// <inheritdoc/> protected override async ValueTask<bool> InitializeUIAsync()
protected override async Task OpenUIAsync()
{ {
bool metaInitialized = await metadataService.InitializeAsync().ConfigureAwait(false); if (await metadataService.InitializeAsync().ConfigureAwait(false))
if (metaInitialized)
{ {
try try
{ {
@@ -167,33 +165,29 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
.GetAchievementGoalsAsync(CancellationToken) .GetAchievementGoalsAsync(CancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
sortedGoals = AchievementGoalView.List(goals); sortedGoals = goals.SortBy(goal => goal.Order).SelectList(AchievementGoalView.From);
archives = achievementService.ArchiveCollection; archives = achievementService.ArchiveCollection;
} }
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
AchievementGoals = sortedGoals; AchievementGoals = sortedGoals;
Archives = archives; Archives = archives;
SelectedArchive = achievementService.CurrentArchive; SelectedArchive = achievementService.CurrentArchive;
return true;
IsInitialized = true;
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// User canceled the loading operation,
// Indicate initialization not succeed.
openUITaskCompletionSource.TrySetResult(false);
return;
} }
} }
openUITaskCompletionSource.TrySetResult(metaInitialized); return false;
} }
[Command("AddArchiveCommand")] [Command("AddArchiveCommand")]
private async Task AddArchiveAsync() private async Task AddArchiveAsync()
{ {
if (Archives != null) if (Archives is null)
{ {
// ContentDialog must be created by main thread. // ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
@@ -212,10 +206,10 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
infoBarService.Success(string.Format(SH.ViewModelAchievementArchiveAdded, name)); infoBarService.Success(string.Format(SH.ViewModelAchievementArchiveAdded, name));
break; break;
case ArchiveAddResult.InvalidName: case ArchiveAddResult.InvalidName:
infoBarService.Information(SH.ViewModelAchievementArchiveInvalidName); infoBarService.Warning(SH.ViewModelAchievementArchiveInvalidName);
break; break;
case ArchiveAddResult.AlreadyExists: case ArchiveAddResult.AlreadyExists:
infoBarService.Information(string.Format(SH.ViewModelAchievementArchiveAlreadyExists, name)); infoBarService.Warning(string.Format(SH.ViewModelAchievementArchiveAlreadyExists, name));
break; break;
default: default:
throw Must.NeverHappen(); throw Must.NeverHappen();
@@ -227,12 +221,12 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[Command("RemoveArchiveCommand")] [Command("RemoveArchiveCommand")]
private async Task RemoveArchiveAsync() private async Task RemoveArchiveAsync()
{ {
if (Archives != null && SelectedArchive != null) if (Archives is not null && SelectedArchive is not null)
{ {
string title = string.Format(SH.ViewModelAchievementRemoveArchiveTitle, SelectedArchive.Name);
string content = SH.ViewModelAchievementRemoveArchiveContent;
ContentDialogResult result = await contentDialogFactory ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync( .CreateForConfirmCancelAsync(title, content)
string.Format(SH.ViewModelAchievementRemoveArchiveTitle, SelectedArchive.Name),
SH.ViewModelAchievementRemoveArchiveContent)
.ConfigureAwait(false); .ConfigureAwait(false);
if (result == ContentDialogResult.Primary) if (result == ContentDialogResult.Primary)
@@ -258,8 +252,9 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[Command("ExportAsUIAFToFileCommand")] [Command("ExportAsUIAFToFileCommand")]
private async Task ExportAsUIAFToFileAsync() private async Task ExportAsUIAFToFileAsync()
{ {
if (SelectedArchive != null && Achievements != null) if (SelectedArchive is not null && Achievements is not null)
{ {
string fileName = $"{achievementService.CurrentArchive?.Name}.json";
Dictionary<string, IList<string>> fileTypes = new() Dictionary<string, IList<string>> fileTypes = new()
{ {
[SH.ViewModelAchievementExportFileType] = ".json".Enumerate().ToList(), [SH.ViewModelAchievementExportFileType] = ".json".Enumerate().ToList(),
@@ -267,11 +262,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
FileSavePicker picker = serviceProvider FileSavePicker picker = serviceProvider
.GetRequiredService<IPickerFactory>() .GetRequiredService<IPickerFactory>()
.GetFileSavePicker( .GetFileSavePicker(PickerLocationId.Desktop, fileName, SH.FilePickerExportCommit, fileTypes);
PickerLocationId.Desktop,
$"{achievementService.CurrentArchive?.Name}.json",
SH.FilePickerExportCommit,
fileTypes);
(bool isPickerOk, ValueFile file) = await picker.TryPickSaveFileAsync().ConfigureAwait(false); (bool isPickerOk, ValueFile file) = await picker.TryPickSaveFileAsync().ConfigureAwait(false);
if (isPickerOk) if (isPickerOk)
@@ -294,7 +285,8 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
{ {
if (await achievementImporter.FromClipboardAsync().ConfigureAwait(false)) if (await achievementImporter.FromClipboardAsync().ConfigureAwait(false))
{ {
await UpdateAchievementsAsync(achievementService.CurrentArchive!).ConfigureAwait(false); ArgumentNullException.ThrowIfNull(achievementService.CurrentArchive);
await UpdateAchievementsAsync(achievementService.CurrentArchive).ConfigureAwait(false);
} }
} }
@@ -303,13 +295,15 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
{ {
if (await achievementImporter.FromFileAsync().ConfigureAwait(false)) if (await achievementImporter.FromFileAsync().ConfigureAwait(false))
{ {
await UpdateAchievementsAsync(achievementService.CurrentArchive!).ConfigureAwait(false); ArgumentNullException.ThrowIfNull(achievementService.CurrentArchive);
await UpdateAchievementsAsync(achievementService.CurrentArchive).ConfigureAwait(false);
} }
} }
private async Task UpdateAchievementsAsync(EntityAchievementArchive? archive) private async ValueTask UpdateAchievementsAsync(EntityAchievementArchive? archive)
{ {
if (archive == null) // TODO: immediately clear values
if (archive is null)
{ {
return; return;
} }
@@ -319,8 +313,8 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
if (TryGetAchievements(archive, achievements, out List<AchievementView>? combined)) if (TryGetAchievements(archive, achievements, out List<AchievementView>? combined))
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
Achievements = new(combined, true); // Assemble achievements on the UI thread.
Achievements = new(combined, true);
UpdateAchievementsFinishPercent(); UpdateAchievementsFinishPercent();
UpdateAchievementsFilterByGoal(SelectedAchievementGoal); UpdateAchievementsFilterByGoal(SelectedAchievementGoal);
UpdateAchievementsSort(); UpdateAchievementsSort();
@@ -331,13 +325,13 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
{ {
try try
{ {
combined = achievementService.GetAchievementViews(archive, achievements); combined = achievementService.GetAchievementViewList(archive, achievements);
return true; return true;
} }
catch (Core.ExceptionService.UserdataCorruptedException ex) catch (Core.ExceptionService.UserdataCorruptedException ex)
{ {
combined = default;
infoBarService.Error(ex); infoBarService.Error(ex);
combined = default;
return false; return false;
} }
} }
@@ -345,7 +339,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[Command("SortUncompletedSwitchCommand")] [Command("SortUncompletedSwitchCommand")]
private void UpdateAchievementsSort() private void UpdateAchievementsSort()
{ {
if (Achievements != null) if (Achievements is not null)
{ {
if (IsUncompletedItemsFirst) if (IsUncompletedItemsFirst)
{ {
@@ -361,15 +355,16 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
private void UpdateAchievementsFilterByGoal(AchievementGoalView? goal) private void UpdateAchievementsFilterByGoal(AchievementGoalView? goal)
{ {
if (Achievements != null) if (Achievements is not null)
{ {
if (goal == null) if (goal is null)
{ {
Achievements.Filter = null; Achievements.Filter = null;
} }
else else
{ {
Achievements.Filter = (object o) => o is AchievementView view && view.Inner.Goal == goal.Id; Model.Primitive.AchievementGoalId goalId = goal.Id;
Achievements.Filter = (object o) => o is AchievementView view && view.Inner.Goal == goalId;
} }
} }
} }
@@ -377,15 +372,15 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[Command("SearchAchievementCommand")] [Command("SearchAchievementCommand")]
private void UpdateAchievementsFilterBySearch(string? search) private void UpdateAchievementsFilterBySearch(string? search)
{ {
if (Achievements != null) if (Achievements is not null)
{ {
SetProperty(ref selectedAchievementGoal, null); SetProperty(ref selectedAchievementGoal, null);
if (!string.IsNullOrEmpty(search)) if (!string.IsNullOrEmpty(search))
{ {
if (search.Length == 5 && int.TryParse(search, out int achievementId)) if (uint.TryParse(search, out uint achievementId))
{ {
Achievements.Filter = obj => ((AchievementView)obj).Inner.Id.Value == achievementId; Achievements.Filter = obj => ((AchievementView)obj).Inner.Id == achievementId;
} }
else else
{ {
@@ -408,7 +403,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[Command("SaveAchievementCommand")] [Command("SaveAchievementCommand")]
private void SaveAchievement(AchievementView? achievement) private void SaveAchievement(AchievementView? achievement)
{ {
if (achievement != null) if (achievement is not null)
{ {
achievementService.SaveAchievement(achievement); achievementService.SaveAchievement(achievement);
UpdateAchievementsFinishPercent(); UpdateAchievementsFinishPercent();

View File

@@ -10,20 +10,12 @@ namespace Snap.Hutao.ViewModel.Achievement;
/// <summary> /// <summary>
/// 简化的成就视图模型 /// 简化的成就视图模型
/// </summary> /// </summary>
[ConstructorGenerated(CallBaseConstructor = true)]
[Injection(InjectAs.Transient)] [Injection(InjectAs.Transient)]
internal sealed class AchievementViewModelSlim : Abstraction.ViewModelSlim<View.Page.AchievementPage> internal sealed partial class AchievementViewModelSlim : Abstraction.ViewModelSlim<View.Page.AchievementPage>
{ {
private List<AchievementStatistics>? statisticsList; private List<AchievementStatistics>? statisticsList;
/// <summary>
/// 构造一个新的简化的成就视图模型
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public AchievementViewModelSlim(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
/// <summary> /// <summary>
/// 统计列表 /// 统计列表
/// </summary> /// </summary>

View File

@@ -13,6 +13,7 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
[HighQuality] [HighQuality]
internal sealed class AvatarProperty : INameIcon internal sealed class AvatarProperty : INameIcon
{ {
// TODO: use FrozenDictionary
private static readonly ImmutableDictionary<FightProperty, Uri> PropertyIcons = new Dictionary<FightProperty, Uri>() private static readonly ImmutableDictionary<FightProperty, Uri> PropertyIcons = new Dictionary<FightProperty, Uri>()
{ {
[FightProperty.FIGHT_PROP_SKILL_CD_MINUS_RATIO] = Web.HutaoEndpoints.StaticFile("Property", "UI_Icon_CDReduce.png").ToUri(), [FightProperty.FIGHT_PROP_SKILL_CD_MINUS_RATIO] = Web.HutaoEndpoints.StaticFile("Property", "UI_Icon_CDReduce.png").ToUri(),

View File

@@ -22,17 +22,16 @@ using Snap.Hutao.Web.Response;
using Windows.Graphics.Imaging; using Windows.Graphics.Imaging;
using Windows.Storage.Streams; using Windows.Storage.Streams;
using Windows.UI; using Windows.UI;
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta; using CalculatorAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalcClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient; using CalculatorClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
using CalcConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption; using CalculatorConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
using CalcItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item; using CalculatorItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
using CalcItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper; using CalculatorItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper;
namespace Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.ViewModel.AvatarProperty;
/// <summary> /// <summary>
/// 角色属性视图模型 /// 角色属性视图模型
/// TODO: support page unload as cancellation
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[ConstructorGenerated] [ConstructorGenerated]
@@ -65,51 +64,45 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
} }
} }
/// <inheritdoc/> protected override async ValueTask<bool> InitializeUIAsync()
protected override Task OpenUIAsync()
{ {
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{ {
return RefreshCoreAsync(userAndUid, RefreshOption.None, CancellationToken); await RefreshCoreAsync(userAndUid, RefreshOption.None, CancellationToken).ConfigureAwait(false);
return true;
} }
return Task.CompletedTask; return false;
} }
[Command("RefreshFromEnkaApiCommand")] [Command("RefreshFromEnkaApiCommand")]
private Task RefreshByEnkaApiAsync() private async Task RefreshByEnkaApiAsync()
{ {
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{ {
return RefreshCoreAsync(userAndUid, RefreshOption.RequestFromEnkaAPI, CancellationToken); await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromEnkaAPI, CancellationToken).ConfigureAwait(false);
} }
return Task.CompletedTask;
} }
[Command("RefreshFromHoyolabGameRecordCommand")] [Command("RefreshFromHoyolabGameRecordCommand")]
private Task RefreshByHoyolabGameRecordAsync() private async Task RefreshByHoyolabGameRecordAsync()
{ {
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{ {
return RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabGameRecord, CancellationToken); await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabGameRecord, CancellationToken).ConfigureAwait(false);
} }
return Task.CompletedTask;
} }
[Command("RefreshFromHoyolabCalculateCommand")] [Command("RefreshFromHoyolabCalculateCommand")]
private Task RefreshByHoyolabCalculateAsync() private async Task RefreshByHoyolabCalculateAsync()
{ {
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{ {
return RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabCalculate, CancellationToken); await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabCalculate, CancellationToken).ConfigureAwait(false);
} }
return Task.CompletedTask;
} }
private async Task RefreshCoreAsync(UserAndUid userAndUid, RefreshOption option, CancellationToken token) private async ValueTask RefreshCoreAsync(UserAndUid userAndUid, RefreshOption option, CancellationToken token)
{ {
try try
{ {
@@ -163,11 +156,11 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
[Command("CultivateCommand")] [Command("CultivateCommand")]
private async Task CultivateAsync(AvatarView? avatar) private async Task CultivateAsync(AvatarView? avatar)
{ {
if (avatar != null) if (avatar is not null)
{ {
if (userService.Current != null) if (userService.Current is not null)
{ {
if (avatar.Weapon == null) if (avatar.Weapon is null)
{ {
infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull); infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull);
return; return;
@@ -177,21 +170,21 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable());
CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance<CultivatePromotionDeltaDialog>(options); CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance<CultivatePromotionDeltaDialog>(options);
(bool isOk, CalcAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (isOk) if (isOk)
{ {
Response<CalcConsumption> consumptionResponse = await serviceProvider Response<CalculatorConsumption> consumptionResponse = await serviceProvider
.GetRequiredService<CalcClient>() .GetRequiredService<CalculatorClient>()
.ComputeAsync(userService.Current.Entity, delta) .ComputeAsync(userService.Current.Entity, delta)
.ConfigureAwait(false); .ConfigureAwait(false);
if (consumptionResponse.IsOk()) if (consumptionResponse.IsOk())
{ {
ICultivationService cultivationService = serviceProvider.GetRequiredService<ICultivationService>(); ICultivationService cultivationService = serviceProvider.GetRequiredService<ICultivationService>();
CalcConsumption consumption = consumptionResponse.Data; CalculatorConsumption consumption = consumptionResponse.Data;
List<CalcItem> items = CalcItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); List<CalculatorItem> items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
bool avatarSaved = await cultivationService bool avatarSaved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -229,7 +222,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
[Command("ExportAsImageCommand")] [Command("ExportAsImageCommand")]
private async Task ExportAsImageAsync(FrameworkElement? element) private async Task ExportAsImageAsync(FrameworkElement? element)
{ {
if (element != null && element.IsLoaded) if (element is { IsLoaded: true })
{ {
RenderTargetBitmap bitmap = new(); RenderTargetBitmap bitmap = new();
await bitmap.RenderAsync(element); await bitmap.RenderAsync(element);

View File

@@ -50,7 +50,7 @@ internal sealed class WeaponView : Equip, ICalculableSource<ICalculableWeapon>
/// <summary> /// <summary>
/// 最大等级 /// 最大等级
/// </summary> /// </summary>
internal uint MaxLevel { get => ((int)Quality) >= 3 ? 90U : 70U; } internal uint MaxLevel { get => Model.Metadata.Weapon.Weapon.GetMaxLevelByQuality(Quality); }
/// <inheritdoc/> /// <inheritdoc/>
public ICalculableWeapon ToCalculable() public ICalculableWeapon ToCalculable()

View File

@@ -23,18 +23,19 @@ internal sealed class ReliquarySetView
{ {
ReliquarySets sets = reliquarySetRate.Item; ReliquarySets sets = reliquarySetRate.Item;
if (sets.Count >= 1) if (!sets.IsNullOrEmpty())
{ {
StringBuilder nameBuilder = new(); StringBuilder nameBuilder = new();
List<Uri> icons = new(2); List<Uri> icons = new(2);
foreach (ReliquarySet set in CollectionsMarshal.AsSpan(sets)) foreach (ReliquarySet set in CollectionsMarshal.AsSpan(sets))
{ {
// TODO use set EquipAffixSetIds
Model.Metadata.Reliquary.ReliquarySet metaSet = idReliquarySetMap[set.EquipAffixId / 10]; Model.Metadata.Reliquary.ReliquarySet metaSet = idReliquarySetMap[set.EquipAffixId / 10];
if (nameBuilder.Length != 0) if (nameBuilder.Length > 0)
{ {
nameBuilder.Append(Environment.NewLine); nameBuilder.AppendLine();
} }
nameBuilder.Append(set.Count).Append('×').Append(metaSet.Name); nameBuilder.Append(set.Count).Append('×').Append(metaSet.Name);

View File

@@ -73,8 +73,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task OpenUIAsync() protected override async Task OpenUIAsync()
{ {
bool metaInitialized = await metadataService.InitializeAsync().ConfigureAwait(false); if (await metadataService.InitializeAsync().ConfigureAwait(false))
if (metaInitialized)
{ {
ObservableCollection<CultivateProject> projects = cultivationService.ProjectCollection; ObservableCollection<CultivateProject> projects = cultivationService.ProjectCollection;
CultivateProject? selected = cultivationService.Current; CultivateProject? selected = cultivationService.Current;
@@ -82,9 +81,12 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
Projects = projects; Projects = projects;
SelectedProject = selected; SelectedProject = selected;
IsInitialized = true;
}
else
{
IsInitialized = false;
} }
IsInitialized = metaInitialized;
} }
[Command("AddProjectCommand")] [Command("AddProjectCommand")]
@@ -122,7 +124,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
[Command("RemoveProjectCommand")] [Command("RemoveProjectCommand")]
private async Task RemoveProjectAsync(CultivateProject? project) private async Task RemoveProjectAsync(CultivateProject? project)
{ {
if (project != null) if (project is not null)
{ {
await cultivationService.RemoveProjectAsync(project).ConfigureAwait(false); await cultivationService.RemoveProjectAsync(project).ConfigureAwait(false);
@@ -133,7 +135,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
private async Task UpdateEntryCollectionAsync(CultivateProject? project) private async Task UpdateEntryCollectionAsync(CultivateProject? project)
{ {
if (project != null) if (project is not null)
{ {
List<Material> materials = await metadataService.GetMaterialsAsync().ConfigureAwait(false); List<Material> materials = await metadataService.GetMaterialsAsync().ConfigureAwait(false);
Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
@@ -154,7 +156,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
[Command("RemoveEntryCommand")] [Command("RemoveEntryCommand")]
private async Task RemoveEntryAsync(CultivateEntryView? entry) private async Task RemoveEntryAsync(CultivateEntryView? entry)
{ {
if (entry != null) if (entry is not null)
{ {
CultivateEntries!.Remove(entry); CultivateEntries!.Remove(entry);
await cultivationService.RemoveCultivateEntryAsync(entry.EntryId).ConfigureAwait(false); await cultivationService.RemoveCultivateEntryAsync(entry.EntryId).ConfigureAwait(false);
@@ -165,7 +167,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
[Command("SaveInventoryItemCommand")] [Command("SaveInventoryItemCommand")]
private void SaveInventoryItem(InventoryItemView? inventoryItem) private void SaveInventoryItem(InventoryItemView? inventoryItem)
{ {
if (inventoryItem != null) if (inventoryItem is not null)
{ {
cultivationService.SaveInventoryItem(inventoryItem); cultivationService.SaveInventoryItem(inventoryItem);
UpdateStatisticsItemsAsync().SafeForget(); UpdateStatisticsItemsAsync().SafeForget();
@@ -175,7 +177,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
[Command("FinishStateCommand")] [Command("FinishStateCommand")]
private void UpdateFinishedState(CultivateItemView? item) private void UpdateFinishedState(CultivateItemView? item)
{ {
if (item != null) if (item is not null)
{ {
item.IsFinished = !item.IsFinished; item.IsFinished = !item.IsFinished;
cultivationService.SaveCultivateItem(item); cultivationService.SaveCultivateItem(item);
@@ -185,7 +187,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
private async Task UpdateStatisticsItemsAsync() private async Task UpdateStatisticsItemsAsync()
{ {
if (SelectedProject != null) if (SelectedProject is not null)
{ {
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
CancellationToken token = StatisticsCancellationTokenSource.Register(); CancellationToken token = StatisticsCancellationTokenSource.Register();
@@ -208,7 +210,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
[Command("NavigateToPageCommand")] [Command("NavigateToPageCommand")]
private void NavigateToPage(string? typeString) private void NavigateToPage(string? typeString)
{ {
if (typeString != null) if (typeString is not null)
{ {
serviceProvider serviceProvider
.GetRequiredService<INavigationService>() .GetRequiredService<INavigationService>()

View File

@@ -17,17 +17,17 @@ internal static class ItemHelper
/// <returns>合并且排序好的列表</returns> /// <returns>合并且排序好的列表</returns>
public static List<Item> Merge(List<Item>? left, List<Item>? right) public static List<Item> Merge(List<Item>? left, List<Item>? right)
{ {
if (left == null && right == null) if (left.IsNullOrEmpty() && right.IsNullOrEmpty())
{ {
return new(0); return new(0);
} }
if (right == null) if (right.IsNullOrEmpty())
{ {
return left!; return left!;
} }
if (left == null) if (left.IsNullOrEmpty())
{ {
return right!; return right!;
} }
@@ -37,7 +37,7 @@ internal static class ItemHelper
foreach (Item item in right) foreach (Item item in right)
{ {
if (result.SingleOrDefault(i => i.Id == item.Id) is Item existed) if (result.SingleOrDefault(i => i.Id == item.Id) is { } existed)
{ {
existed.Num += item.Num; existed.Num += item.Num;
} }