refactor welcome view model

This commit is contained in:
Lightczx
2023-08-15 17:26:38 +08:00
parent 4c50479e96
commit d08d2b406f
44 changed files with 481 additions and 338 deletions

View File

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

View File

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

View File

@@ -1 +1 @@
CA1501: 6
CA1501: 8

View File

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

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.InterChange.Achievement;
/// UIAF格式的信息
/// </summary>
[HighQuality]
internal sealed class UIAFInfo : IMappingFrom<UIAFInfo, IServiceProvider>
internal sealed class UIAFInfo : IMappingFrom<UIAFInfo, RuntimeOptions>
{
/// <summary>
/// 导出的 App 名称
@@ -45,15 +45,8 @@ internal sealed class UIAFInfo : IMappingFrom<UIAFInfo, IServiceProvider>
[JsonPropertyName("uiaf_version")]
public string? UIAFVersion { get; set; }
/// <summary>
/// 构造一个新的专用 UIAF 信息
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <returns>专用 UIAF 信息</returns>
public static UIAFInfo From(IServiceProvider serviceProvider)
public static UIAFInfo From(RuntimeOptions runtimeOptions)
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
return new()
{
ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),

View File

@@ -23,7 +23,7 @@ internal sealed class ItemIconConverter : ValueConverter<string, Uri>
return default!;
}
return name.StartsWith("UI_RelicIcon_")
return name.StartsWith("UI_RelicIcon_", StringComparison.Ordinal)
? RelicIconConverter.IconNameToUri(name)
: Web.HutaoEndpoints.StaticFile("ItemIcon", $"{name}.png").ToUri();
}

View File

@@ -23,7 +23,7 @@ internal sealed class SkillIconConverter : ValueConverter<string, Uri>
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();
}

View File

@@ -19,7 +19,7 @@ internal sealed class LevelDescription
/// 格式化的等级
/// </summary>
[JsonIgnore]
public string LevelFormatted { get => string.Format(SH.ModelWeaponAffixFormat, Level + 1); }
public string LevelFormatted { get => SH.ModelWeaponAffixFormat.Format(Level + 1); }
/// <summary>
/// 描述

View File

@@ -39,6 +39,6 @@ internal sealed unsafe class IdentityConverter<TWrapper> : JsonConverter<TWrappe
/// <inheritdoc/>
public override void WriteAsPropertyName(Utf8JsonWriter writer, TWrapper value, JsonSerializerOptions options)
{
writer.WritePropertyName((*(uint*)&value).ToString());
writer.WritePropertyName($"{*(uint*)&value}");
}
}

View File

@@ -5819,5 +5819,14 @@ namespace Snap.Hutao.Resource.Localization {
return ResourceManager.GetString("WebResponseFormat", resourceCulture);
}
}
/// <summary>
/// 查找类似 [{0}] 中的 [{1}] 网络请求异常,请稍后再试 的本地化字符串。
/// </summary>
internal static string WebResponseRequestExceptionFormat {
get {
return ResourceManager.GetString("WebResponseRequestExceptionFormat", resourceCulture);
}
}
}
}

View File

@@ -2093,4 +2093,7 @@
<data name="WebResponseFormat" xml:space="preserve">
<value>状态:{0} | 信息:{1}</value>
</data>
<data name="WebResponseRequestExceptionFormat" xml:space="preserve">
<value>[{0}] 中的 [{1}] 网络请求异常,请稍后再试</value>
</data>
</root>

View File

@@ -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<DbSt
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
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<DbSt
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == key);
appDbContext.Settings.AddAndSave(new(key, value.ToString()));
appDbContext.Settings.AddAndSave(new(key, $"{value}"));
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Service.Achievement;
/// </summary>
[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;

View File

@@ -16,7 +16,7 @@ namespace Snap.Hutao.Service.Achievement;
/// 成就数据库服务
/// </summary>
[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);

View File

@@ -51,19 +51,14 @@ internal sealed partial class AchievementService
public async ValueTask<UIAF> ExportToUIAFAsync(AchievementArchive archive)
{
await taskContext.SwitchToBackgroundAsync();
using (IServiceScope scope = serviceProvider.CreateScope())
List<UIAFItem> list = achievementDbService
.GetAchievementListByArchiveId(archive.InnerId)
.SelectList(UIAFItem.From);
return new()
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
List<UIAFItem> list = achievementDbService
.GetAchievementListByArchiveId(archive.InnerId)
.SelectList(UIAFItem.From);
return new()
{
Info = UIAFInfo.From(scope.ServiceProvider),
List = list,
};
}
Info = UIAFInfo.From(runtimeOptions),
List = list,
};
}
}

View File

@@ -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<AchievementArchive, Message.AchievementArchiveChangedMessage> dbCurrent;
private readonly AchievementDbBulkOperation achievementDbBulkOperation;
private readonly IAchievementDbService achievementDbService;
private readonly IServiceProvider serviceProvider;
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
/// <inheritdoc/>

View File

@@ -40,6 +40,6 @@ internal readonly struct ImportResult
/// <inheritdoc/>
public override string ToString()
{
return string.Format(SH.ServiceAchievementImportResultFormat, Add, Update, Remove);
return SH.ServiceAchievementImportResultFormat.Format(Add, Update, Remove);
}
}

View File

@@ -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<int, string> contentMap, List<AnnouncementListWrapper> announcementListWrappers)

View File

@@ -26,7 +26,7 @@ namespace Snap.Hutao.Service.AvatarInfo;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
[Injection(InjectAs.Singleton)]
internal sealed partial class AvatarInfoDbBulkOperation
{
private readonly IServiceProvider serviceProvider;

View File

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

View File

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

View File

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

View File

@@ -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
/// </summary>
public NameValue<int>? 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
{

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,8 @@ internal interface IUserDbService
ValueTask DeleteUserByIdAsync(Guid id);
ValueTask DeleteUsersAsync();
ValueTask<List<Model.Entity.User>> GetUserListAsync();
ValueTask UpdateUserAsync(Model.Entity.User user);

View File

@@ -63,4 +63,9 @@ internal interface IUserService
/// <param name="user">待移除的用户</param>
/// <returns>任务</returns>
ValueTask RemoveUserAsync(BindingUser user);
}
internal interface IUserServiceUnsafe
{
ValueTask UnsafeRemoveUsersAsync();
}

View File

@@ -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<AppDbContext>();
await appDbContext.Users.ExecuteDeleteAsync().ConfigureAwait(false);
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Snap.Hutao.Service.User;
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IUserService))]
internal sealed partial class UserService : IUserService
internal sealed partial class UserService : IUserService, IUserServiceUnsafe
{
private readonly ScopedDbCurrent<BindingUser, Model.Entity.User, UserChangedMessage> 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);
}
/// <inheritdoc/>
public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
{

View File

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

View File

@@ -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;
/// <summary>
/// 下载信息
/// </summary>
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<StreamCopyStatus> progress;
private string description = SH.ViewModelWelcomeDownloadSummaryDefault;
private double progressValue;
private long updateCount;
/// <summary>
/// 构造一个新的下载信息
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="fileName">压缩文件名称</param>
public DownloadSummary(IServiceProvider serviceProvider, string fileName)
{
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
httpClient = serviceProvider.GetRequiredService<HttpClient>();
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
this.serviceProvider = serviceProvider;
DisplayName = fileName;
this.fileName = fileName;
fileUrl = Web.HutaoEndpoints.StaticZip(fileName);
progress = new(UpdateProgressStatus);
}
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get; init; }
/// <summary>
/// 描述
/// </summary>
public string Description { get => description; private set => SetProperty(ref description, value); }
/// <summary>
/// 进度值最大1
/// </summary>
public double ProgressValue { get => progressValue; set => SetProperty(ref progressValue, value); }
/// <summary>
/// 异步下载并解压
/// </summary>
/// <returns>任务</returns>
public async Task<bool> DownloadAndExtractAsync()
{
ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>();
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<IImageCache>().As<IImageCacheFilePathOperation>()!;
using (ZipArchive archive = new(stream))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
string destPath = imageCache.GetFileFromCategoryAndName(fileName, entry.FullName);
entry.ExtractToFile(destPath, true);
}
}
}
}

View File

@@ -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;
/// <summary>
/// 欢迎视图模型
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class WelcomeViewModel : ObservableObject
{
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private ObservableCollection<DownloadSummary>? downloadSummaries;
/// <summary>
/// 下载信息
/// </summary>
public ObservableCollection<DownloadSummary>? DownloadSummaries { get => downloadSummaries; set => SetProperty(ref downloadSummaries, value); }
[Command("OpenUICommand")]
private async Task OpenUIAsync()
{
IEnumerable<DownloadSummary> 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<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
StaticResource.FulfillAllContracts();
try
{
new ToastContentBuilder()
.AddText(SH.ViewModelWelcomeDownloadCompleteTitle)
.AddText(SH.ViewModelWelcomeDownloadCompleteMessage)
.Show();
}
catch (COMException)
{
// 0x803E0105
}
}
private IEnumerable<DownloadSummary> GenerateStaticResourceDownloadTasks()
{
Dictionary<string, DownloadSummary> 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);
}
}

View File

@@ -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<IGameLocatorFactory>().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<IInfoBarService>();
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<SignInWebViewDialog>();
await dialog.ShowAsync();
// TODO remove this.
// SignInWebViewDialog dialog = serviceProvider.CreateInstance<SignInWebViewDialog>();
// await dialog.ShowAsync();
}
[Command("UpdateCheckCommand")]
private async Task CheckUpdateAsync()
{
#if DEBUG
await serviceProvider
.GetRequiredService<Service.Navigation.INavigationService>()
.NavigateAsync<View.Page.TestPage>(Service.Navigation.INavigationAwaiter.Default)
await navigationService
.NavigateAsync<View.Page.TestPage>(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<IPickerFactory>()
(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<IInfoBarService>().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<IInfoBarService>();
try
{
serviceProvider.GetRequiredService<IClipboardInterop>().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<INavigationService>().Navigate<View.Page.HutaoPassportPage>(INavigationAwaiter.Default);
navigationService.Navigate<View.Page.HutaoPassportPage>(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<IContentDialogFactory>()
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<AppDbContext>();
await appDbContext.Users.ExecuteDeleteAsync().ConfigureAwait(false);
await @unsafe.UnsafeRemoveUsersAsync().ConfigureAwait(false);
AppInstance.Restart(string.Empty);
}
}

View File

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

View File

@@ -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;
/// <summary>
/// 欢迎视图模型
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class WelcomeViewModel : ObservableObject
{
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private ObservableCollection<DownloadSummary>? downloadSummaries;
/// <summary>
/// 下载信息
/// </summary>
public ObservableCollection<DownloadSummary>? DownloadSummaries { get => downloadSummaries; set => SetProperty(ref downloadSummaries, value); }
[Command("OpenUICommand")]
private async Task OpenUIAsync()
{
IEnumerable<DownloadSummary> 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<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
StaticResource.FulfillAllContracts();
try
{
new ToastContentBuilder()
.AddText(SH.ViewModelWelcomeDownloadCompleteTitle)
.AddText(SH.ViewModelWelcomeDownloadCompleteMessage)
.Show();
}
catch (COMException)
{
// 0x803E0105
}
}
private IEnumerable<DownloadSummary> GenerateStaticResourceDownloadTasks()
{
Dictionary<string, DownloadSummary> 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);
}
/// <summary>
/// 下载信息
/// </summary>
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<StreamCopyStatus> progress;
private string description = SH.ViewModelWelcomeDownloadSummaryDefault;
private double progressValue;
private long updateCount;
/// <summary>
/// 构造一个新的下载信息
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="fileName">压缩文件名称</param>
public DownloadSummary(IServiceProvider serviceProvider, string fileName)
{
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
httpClient = serviceProvider.GetRequiredService<HttpClient>();
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
this.serviceProvider = serviceProvider;
DisplayName = fileName;
this.fileName = fileName;
fileUrl = Web.HutaoEndpoints.StaticZip(fileName);
progress = new(UpdateProgressStatus);
}
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get; init; }
/// <summary>
/// 描述
/// </summary>
public string Description { get => description; private set => SetProperty(ref description, value); }
/// <summary>
/// 进度值最大1
/// </summary>
public double ProgressValue { get => progressValue; set => SetProperty(ref progressValue, value); }
/// <summary>
/// 异步下载并解压
/// </summary>
/// <returns>任务</returns>
public async Task<bool> DownloadAndExtractAsync()
{
ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>();
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<IImageCache>().As<IImageCacheFilePathOperation>()!;
using (ZipArchive archive = new(stream))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
string destPath = imageCache.GetFileFromCategoryAndName(fileName, entry.FullName);
entry.ExtractToFile(destPath, true);
}
}
}
}
}

View File

@@ -36,7 +36,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>当前是否上传了数据</returns>
public async Task<Response<bool>> CheckRecordUploadedAsync(PlayerUid uid, CancellationToken token = default)
public async ValueTask<Response<bool>> CheckRecordUploadedAsync(PlayerUid uid, CancellationToken token = default)
{
Response<bool>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<bool>>(HutaoEndpoints.RecordCheck(uid.Value), options, logger, token)
@@ -52,7 +52,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>排行信息</returns>
public async Task<Response<RankInfo>> GetRankAsync(PlayerUid uid, CancellationToken token = default)
public async ValueTask<Response<RankInfo>> GetRankAsync(PlayerUid uid, CancellationToken token = default)
{
Response<RankInfo>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<RankInfo>>(HutaoEndpoints.RecordRank(uid.Value), options, logger, token)
@@ -67,7 +67,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>总览信息</returns>
public async Task<Response<Overview>> GetOverviewAsync(CancellationToken token = default)
public async ValueTask<Response<Overview>> GetOverviewAsync(CancellationToken token = default)
{
Response<Overview>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<Overview>>(HutaoEndpoints.StatisticsOverview, options, logger, token)
@@ -82,7 +82,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色出场率</returns>
public async Task<Response<List<AvatarAppearanceRank>>> GetAvatarAttendanceRatesAsync(CancellationToken token = default)
public async ValueTask<Response<List<AvatarAppearanceRank>>> GetAvatarAttendanceRatesAsync(CancellationToken token = default)
{
Response<List<AvatarAppearanceRank>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<List<AvatarAppearanceRank>>>(HutaoEndpoints.StatisticsAvatarAttendanceRate, options, logger, token)
@@ -97,7 +97,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色出场率</returns>
public async Task<Response<List<AvatarUsageRank>>> GetAvatarUtilizationRatesAsync(CancellationToken token = default)
public async ValueTask<Response<List<AvatarUsageRank>>> GetAvatarUtilizationRatesAsync(CancellationToken token = default)
{
Response<List<AvatarUsageRank>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<List<AvatarUsageRank>>>(HutaoEndpoints.StatisticsAvatarUtilizationRate, options, logger, token)
@@ -112,7 +112,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色/武器/圣遗物搭配</returns>
public async Task<Response<List<AvatarCollocation>>> GetAvatarCollocationsAsync(CancellationToken token = default)
public async ValueTask<Response<List<AvatarCollocation>>> GetAvatarCollocationsAsync(CancellationToken token = default)
{
Response<List<AvatarCollocation>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<List<AvatarCollocation>>>(HutaoEndpoints.StatisticsAvatarAvatarCollocation, options, logger, token)
@@ -127,7 +127,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色/武器/圣遗物搭配</returns>
public async Task<Response<List<WeaponCollocation>>> GetWeaponCollocationsAsync(CancellationToken token = default)
public async ValueTask<Response<List<WeaponCollocation>>> GetWeaponCollocationsAsync(CancellationToken token = default)
{
Response<List<WeaponCollocation>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<List<WeaponCollocation>>>(HutaoEndpoints.StatisticsWeaponWeaponCollocation, options, logger, token)
@@ -142,7 +142,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色图片列表</returns>
public async Task<Response<List<AvatarConstellationInfo>>> GetAvatarHoldingRatesAsync(CancellationToken token = default)
public async ValueTask<Response<List<AvatarConstellationInfo>>> GetAvatarHoldingRatesAsync(CancellationToken token = default)
{
Response<List<AvatarConstellationInfo>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<List<AvatarConstellationInfo>>>(HutaoEndpoints.StatisticsAvatarHoldingRate, options, logger, token)
@@ -157,7 +157,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>队伍出场列表</returns>
public async Task<Response<List<TeamAppearance>>> GetTeamCombinationsAsync(CancellationToken token = default)
public async ValueTask<Response<List<TeamAppearance>>> GetTeamCombinationsAsync(CancellationToken token = default)
{
Response<List<TeamAppearance>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<List<TeamAppearance>>>(HutaoEndpoints.StatisticsTeamCombination, options, logger, token)
@@ -172,7 +172,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家记录</returns>
public async Task<SimpleRecord?> GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default)
public async ValueTask<SimpleRecord?> GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default)
{
IGameRecordClient gameRecordClient = serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
@@ -212,7 +212,7 @@ internal sealed partial class HomaSpiralAbyssClient
/// <param name="playerRecord">玩家记录</param>
/// <param name="token">取消令牌</param>
/// <returns>响应</returns>
public async Task<Response<string>> UploadRecordAsync(SimpleRecord playerRecord, CancellationToken token = default)
public async ValueTask<Response<string>> UploadRecordAsync(SimpleRecord playerRecord, CancellationToken token = default)
{
Response<string>? resp = await httpClient
.TryCatchPostAsJsonAsync<SimpleRecord, Response<string>>(HutaoEndpoints.RecordUpload, playerRecord, options, logger, token)

View File

@@ -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;
/// <summary>
@@ -14,10 +16,18 @@ internal sealed class ReliquarySetsConverter : JsonConverter<ReliquarySets>
/// <inheritdoc/>
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<ReliquarySet> sets = new();
foreach (StringSegment segment in new StringTokenizer(source, Separator.ToArray()))
{
if (segment.HasValue)
{
sets.Add(new(segment.Value));
}
}
return new(sets);
}
else
{

View File

@@ -38,7 +38,7 @@ internal sealed class SimpleRank
/// <returns>新的简单数值</returns>
public static SimpleRank? FromRank(Rank? rank)
{
if (rank == null)
if (rank is null)
{
return null;
}

View File

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

View File

@@ -15,11 +15,11 @@ internal sealed class Team
/// 上半
/// </summary>
[JsonConverter(typeof(SeparatorCommaInt32EnumerableConverter))]
public IEnumerable<int> UpHalf { get; set; } = null!;
public IEnumerable<int> UpHalf { get; set; } = default!;
/// <summary>
/// 下半
/// </summary>
[JsonConverter(typeof(SeparatorCommaInt32EnumerableConverter))]
public IEnumerable<int> DownHalf { get; set; } = null!;
public IEnumerable<int> DownHalf { get; set; } = default!;
}

View File

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

View File

@@ -31,7 +31,7 @@ internal struct QueryStringParameter
}
/// <inheritdoc/>
public override string ToString()
public override readonly string ToString()
{
return $"{Name}={Value}";
}

View File

@@ -80,23 +80,22 @@ internal class Response
/// <returns>本体或默认值,当本体为 null 时 返回默认值</returns>
public static Response<TData> DefaultIfNull<TData, TOther>(Response<TOther>? 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);
}
}
/// <inheritdoc/>
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<TData> : Response, IJsResult
{
if (ReturnCode == 0)
{
data = Data!;
ArgumentNullException.ThrowIfNull(Data);
data = Data;
return true;
}
else