diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs index fe42e6ea..6d51142e 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs @@ -17,9 +17,9 @@ namespace Snap.Hutao.SourceGeneration.DependencyInjection; internal sealed class InjectionGenerator : IIncrementalGenerator { public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute"; - private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton"; + public const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton"; public const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient"; - private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped"; + public const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped"; private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true); diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/ServiceAnalyzer.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/ServiceAnalyzer.cs new file mode 100644 index 00000000..8f494096 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/ServiceAnalyzer.cs @@ -0,0 +1,76 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Snap.Hutao.SourceGeneration.Primitive; +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace Snap.Hutao.SourceGeneration.DependencyInjection; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal class ServiceAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor NonSingletonUseServiceProviderDescriptor = new("SH301", "Non Singleton service should avoid direct use of IServiceProvider", "Non Singleton service should avoid direct use of IServiceProvider", "Quality", DiagnosticSeverity.Info, true); + private static readonly DiagnosticDescriptor SingletonServiceCaptureNonSingletonServiceDescriptor = new("SH302", "Singleton service should avoid keep reference of non singleton service", "Singleton service should avoid keep reference of non singleton service", "Quality", DiagnosticSeverity.Info, true); + + public override ImmutableArray SupportedDiagnostics { get => new DiagnosticDescriptor[] + { + NonSingletonUseServiceProviderDescriptor, + SingletonServiceCaptureNonSingletonServiceDescriptor, + }.ToImmutableArray(); } + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(CompilationStart); + } + + private static void CompilationStart(CompilationStartAnalysisContext context) + { + context.RegisterSyntaxNodeAction(HandleNonSingletonUseServiceProvider, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(HandleSingletonServiceCaptureNonSingletonService, SyntaxKind.ClassDeclaration); + } + + private static void HandleNonSingletonUseServiceProvider(SyntaxNodeAnalysisContext context) + { + ClassDeclarationSyntax classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + if (classDeclarationSyntax.HasAttributeLists()) + { + INamedTypeSymbol? classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); + if (classSymbol is not null) + { + foreach (AttributeData attributeData in classSymbol.GetAttributes()) + { + if (attributeData.AttributeClass!.ToDisplayString() is InjectionGenerator.AttributeName) + { + string serviceType = attributeData.ConstructorArguments[0].ToCSharpString(); + if (serviceType is InjectionGenerator.InjectAsTransientName or InjectionGenerator.InjectAsScopedName) + { + HandleNonSingletonUseServiceProviderActual(context, classSymbol); + } + } + } + } + } + } + + private static void HandleNonSingletonUseServiceProviderActual(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol) + { + ISymbol? symbol = classSymbol.GetMembers().Where(m => m is IFieldSymbol f && f.Type.ToDisplayString() == "System.IServiceProvider").SingleOrDefault(); + + if (symbol is not null) + { + Diagnostic diagnostic = Diagnostic.Create(NonSingletonUseServiceProviderDescriptor, symbol.Locations.FirstOrDefault()); + context.ReportDiagnostic(diagnostic); + } + } + + private static void HandleSingletonServiceCaptureNonSingletonService(SyntaxNodeAnalysisContext context) + { + //classSymbol.GetMembers().Where(m => m is IFieldSymbol { IsReadOnly: true, DeclaredAccessibility: Accessibility.Private } f); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt b/src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt index 7e7498af..2a7c1196 100644 --- a/src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt +++ b/src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt @@ -1 +1 @@ -CA1501: 6 \ No newline at end of file +CA1501: 8 \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/StringExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/StringExtension.cs index c5af0b2e..43f61ed4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/StringExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/StringExtension.cs @@ -40,4 +40,10 @@ internal static class StringExtension { return string.Format(CultureInfo.CurrentCulture, value, arg0, arg1); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Format(this string value, object? arg0, object? arg1, object? arg2) + { + return string.Format(CultureInfo.CurrentCulture, value, arg0, arg1, arg2); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs index 49560707..7df5c9fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs @@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.InterChange.Achievement; /// UIAF格式的信息 /// [HighQuality] -internal sealed class UIAFInfo : IMappingFrom +internal sealed class UIAFInfo : IMappingFrom { /// /// 导出的 App 名称 @@ -45,15 +45,8 @@ internal sealed class UIAFInfo : IMappingFrom [JsonPropertyName("uiaf_version")] public string? UIAFVersion { get; set; } - /// - /// 构造一个新的专用 UIAF 信息 - /// - /// 服务提供器 - /// 专用 UIAF 信息 - public static UIAFInfo From(IServiceProvider serviceProvider) + public static UIAFInfo From(RuntimeOptions runtimeOptions) { - RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService(); - return new() { ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(), diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ItemIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ItemIconConverter.cs index fd88c91c..8036c32d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ItemIconConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ItemIconConverter.cs @@ -23,7 +23,7 @@ internal sealed class ItemIconConverter : ValueConverter return default!; } - return name.StartsWith("UI_RelicIcon_") + return name.StartsWith("UI_RelicIcon_", StringComparison.Ordinal) ? RelicIconConverter.IconNameToUri(name) : Web.HutaoEndpoints.StaticFile("ItemIcon", $"{name}.png").ToUri(); } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs index b59ceaef..f6115ce3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs @@ -23,7 +23,7 @@ internal sealed class SkillIconConverter : ValueConverter return Web.HutaoEndpoints.UIIconNone; } - return name.StartsWith("UI_Talent_") + return name.StartsWith("UI_Talent_", StringComparison.Ordinal) ? Web.HutaoEndpoints.StaticFile("Talent", $"{name}.png").ToUri() : Web.HutaoEndpoints.StaticFile("Skill", $"{name}.png").ToUri(); } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs index 69966073..f38a6727 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs @@ -19,7 +19,7 @@ internal sealed class LevelDescription /// 格式化的等级 /// [JsonIgnore] - public string LevelFormatted { get => string.Format(SH.ModelWeaponAffixFormat, Level + 1); } + public string LevelFormatted { get => SH.ModelWeaponAffixFormat.Format(Level + 1); } /// /// 描述 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Primitive/Converter/IdentityConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Primitive/Converter/IdentityConverter.cs index 94e8d1dd..59f26947 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Primitive/Converter/IdentityConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Primitive/Converter/IdentityConverter.cs @@ -39,6 +39,6 @@ internal sealed unsafe class IdentityConverter : JsonConverter public override void WriteAsPropertyName(Utf8JsonWriter writer, TWrapper value, JsonSerializerOptions options) { - writer.WritePropertyName((*(uint*)&value).ToString()); + writer.WritePropertyName($"{*(uint*)&value}"); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs index 81f41b8e..de9dfcf7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs @@ -5819,5 +5819,14 @@ namespace Snap.Hutao.Resource.Localization { return ResourceManager.GetString("WebResponseFormat", resourceCulture); } } + + /// + /// 查找类似 [{0}] 中的 [{1}] 网络请求异常,请稍后再试 的本地化字符串。 + /// + internal static string WebResponseRequestExceptionFormat { + get { + return ResourceManager.GetString("WebResponseRequestExceptionFormat", resourceCulture); + } + } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 1a7d553c..a64d1819 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2093,4 +2093,7 @@ 状态:{0} | 信息:{1} + + [{0}] 中的 [{1}] 网络请求异常,请稍后再试 + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs index 084fe730..565201fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.Extensions.Options; using Snap.Hutao.Core.Database; using Snap.Hutao.Model.Entity.Database; +using System.Globalization; using System.Runtime.CompilerServices; namespace Snap.Hutao.Service.Abstraction; @@ -85,7 +86,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions(); string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value; - storage = value is null ? defaultValue : int.Parse(value); + storage = value is null ? defaultValue : int.Parse(value, CultureInfo.InvariantCulture); } return storage.Value; @@ -211,7 +212,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions(); appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == key); - appDbContext.Settings.AddAndSave(new(key, value.ToString())); + appDbContext.Settings.AddAndSave(new(key, $"{value}")); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbBulkOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbBulkOperation.cs index 68410909..92d6195e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbBulkOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbBulkOperation.cs @@ -15,7 +15,7 @@ namespace Snap.Hutao.Service.Achievement; /// [HighQuality] [ConstructorGenerated] -[Injection(InjectAs.Scoped)] +[Injection(InjectAs.Singleton)] internal sealed partial class AchievementDbBulkOperation { private readonly IServiceProvider serviceProvider; @@ -76,7 +76,10 @@ internal sealed partial class AchievementDbBulkOperation continue; } - if (entity!.Id < uiaf!.Id) + ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(uiaf); + + if (entity.Id < uiaf.Id) { moveEntity = true; moveUIAF = false; @@ -164,7 +167,10 @@ internal sealed partial class AchievementDbBulkOperation continue; } - if (oldEntity!.Id < newEntity!.Id) + ArgumentNullException.ThrowIfNull(oldEntity); + ArgumentNullException.ThrowIfNull(newEntity); + + if (oldEntity.Id < newEntity.Id) { moveOld = true; moveNew = false; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs index fc776845..c5703dd3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs @@ -16,7 +16,7 @@ namespace Snap.Hutao.Service.Achievement; /// 成就数据库服务 /// [ConstructorGenerated] -[Injection(InjectAs.Scoped, typeof(IAchievementDbService))] +[Injection(InjectAs.Singleton, typeof(IAchievementDbService))] internal sealed partial class AchievementDbService : IAchievementDbService { private readonly IServiceProvider serviceProvider; @@ -66,7 +66,7 @@ internal sealed partial class AchievementDbService : IAchievementDbService .AsNoTracking() .Where(a => a.ArchiveId == archiveId) .Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED) - .OrderByDescending(a => a.Time.ToString()) + .OrderByDescending(a => a.Time) .Take(take) .ToListAsync() .ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Interchange.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Interchange.cs index 40abe712..1a028dda 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Interchange.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Interchange.cs @@ -51,19 +51,14 @@ internal sealed partial class AchievementService public async ValueTask ExportToUIAFAsync(AchievementArchive archive) { await taskContext.SwitchToBackgroundAsync(); - using (IServiceScope scope = serviceProvider.CreateScope()) + List list = achievementDbService + .GetAchievementListByArchiveId(archive.InnerId) + .SelectList(UIAFItem.From); + + return new() { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - - List list = achievementDbService - .GetAchievementListByArchiveId(archive.InnerId) - .SelectList(UIAFItem.From); - - return new() - { - Info = UIAFInfo.From(scope.ServiceProvider), - List = list, - }; - } + Info = UIAFInfo.From(runtimeOptions), + List = list, + }; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs index 6c8b25be..109e4be2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Model.Entity; @@ -23,7 +24,7 @@ internal sealed partial class AchievementService : IAchievementService private readonly ScopedDbCurrent dbCurrent; private readonly AchievementDbBulkOperation achievementDbBulkOperation; private readonly IAchievementDbService achievementDbService; - private readonly IServiceProvider serviceProvider; + private readonly RuntimeOptions runtimeOptions; private readonly ITaskContext taskContext; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportResult.cs index 65acc8e6..cd114b4a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportResult.cs @@ -40,6 +40,6 @@ internal readonly struct ImportResult /// public override string ToString() { - return string.Format(SH.ServiceAchievementImportResultFormat, Add, Update, Remove); + return SH.ServiceAchievementImportResultFormat.Format(Add, Update, Remove); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs index 264b6209..29efe104 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs @@ -28,7 +28,9 @@ internal sealed partial class AnnouncementService : IAnnouncementService // 缓存中存在记录,直接返回 if (memoryCache.TryGetValue(CacheKey, out object? cache)) { - return (AnnouncementWrapper)cache!; + AnnouncementWrapper? wrapper = (AnnouncementWrapper?)cache; + ArgumentNullException.ThrowIfNull(wrapper); + return wrapper; } await taskContext.SwitchToBackgroundAsync(); @@ -60,7 +62,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService } } - return null!; + return default!; } private static void JoinAnnouncements(Dictionary contentMap, List announcementListWrappers) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbBulkOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbBulkOperation.cs index 8533d56e..d5e61999 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbBulkOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbBulkOperation.cs @@ -26,7 +26,7 @@ namespace Snap.Hutao.Service.AvatarInfo; /// [HighQuality] [ConstructorGenerated] -[Injection(InjectAs.Scoped)] +[Injection(InjectAs.Singleton)] internal sealed partial class AvatarInfoDbBulkOperation { private readonly IServiceProvider serviceProvider; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbService.cs index 029e21b9..59455ba6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbService.cs @@ -10,7 +10,7 @@ using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo; namespace Snap.Hutao.Service.AvatarInfo; [ConstructorGenerated] -[Injection(InjectAs.Scoped, typeof(IAvatarInfoDbService))] +[Injection(InjectAs.Singleton, typeof(IAvatarInfoDbService))] internal sealed partial class AvatarInfoDbService : IAvatarInfoDbService { private readonly IServiceProvider serviceProvider; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs index 0704fc9a..a4d82b30 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs @@ -117,7 +117,8 @@ internal sealed class SummaryAvatarFactory MetadataWeapon weapon = metadataContext.IdWeaponMap[equip.ItemId]; // AffixMap can be null when it's a white weapon. - uint affixLevel = equip.Weapon!.AffixMap?.SingleOrDefault().Value ?? 0U; + ArgumentNullException.ThrowIfNull(equip.Weapon); + uint affixLevel = equip.Weapon.AffixMap?.SingleOrDefault().Value ?? 0U; WeaponStat? mainStat = equip.Flat.WeaponStats?.ElementAtOrDefault(0); WeaponStat? subStat = equip.Flat.WeaponStats?.ElementAtOrDefault(1); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs index c604bddb..4393a93f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs @@ -121,7 +121,8 @@ internal sealed class SummaryReliquaryFactory // 从喵插件抓取的圣遗物评分权重 // 部分复杂的角色暂时使用了默认值 ReliquaryAffixWeight affixWeight = metadataContext.IdReliquaryAffixWeightMap.GetValueOrDefault(avatarInfo.AvatarId, ReliquaryAffixWeight.Default); - ReliquaryMainAffixLevel maxRelicLevel = metadataContext.ReliquaryLevels.Where(r => r.Rank == reliquary.RankLevel).MaxBy(r => r.Level)!; + ReliquaryMainAffixLevel? maxRelicLevel = metadataContext.ReliquaryLevels.Where(r => r.Rank == reliquary.RankLevel).MaxBy(r => r.Level); + ArgumentNullException.ThrowIfNull(maxRelicLevel); float percent = relicLevel.PropertyMap[property] / maxRelicLevel.PropertyMap[property]; float baseScore = 8 * percent * affixWeight[property]; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs index 2fd659ef..0c59c5dc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs @@ -6,6 +6,7 @@ using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Notification; +using System.Globalization; namespace Snap.Hutao.Service.DailyNote; @@ -52,14 +53,14 @@ internal sealed class DailyNoteOptions : DbStoreOptions /// public NameValue? SelectedRefreshTime { - 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, CultureInfo.InvariantCulture)), RefreshTimes[1]); set { if (value is not null) { if (scheduleTaskInterop.RegisterForDailyNoteRefresh(value.Value)) { - SetOption(ref selectedRefreshTime, SettingEntry.DailyNoteRefreshSeconds, value, value => value.Value.ToString()); + SetOption(ref selectedRefreshTime, SettingEntry.DailyNoteRefreshSeconds, value, value => $"{value.Value}"); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs index 162c0f76..0f44636c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs @@ -133,7 +133,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory default: // ItemId string length not correct. - ThrowHelper.UserdataCorrupted(string.Format(SH.ServiceGachaStatisticsFactoryItemIdInvalid, item.ItemId), null!); + ThrowHelper.UserdataCorrupted(SH.ServiceGachaStatisticsFactoryItemIdInvalid.Format(item.ItemId), default!); break; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactory.cs index 506e4eb2..e58faac7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactory.cs @@ -52,7 +52,7 @@ internal sealed class HutaoStatisticsFactory { 8U => context.IdAvatarMap[item.Item], 5U => context.IdWeaponMap[item.Item], - _ => throw ThrowHelper.UserdataCorrupted(string.Format(SH.ServiceGachaStatisticsFactoryItemIdInvalid, item.Item), null!), + _ => throw ThrowHelper.UserdataCorrupted(SH.ServiceGachaStatisticsFactoryItemIdInvalid.Format(item.Item), default!), }; StatisticsItem statisticsItem = source.ToStatisticsItem(unchecked((int)item.Count)); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs index ee3d2ad9..6565294a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs @@ -91,7 +91,7 @@ internal sealed partial class GameService : IGameService { string gamePath = appOptions.GamePath; string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); - bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase); + bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.Ordinal); if (!File.Exists(configPath)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserDbService.cs index 5fb15f32..82274eb3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserDbService.cs @@ -9,6 +9,8 @@ internal interface IUserDbService ValueTask DeleteUserByIdAsync(Guid id); + ValueTask DeleteUsersAsync(); + ValueTask> GetUserListAsync(); ValueTask UpdateUserAsync(Model.Entity.User user); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs index 4bd5da86..4d389a79 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs @@ -63,4 +63,9 @@ internal interface IUserService /// 待移除的用户 /// 任务 ValueTask RemoveUserAsync(BindingUser user); +} + +internal interface IUserServiceUnsafe +{ + ValueTask UnsafeRemoveUsersAsync(); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserDbService.cs index ee77d3df..2f11d931 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserDbService.cs @@ -59,4 +59,13 @@ internal sealed partial class UserDbService : IUserDbService await appDbContext.Users.AddAndSaveAsync(user).ConfigureAwait(false); } } + + public async ValueTask DeleteUsersAsync() + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.Users.ExecuteDeleteAsync().ConfigureAwait(false); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index 3b981fa4..4c880bad 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -23,7 +23,7 @@ namespace Snap.Hutao.Service.User; /// [ConstructorGenerated] [Injection(InjectAs.Singleton, typeof(IUserService))] -internal sealed partial class UserService : IUserService +internal sealed partial class UserService : IUserService, IUserServiceUnsafe { private readonly ScopedDbCurrent dbCurrent; private readonly IUserInitializationService userInitializationService; @@ -57,6 +57,12 @@ internal sealed partial class UserService : IUserService messenger.Send(new UserRemovedMessage(user.Entity)); } + public async ValueTask UnsafeRemoveUsersAsync() + { + await taskContext.SwitchToBackgroundAsync(); + await userDbService.DeleteUsersAsync().ConfigureAwait(false); + } + /// public async ValueTask> GetUserCollectionAsync() { diff --git a/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml.cs index 715a531b..4c6a331c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Microsoft.UI.Xaml.Controls; -using Snap.Hutao.ViewModel; +using Snap.Hutao.ViewModel.Guide; namespace Snap.Hutao.View; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/DownloadSummary.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/DownloadSummary.cs new file mode 100644 index 00000000..26aad1a1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/DownloadSummary.cs @@ -0,0 +1,130 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Common; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI.Notifications; +using Snap.Hutao.Core; +using Snap.Hutao.Core.Caching; +using Snap.Hutao.Core.IO; +using Snap.Hutao.Core.Setting; +using System.Collections.ObjectModel; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.ViewModel.Guide; + +/// +/// 下载信息 +/// +internal sealed class DownloadSummary : ObservableObject +{ + private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; + private readonly HttpClient httpClient; + private readonly string fileName; + private readonly string fileUrl; + private readonly Progress progress; + private string description = SH.ViewModelWelcomeDownloadSummaryDefault; + private double progressValue; + private long updateCount; + + /// + /// 构造一个新的下载信息 + /// + /// 服务提供器 + /// 压缩文件名称 + public DownloadSummary(IServiceProvider serviceProvider, string fileName) + { + taskContext = serviceProvider.GetRequiredService(); + httpClient = serviceProvider.GetRequiredService(); + RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent); + + this.serviceProvider = serviceProvider; + + DisplayName = fileName; + this.fileName = fileName; + fileUrl = Web.HutaoEndpoints.StaticZip(fileName); + + progress = new(UpdateProgressStatus); + } + + /// + /// 显示名称 + /// + public string DisplayName { get; init; } + + /// + /// 描述 + /// + public string Description { get => description; private set => SetProperty(ref description, value); } + + /// + /// 进度值,最大1 + /// + public double ProgressValue { get => progressValue; set => SetProperty(ref progressValue, value); } + + /// + /// 异步下载并解压 + /// + /// 任务 + public async Task DownloadAndExtractAsync() + { + ILogger logger = serviceProvider.GetRequiredService>(); + try + { + HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + long contentLength = response.Content.Headers.ContentLength ?? 0; + logger.LogInformation("Begin download, length: {length}", contentLength); + using (Stream content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + using (TempFileStream temp = new(FileMode.OpenOrCreate, FileAccess.ReadWrite)) + { + await new StreamCopyWorker(content, temp, contentLength).CopyAsync(progress).ConfigureAwait(false); + ExtractFiles(temp); + + await taskContext.SwitchToMainThreadAsync(); + ProgressValue = 1; + Description = SH.ViewModelWelcomeDownloadSummaryComplete; + return true; + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Download Static Zip failed"); + await taskContext.SwitchToMainThreadAsync(); + Description = ex is HttpRequestException httpRequestException + ? $"{SH.ViewModelWelcomeDownloadSummaryException} - HTTP {httpRequestException.StatusCode:D}" + : SH.ViewModelWelcomeDownloadSummaryException; + return false; + } + } + + private void UpdateProgressStatus(StreamCopyStatus status) + { + if (Interlocked.Increment(ref updateCount) % 40 == 0) + { + Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}"; + ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes; + } + } + + private void ExtractFiles(Stream stream) + { + IImageCacheFilePathOperation imageCache = serviceProvider.GetRequiredService().As()!; + + using (ZipArchive archive = new(stream)) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + string destPath = imageCache.GetFileFromCategoryAndName(fileName, entry.FullName); + entry.ExtractToFile(destPath, true); + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/WelcomeViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/WelcomeViewModel.cs new file mode 100644 index 00000000..864e6c5d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/WelcomeViewModel.cs @@ -0,0 +1,112 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Common; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI.Notifications; +using Snap.Hutao.Core; +using Snap.Hutao.Core.Caching; +using Snap.Hutao.Core.IO; +using Snap.Hutao.Core.Setting; +using System.Collections.ObjectModel; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.ViewModel.Guide; + +/// +/// 欢迎视图模型 +/// +[HighQuality] +[ConstructorGenerated] +[Injection(InjectAs.Scoped)] +internal sealed partial class WelcomeViewModel : ObservableObject +{ + private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; + + private ObservableCollection? downloadSummaries; + + /// + /// 下载信息 + /// + public ObservableCollection? DownloadSummaries { get => downloadSummaries; set => SetProperty(ref downloadSummaries, value); } + + [Command("OpenUICommand")] + private async Task OpenUIAsync() + { + IEnumerable downloadSummaries = GenerateStaticResourceDownloadTasks(); + + DownloadSummaries = downloadSummaries.ToObservableCollection(); + + await Parallel.ForEachAsync(downloadSummaries, async (summary, token) => + { + if (await summary.DownloadAndExtractAsync().ConfigureAwait(false)) + { + taskContext.InvokeOnMainThread(() => DownloadSummaries.Remove(summary)); + } + }).ConfigureAwait(true); + + serviceProvider.GetRequiredService().Send(new Message.WelcomeStateCompleteMessage()); + StaticResource.FulfillAllContracts(); + + try + { + new ToastContentBuilder() + .AddText(SH.ViewModelWelcomeDownloadCompleteTitle) + .AddText(SH.ViewModelWelcomeDownloadCompleteMessage) + .Show(); + } + catch (COMException) + { + // 0x803E0105 + } + } + + private IEnumerable GenerateStaticResourceDownloadTasks() + { + Dictionary downloadSummaries = new(); + + if (StaticResource.IsContractUnfulfilled(StaticResource.V1Contract)) + { + downloadSummaries.TryAdd("Bg", new(serviceProvider, "Bg")); + downloadSummaries.TryAdd("AvatarIcon", new(serviceProvider, "AvatarIcon")); + downloadSummaries.TryAdd("GachaAvatarIcon", new(serviceProvider, "GachaAvatarIcon")); + downloadSummaries.TryAdd("GachaAvatarImg", new(serviceProvider, "GachaAvatarImg")); + downloadSummaries.TryAdd("EquipIcon", new(serviceProvider, "EquipIcon")); + downloadSummaries.TryAdd("GachaEquipIcon", new(serviceProvider, "GachaEquipIcon")); + downloadSummaries.TryAdd("NameCardPic", new(serviceProvider, "NameCardPic")); + downloadSummaries.TryAdd("Skill", new(serviceProvider, "Skill")); + downloadSummaries.TryAdd("Talent", new(serviceProvider, "Talent")); + } + + if (StaticResource.IsContractUnfulfilled(StaticResource.V2Contract)) + { + downloadSummaries.TryAdd("AchievementIcon", new(serviceProvider, "AchievementIcon")); + downloadSummaries.TryAdd("ItemIcon", new(serviceProvider, "ItemIcon")); + downloadSummaries.TryAdd("IconElement", new(serviceProvider, "IconElement")); + downloadSummaries.TryAdd("RelicIcon", new(serviceProvider, "RelicIcon")); + } + + if (StaticResource.IsContractUnfulfilled(StaticResource.V3Contract)) + { + downloadSummaries.TryAdd("Skill", new(serviceProvider, "Skill")); + downloadSummaries.TryAdd("Talent", new(serviceProvider, "Talent")); + } + + if (StaticResource.IsContractUnfulfilled(StaticResource.V4Contract)) + { + downloadSummaries.TryAdd("AvatarIcon", new(serviceProvider, "AvatarIcon")); + } + + if (StaticResource.IsContractUnfulfilled(StaticResource.V5Contract)) + { + downloadSummaries.TryAdd("MonsterIcon", new(serviceProvider, "MonsterIcon")); + } + + return downloadSummaries.Select(x => x.Value); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs index bc6fbab4..1b0ef8ad 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs @@ -11,14 +11,13 @@ using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Windowing; using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model; -using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Service; using Snap.Hutao.Service.GachaLog.QueryProvider; using Snap.Hutao.Service.Game.Locator; using Snap.Hutao.Service.Hutao; using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Notification; -using Snap.Hutao.View.Dialog; +using Snap.Hutao.Service.User; using System.Globalization; using System.IO; using System.Runtime.InteropServices; @@ -34,7 +33,13 @@ namespace Snap.Hutao.ViewModel; [Injection(InjectAs.Scoped)] internal sealed partial class SettingViewModel : Abstraction.ViewModel { - private readonly IServiceProvider serviceProvider; + private readonly IGameLocatorFactory gameLocatorFactory; + private readonly IClipboardInterop clipboardInterop; + private readonly IContentDialogFactory contentDialogFactory; + private readonly INavigationService navigationService; + private readonly IPickerFactory pickerFactory; + private readonly IUserService userService; + private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; private readonly AppOptions options; private readonly RuntimeOptions runtimeOptions; @@ -89,10 +94,17 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel } } + [Command("ResetStaticResourceCommand")] + private static void ResetStaticResource() + { + StaticResource.FailAllContracts(); + AppInstance.Restart(string.Empty); + } + [Command("SetGamePathCommand")] private async Task SetGamePathAsync() { - IGameLocator locator = serviceProvider.GetRequiredService().Create(GameLocationSource.Manual); + IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual); (bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false); if (isOk) @@ -110,9 +122,8 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel if (!string.IsNullOrEmpty(gamePath)) { string cacheFilePath = GachaLogQueryWebCacheProvider.GetCacheFile(gamePath); - string cacheFolder = Path.GetDirectoryName(cacheFilePath)!; + string? cacheFolder = Path.GetDirectoryName(cacheFilePath); - IInfoBarService infoBarService = serviceProvider.GetRequiredService(); if (Directory.Exists(cacheFolder)) { try @@ -129,7 +140,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel } else { - infoBarService.Warning(string.Format(SH.ViewModelSettingClearWebCachePathInvalid, cacheFolder)); + infoBarService.Warning(SH.ViewModelSettingClearWebCachePathInvalid.Format(cacheFolder)); } } } @@ -139,17 +150,18 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel { // ContentDialog must be created by main thread. await taskContext.SwitchToMainThreadAsync(); - SignInWebViewDialog dialog = serviceProvider.CreateInstance(); - await dialog.ShowAsync(); + + // TODO remove this. + // SignInWebViewDialog dialog = serviceProvider.CreateInstance(); + // await dialog.ShowAsync(); } [Command("UpdateCheckCommand")] private async Task CheckUpdateAsync() { #if DEBUG - await serviceProvider - .GetRequiredService() - .NavigateAsync(Service.Navigation.INavigationAwaiter.Default) + await navigationService + .NavigateAsync(INavigationAwaiter.Default) .ConfigureAwait(false); #else await Windows.System.Launcher.LaunchUriAsync(new(@"ms-windows-store://pdp/?productid=9PH4NXJ2JN52")); @@ -159,8 +171,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel [Command("SetDataFolderCommand")] private async Task SetDataFolderAsync() { - (bool isOk, string folder) = await serviceProvider - .GetRequiredService() + (bool isOk, string folder) = await pickerFactory .GetFolderPicker() .TryPickSingleFolderAsync() .ConfigureAwait(false); @@ -168,24 +179,16 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel if (isOk) { LocalSetting.Set(SettingKeys.DataFolderPath, folder); - serviceProvider.GetRequiredService().Success(SH.ViewModelSettingSetDataFolderSuccess); + infoBarService.Success(SH.ViewModelSettingSetDataFolderSuccess); } } - [Command("ResetStaticResourceCommand")] - private void ResetStaticResource() - { - StaticResource.FailAllContracts(); - AppInstance.Restart(string.Empty); - } - [Command("CopyDeviceIdCommand")] private void CopyDeviceId() { - IInfoBarService infoBarService = serviceProvider.GetRequiredService(); try { - serviceProvider.GetRequiredService().SetText(HutaoOptions.DeviceId); + clipboardInterop.SetText(HutaoOptions.DeviceId); infoBarService.Success(SH.ViewModelSettingCopyDeviceIdSuccess); } catch (COMException ex) @@ -197,7 +200,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel [Command("NavigateToHutaoPassportCommand")] private void NavigateToHutaoPassport() { - serviceProvider.GetRequiredService().Navigate(INavigationAwaiter.Default); + navigationService.Navigate(INavigationAwaiter.Default); } [Command("OpenCacheFolderCommand")] @@ -215,18 +218,15 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel [Command("DeleteUsersCommand")] private async Task DangerousDeleteUsersAsync() { - using (IServiceScope scope = serviceProvider.CreateScope()) + if (userService is IUserServiceUnsafe @unsafe) { - ContentDialogResult result = await scope.ServiceProvider - .GetRequiredService() + ContentDialogResult result = await contentDialogFactory .CreateForConfirmCancelAsync(SH.ViewDialogSettingDeleteUserDataTitle, SH.ViewDialogSettingDeleteUserDataContent) .ConfigureAwait(false); - if (result == ContentDialogResult.Primary) + if (result is ContentDialogResult.Primary) { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - await appDbContext.Users.ExecuteDeleteAsync().ConfigureAwait(false); - + await @unsafe.UnsafeRemoveUsersAsync().ConfigureAwait(false); AppInstance.Restart(string.Empty); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs index dc554d39..84a54a8d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs @@ -14,9 +14,6 @@ namespace Snap.Hutao.ViewModel; [Injection(InjectAs.Scoped)] internal sealed partial class TestViewModel : Abstraction.ViewModel { - private readonly IServiceProvider serviceProvider; - private readonly ITaskContext taskContext; - /// protected override Task OpenUIAsync() { @@ -24,7 +21,7 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel } [Command("RestartAppCommand")] - private void RestartApp(bool elevated) + private static void RestartApp(bool elevated) { AppInstance.Restart(string.Empty); } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs deleted file mode 100644 index 469ef98a..00000000 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using CommunityToolkit.Common; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; -using CommunityToolkit.WinUI.Notifications; -using Snap.Hutao.Core; -using Snap.Hutao.Core.Caching; -using Snap.Hutao.Core.IO; -using Snap.Hutao.Core.Setting; -using System.Collections.ObjectModel; -using System.IO; -using System.IO.Compression; -using System.Net.Http; -using System.Runtime.InteropServices; - -namespace Snap.Hutao.ViewModel; - -/// -/// 欢迎视图模型 -/// -[HighQuality] -[ConstructorGenerated] -[Injection(InjectAs.Scoped)] -internal sealed partial class WelcomeViewModel : ObservableObject -{ - private readonly IServiceProvider serviceProvider; - private readonly ITaskContext taskContext; - - private ObservableCollection? downloadSummaries; - - /// - /// 下载信息 - /// - public ObservableCollection? DownloadSummaries { get => downloadSummaries; set => SetProperty(ref downloadSummaries, value); } - - [Command("OpenUICommand")] - private async Task OpenUIAsync() - { - IEnumerable downloadSummaries = GenerateStaticResourceDownloadTasks(); - - DownloadSummaries = downloadSummaries.ToObservableCollection(); - - await Parallel.ForEachAsync(downloadSummaries, async (summary, token) => - { - if (await summary.DownloadAndExtractAsync().ConfigureAwait(false)) - { - taskContext.InvokeOnMainThread(() => DownloadSummaries.Remove(summary)); - } - }).ConfigureAwait(true); - - serviceProvider.GetRequiredService().Send(new Message.WelcomeStateCompleteMessage()); - StaticResource.FulfillAllContracts(); - - try - { - new ToastContentBuilder() - .AddText(SH.ViewModelWelcomeDownloadCompleteTitle) - .AddText(SH.ViewModelWelcomeDownloadCompleteMessage) - .Show(); - } - catch (COMException) - { - // 0x803E0105 - } - } - - private IEnumerable GenerateStaticResourceDownloadTasks() - { - Dictionary downloadSummaries = new(); - - if (StaticResource.IsContractUnfulfilled(StaticResource.V1Contract)) - { - downloadSummaries.TryAdd("Bg", new(serviceProvider, "Bg")); - downloadSummaries.TryAdd("AvatarIcon", new(serviceProvider, "AvatarIcon")); - downloadSummaries.TryAdd("GachaAvatarIcon", new(serviceProvider, "GachaAvatarIcon")); - downloadSummaries.TryAdd("GachaAvatarImg", new(serviceProvider, "GachaAvatarImg")); - downloadSummaries.TryAdd("EquipIcon", new(serviceProvider, "EquipIcon")); - downloadSummaries.TryAdd("GachaEquipIcon", new(serviceProvider, "GachaEquipIcon")); - downloadSummaries.TryAdd("NameCardPic", new(serviceProvider, "NameCardPic")); - downloadSummaries.TryAdd("Skill", new(serviceProvider, "Skill")); - downloadSummaries.TryAdd("Talent", new(serviceProvider, "Talent")); - } - - if (StaticResource.IsContractUnfulfilled(StaticResource.V2Contract)) - { - downloadSummaries.TryAdd("AchievementIcon", new(serviceProvider, "AchievementIcon")); - downloadSummaries.TryAdd("ItemIcon", new(serviceProvider, "ItemIcon")); - downloadSummaries.TryAdd("IconElement", new(serviceProvider, "IconElement")); - downloadSummaries.TryAdd("RelicIcon", new(serviceProvider, "RelicIcon")); - } - - if (StaticResource.IsContractUnfulfilled(StaticResource.V3Contract)) - { - downloadSummaries.TryAdd("Skill", new(serviceProvider, "Skill")); - downloadSummaries.TryAdd("Talent", new(serviceProvider, "Talent")); - } - - if (StaticResource.IsContractUnfulfilled(StaticResource.V4Contract)) - { - downloadSummaries.TryAdd("AvatarIcon", new(serviceProvider, "AvatarIcon")); - } - - if (StaticResource.IsContractUnfulfilled(StaticResource.V5Contract)) - { - downloadSummaries.TryAdd("MonsterIcon", new(serviceProvider, "MonsterIcon")); - } - - return downloadSummaries.Select(x => x.Value); - } - - /// - /// 下载信息 - /// - internal sealed class DownloadSummary : ObservableObject - { - private readonly IServiceProvider serviceProvider; - private readonly ITaskContext taskContext; - private readonly HttpClient httpClient; - private readonly string fileName; - private readonly string fileUrl; - private readonly Progress progress; - private string description = SH.ViewModelWelcomeDownloadSummaryDefault; - private double progressValue; - private long updateCount; - - /// - /// 构造一个新的下载信息 - /// - /// 服务提供器 - /// 压缩文件名称 - public DownloadSummary(IServiceProvider serviceProvider, string fileName) - { - taskContext = serviceProvider.GetRequiredService(); - httpClient = serviceProvider.GetRequiredService(); - RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService(); - httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent); - - this.serviceProvider = serviceProvider; - - DisplayName = fileName; - this.fileName = fileName; - fileUrl = Web.HutaoEndpoints.StaticZip(fileName); - - progress = new(UpdateProgressStatus); - } - - /// - /// 显示名称 - /// - public string DisplayName { get; init; } - - /// - /// 描述 - /// - public string Description { get => description; private set => SetProperty(ref description, value); } - - /// - /// 进度值,最大1 - /// - public double ProgressValue { get => progressValue; set => SetProperty(ref progressValue, value); } - - /// - /// 异步下载并解压 - /// - /// 任务 - public async Task DownloadAndExtractAsync() - { - ILogger logger = serviceProvider.GetRequiredService>(); - try - { - HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); - long contentLength = response.Content.Headers.ContentLength ?? 0; - logger.LogInformation("Begin download, length: {length}", contentLength); - using (Stream content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - { - using (TempFileStream temp = new(FileMode.OpenOrCreate, FileAccess.ReadWrite)) - { - await new StreamCopyWorker(content, temp, contentLength).CopyAsync(progress).ConfigureAwait(false); - ExtractFiles(temp); - - await taskContext.SwitchToMainThreadAsync(); - ProgressValue = 1; - Description = SH.ViewModelWelcomeDownloadSummaryComplete; - return true; - } - } - } - catch (Exception ex) - { - logger.LogError(ex, "Download Static Zip failed"); - await taskContext.SwitchToMainThreadAsync(); - Description = ex is HttpRequestException httpRequestException - ? $"{SH.ViewModelWelcomeDownloadSummaryException} - HTTP {httpRequestException.StatusCode:D}" - : SH.ViewModelWelcomeDownloadSummaryException; - return false; - } - } - - private void UpdateProgressStatus(StreamCopyStatus status) - { - if (Interlocked.Increment(ref updateCount) % 40 == 0) - { - Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}"; - ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes; - } - } - - private void ExtractFiles(Stream stream) - { - IImageCacheFilePathOperation imageCache = serviceProvider.GetRequiredService().As()!; - - using (ZipArchive archive = new(stream)) - { - foreach (ZipArchiveEntry entry in archive.Entries) - { - string destPath = imageCache.GetFileFromCategoryAndName(fileName, entry.FullName); - entry.ExtractToFile(destPath, true); - } - } - } - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaSpiralAbyssClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaSpiralAbyssClient.cs index 032eef79..8f3f678f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaSpiralAbyssClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaSpiralAbyssClient.cs @@ -36,7 +36,7 @@ internal sealed partial class HomaSpiralAbyssClient /// uid /// 取消令牌 /// 当前是否上传了数据 - public async Task> CheckRecordUploadedAsync(PlayerUid uid, CancellationToken token = default) + public async ValueTask> CheckRecordUploadedAsync(PlayerUid uid, CancellationToken token = default) { Response? resp = await httpClient .TryCatchGetFromJsonAsync>(HutaoEndpoints.RecordCheck(uid.Value), options, logger, token) @@ -52,7 +52,7 @@ internal sealed partial class HomaSpiralAbyssClient /// uid /// 取消令牌 /// 排行信息 - public async Task> GetRankAsync(PlayerUid uid, CancellationToken token = default) + public async ValueTask> GetRankAsync(PlayerUid uid, CancellationToken token = default) { Response? resp = await httpClient .TryCatchGetFromJsonAsync>(HutaoEndpoints.RecordRank(uid.Value), options, logger, token) @@ -67,7 +67,7 @@ internal sealed partial class HomaSpiralAbyssClient /// /// 取消令牌 /// 总览信息 - public async Task> GetOverviewAsync(CancellationToken token = default) + public async ValueTask> GetOverviewAsync(CancellationToken token = default) { Response? resp = await httpClient .TryCatchGetFromJsonAsync>(HutaoEndpoints.StatisticsOverview, options, logger, token) @@ -82,7 +82,7 @@ internal sealed partial class HomaSpiralAbyssClient /// /// 取消令牌 /// 角色出场率 - public async Task>> GetAvatarAttendanceRatesAsync(CancellationToken token = default) + public async ValueTask>> GetAvatarAttendanceRatesAsync(CancellationToken token = default) { Response>? resp = await httpClient .TryCatchGetFromJsonAsync>>(HutaoEndpoints.StatisticsAvatarAttendanceRate, options, logger, token) @@ -97,7 +97,7 @@ internal sealed partial class HomaSpiralAbyssClient /// /// 取消令牌 /// 角色出场率 - public async Task>> GetAvatarUtilizationRatesAsync(CancellationToken token = default) + public async ValueTask>> GetAvatarUtilizationRatesAsync(CancellationToken token = default) { Response>? resp = await httpClient .TryCatchGetFromJsonAsync>>(HutaoEndpoints.StatisticsAvatarUtilizationRate, options, logger, token) @@ -112,7 +112,7 @@ internal sealed partial class HomaSpiralAbyssClient /// /// 取消令牌 /// 角色/武器/圣遗物搭配 - public async Task>> GetAvatarCollocationsAsync(CancellationToken token = default) + public async ValueTask>> GetAvatarCollocationsAsync(CancellationToken token = default) { Response>? resp = await httpClient .TryCatchGetFromJsonAsync>>(HutaoEndpoints.StatisticsAvatarAvatarCollocation, options, logger, token) @@ -127,7 +127,7 @@ internal sealed partial class HomaSpiralAbyssClient /// /// 取消令牌 /// 角色/武器/圣遗物搭配 - public async Task>> GetWeaponCollocationsAsync(CancellationToken token = default) + public async ValueTask>> GetWeaponCollocationsAsync(CancellationToken token = default) { Response>? resp = await httpClient .TryCatchGetFromJsonAsync>>(HutaoEndpoints.StatisticsWeaponWeaponCollocation, options, logger, token) @@ -142,7 +142,7 @@ internal sealed partial class HomaSpiralAbyssClient /// /// 取消令牌 /// 角色图片列表 - public async Task>> GetAvatarHoldingRatesAsync(CancellationToken token = default) + public async ValueTask>> GetAvatarHoldingRatesAsync(CancellationToken token = default) { Response>? resp = await httpClient .TryCatchGetFromJsonAsync>>(HutaoEndpoints.StatisticsAvatarHoldingRate, options, logger, token) @@ -157,7 +157,7 @@ internal sealed partial class HomaSpiralAbyssClient /// /// 取消令牌 /// 队伍出场列表 - public async Task>> GetTeamCombinationsAsync(CancellationToken token = default) + public async ValueTask>> GetTeamCombinationsAsync(CancellationToken token = default) { Response>? resp = await httpClient .TryCatchGetFromJsonAsync>>(HutaoEndpoints.StatisticsTeamCombination, options, logger, token) @@ -172,7 +172,7 @@ internal sealed partial class HomaSpiralAbyssClient /// 用户与角色 /// 取消令牌 /// 玩家记录 - public async Task GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default) + public async ValueTask GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default) { IGameRecordClient gameRecordClient = serviceProvider .GetRequiredService>() @@ -212,7 +212,7 @@ internal sealed partial class HomaSpiralAbyssClient /// 玩家记录 /// 取消令牌 /// 响应 - public async Task> UploadRecordAsync(SimpleRecord playerRecord, CancellationToken token = default) + public async ValueTask> UploadRecordAsync(SimpleRecord playerRecord, CancellationToken token = default) { Response? resp = await httpClient .TryCatchPostAsJsonAsync>(HutaoEndpoints.RecordUpload, playerRecord, options, logger, token) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Converter/ReliquarySetsConverter.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Converter/ReliquarySetsConverter.cs index e4c58d6d..5b87e9eb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Converter/ReliquarySetsConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Converter/ReliquarySetsConverter.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.Extensions.Primitives; + namespace Snap.Hutao.Web.Hutao.Model.Converter; /// @@ -14,10 +16,18 @@ internal sealed class ReliquarySetsConverter : JsonConverter /// public override ReliquarySets? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.GetString() is string source) + if (reader.GetString() is { } source) { - string[] sets = source.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - return new(sets.Select(set => new ReliquarySet(set))); + List sets = new(); + foreach (StringSegment segment in new StringTokenizer(source, Separator.ToArray())) + { + if (segment.HasValue) + { + sets.Add(new(segment.Value)); + } + } + + return new(sets); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/SimpleRank.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/SimpleRank.cs index 73c80517..1ec227c2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/SimpleRank.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/SimpleRank.cs @@ -38,7 +38,7 @@ internal sealed class SimpleRank /// 新的简单数值 public static SimpleRank? FromRank(Rank? rank) { - if (rank == null) + if (rank is null) { return null; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ReliquarySet.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ReliquarySet.cs index 0bc584b2..4a2dd9f2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ReliquarySet.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ReliquarySet.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Primitive; +using System.Globalization; namespace Snap.Hutao.Web.Hutao.Model; @@ -19,8 +20,8 @@ internal sealed class ReliquarySet { string[]? deconstructed = set.Split('-'); - EquipAffixId = uint.Parse(deconstructed[0]); - Count = int.Parse(deconstructed[1]); + EquipAffixId = uint.Parse(deconstructed[0], CultureInfo.InvariantCulture); + Count = int.Parse(deconstructed[1], CultureInfo.InvariantCulture); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Team.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Team.cs index 13152583..2b125778 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Team.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Team.cs @@ -15,11 +15,11 @@ internal sealed class Team /// 上半 /// [JsonConverter(typeof(SeparatorCommaInt32EnumerableConverter))] - public IEnumerable UpHalf { get; set; } = null!; + public IEnumerable UpHalf { get; set; } = default!; /// /// 下半 /// [JsonConverter(typeof(SeparatorCommaInt32EnumerableConverter))] - public IEnumerable DownHalf { get; set; } = null!; + public IEnumerable DownHalf { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs index a0703023..fba40683 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs @@ -72,7 +72,7 @@ internal readonly struct QueryString { string name; string? value; - int indexOfEquals = pair.IndexOf('='); + int indexOfEquals = pair.IndexOf('=', StringComparison.Ordinal); if (indexOfEquals == -1) { name = pair; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryStringParameter.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryStringParameter.cs index 2f6acb86..4c652b12 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryStringParameter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryStringParameter.cs @@ -31,7 +31,7 @@ internal struct QueryStringParameter } /// - public override string ToString() + public override readonly string ToString() { return $"{Name}={Value}"; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs index 47651f8a..e9cfc3e4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs @@ -80,23 +80,22 @@ internal class Response /// 本体或默认值,当本体为 null 时 返回默认值 public static Response DefaultIfNull(Response? response, [CallerMemberName] string callerName = default!) { - if (response != null) + if (response is not null) { Must.Argument(response.ReturnCode != 0, "返回代码必须为0"); - - // 0x26F19335 is a magic number that hashed from "Snap.Hutao" return new(response.ReturnCode, response.Message, default); } else { - return new(0x26F19335, $"[{callerName}] 中的 [{typeof(TData).Name}] 网络请求异常,请稍后再试", default); + // Magic number that hashed from "Snap.Hutao" + return new(0x26F19335, SH.WebResponseRequestExceptionFormat.Format(callerName, typeof(TData).Name), default); } } /// public override string ToString() { - return string.Format(SH.WebResponseFormat, ReturnCode, Message); + return SH.WebResponseFormat.Format(ReturnCode, Message); } } @@ -163,7 +162,8 @@ internal sealed class Response : Response, IJsResult { if (ReturnCode == 0) { - data = Data!; + ArgumentNullException.ThrowIfNull(Data); + data = Data; return true; } else