From c4e4ffebd623ffca210c004560531db7b7317c0c Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 16 Aug 2023 17:19:43 +0800 Subject: [PATCH] refactor viewmodel --- .../UniversalAnalyzer.cs | 6 + .../Control/Image/CompositionExtension.cs | 42 ++-- .../Core/Abstraction/IMappingFrom.cs | 7 + .../IocHttpClientConfiguration.cs | 1 + .../Abstraction/IContentDialogFactory.cs | 6 + .../Factory/ContentDialogFactory.cs | 33 +-- .../Model/Entity/GachaArchive.Operation.cs | 39 --- .../Model/InterChange/GachaLog/UIGFInfo.cs | 13 +- .../Achievement/AchievementDbService.cs | 3 +- .../Service/AvatarInfo/AvatarInfoService.cs | 4 +- .../DailyNoteNotificationOperation.cs | 226 +++++++++--------- .../Service/DailyNote/DailyNoteNotifyInfo.cs | 20 ++ .../Service/DailyNote/DailyNoteService.cs | 3 +- .../Factory/GachaStatisticsFactory.cs | 32 +-- .../GachaLog/Factory/PullPrediction.cs | 24 +- .../Factory/TypedWishSummaryBuilder.cs | 35 +-- .../Factory/TypedWishSummaryBuilderContext.cs | 62 +++++ .../Service/GachaLog/GachaArchiveOperation.cs | 13 +- .../Service/GachaLog/GachaItemSaveContext.cs | 27 ++- .../Service/GachaLog/GachaLogDbService.cs | 28 ++- .../Service/GachaLog/GachaLogFetchContext.cs | 32 +-- .../GachaLog/GachaLogHutaoCloudService.cs | 3 +- .../Service/GachaLog/GachaLogService.cs | 5 +- .../GachaLogServiceMetadataContext.cs | 4 +- .../Service/GachaLog/IGachaLogDbService.cs | 4 + .../GachaLog/QueryProvider/GachaLogQuery.cs | 2 +- .../GachaLogQueryManualInputProvider.cs | 16 +- .../GachaLogQueryProviderFactory.cs | 1 + .../GachaLogQueryWebCacheProvider.cs | 84 +++---- .../Service/GachaLog/UIGFExportService.cs | 7 +- .../Service/GachaLog/UIGFImportService.cs | 5 +- .../Game/GameFileOperationException.cs | 2 +- .../Snap.Hutao/Service/Game/GameService.cs | 67 +++--- .../Snap.Hutao/Service/Game/LaunchOptions.cs | 5 +- .../Game/Locator/GameLocatorFactory.cs | 1 + .../Service/Game/Locator/ManualGameLocator.cs | 2 +- .../Game/Locator/RegistryLauncherLocator.cs | 11 +- .../Service/Game/Package/PackageConverter.cs | 23 +- .../Service/Game/RegistryInterop.cs | 5 +- .../Snap.Hutao/Service/Hutao/HutaoCache.cs | 4 +- .../Snap.Hutao/Service/Hutao/HutaoService.cs | 54 +---- .../Service/Hutao/HutaoUserOptions.cs | 2 +- .../Service/Hutao/IObjectCacheDbService.cs | 13 + .../Service/Hutao/ObjectCacheDbService.cs | 68 ++++++ .../Metadata/MetadataService.Constants.cs | 2 + .../Service/Navigation/NavigationService.cs | 11 +- .../Service/Notification/InfoBarService.cs | 6 +- .../SpiralAbyss/SpiralAbyssRecordDbService.cs | 2 +- .../SpiralAbyss/SpiralAbyssRecordService.cs | 10 +- .../Snap.Hutao/Service/User/UserService.cs | 11 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 1 - .../Control/BottomTextSmallControl.xaml.cs | 2 +- .../CultivatePromotionDeltaDialog.xaml.cs | 49 ++-- .../View/Dialog/GachaLogImportDialog.xaml.cs | 2 +- .../GachaLogRefreshProgressDialog.xaml.cs | 2 +- .../View/Dialog/GachaLogUrlDialog.xaml.cs | 2 +- .../LaunchGameAccountNameDialog.xaml.cs | 2 +- .../View/Dialog/SignInWebViewDialog.xaml.cs | 2 +- .../Snap.Hutao/View/Dialog/UserDialog.xaml.cs | 2 +- .../View/Page/LoginHoyoverseUserPage.xaml.cs | 6 +- .../Snap.Hutao/View/TitleView.xaml.cs | 2 +- .../Achievement/AchievementImporter.cs | 19 +- .../Achievement/AchievementViewModel.cs | 26 +- .../AvatarProperty/AvatarProperty.cs | 3 +- .../AvatarProperty/AvatarPropertyViewModel.cs | 92 +++---- .../ViewModel/AvatarProperty/WeaponView.cs | 2 +- .../Snap.Hutao/ViewModel/Complex/Team.cs | 5 +- .../ViewModel/Complex/TeamAppearanceView.cs | 2 +- .../Cultivation/CultivationViewModel.cs | 15 +- .../ViewModel/DailyNote/DailyNoteViewModel.cs | 6 +- .../ViewModel/GachaLog/GachaLogViewModel.cs | 15 +- .../ViewModel/GachaLog/HutaoCloudViewModel.cs | 6 +- .../ViewModel/GachaLog/TypedWishSummary.cs | 12 +- .../Snap.Hutao/ViewModel/GachaLog/Wish.cs | 2 +- .../ViewModel/Game/LaunchGameViewModel.cs | 21 +- .../ViewModel/Guide/DownloadSummary.cs | 9 +- .../ViewModel/Home/AnnouncementViewModel.cs | 2 +- .../ViewModel/HutaoPassportViewModel.cs | 3 +- .../ViewModel/SpiralAbyss/BattleView.cs | 2 +- .../ViewModel/SpiralAbyss/FloorView.cs | 2 +- .../ViewModel/SpiralAbyss/LevelView.cs | 2 +- .../SpiralAbyss/SpiralAbyssRecordViewModel.cs | 9 +- .../Snap.Hutao/ViewModel/User/UserAndUid.cs | 1 + .../ViewModel/User/UserViewModel.cs | 8 +- .../ViewModel/Wiki/WikiAvatarViewModel.cs | 13 +- .../ViewModel/Wiki/WikiWeaponViewModel.cs | 15 +- .../Web/Bridge/CoreWebView2Extension.cs | 6 +- .../Web/Bridge/MiHoYoJSInterface.cs | 30 +-- .../Bridge/Model/DynamicSecrect2Playload.cs | 1 + .../Snap.Hutao/Web/Bridge/Model/JsParam.cs | 2 +- .../Snap.Hutao/Web/Enka/EnkaClient.cs | 8 +- .../Snap.Hutao/Web/Enka/Model/EnkaResponse.cs | 2 +- .../Snap.Hutao/Web/Geetest/GeetestClient.cs | 6 +- .../Web/Hoyolab/App/Account/AccountClient.cs | 2 +- .../Web/Hoyolab/Bbs/User/IUserClient.cs | 2 +- .../Web/Hoyolab/Bbs/User/UserClient.cs | 5 +- .../Web/Hoyolab/Bbs/User/UserClientOversea.cs | 5 +- .../Snap.Hutao/Web/Hoyolab/Images.cs | 13 - .../Web/Hutao/HomaGachaLogClient.cs | 2 +- .../Model/Converter/ReliquarySetsConverter.cs | 2 +- .../Snap.Hutao/Web/Response/Response.cs | 11 +- 101 files changed, 814 insertions(+), 733 deletions(-) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.Operation.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifyInfo.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Hutao/IObjectCacheDbService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Hutao/ObjectCacheDbService.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Images.cs diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs index cbffff49..a8ad8607 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs @@ -277,6 +277,12 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer } } + if (syntax.Operand is DefaultExpressionSyntax expression) + { + return; + } + + Location location = syntax.GetLocation(); Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location); context.ReportDiagnostic(diagnostic); diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionExtension.cs index a27ba5d9..aa7ff0b1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionExtension.cs @@ -52,12 +52,15 @@ internal static class CompositionExtension Mode = blendEffectMode, }; - CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); + using (effect) + { + CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); - brush.SetSourceParameter(Background, background); - brush.SetSourceParameter(Foreground, foreground); + brush.SetSourceParameter(Background, background); + brush.SetSourceParameter(Foreground, foreground); - return brush; + return brush; + } } /// @@ -75,11 +78,14 @@ internal static class CompositionExtension Source = new CompositionEffectSourceParameter(Source), }; - CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); + using (effect) + { + CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); - brush.SetSourceParameter(Source, source); + brush.SetSourceParameter(Source, source); - return brush; + return brush; + } } /// @@ -97,11 +103,14 @@ internal static class CompositionExtension Source = new CompositionEffectSourceParameter(Source), }; - CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); + using (effect) + { + CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); - brush.SetSourceParameter(Source, sourceBrush); + brush.SetSourceParameter(Source, sourceBrush); - return brush; + return brush; + } } /// @@ -116,18 +125,21 @@ internal static class CompositionExtension CompositionBrush sourceBrush, CompositionBrush alphaMask) { - AlphaMaskEffect maskEffect = new() + AlphaMaskEffect effect = new() { AlphaMask = new CompositionEffectSourceParameter(AlphaMask), Source = new CompositionEffectSourceParameter(Source), }; - CompositionEffectBrush brush = compositor.CreateEffectFactory(maskEffect).CreateBrush(); + using (effect) + { + CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); - brush.SetSourceParameter(AlphaMask, alphaMask); - brush.SetSourceParameter(Source, sourceBrush); + brush.SetSourceParameter(AlphaMask, alphaMask); + brush.SetSourceParameter(Source, sourceBrush); - return brush; + return brush; + } } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IMappingFrom.cs b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IMappingFrom.cs index e17b9c92..d4ba0bd9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IMappingFrom.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IMappingFrom.cs @@ -17,4 +17,11 @@ internal interface IMappingFrom { [Pure] static abstract TSelf From(T1 t1, T2 t2); +} + +internal interface IMappingFrom + where TSelf : IMappingFrom +{ + [Pure] + static abstract TSelf From(T1 t1, T2 t2, T3 t3); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs index b5876a2d..db80c281 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -88,6 +88,7 @@ internal static partial class IocHttpClientConfiguration /// HoYoLAB web /// /// 配置后的客户端 + [SuppressMessage("", "IDE0051")] private static void XRpc4Configuration(HttpClient client) { client.Timeout = Timeout.InfiniteTimeSpan; diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IContentDialogFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IContentDialogFactory.cs index 1794360a..b09c2f6f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IContentDialogFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IContentDialogFactory.cs @@ -34,4 +34,10 @@ internal interface IContentDialogFactory /// 标题 /// 内容对话框 ValueTask CreateForIndeterminateProgressAsync(string title); + + TContentDialog CreateInstance(params object[] parameters) + where TContentDialog : ContentDialog; + + ValueTask CreateInstanceAsync(params object[] parameters) + where TContentDialog : ContentDialog; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs index d89d5359..3716ee64 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs @@ -3,27 +3,19 @@ using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Factory.Abstraction; +using System.Security.Authentication; namespace Snap.Hutao.Factory; /// [HighQuality] -[Injection(InjectAs.Transient, typeof(IContentDialogFactory))] -internal sealed class ContentDialogFactory : IContentDialogFactory +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IContentDialogFactory))] +internal sealed partial class ContentDialogFactory : IContentDialogFactory { - private readonly MainWindow mainWindow; + private readonly IServiceProvider serviceProvider; private readonly ITaskContext taskContext; - - /// - /// 构造一个新的内容对话框工厂 - /// - /// 任务上下文 - /// 主窗体 - public ContentDialogFactory(ITaskContext taskContext, MainWindow mainWindow) - { - this.taskContext = taskContext; - this.mainWindow = mainWindow; - } + private readonly MainWindow mainWindow; /// public async ValueTask CreateForConfirmAsync(string title, string content) @@ -71,4 +63,17 @@ internal sealed class ContentDialogFactory : IContentDialogFactory return dialog; } + + public async ValueTask CreateInstanceAsync(params object[] parameters) + where TContentDialog : ContentDialog + { + await taskContext.SwitchToMainThreadAsync(); + return serviceProvider.CreateInstance(); + } + + public TContentDialog CreateInstance(params object[] parameters) + where TContentDialog : ContentDialog + { + return serviceProvider.CreateInstance(); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.Operation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.Operation.cs deleted file mode 100644 index 369e09ab..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.Operation.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Snap.Hutao.Core.Database; -using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Service.GachaLog; -using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; - -namespace Snap.Hutao.Model.Entity; - -/// -/// 操作部分 -/// -internal sealed partial class GachaArchive -{ - /// - /// 保存祈愿物品 - /// - /// 上下文 - [SuppressMessage("", "SH002")] - public void SaveItems(GachaItemSaveContext context) - { - if (context.ItemsToAdd.Count > 0) - { - // 全量刷新 - if (!context.IsLazy) - { - context.GachaItems - .Where(i => i.ArchiveId == InnerId) - .Where(i => i.Id >= context.EndId) - .ExecuteDelete(); - } - - context.GachaItems.AddRangeAndSave(context.ItemsToAdd); - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs index 0d4e8511..0b548800 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs @@ -11,7 +11,7 @@ namespace Snap.Hutao.Model.InterChange.GachaLog; /// UIGF格式的信息 /// [HighQuality] -internal sealed class UIGFInfo : IMappingFrom +internal sealed class UIGFInfo : IMappingFrom { /// /// 用户Uid @@ -58,17 +58,8 @@ internal sealed class UIGFInfo : IMappingFrom - /// 构造一个新的专用 UIGF 信息 - /// - /// 服务提供器 - /// uid - /// 专用 UIGF 信息 - public static UIGFInfo From(IServiceProvider serviceProvider, string uid) + public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid) { - RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService(); - MetadataOptions metadataOptions = serviceProvider.GetRequiredService(); - return new() { Uid = uid, diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs index c5703dd3..73726a78 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs @@ -57,6 +57,7 @@ internal sealed partial class AchievementDbService : IAchievementDbService } } + [SuppressMessage("", "CA1305")] public async ValueTask> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take) { using (IServiceScope scope = serviceProvider.CreateScope()) @@ -66,7 +67,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) + .OrderByDescending(a => a.Time.ToString()) .Take(take) .ToListAsync() .ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs index 78fcc3b9..ada9aadb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs @@ -25,8 +25,8 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService private readonly IAvatarInfoDbService avatarInfoDbService; private readonly ILogger logger; private readonly IMetadataService metadataService; - private readonly IServiceProvider serviceProvider; private readonly ISummaryFactory summaryFactory; + private readonly EnkaClient enkaClient; /// public async ValueTask> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default) @@ -92,8 +92,6 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService private async ValueTask GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default) { - EnkaClient enkaClient = serviceProvider.GetRequiredService(); - return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false) ?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs index b399c97b..e8d58cf2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs @@ -10,6 +10,7 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Auth; using Snap.Hutao.Web.Hoyolab.Takumi.Binding; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote; using Snap.Hutao.Web.Response; +using System.Runtime.InteropServices; using Windows.Foundation.Metadata; namespace Snap.Hutao.Service.DailyNote; @@ -18,38 +19,26 @@ namespace Snap.Hutao.Service.DailyNote; /// 实时便笺通知器 /// [HighQuality] -internal sealed class DailyNoteNotificationOperation +[ConstructorGenerated] +[Injection(InjectAs.Singleton)] +internal sealed partial class DailyNoteNotificationOperation { private const string ToastHeaderIdArgument = "DAILYNOTE"; private const string ToastAttributionUnknown = "Unknown"; + private readonly ITaskContext taskContext; - private readonly IServiceProvider serviceProvider; - private readonly DailyNoteEntry entry; + private readonly IGameService gameService; + private readonly BindingClient bindingClient; + private readonly DailyNoteOptions options; - /// - /// 构造一个新的实时便笺通知器 - /// - /// 服务提供器 - /// 实时便笺入口 - public DailyNoteNotificationOperation(IServiceProvider serviceProvider, DailyNoteEntry entry) - { - taskContext = serviceProvider.GetRequiredService(); - this.serviceProvider = serviceProvider; - this.entry = entry; - } - - /// - /// 异步通知 - /// - /// 任务 - public async ValueTask SendAsync() + public async ValueTask SendAsync(DailyNoteEntry entry) { if (entry.DailyNote is null) { return; } - List notifyInfos = new(); + List notifyInfos = new(); CheckNotifySuppressed(entry, notifyInfos); @@ -58,92 +47,95 @@ internal sealed class DailyNoteNotificationOperation return; } - using (IServiceScope scope = serviceProvider.CreateScope()) + string? attribution = SH.ServiceDailyNoteNotifierAttribution; + + Response> rolesResponse = await bindingClient + .GetUserGameRolesOverseaAwareAsync(entry.User) + .ConfigureAwait(false); + + if (rolesResponse.IsOk()) { - DailyNoteOptions options = scope.ServiceProvider.GetRequiredService(); - - string? attribution = SH.ServiceDailyNoteNotifierAttribution; - - Response> rolesResponse = await scope.ServiceProvider - .GetRequiredService() - .GetUserGameRolesOverseaAwareAsync(entry.User) - .ConfigureAwait(false); - - if (rolesResponse.IsOk()) - { - List roles = rolesResponse.Data.List; - attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? ToastAttributionUnknown; - } - - ToastContentBuilder builder = new ToastContentBuilder() - .AddHeader(ToastHeaderIdArgument, SH.ServiceDailyNoteNotifierTitle, ToastHeaderIdArgument) - .AddAttributionText(attribution) - .AddButton(new ToastButton() - .SetContent(SH.ServiceDailyNoteNotifierActionLaunchGameButton) - .AddArgument(Activation.Action, Activation.LaunchGame) - .AddArgument(Activation.Uid, entry.Uid)) - .AddButton(new ToastButtonDismiss(SH.ServiceDailyNoteNotifierActionLaunchGameDismiss)); - - if (options.IsReminderNotification) - { - builder.SetToastScenario(ToastScenario.Reminder); - } - - if (notifyInfos.Count > 2) - { - builder.AddText(SH.ServiceDailyNoteNotifierMultiValueReached); - - // Desktop and Mobile started supporting adaptive toasts in API contract 3 (Anniversary Update) - if (UniversalApiContract.IsPresent(WindowsVersion.Windows10AnniversaryUpdate)) - { - AdaptiveGroup group = new(); - foreach (NotifyInfo info in notifyInfos) - { - AdaptiveSubgroup subgroup = new() - { - HintWeight = 1, - Children = - { - new AdaptiveImage() { Source = info.AdaptiveIcon, HintRemoveMargin = true, }, - new AdaptiveText() { Text = info.AdaptiveHint, HintAlign = AdaptiveTextAlign.Center, }, - new AdaptiveText() { Text = info.Title, HintAlign = AdaptiveTextAlign.Center, HintStyle = AdaptiveTextStyle.CaptionSubtle, }, - }, - }; - - group.Children.Add(subgroup); - } - - builder.AddVisualChild(group); - } - } - else - { - foreach (NotifyInfo info in notifyInfos) - { - builder.AddText(info.Hint); - } - } - - await taskContext.SwitchToMainThreadAsync(); - builder.Show(toast => toast.SuppressPopup = ShouldSuppressPopup(options)); + List roles = rolesResponse.Data.List; + attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? ToastAttributionUnknown; } + + ToastContentBuilder builder = new ToastContentBuilder() + .AddHeader(ToastHeaderIdArgument, SH.ServiceDailyNoteNotifierTitle, ToastHeaderIdArgument) + .AddAttributionText(attribution) + .AddButton(new ToastButton() + .SetContent(SH.ServiceDailyNoteNotifierActionLaunchGameButton) + .AddArgument(Activation.Action, Activation.LaunchGame) + .AddArgument(Activation.Uid, entry.Uid)) + .AddButton(new ToastButtonDismiss(SH.ServiceDailyNoteNotifierActionLaunchGameDismiss)); + + if (options.IsReminderNotification) + { + builder.SetToastScenario(ToastScenario.Reminder); + } + + if (notifyInfos.Count > 2) + { + builder.AddText(SH.ServiceDailyNoteNotifierMultiValueReached); + + // Desktop and Mobile started supporting adaptive toasts in API contract 3 (Anniversary Update) + if (UniversalApiContract.IsPresent(WindowsVersion.Windows10AnniversaryUpdate)) + { + AdaptiveGroup group = new(); + foreach (DailyNoteNotifyInfo info in notifyInfos) + { + AdaptiveSubgroup subgroup = new() + { + HintWeight = 1, + Children = + { + new AdaptiveImage() { Source = info.AdaptiveIcon, HintRemoveMargin = true, }, + new AdaptiveText() { Text = info.AdaptiveHint, HintAlign = AdaptiveTextAlign.Center, }, + new AdaptiveText() { Text = info.Title, HintAlign = AdaptiveTextAlign.Center, HintStyle = AdaptiveTextStyle.CaptionSubtle, }, + }, + }; + + group.Children.Add(subgroup); + } + + builder.AddVisualChild(group); + } + } + else + { + foreach (DailyNoteNotifyInfo info in notifyInfos) + { + builder.AddText(info.Hint); + } + } + + await taskContext.SwitchToMainThreadAsync(); + builder.Show(toast => toast.SuppressPopup = ShouldSuppressPopup(options)); } - private static void CheckNotifySuppressed(DailyNoteEntry entry, List notifyInfos) + private static void CheckNotifySuppressed(DailyNoteEntry entry, List notifyInfos) { - // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast?tabs=uwp#adding-images // Image limitation. - + // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast?tabs=uwp#adding-images // NotifySuppressed judge - if (entry.DailyNote!.CurrentResin >= entry.ResinNotifyThreshold) + ChcekResinNotifySuppressed(entry, notifyInfos); + CheckHomeCoinNotifySuppressed(entry, notifyInfos); + CheckDailyTaskNotifySuppressed(entry, notifyInfos); + CheckTransformerNotifySuppressed(entry, notifyInfos); + CheckExpeditionNotifySuppressed(entry, notifyInfos); + } + + private static void ChcekResinNotifySuppressed(DailyNoteEntry entry, List notifyInfos) + { + ArgumentNullException.ThrowIfNull(entry.DailyNote); + if (entry.DailyNote.CurrentResin >= entry.ResinNotifyThreshold) { if (!entry.ResinNotifySuppressed) { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierResin, - Web.Hoyolab.Images.UIItemIcon210, + Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_210.png"), $"{entry.DailyNote.CurrentResin}", - string.Format(SH.ServiceDailyNoteNotifierResinCurrent, entry.DailyNote.CurrentResin))); + SH.ServiceDailyNoteNotifierResinCurrent.Format(entry.DailyNote.CurrentResin))); entry.ResinNotifySuppressed = true; } } @@ -151,16 +143,20 @@ internal sealed class DailyNoteNotificationOperation { entry.ResinNotifySuppressed = false; } + } + private static void CheckHomeCoinNotifySuppressed(DailyNoteEntry entry, List notifyInfos) + { + ArgumentNullException.ThrowIfNull(entry.DailyNote); if (entry.DailyNote.CurrentHomeCoin >= entry.HomeCoinNotifyThreshold) { if (!entry.HomeCoinNotifySuppressed) { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierHomeCoin, - Web.Hoyolab.Images.UIItemIcon204, + Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_204.png"), $"{entry.DailyNote.CurrentHomeCoin}", - string.Format(SH.ServiceDailyNoteNotifierHomeCoinCurrent, entry.DailyNote.CurrentHomeCoin))); + SH.ServiceDailyNoteNotifierHomeCoinCurrent.Format(entry.DailyNote.CurrentHomeCoin))); entry.HomeCoinNotifySuppressed = true; } } @@ -168,14 +164,17 @@ internal sealed class DailyNoteNotificationOperation { entry.HomeCoinNotifySuppressed = false; } + } - if (entry.DailyTaskNotify && !entry.DailyNote.IsExtraTaskRewardReceived) + private static void CheckDailyTaskNotifySuppressed(DailyNoteEntry entry, List notifyInfos) + { + if (entry is { DailyTaskNotify: true, DailyNote.IsExtraTaskRewardReceived: false }) { if (!entry.DailyTaskNotifySuppressed) { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierDailyTask, - Web.Hoyolab.Images.UIMarkQuestEventsProce, + Web.HutaoEndpoints.StaticFile("Bg", "UI_MarkQuest_Events_Proce.png"), SH.ServiceDailyNoteNotifierDailyTaskHint, entry.DailyNote.ExtraTaskRewardDescription)); entry.DailyTaskNotifySuppressed = true; @@ -185,14 +184,17 @@ internal sealed class DailyNoteNotificationOperation { entry.DailyTaskNotifySuppressed = false; } + } - if (entry.TransformerNotify && entry.DailyNote.Transformer.Obtained && entry.DailyNote.Transformer.RecoveryTime.Reached) + private static void CheckTransformerNotifySuppressed(DailyNoteEntry entry, List notifyInfos) + { + if (entry is { TransformerNotify: true, DailyNote.Transformer: { Obtained: true, RecoveryTime.Reached: true } }) { if (!entry.TransformerNotifySuppressed) { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierTransformer, - Web.Hoyolab.Images.UIItemIcon220021, + Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_220021.png"), SH.ServiceDailyNoteNotifierTransformerAdaptiveHint, SH.ServiceDailyNoteNotifierTransformerHint)); entry.TransformerNotifySuppressed = true; @@ -202,14 +204,18 @@ internal sealed class DailyNoteNotificationOperation { entry.TransformerNotifySuppressed = false; } + } + private static void CheckExpeditionNotifySuppressed(DailyNoteEntry entry, List notifyInfos) + { + ArgumentNullException.ThrowIfNull(entry.DailyNote); if (entry.ExpeditionNotify && entry.DailyNote.Expeditions.All(e => e.Status == ExpeditionStatus.Finished)) { if (!entry.ExpeditionNotifySuppressed) { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierExpedition, - Web.Hoyolab.Images.UIIconInteeExplore1, + Web.HutaoEndpoints.StaticFile("Bg", "UI_Icon_Intee_Explore_1.png"), SH.ServiceDailyNoteNotifierExpeditionAdaptiveHint, SH.ServiceDailyNoteNotifierExpeditionHint)); entry.ExpeditionNotifySuppressed = true; @@ -224,22 +230,6 @@ internal sealed class DailyNoteNotificationOperation private bool ShouldSuppressPopup(DailyNoteOptions options) { // Prevent notify when we are in game && silent mode. - return options.IsSilentWhenPlayingGame && serviceProvider.GetRequiredService().IsGameRunning(); - } - - private readonly struct NotifyInfo - { - public readonly string Title; - public readonly string AdaptiveIcon; - public readonly string AdaptiveHint; - public readonly string Hint; - - public NotifyInfo(string title, string adaptiveIcon, string adaptiveHint, string hint) - { - Title = title; - AdaptiveIcon = adaptiveIcon; - AdaptiveHint = adaptiveHint; - Hint = hint; - } + return options.IsSilentWhenPlayingGame && gameService.IsGameRunning(); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifyInfo.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifyInfo.cs new file mode 100644 index 00000000..d5b334e7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifyInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.DailyNote; + +internal readonly struct DailyNoteNotifyInfo +{ + public readonly string Title; + public readonly string AdaptiveIcon; + public readonly string AdaptiveHint; + public readonly string Hint; + + public DailyNoteNotifyInfo(string title, string adaptiveIcon, string adaptiveHint, string hint) + { + Title = title; + AdaptiveIcon = adaptiveIcon; + AdaptiveHint = adaptiveHint; + Hint = hint; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs index 00441cfb..8e3ef6e9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs @@ -26,6 +26,7 @@ namespace Snap.Hutao.Service.DailyNote; [Injection(InjectAs.Singleton, typeof(IDailyNoteService))] internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient { + private readonly DailyNoteNotificationOperation dailyNoteNotificationOperation; private readonly IServiceProvider serviceProvider; private readonly IDailyNoteDbService dailyNoteDbService; private readonly IUserService userService; @@ -106,7 +107,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote); // database - await new DailyNoteNotificationOperation(serviceProvider, entry).SendAsync().ConfigureAwait(false); + await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false); entry.DailyNote = dailyNote; await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false); } 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 0f44636c..ae20b701 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs @@ -9,6 +9,7 @@ using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Weapon; using Snap.Hutao.Service.Metadata; using Snap.Hutao.ViewModel.GachaLog; +using Snap.Hutao.Web.Hutao; using System.Runtime.InteropServices; namespace Snap.Hutao.Service.GachaLog.Factory; @@ -21,8 +22,8 @@ namespace Snap.Hutao.Service.GachaLog.Factory; [Injection(InjectAs.Scoped, typeof(IGachaStatisticsFactory))] internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory { - private readonly IServiceProvider serviceProvider; private readonly IMetadataService metadataService; + private readonly HomaGachaLogClient homaGachaLogClient; private readonly ITaskContext taskContext; private readonly AppOptions options; @@ -33,32 +34,25 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory List gachaEvents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false); List historyWishBuilders = gachaEvents.SelectList(gachaEvent => new HistoryWishBuilder(gachaEvent, context)); - return CreateCore(serviceProvider, items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible); + return CreateCore(taskContext, homaGachaLogClient, items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible); } private static GachaStatistics CreateCore( - IServiceProvider serviceProvider, + ITaskContext taskContext, + HomaGachaLogClient gachaLogClient, List items, List historyWishBuilders, in GachaLogServiceMetadataContext context, bool isEmptyHistoryWishVisible) { - TypedWishSummaryBuilder standardWishBuilder = new( - serviceProvider, - SH.ServiceGachaLogFactoryPermanentWishName, - TypedWishSummaryBuilder.IsStandardWish, - Web.Hutao.GachaLog.GachaDistributionType.Standard); - TypedWishSummaryBuilder avatarWishBuilder = new( - serviceProvider, - SH.ServiceGachaLogFactoryAvatarWishName, - TypedWishSummaryBuilder.IsAvatarEventWish, - Web.Hutao.GachaLog.GachaDistributionType.AvatarEvent); - TypedWishSummaryBuilder weaponWishBuilder = new( - serviceProvider, - SH.ServiceGachaLogFactoryWeaponWishName, - TypedWishSummaryBuilder.IsWeaponEventWish, - Web.Hutao.GachaLog.GachaDistributionType.WeaponEvent, - 80); + TypedWishSummaryBuilderContext standardContext = TypedWishSummaryBuilderContext.StandardWish(taskContext, gachaLogClient); + TypedWishSummaryBuilder standardWishBuilder = new(standardContext); + + TypedWishSummaryBuilderContext avatarContext = TypedWishSummaryBuilderContext.AvatarEventWish(taskContext, gachaLogClient); + TypedWishSummaryBuilder avatarWishBuilder = new(avatarContext); + + TypedWishSummaryBuilderContext weaponContext = TypedWishSummaryBuilderContext.WeaponEventWish(taskContext, gachaLogClient); + TypedWishSummaryBuilder weaponWishBuilder = new(weaponContext); Dictionary orangeAvatarCounter = new(); Dictionary purpleAvatarCounter = new(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/PullPrediction.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/PullPrediction.cs index 6b878435..dfe63e32 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/PullPrediction.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/PullPrediction.cs @@ -14,38 +14,26 @@ namespace Snap.Hutao.Service.GachaLog.Factory; /// internal sealed class PullPrediction { - private readonly IServiceProvider serviceProvider; - private readonly ITaskContext taskContext; private readonly TypedWishSummary typedWishSummary; - private readonly GachaDistributionType distributionType; + private readonly TypedWishSummaryBuilderContext context; - /// - /// 构造一个新的抽数预计 - /// - /// 服务提供器 - /// 类型化祈愿统计信息 - /// 分布类型 - public PullPrediction(IServiceProvider serviceProvider, TypedWishSummary typedWishSummary, GachaDistributionType distributionType) + public PullPrediction(TypedWishSummary typedWishSummary, in TypedWishSummaryBuilderContext context) { - this.serviceProvider = serviceProvider; - taskContext = serviceProvider.GetRequiredService(); - this.typedWishSummary = typedWishSummary; - this.distributionType = distributionType; + this.context = context; } public async ValueTask PredictAsync(AsyncBarrier barrier) { - await taskContext.SwitchToBackgroundAsync(); - HomaGachaLogClient gachaLogClient = serviceProvider.GetRequiredService(); - Response response = await gachaLogClient.GetGachaDistributionAsync(distributionType).ConfigureAwait(false); + await context.TaskContext.SwitchToBackgroundAsync(); + Response response = await context.GetGachaDistributionAsync().ConfigureAwait(false); if (response.IsOk()) { PredictResult result = PredictCore(response.Data.Distribution, typedWishSummary); await barrier.SignalAndWaitAsync().ConfigureAwait(false); - await taskContext.SwitchToMainThreadAsync(); + await context.TaskContext.SwitchToMainThreadAsync(); typedWishSummary.ProbabilityOfNextPullIsOrange = result.ProbabilityOfNextPullIsOrange; typedWishSummary.ProbabilityOfPredictedPullLeftToOrange = result.ProbabilityOfPredictedPullLeftToOrange; typedWishSummary.PredictedPullLeftToOrange = result.PredictedPullLeftToOrange; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs index 6163ae3c..36d3da52 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs @@ -6,6 +6,7 @@ using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Abstraction; using Snap.Hutao.ViewModel.GachaLog; using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using Snap.Hutao.Web.Hutao; namespace Snap.Hutao.Service.GachaLog.Factory; @@ -30,12 +31,7 @@ internal sealed class TypedWishSummaryBuilder /// public static readonly Func IsWeaponEventWish = type => type is GachaConfigType.WeaponEventWish; - private readonly IServiceProvider serviceProvider; - private readonly string name; - private readonly int guaranteeOrangeThreshold; - private readonly int guaranteePurpleThreshold; - private readonly Func typeEvaluator; - private readonly Web.Hutao.GachaLog.GachaDistributionType distributionType; + private readonly TypedWishSummaryBuilderContext context; private readonly List averageOrangePullTracker = new(); private readonly List averageUpOrangePullTracker = new(); @@ -54,20 +50,9 @@ internal sealed class TypedWishSummaryBuilder private DateTimeOffset fromTimeTracker = DateTimeOffset.MaxValue; private DateTimeOffset toTimeTracker = DateTimeOffset.MinValue; - public TypedWishSummaryBuilder( - IServiceProvider serviceProvider, - string name, - Func typeEvaluator, - Web.Hutao.GachaLog.GachaDistributionType distributionType, - int guaranteeOrangeThreshold = 90, - int guaranteePurpleThreshold = 10) + public TypedWishSummaryBuilder(in TypedWishSummaryBuilderContext context) { - this.serviceProvider = serviceProvider; - this.name = name; - this.typeEvaluator = typeEvaluator; - this.guaranteeOrangeThreshold = guaranteeOrangeThreshold; - this.guaranteePurpleThreshold = guaranteePurpleThreshold; - this.distributionType = distributionType; + this.context = context; } /// @@ -78,7 +63,7 @@ internal sealed class TypedWishSummaryBuilder /// 是否为Up物品 public void Track(GachaItem item, ISummaryItemSource source, bool isUp) { - if (typeEvaluator(item.GachaType)) + if (context.TypeEvaluator(item.GachaType)) { ++lastOrangePullTracker; ++lastPurplePullTracker; @@ -129,13 +114,13 @@ internal sealed class TypedWishSummaryBuilder public TypedWishSummary ToTypedWishSummary(AsyncBarrier barrier) { - summaryItems.CompleteAdding(guaranteeOrangeThreshold); + summaryItems.CompleteAdding(context.GuaranteeOrangeThreshold); double totalCount = totalCountTracker; TypedWishSummary summary = new() { // base - Name = name, + Name = context.Name, From = fromTimeTracker, To = toTimeTracker, TotalCount = totalCountTracker, @@ -144,9 +129,9 @@ internal sealed class TypedWishSummaryBuilder MaxOrangePull = maxOrangePullTracker, MinOrangePull = minOrangePullTracker, LastOrangePull = lastOrangePullTracker, - GuaranteeOrangeThreshold = guaranteeOrangeThreshold, + GuaranteeOrangeThreshold = context.GuaranteeOrangeThreshold, LastPurplePull = lastPurplePullTracker, - GuaranteePurpleThreshold = guaranteePurpleThreshold, + GuaranteePurpleThreshold = context.GuaranteePurpleThreshold, TotalOrangePull = totalOrangePullTracker, TotalPurplePull = totalPurplePullTracker, TotalBluePull = totalBluePullTracker, @@ -158,7 +143,7 @@ internal sealed class TypedWishSummaryBuilder OrangeList = summaryItems, }; - new PullPrediction(serviceProvider, summary, distributionType).PredictAsync(barrier).SafeForget(); + new PullPrediction(summary, context).PredictAsync(barrier).SafeForget(); return summary; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs new file mode 100644 index 00000000..cd778b05 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs @@ -0,0 +1,62 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.GachaLog; +using Snap.Hutao.Web.Response; + +namespace Snap.Hutao.Service.GachaLog.Factory; + +internal readonly struct TypedWishSummaryBuilderContext +{ + public readonly ITaskContext TaskContext; + public readonly HomaGachaLogClient GachaLogClient; + public readonly string Name; + public readonly int GuaranteeOrangeThreshold; + public readonly int GuaranteePurpleThreshold; + public readonly Func TypeEvaluator; + public readonly GachaDistributionType DistributionType; + + private static readonly Func IsStandardWish = type => type is GachaConfigType.StandardWish; + private static readonly Func IsAvatarEventWish = type => type is GachaConfigType.AvatarEventWish or GachaConfigType.AvatarEventWish2; + private static readonly Func IsWeaponEventWish = type => type is GachaConfigType.WeaponEventWish; + + public TypedWishSummaryBuilderContext( + ITaskContext taskContext, + HomaGachaLogClient gachaLogClient, + string name, + int guaranteeOrangeThreshold, + int guaranteePurpleThreshold, + Func typeEvaluator, + GachaDistributionType distributionType) + { + TaskContext = taskContext; + GachaLogClient = gachaLogClient; + Name = name; + GuaranteeOrangeThreshold = guaranteeOrangeThreshold; + GuaranteePurpleThreshold = guaranteePurpleThreshold; + TypeEvaluator = typeEvaluator; + DistributionType = distributionType; + } + + public static TypedWishSummaryBuilderContext StandardWish(ITaskContext taskContext, HomaGachaLogClient gachaLogClient) + { + return new(taskContext, gachaLogClient, SH.ServiceGachaLogFactoryPermanentWishName, 90, 10, IsStandardWish, GachaDistributionType.Standard); + } + + public static TypedWishSummaryBuilderContext AvatarEventWish(ITaskContext taskContext, HomaGachaLogClient gachaLogClient) + { + return new(taskContext, gachaLogClient, SH.ServiceGachaLogFactoryAvatarWishName, 90, 10, IsAvatarEventWish, GachaDistributionType.AvatarEvent); + } + + public static TypedWishSummaryBuilderContext WeaponEventWish(ITaskContext taskContext, HomaGachaLogClient gachaLogClient) + { + return new(taskContext, gachaLogClient, SH.ServiceGachaLogFactoryWeaponWishName, 80, 10, IsWeaponEventWish, GachaDistributionType.WeaponEvent); + } + + public ValueTask> GetGachaDistributionAsync() + { + return GachaLogClient.GetGachaDistributionAsync(DistributionType); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveOperation.cs index f11e08f3..6bf5dc77 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveOperation.cs @@ -11,22 +11,15 @@ namespace Snap.Hutao.Service.GachaLog; /// internal static class GachaArchiveOperation { - public static void GetOrAdd(IServiceProvider serviceProvider, string uid, ObservableCollection archives, [NotNull] out GachaArchive? archive) + public static void GetOrAdd(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, string uid, ObservableCollection archives, [NotNull] out GachaArchive? archive) { archive = archives.SingleOrDefault(a => a.Uid == uid); if (archive is null) { GachaArchive created = GachaArchive.From(uid); - using (IServiceScope scope = serviceProvider.CreateScope()) - { - IGachaLogDbService gachaLogDbService = scope.ServiceProvider.GetRequiredService(); - gachaLogDbService.AddGachaArchive(created); - - ITaskContext taskContext = scope.ServiceProvider.GetRequiredService(); - taskContext.InvokeOnMainThread(() => archives.Add(created)); - } - + gachaLogDbService.AddGachaArchive(created); + taskContext.InvokeOnMainThread(() => archives.Add(created)); archive = created; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaItemSaveContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaItemSaveContext.cs index 0258fb8a..d62231a8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaItemSaveContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaItemSaveContext.cs @@ -29,20 +29,27 @@ internal readonly struct GachaItemSaveContext /// /// 数据集 /// - public readonly DbSet GachaItems; + public readonly IGachaLogDbService GachaLogDbService; - /// - /// 构造一个新的祈愿物品 - /// - /// 待添加物品 - /// 是否懒惰 - /// 结尾 Id - /// 数据集 - public GachaItemSaveContext(List itemsToAdd, bool isLazy, long endId, DbSet gachaItems) + public GachaItemSaveContext(List itemsToAdd, bool isLazy, long endId, IGachaLogDbService gachaLogDbService) { ItemsToAdd = itemsToAdd; IsLazy = isLazy; EndId = endId; - GachaItems = gachaItems; + GachaLogDbService = gachaLogDbService; + } + + public void SaveItems(GachaArchive archive) + { + if (ItemsToAdd.Count > 0) + { + // 全量刷新 + if (!IsLazy) + { + GachaLogDbService.DeleteNewerGachaItemsByArchiveIdAndEndId(archive.InnerId, EndId); + } + + GachaLogDbService.AddGachaItems(ItemsToAdd); + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs index 50892f99..b963b45d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs @@ -13,7 +13,7 @@ using System.Collections.ObjectModel; namespace Snap.Hutao.Service.GachaLog; [ConstructorGenerated] -[Injection(InjectAs.Scoped, typeof(IGachaLogDbService))] +[Injection(InjectAs.Singleton, typeof(IGachaLogDbService))] internal sealed partial class GachaLogDbService : IGachaLogDbService { private readonly IServiceProvider serviceProvider; @@ -30,7 +30,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService } catch (SqliteException ex) { - string message = string.Format(SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage, ex.Message); + string message = SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage.Format(ex.Message); throw ThrowHelper.UserdataCorrupted(message, ex); } } @@ -202,4 +202,26 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService await appDbContext.GachaItems.AddRangeAndSaveAsync(items).ConfigureAwait(false); } } -} + + public void AddGachaItems(List items) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.GachaItems.AddRangeAndSave(items); + } + } + + public void DeleteNewerGachaItemsByArchiveIdAndEndId(Guid archiveId, long endId) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.GachaItems + .AsNoTracking() + .Where(i => i.ArchiveId == archiveId) + .Where(i => i.Id >= endId) + .ExecuteDelete(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs index ad34ec84..2aaaa057 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs @@ -49,19 +49,15 @@ internal struct GachaLogFetchContext /// public GachaConfigType CurrentType; - private readonly IServiceProvider serviceProvider; private readonly GachaLogServiceMetadataContext serviceContext; + private readonly IGachaLogDbService gachaLogDbService; + private readonly ITaskContext taskContext; private readonly bool isLazy; - /// - /// 构造一个新的祈愿记录获取上下文 - /// - /// 服务提供器 - /// 祈愿服务上下文 - /// 是否为懒惰模式 - public GachaLogFetchContext(IServiceProvider serviceProvider, in GachaLogServiceMetadataContext serviceContext, bool isLazy) + public GachaLogFetchContext(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, in GachaLogServiceMetadataContext serviceContext, bool isLazy) { - this.serviceProvider = serviceProvider; + this.gachaLogDbService = gachaLogDbService; + this.taskContext = taskContext; this.serviceContext = serviceContext; this.isLazy = isLazy; } @@ -99,7 +95,7 @@ internal struct GachaLogFetchContext { if (TargetArchive is null) { - GachaArchiveOperation.GetOrAdd(serviceProvider, item.Uid, archives, out TargetArchive); + GachaArchiveOperation.GetOrAdd(gachaLogDbService, taskContext, item.Uid, archives, out TargetArchive); } DbEndId ??= gachaLogDbService.GetNewestGachaItemIdByArchiveIdAndQueryType(TargetArchive.InnerId, CurrentType); @@ -131,7 +127,8 @@ internal struct GachaLogFetchContext /// 物品 public void AddItem(GachaLogItem item) { - ItemsToAdd.Add(GachaItem.From(TargetArchive!.InnerId, item, serviceContext.GetItemId(item))); + ArgumentNullException.ThrowIfNull(TargetArchive); + ItemsToAdd.Add(GachaItem.From(TargetArchive.InnerId, item, serviceContext.GetItemId(item))); FetchStatus.Items.Add(serviceContext.GetItemByNameAndType(item.Name, item.ItemType)); QueryOptions.EndId = item.Id; } @@ -141,16 +138,11 @@ internal struct GachaLogFetchContext /// public readonly void SaveItems() { - using (IServiceScope scope = serviceProvider.CreateScope()) + // While no item is fetched, archive can be null. + if (TargetArchive is not null) { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - - // While no item is fetched, archive can be null. - if (TargetArchive is not null) - { - GachaItemSaveContext saveContext = new(ItemsToAdd, isLazy, QueryOptions.EndId, appDbContext.GachaItems); - TargetArchive.SaveItems(saveContext); - } + GachaItemSaveContext saveContext = new(ItemsToAdd, isLazy, QueryOptions.EndId, gachaLogDbService); + saveContext.SaveItems(TargetArchive); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs index aac86540..cd9cf702 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs @@ -25,9 +25,9 @@ namespace Snap.Hutao.Service.GachaLog; [Injection(InjectAs.Scoped, typeof(IGachaLogHutaoCloudService))] internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudService { + private readonly IMetadataService metadataService; private readonly HomaGachaLogClient homaGachaLogClient; private readonly IGachaLogDbService gachaLogDbService; - private readonly IServiceProvider serviceProvider; /// public ValueTask>> GetGachaEntriesAsync(CancellationToken token = default) @@ -92,7 +92,6 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer Response response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false); if (response.IsOk()) { - IMetadataService metadataService = serviceProvider.GetRequiredService(); if (await metadataService.InitializeAsync().ConfigureAwait(false)) { Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs index 008502a6..3796c577 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs @@ -29,7 +29,6 @@ internal sealed partial class GachaLogService : IGachaLogService private readonly IGachaStatisticsFactory gachaStatisticsFactory; private readonly IUIGFExportService gachaLogExportService; private readonly IUIGFImportService gachaLogImportService; - private readonly IServiceProvider serviceProvider; private readonly IMetadataService metadataService; private readonly ILogger logger; private readonly GachaInfoClient gachaInfoClient; @@ -160,7 +159,7 @@ internal sealed partial class GachaLogService : IGachaLogService private async ValueTask> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress progress, CancellationToken token) { ArgumentNullException.ThrowIfNull(ArchiveCollection); - GachaLogFetchContext fetchContext = new(serviceProvider, context, isLazy); + GachaLogFetchContext fetchContext = new(gachaLogDbService, taskContext, context, isLazy); foreach (GachaConfigType configType in GachaLog.QueryTypes) { @@ -172,7 +171,7 @@ internal sealed partial class GachaLogService : IGachaLogService .GetGachaLogPageAsync(fetchContext.QueryOptions, token) .ConfigureAwait(false); - if (response.TryGetData(out GachaLogPage? page, serviceProvider)) + if (response.TryGetData(out GachaLogPage? page)) { List items = page.List; fetchContext.ResetForProcessingPage(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceMetadataContext.cs index e56e7cd4..0a9edc5a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceMetadataContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceMetadataContext.cs @@ -121,12 +121,12 @@ internal readonly struct GachaLogServiceMetadataContext { if (item.ItemType == SH.ModelInterchangeUIGFItemTypeAvatar) { - return NameAvatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0; + return NameAvatarMap.GetValueOrDefault(item.Name)?.Id ?? 0; } if (item.ItemType == SH.ModelInterchangeUIGFItemTypeWeapon) { - return NameWeaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0; + return NameWeaponMap.GetValueOrDefault(item.Name)?.Id ?? 0; } return 0U; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogDbService.cs index 61790b3b..4940c1d6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogDbService.cs @@ -13,10 +13,14 @@ internal interface IGachaLogDbService ValueTask AddGachaArchiveAsync(GachaArchive archive); + void AddGachaItems(List items); + ValueTask AddGachaItemsAsync(List items); ValueTask DeleteGachaArchiveByIdAsync(Guid archiveId); + void DeleteNewerGachaItemsByArchiveIdAndEndId(Guid archiveId, long endId); + ValueTask GetGachaArchiveByUidAsync(string uid, CancellationToken token); ObservableCollection GetGachaArchiveCollection(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs index 95c6347c..231800af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs @@ -31,7 +31,7 @@ internal readonly struct GachaLogQuery public GachaLogQuery(string query) { Query = query; - IsOversea = query.Contains("hoyoverse.com"); + IsOversea = query.Contains("hoyoverse.com", StringComparison.OrdinalIgnoreCase); Message = string.Empty; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs index 32298d6d..f0b791ff 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Service.Metadata; using Snap.Hutao.View.Dialog; using Snap.Hutao.Web.Request.QueryString; @@ -15,34 +16,29 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider; [Injection(InjectAs.Transient)] internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryProvider { - private readonly IServiceProvider serviceProvider; + private readonly IContentDialogFactory contentDialogFactory; private readonly MetadataOptions metadataOptions; - private readonly ITaskContext taskContext; /// public async ValueTask> GetQueryAsync() { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - GachaLogUrlDialog dialog = serviceProvider.CreateInstance(); + GachaLogUrlDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); (bool isOk, string queryString) = await dialog.GetInputUrlAsync().ConfigureAwait(false); if (isOk) { QueryString query = QueryString.Parse(queryString); - string queryLanguageCode = query["lang"]; if (query["auth_appid"] == "webview_gacha") { + string queryLanguageCode = query["lang"]; if (metadataOptions.IsCurrentLocale(queryLanguageCode)) { return new(true, new(queryString)); } else { - string message = string.Format( - SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale, - queryLanguageCode, - metadataOptions.LanguageCode); + string message = SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale + .Format(queryLanguageCode, metadataOptions.LanguageCode); return new(false, message); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryProviderFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryProviderFactory.cs index e26a43b4..55752039 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryProviderFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryProviderFactory.cs @@ -7,6 +7,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider; [Injection(InjectAs.Transient, typeof(IGachaLogQueryProviderFactory))] internal sealed partial class GachaLogQueryProviderFactory : IGachaLogQueryProviderFactory { + [SuppressMessage("", "SH301")] private readonly IServiceProvider serviceProvider; public IGachaLogQueryProvider Create(RefreshOption option) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs index 9953a746..6a7e74fd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs @@ -35,7 +35,9 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv ? GameConstants.GenshinImpactData : GameConstants.YuanShenData; - DirectoryInfo webCacheFolder = new(Path.Combine(Path.GetDirectoryName(path)!, dataFolder, "webCaches")); + string? directory = Path.GetDirectoryName(path); + ArgumentNullException.ThrowIfNull(directory); + DirectoryInfo webCacheFolder = new(Path.Combine(directory, dataFolder, "webCaches")); Regex versionRegex = VersionRegex(); DirectoryInfo? lastestVersionCacheFolder = webCacheFolder .EnumerateDirectories() @@ -51,53 +53,45 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv { (bool isOk, string path) = await gameService.GetGamePathAsync().ConfigureAwait(false); - if (isOk && (!string.IsNullOrEmpty(path))) - { - string cacheFile = GetCacheFile(path); - - using (TempFile? tempFile = TempFile.CopyFrom(cacheFile)) - { - if (tempFile.TryGetValue(out TempFile file)) - { - using (FileStream fileStream = new(file.Path, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - using (MemoryStream memoryStream = new()) - { - await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); - string? result = Match(memoryStream, cacheFile.Contains(GameConstants.GenshinImpactData)); - - if (!string.IsNullOrEmpty(result)) - { - QueryString query = QueryString.Parse(result.TrimEnd("#/log")); - string queryLanguageCode = query["lang"]; - if (metadataOptions.IsCurrentLocale(queryLanguageCode)) - { - return new(true, new(result)); - } - else - { - string message = string.Format( - SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale, - queryLanguageCode, - metadataOptions.LanguageCode); - return new(false, message); - } - } - else - { - return new(false, SH.ServiceGachaLogUrlProviderCacheUrlNotFound); - } - } - } - } - - return new(false, string.Format(Regex.Unescape(SH.ServiceGachaLogUrlProviderCachePathNotFound), cacheFile)); - } - } - else + if (!isOk || string.IsNullOrEmpty(path)) { return new(false, SH.ServiceGachaLogUrlProviderCachePathInvalid); } + + string cacheFile = GetCacheFile(path); + using (TempFile? tempFile = TempFile.CopyFrom(cacheFile)) + { + if (!tempFile.TryGetValue(out TempFile file)) + { + return new(false, Regex.Unescape(SH.ServiceGachaLogUrlProviderCachePathNotFound).Format(cacheFile)); + } + + using (FileStream fileStream = new(file.Path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (MemoryStream memoryStream = new()) + { + await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); + string? result = Match(memoryStream, cacheFile.Contains(GameConstants.GenshinImpactData, StringComparison.Ordinal)); + + if (string.IsNullOrEmpty(result)) + { + return new(false, SH.ServiceGachaLogUrlProviderCacheUrlNotFound); + } + + QueryString query = QueryString.Parse(result.TrimEnd("#/log")); + string queryLanguageCode = query["lang"]; + + if (metadataOptions.IsCurrentLocale(queryLanguageCode)) + { + return new(true, new(result)); + } + + string message = SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale + .Format(queryLanguageCode, metadataOptions.LanguageCode); + return new(false, message); + } + } + } } private static string? Match(MemoryStream stream, bool isOversea) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFExportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFExportService.cs index e2463449..21d420b6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFExportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFExportService.cs @@ -1,9 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.InterChange.GachaLog; +using Snap.Hutao.Service.Metadata; namespace Snap.Hutao.Service.GachaLog; @@ -14,8 +16,9 @@ namespace Snap.Hutao.Service.GachaLog; [Injection(InjectAs.Scoped, typeof(IUIGFExportService))] internal sealed partial class UIGFExportService : IUIGFExportService { - private readonly IServiceProvider serviceProvider; private readonly IGachaLogDbService gachaLogDbService; + private readonly RuntimeOptions runtimeOptions; + private readonly MetadataOptions metadataOptions; private readonly ITaskContext taskContext; /// @@ -29,7 +32,7 @@ internal sealed partial class UIGFExportService : IUIGFExportService UIGF uigf = new() { - Info = UIGFInfo.From(serviceProvider, archive.Uid), + Info = UIGFInfo.From(runtimeOptions, metadataOptions, archive.Uid), List = list, }; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs index 7b9e9471..76a7c99d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs @@ -18,7 +18,6 @@ internal sealed partial class UIGFImportService : IUIGFImportService { private readonly ILogger logger; private readonly MetadataOptions metadataOptions; - private readonly IServiceProvider serviceProvider; private readonly IGachaLogDbService gachaLogDbService; private readonly ITaskContext taskContext; @@ -29,11 +28,11 @@ internal sealed partial class UIGFImportService : IUIGFImportService if (!metadataOptions.IsCurrentLocale(uigf.Info.Language)) { - string message = string.Format(SH.ServiceGachaUIGFImportLanguageNotMatch, uigf.Info.Language, metadataOptions.LanguageCode); + string message = SH.ServiceGachaUIGFImportLanguageNotMatch.Format(uigf.Info.Language, metadataOptions.LanguageCode); ThrowHelper.InvalidOperation(message, null); } - GachaArchiveOperation.GetOrAdd(serviceProvider, uigf.Info.Uid, archives, out GachaArchive? archive); + GachaArchiveOperation.GetOrAdd(gachaLogDbService, taskContext, uigf.Info.Uid, archives, out GachaArchive? archive); Guid archiveId = archive.InnerId; long trimId = gachaLogDbService.GetOldestGachaItemIdByArchiveId(archiveId); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileOperationException.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileOperationException.cs index bdf7c5ed..59d3a000 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileOperationException.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileOperationException.cs @@ -15,7 +15,7 @@ internal sealed class GameFileOperationException : Exception /// 消息 /// 内部错误 public GameFileOperationException(string message, Exception innerException) - : base(string.Format(SH.ServiceGameFileOperationExceptionMessage, message), innerException) + : base(SH.ServiceGameFileOperationExceptionMessage.Format(message), innerException) { } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs index 6565294a..2ce14a2f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs @@ -82,7 +82,7 @@ internal sealed partial class GameService : IGameService } else { - return new(false, null!); + return new(false, default!); } } @@ -112,9 +112,11 @@ internal sealed partial class GameService : IGameService public bool SetChannelOptions(LaunchScheme scheme) { string gamePath = appOptions.GamePath; - string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFileName); + string? directory = Path.GetDirectoryName(gamePath); + ArgumentException.ThrowIfNullOrEmpty(directory); + string configPath = Path.Combine(directory, ConfigFileName); - List elements = null!; + List elements = default!; try { using (FileStream readStream = File.OpenRead(configPath)) @@ -124,11 +126,11 @@ internal sealed partial class GameService : IGameService } catch (FileNotFoundException ex) { - ThrowHelper.GameFileOperation(string.Format(SH.ServiceGameSetMultiChannelConfigFileNotFound, configPath), ex); + ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); } catch (DirectoryNotFoundException ex) { - ThrowHelper.GameFileOperation(string.Format(SH.ServiceGameSetMultiChannelConfigFileNotFound, configPath), ex); + ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); } catch (UnauthorizedAccessException ex) { @@ -168,7 +170,8 @@ internal sealed partial class GameService : IGameService public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) { string gamePath = appOptions.GamePath; - string gameFolder = Path.GetDirectoryName(gamePath)!; + string? gameFolder = Path.GetDirectoryName(gamePath); + ArgumentException.ThrowIfNullOrEmpty(gameFolder); string gameFileName = Path.GetFileName(gamePath); progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); @@ -241,39 +244,36 @@ internal sealed partial class GameService : IGameService } string gamePath = appOptions.GamePath; - if (string.IsNullOrWhiteSpace(gamePath)) + ArgumentNullException.ThrowIfNullOrEmpty(gamePath); + + using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath)) { - // TODO: throw exception - return; - } - - Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath); - - try - { - bool isFirstInstance = Interlocked.Increment(ref runningGamesCounter) == 1; - - game.Start(); - - bool isAdvancedOptionsAllowed = runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled; - if (isAdvancedOptionsAllowed && launchOptions.MultipleInstances && !isFirstInstance) + try { - ProcessInterop.DisableProtection(game, gamePath); - } + bool isFirstInstance = Interlocked.Increment(ref runningGamesCounter) == 1; - if (isAdvancedOptionsAllowed && launchOptions.UnlockFps) - { - await ProcessInterop.UnlockFpsAsync(serviceProvider, game, default).ConfigureAwait(false); + game.Start(); + + bool isAdvancedOptionsAllowed = runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled; + if (isAdvancedOptionsAllowed && launchOptions.MultipleInstances && !isFirstInstance) + { + ProcessInterop.DisableProtection(game, gamePath); + } + + if (isAdvancedOptionsAllowed && launchOptions.UnlockFps) + { + await ProcessInterop.UnlockFpsAsync(serviceProvider, game, default).ConfigureAwait(false); + } + else + { + await game.WaitForExitAsync().ConfigureAwait(false); + } } - else + finally { - await game.WaitForExitAsync().ConfigureAwait(false); + Interlocked.Decrement(ref runningGamesCounter); } } - finally - { - Interlocked.Decrement(ref runningGamesCounter); - } } /// @@ -373,7 +373,8 @@ internal sealed partial class GameService : IGameService public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) { await taskContext.SwitchToMainThreadAsync(); - gameAccounts!.Remove(gameAccount); + ArgumentNullException.ThrowIfNull(gameAccounts); + gameAccounts.Remove(gameAccount); await taskContext.SwitchToBackgroundAsync(); await gameDbService.DeleteGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index baf237b3..b2813d54 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -5,6 +5,7 @@ using Microsoft.UI.Windowing; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; +using System.Globalization; using Windows.Graphics; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Gdi; @@ -140,12 +141,12 @@ internal sealed class LaunchOptions : DbStoreOptions [AllowNull] public NameValue Monitor { - get => GetOption(ref monitor, SettingEntry.LaunchMonitor, index => Monitors[int.Parse(index) - 1], Monitors[0]); + get => GetOption(ref monitor, SettingEntry.LaunchMonitor, index => Monitors[int.Parse(index, CultureInfo.InvariantCulture) - 1], Monitors[0]); set { if (value is not null) { - SetOption(ref monitor, SettingEntry.LaunchMonitor, value, selected => selected.Value.ToString()); + SetOption(ref monitor, SettingEntry.LaunchMonitor, value, selected => selected.Value.ToString(CultureInfo.InvariantCulture)); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactory.cs index bcd7ef91..69872349 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactory.cs @@ -7,6 +7,7 @@ namespace Snap.Hutao.Service.Game.Locator; [Injection(InjectAs.Transient, typeof(IGameLocatorFactory))] internal sealed partial class GameLocatorFactory : IGameLocatorFactory { + [SuppressMessage("", "SH301")] private readonly IServiceProvider serviceProvider; public IGameLocator Create(GameLocationSource source) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs index d685bee8..7f281662 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs @@ -39,6 +39,6 @@ internal sealed partial class ManualGameLocator : IGameLocator } } - return new(false, null!); + return new(false, default!); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs index e34cd8e1..3ec14227 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs @@ -32,7 +32,8 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator else { string? path = Path.GetDirectoryName(result.Value); - string configPath = Path.Combine(path!, GameConstants.ConfigFileName); + ArgumentException.ThrowIfNullOrEmpty(path); + string configPath = Path.Combine(path, GameConstants.ConfigFileName); string? escapedPath; using (FileStream stream = File.OpenRead(configPath)) { @@ -64,12 +65,12 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator } else { - return new(false, null!); + return new(false, default!); } } else { - return new(false, null!); + return new(false, default!); } } } @@ -80,10 +81,10 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator // 不包含中文 // Some one's folder might begin with 'u' - if (!hex4Result.Contains(@"\u")) + if (!hex4Result.Contains(@"\u", StringComparison.Ordinal)) { // fix path with \ - hex4Result = hex4Result.Replace(@"\", @"\\"); + hex4Result = hex4Result.Replace(@"\", @"\\", StringComparison.Ordinal); } return Regex.Unescape(hex4Result); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs index 29dfbf0f..7321a6f5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -116,7 +116,9 @@ internal sealed partial class PackageConverter if (entry.Length != 0) { string targetPath = Path.Combine(gameFolder, entry.FullName); - Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); + string? directory = Path.GetDirectoryName(targetPath); + ArgumentException.ThrowIfNullOrEmpty(directory); + Directory.CreateDirectory(directory); entry.ExtractToFile(targetPath, true); } } @@ -180,7 +182,8 @@ internal sealed partial class PackageConverter Regex dataFolderRegex = DataFolderRegex(); while (await reader.ReadLineAsync().ConfigureAwait(false) is { } row && !string.IsNullOrEmpty(row)) { - VersionItem item = JsonSerializer.Deserialize(row, options)!; + VersionItem? item = JsonSerializer.Deserialize(row, options); + ArgumentNullException.ThrowIfNull(item); item.RelativePath = dataFolderRegex.Replace(item.RelativePath, "{0}"); results.Add(item.RelativePath, item); } @@ -231,7 +234,7 @@ internal sealed partial class PackageConverter private async ValueTask SkipOrDownloadAsync(ItemOperationInfo info, PackageConvertContext context, IProgress progress) { // 还原正确的远程地址 - string remoteName = string.Format(info.Remote.RelativePath, context.ToDataFolderName); + string remoteName = info.Remote.RelativePath.Format(context.ToDataFolderName); string cacheFile = context.GetServerCacheTargetFilePath(remoteName); if (File.Exists(cacheFile)) @@ -276,7 +279,7 @@ internal sealed partial class PackageConverter { // System.IO.IOException: The response ended prematurely. // System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream. - ThrowHelper.PackageConvert(string.Format(SH.ServiceGamePackageRequestScatteredFileFailed, remoteName), ex); + ThrowHelper.PackageConvert(SH.ServiceGamePackageRequestScatteredFileFailed.Format(remoteName), ex); } } } @@ -312,17 +315,21 @@ internal sealed partial class PackageConverter if (backup) { - string localFileName = string.Format(info.Local.RelativePath, context.FromDataFolder); + string localFileName = info.Local.RelativePath.Format(context.FromDataFolder); string localFilePath = context.GetGameFolderFilePath(localFileName); - Directory.CreateDirectory(Path.GetDirectoryName(localFilePath)!); + string? directory = Path.GetDirectoryName(localFilePath); + ArgumentException.ThrowIfNullOrEmpty(directory); + Directory.CreateDirectory(directory); File.Move(localFilePath, context.GetServerCacheBackupFilePath(localFileName), true); } if (target) { - string targetFileName = string.Format(info.Remote.RelativePath, context.ToDataFolder); + string targetFileName = info.Remote.RelativePath.Format(context.ToDataFolder); string targetFilePath = context.GetGameFolderFilePath(targetFileName); - Directory.CreateDirectory(Path.GetDirectoryName(targetFilePath)!); + string? directory = Path.GetDirectoryName(targetFilePath); + ArgumentException.ThrowIfNullOrEmpty(directory); + Directory.CreateDirectory(directory); File.Move(context.GetServerCacheTargetFilePath(targetFileName), targetFilePath, true); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs index f4ec80eb..f4325979 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs @@ -88,7 +88,8 @@ internal static class RegistryInterop private static string GetPowerShellLocation() { - string paths = Environment.GetEnvironmentVariable("Path")!; + string? paths = Environment.GetEnvironmentVariable("Path"); + ArgumentException.ThrowIfNullOrEmpty(paths); foreach (StringSegment path in new StringTokenizer(paths, ';'.ToArray())) { @@ -101,6 +102,6 @@ internal static class RegistryInterop } } - throw ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, null!); + throw ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs index f172b865..47360271 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs @@ -184,7 +184,7 @@ internal sealed partial class HutaoCache : IHutaoCache AvatarAppearanceRanks = avatarAppearanceRanksRaw.SortByDescending(r => r.Floor).SelectList(rank => new AvatarRankView { - Floor = string.Format(SH.ModelBindingHutaoComplexRankFloor, rank.Floor), + Floor = SH.ModelBindingHutaoComplexRankFloor.Format(rank.Floor), Avatars = rank.Ranks.SortByDescending(r => r.Rate).SelectList(rank => new AvatarView(idAvatarMap[rank.Item], rank.Rate)), }); } @@ -201,7 +201,7 @@ internal sealed partial class HutaoCache : IHutaoCache AvatarUsageRanks = avatarUsageRanksRaw.SortByDescending(r => r.Floor).SelectList(rank => new AvatarRankView { - Floor = string.Format(SH.ModelBindingHutaoComplexRankFloor, rank.Floor), + Floor = SH.ModelBindingHutaoComplexRankFloor.Format(rank.Floor), Avatars = rank.Ranks.SortByDescending(r => r.Rate).SelectList(rank => new AvatarView(idAvatarMap[rank.Item], rank.Rate)), }); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs index ecd589be..5f5c6814 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs @@ -19,9 +19,10 @@ namespace Snap.Hutao.Service.Hutao; [Injection(InjectAs.Scoped, typeof(IHutaoService))] internal sealed partial class HutaoService : IHutaoService { - private static readonly TimeSpan CacheExpireTime = TimeSpan.FromHours(4); + private readonly TimeSpan cacheExpireTime = TimeSpan.FromHours(4); + + private readonly IObjectCacheDbService objectCacheDbService; private readonly HomaSpiralAbyssClient homaClient; - private readonly IServiceProvider serviceProvider; private readonly JsonSerializerOptions options; private readonly IMemoryCache memoryCache; @@ -67,59 +68,30 @@ internal sealed partial class HutaoService : IHutaoService return FromCacheOrWebAsync(nameof(TeamAppearance), homaClient.GetTeamCombinationsAsync); } - private async ValueTask FromCacheOrWebAsync(string typeName, Func>> taskFunc) - where T : new() + private async ValueTask FromCacheOrWebAsync(string typeName, Func>> taskFunc) + where T : class, new() { string key = $"{nameof(HutaoService)}.Cache.{typeName}"; if (memoryCache.TryGetValue(key, out object? cache)) { - return (T)cache!; + T? t = cache as T; + ArgumentNullException.ThrowIfNull(t); + return t; } - using (IServiceScope scope = serviceProvider.CreateScope()) + if (await objectCacheDbService.GetObjectOrDefaultAsync(key).ConfigureAwait(false) is { } value) { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - - if (appDbContext.ObjectCache.SingleOrDefault(e => e.Key == key) is { } entry) - { - if (entry.IsExpired) - { - await appDbContext.ObjectCache.RemoveAndSaveAsync(entry).ConfigureAwait(false); - } - else - { - T value = JsonSerializer.Deserialize(entry.Value!, options)!; - return memoryCache.Set(key, value, TimeSpan.FromMinutes(30)); - } - } + return memoryCache.Set(key, value, cacheExpireTime); } Response webResponse = await taskFunc(default).ConfigureAwait(false); T? data = webResponse.Data; - try + if (data is not null) { - if (data is not null) - { - using (IServiceScope scope = serviceProvider.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - - await appDbContext.ObjectCache.AddAndSaveAsync(new() - { - Key = key, - ExpireTime = DateTimeOffset.Now.Add(CacheExpireTime), - Value = JsonSerializer.Serialize(data, options), - }).ConfigureAwait(false); - } - } - } - catch (Microsoft.EntityFrameworkCore.DbUpdateException) - { - // DbUpdateException: An error occurred while saving the entity changes. - // TODO: Not ignore it. + await objectCacheDbService.AddObjectCacheAsync(key, cacheExpireTime, data).ConfigureAwait(false); } - return memoryCache.Set(key, data ?? new(), CacheExpireTime); + return memoryCache.Set(key, data ?? new(), cacheExpireTime); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs index 2dc82229..e6e510aa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs @@ -86,7 +86,7 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions DateTimeOffset.Now; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/IObjectCacheDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/IObjectCacheDbService.cs new file mode 100644 index 00000000..fafaf6f6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/IObjectCacheDbService.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Hutao; + +internal interface IObjectCacheDbService +{ + ValueTask AddObjectCacheAsync(string key, TimeSpan expire, T data) + where T : class; + + ValueTask GetObjectOrDefaultAsync(string key) + where T : class; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/ObjectCacheDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/ObjectCacheDbService.cs new file mode 100644 index 00000000..cb9e8631 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/ObjectCacheDbService.cs @@ -0,0 +1,68 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Model; +using Snap.Hutao.Web.Response; + +namespace Snap.Hutao.Service.Hutao; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IObjectCacheDbService))] +internal sealed partial class ObjectCacheDbService : IObjectCacheDbService +{ + private readonly IServiceProvider serviceProvider; + private readonly JsonSerializerOptions jsonSerializerOptions; + + public async ValueTask AddObjectCacheAsync(string key, TimeSpan expire, T data) + where T : class + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + await appDbContext.ObjectCache.AddAndSaveAsync(new() + { + Key = key, + ExpireTime = DateTimeOffset.Now.Add(expire), + Value = JsonSerializer.Serialize(data, jsonSerializerOptions), + }).ConfigureAwait(false); + } + } + + public async ValueTask GetObjectOrDefaultAsync(string key) + where T : class + { + try + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + if (await appDbContext.ObjectCache.SingleOrDefaultAsync(e => e.Key == key).ConfigureAwait(false) is { } entry) + { + if (!entry.IsExpired) + { + ArgumentNullException.ThrowIfNull(entry.Value); + T? value = JsonSerializer.Deserialize(entry.Value, jsonSerializerOptions); + return value; + } + + await appDbContext.ObjectCache.RemoveAndSaveAsync(entry).ConfigureAwait(false); + } + } + } + catch (DbUpdateException ex) + { + ThrowHelper.DatabaseCorrupted($"无法存储 Key:{key} 对应的值到数据库缓存", ex); + } + + return default!; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Constants.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Constants.cs index 662ddc54..482bdc56 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Constants.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Constants.cs @@ -8,6 +8,7 @@ namespace Snap.Hutao.Service.Metadata; /// internal partial class MetadataService { +#pragma warning disable CA1823 private const string FileNameAchievement = "Achievement"; private const string FileNameAchievementGoal = "AchievementGoal"; private const string FileNameAvatar = "Avatar"; @@ -30,4 +31,5 @@ internal partial class MetadataService private const string FileNameWeapon = "Weapon"; private const string FileNameWeaponCurve = "WeaponCurve"; private const string FileNameWeaponPromote = "WeaponPromote"; +#pragma warning restore } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs index 7af3a781..b2218f00 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs @@ -132,7 +132,7 @@ internal sealed class NavigationService : INavigationService, INavigationInitial case NavigationResult.AlreadyNavigatedTo: { - if (frame!.Content is ScopedPage scopedPage) + if (frame is { Content: ScopedPage scopedPage }) { await scopedPage.NotifyRecipientAsync((INavigationData)data).ConfigureAwait(false); } @@ -158,11 +158,9 @@ internal sealed class NavigationService : INavigationService, INavigationInitial { taskContext.InvokeOnMainThread(() => { - bool canGoBack = frame?.CanGoBack ?? false; - - if (canGoBack) + if (frame is { CanGoBack: true }) { - frame!.GoBack(); + frame.GoBack(); SyncSelectedNavigationViewItemWith(frame.Content.GetType()); } }); @@ -234,6 +232,7 @@ internal sealed class NavigationService : INavigationService, INavigationInitial private void OnPaneStateChanged(NavigationView sender, object args) { - LocalSetting.Set(SettingKeys.IsNavPaneOpen, NavigationView!.IsPaneOpen); + ArgumentNullException.ThrowIfNull(NavigationView); + LocalSetting.Set(SettingKeys.IsNavPaneOpen, NavigationView.IsPaneOpen); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs index a504fb62..1f69dae3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs @@ -125,7 +125,8 @@ internal sealed class InfoBarService : IInfoBarService }; infoBar.Closed += infobarClosedEventHandler; - collection!.Add(infoBar); + ArgumentNullException.ThrowIfNull(collection); + collection.Add(infoBar); if (delay > 0) { @@ -137,7 +138,8 @@ internal sealed class InfoBarService : IInfoBarService private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args) { - taskContext.InvokeOnMainThread(() => collection!.Remove(sender)); + ArgumentNullException.ThrowIfNull(collection); + taskContext.InvokeOnMainThread(() => collection.Remove(sender)); sender.Closed -= infobarClosedEventHandler; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordDbService.cs index 64880fc8..7e5833e2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordDbService.cs @@ -10,7 +10,7 @@ using System.Collections.ObjectModel; namespace Snap.Hutao.Service.SpiralAbyss; [ConstructorGenerated] -[Injection(InjectAs.Scoped, typeof(ISpiralAbyssRecordDbService))] +[Injection(InjectAs.Singleton, typeof(ISpiralAbyssRecordDbService))] internal sealed partial class SpiralAbyssRecordDbService : ISpiralAbyssRecordDbService { private readonly IServiceProvider serviceProvider; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs index 4015061d..ba71507f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs @@ -18,8 +18,8 @@ namespace Snap.Hutao.Service.SpiralAbyss; [Injection(InjectAs.Scoped, typeof(ISpiralAbyssRecordService))] internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordService { - private readonly IServiceProvider serviceProvider; private readonly ITaskContext taskContext; + private readonly IOverseaSupportFactory gameRecordClientFactory; private readonly ISpiralAbyssRecordDbService spiralAbyssRecordDbService; private string? uid; @@ -50,8 +50,7 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi private async ValueTask RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule) { - Response response = await serviceProvider - .GetRequiredService>() + Response response = await gameRecordClientFactory .Create(userAndUid.User.IsOversea) .GetSpiralAbyssAsync(userAndUid, schedule) .ConfigureAwait(false); @@ -60,7 +59,8 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi { Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data; - if (spiralAbysses!.SingleOrDefault(s => s.ScheduleId == webSpiralAbyss.ScheduleId) is { } existEntry) + ArgumentNullException.ThrowIfNull(spiralAbysses); + if (spiralAbysses.SingleOrDefault(s => s.ScheduleId == webSpiralAbyss.ScheduleId) is { } existEntry) { await taskContext.SwitchToMainThreadAsync(); existEntry.UpdateSpiralAbyss(webSpiralAbyss); @@ -73,7 +73,7 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi SpiralAbyssEntry newEntry = SpiralAbyssEntry.From(userAndUid.Uid.Value, webSpiralAbyss); await taskContext.SwitchToMainThreadAsync(); - spiralAbysses!.Insert(0, newEntry); + spiralAbysses.Insert(0, newEntry); await taskContext.SwitchToBackgroundAsync(); await spiralAbyssRecordDbService.AddSpiralAbyssEntryAsync(newEntry).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index 4c880bad..d2b81738 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -47,7 +47,8 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe { // Sync cache await taskContext.SwitchToMainThreadAsync(); - userCollection!.Remove(user); + ArgumentNullException.ThrowIfNull(userCollection); + userCollection.Remove(user); userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid); // Sync database @@ -132,7 +133,8 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe } // 检查 mid 对应用户是否存在 - if (TryGetUser(userCollection!, mid, out BindingUser? user)) + ArgumentNullException.ThrowIfNull(userCollection); + if (TryGetUser(userCollection, mid, out BindingUser? user)) { if (cookie.TryGetSToken(isOversea, out Cookie? stoken)) { @@ -172,7 +174,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe user.CookieToken ??= new(); // Sync ui and database - user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken!; + user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken; await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); return true; @@ -216,7 +218,8 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe // Sync database await taskContext.SwitchToBackgroundAsync(); await userDbService.AddUserAsync(newUser.Entity).ConfigureAwait(false); - return new(UserOptionResult.Added, newUser.UserInfo!.Uid); + ArgumentNullException.ThrowIfNull(newUser.UserInfo); + return new(UserOptionResult.Added, newUser.UserInfo.Uid); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 2dea564b..c644ca09 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -288,7 +288,6 @@ - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/BottomTextSmallControl.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Control/BottomTextSmallControl.xaml.cs index 8e324dc8..2d2e8718 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/BottomTextSmallControl.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/BottomTextSmallControl.xaml.cs @@ -16,6 +16,6 @@ internal sealed partial class BottomTextSmallControl : UserControl { public BottomTextSmallControl() { - this.InitializeComponent(); + InitializeComponent(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs index 91a12546..55647e7c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs @@ -24,6 +24,7 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog /// /// 服务提供器 /// 选项 + [SuppressMessage("", "SH002")] public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, CalculableOptions options) { InitializeComponent(); @@ -40,37 +41,35 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog /// 异步获取提升差异 /// /// 提升差异 - public async Task> GetPromotionDeltaAsync() + public async ValueTask> GetPromotionDeltaAsync() { await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); - if (result == ContentDialogResult.Primary) - { - AvatarPromotionDelta delta = new() - { - AvatarId = Avatar?.AvatarId ?? 0, - AvatarLevelCurrent = Avatar?.LevelCurrent ?? 0, - AvatarLevelTarget = Avatar?.LevelTarget ?? 0, - SkillList = Avatar?.Skills.SelectList(s => new PromotionDelta() - { - Id = s.GroupId, - LevelCurrent = s.LevelCurrent, - LevelTarget = s.LevelTarget, - }), - Weapon = Weapon is null ? null : new PromotionDelta() - { - Id = Weapon.WeaponId, - LevelCurrent = Weapon.LevelCurrent, - LevelTarget = Weapon.LevelTarget, - }, - }; - - return new(true, delta); - } - else + if (result != ContentDialogResult.Primary) { return new(false, default!); } + + AvatarPromotionDelta delta = new() + { + AvatarId = Avatar?.AvatarId ?? 0, + AvatarLevelCurrent = Avatar?.LevelCurrent ?? 0, + AvatarLevelTarget = Avatar?.LevelTarget ?? 0, + SkillList = Avatar?.Skills.SelectList(s => new PromotionDelta() + { + Id = s.GroupId, + LevelCurrent = s.LevelCurrent, + LevelTarget = s.LevelTarget, + }), + Weapon = Weapon is null ? null : new PromotionDelta() + { + Id = Weapon.WeaponId, + LevelCurrent = Weapon.LevelCurrent, + LevelTarget = Weapon.LevelTarget, + }, + }; + + return new(true, delta); } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs index f6a6f619..ca82e98e 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs @@ -35,7 +35,7 @@ internal sealed partial class GachaLogImportDialog : ContentDialog /// 异步获取导入选项 /// /// 是否导入 - public async Task GetShouldImportAsync() + public async ValueTask GetShouldImportAsync() { await taskContext.SwitchToMainThreadAsync(); return await ShowAsync() == ContentDialogResult.Primary; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs index 5117b29e..e5ff858b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs @@ -38,7 +38,7 @@ internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog // TODO: test new binding approach GachaItemsPresenter.Header = state.AuthKeyTimeout ? SH.ViewDialogGachaLogRefreshProgressAuthkeyTimeout - : string.Format(SH.ViewDialogGachaLogRefreshProgressDescription, state.ConfigType.GetLocalizedDescription()); + : SH.ViewDialogGachaLogRefreshProgressDescription.Format(state.ConfigType.GetLocalizedDescription()); // Binding not working here. GachaItemsPresenter.Items.Clear(); diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs index 9036c10f..d1bdad21 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs @@ -29,7 +29,7 @@ internal sealed partial class GachaLogUrlDialog : ContentDialog /// 获取输入的Url /// /// 输入的结果 - public async Task> GetInputUrlAsync() + public async ValueTask> GetInputUrlAsync() { await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGameAccountNameDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGameAccountNameDialog.xaml.cs index feb12843..a11b04a0 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGameAccountNameDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGameAccountNameDialog.xaml.cs @@ -29,7 +29,7 @@ internal sealed partial class LaunchGameAccountNameDialog : ContentDialog /// 获取输入的Cookie /// /// 输入的结果 - public async Task> GetInputNameAsync() + public async ValueTask> GetInputNameAsync() { await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs index 40611b1b..5468b5aa 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs @@ -36,7 +36,7 @@ internal sealed partial class SignInWebViewDialog : ContentDialog InitializeAsync().SafeForget(); } - private async Task InitializeAsync() + private async ValueTask InitializeAsync() { await WebView.EnsureCoreWebView2Async(); CoreWebView2 coreWebView2 = WebView.CoreWebView2; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml.cs index 093e6811..5bf0cbdd 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml.cs @@ -29,7 +29,7 @@ internal sealed partial class UserDialog : ContentDialog /// 获取输入的Cookie /// /// 输入的结果 - public async Task> GetInputCookieAsync() + public async ValueTask> GetInputCookieAsync() { await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs index f660a987..51bd1d4a 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs @@ -28,7 +28,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control InitializeComponent(); } - private static async Task GetUidFromCookieAsync(IServiceProvider serviceProvider, Cookie cookie, CancellationToken token = default) + private static async ValueTask GetUidFromCookieAsync(IServiceProvider serviceProvider, Cookie cookie, CancellationToken token = default) { JsonSerializerOptions options = serviceProvider.GetRequiredService(); ILogger logger = serviceProvider.GetRequiredService>(); @@ -40,9 +40,9 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control .TryCatchGetFromJsonAsync>(ApiOsEndpoints.WebApiOsAccountLoginByCookie, options, logger, token) .ConfigureAwait(false); - if (resp != null) + if (resp is not null) { - return resp.Data.AccountInfo.AccountId.ToString(); + return $"{resp.Data.AccountInfo.AccountId}"; } return string.Empty; diff --git a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs index ab24d69f..28118bba 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs @@ -36,7 +36,7 @@ internal sealed partial class TitleView : UserControl #else SH.AppNameAndVersion; #endif - return string.Format(format, hutaoOptions.Version); + return format.Format(hutaoOptions.Version); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs index dbf77df0..b91fb822 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs @@ -23,9 +23,11 @@ namespace Snap.Hutao.ViewModel.Achievement; [Injection(InjectAs.Scoped)] internal sealed partial class AchievementImporter { + private readonly IContentDialogFactory contentDialogFactory; private readonly IAchievementService achievementService; - private readonly IServiceProvider serviceProvider; + private readonly IClipboardInterop clipboardInterop; private readonly IInfoBarService infoBarService; + private readonly IPickerFactory pickerFactory; private readonly JsonSerializerOptions options; private readonly ITaskContext taskContext; @@ -62,8 +64,7 @@ internal sealed partial class AchievementImporter { if (achievementService.CurrentArchive is { } archive) { - ValueResult pickerResult = await serviceProvider - .GetRequiredService() + ValueResult pickerResult = await pickerFactory .GetFileOpenPicker(PickerLocationId.Desktop, SH.FilePickerImportCommit, ".json") .TryPickSingleFileAsync() .ConfigureAwait(false); @@ -94,10 +95,7 @@ internal sealed partial class AchievementImporter { try { - return await serviceProvider - .GetRequiredService() - .DeserializeFromJsonAsync() - .ConfigureAwait(false); + return await clipboardInterop.DeserializeFromJsonAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -110,16 +108,13 @@ internal sealed partial class AchievementImporter { if (uiaf.IsCurrentVersionSupported()) { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - AchievementImportDialog importDialog = serviceProvider.CreateInstance(uiaf); + AchievementImportDialog importDialog = await contentDialogFactory.CreateInstanceAsync(uiaf).ConfigureAwait(false); (bool isOk, ImportStrategy strategy) = await importDialog.GetImportStrategyAsync().ConfigureAwait(false); if (isOk) { await taskContext.SwitchToMainThreadAsync(); - ContentDialog dialog = await serviceProvider - .GetRequiredService() + ContentDialog dialog = await contentDialogFactory .CreateForIndeterminateProgressAsync(SH.ViewModelAchievementImportProgress) .ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs index 115511ef..76ef8dea 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs @@ -28,13 +28,13 @@ namespace Snap.Hutao.ViewModel.Achievement; [Injection(InjectAs.Scoped)] internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INavigationRecipient { - private static readonly SortDescription UncompletedItemsFirstSortDescription = new(nameof(AchievementView.IsChecked), SortDirection.Ascending); - private static readonly SortDescription CompletionTimeSortDescription = new(nameof(AchievementView.Time), SortDirection.Descending); + private readonly SortDescription uncompletedItemsFirstSortDescription = new(nameof(AchievementView.IsChecked), SortDirection.Ascending); + private readonly SortDescription completionTimeSortDescription = new(nameof(AchievementView.Time), SortDirection.Descending); private readonly IContentDialogFactory contentDialogFactory; + private readonly IPickerFactory pickerFactory; private readonly AchievementImporter achievementImporter; private readonly IAchievementService achievementService; - private readonly IServiceProvider serviceProvider; private readonly IMetadataService metadataService; private readonly IInfoBarService infoBarService; private readonly JsonSerializerOptions options; @@ -189,9 +189,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav { if (Archives is null) { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - AchievementArchiveCreateDialog dialog = serviceProvider.CreateInstance(); + AchievementArchiveCreateDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); (bool isOk, string name) = await dialog.GetInputAsync().ConfigureAwait(false); if (isOk) @@ -203,13 +201,13 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav case ArchiveAddResult.Added: await taskContext.SwitchToMainThreadAsync(); SelectedArchive = achievementService.CurrentArchive; - infoBarService.Success(string.Format(SH.ViewModelAchievementArchiveAdded, name)); + infoBarService.Success(SH.ViewModelAchievementArchiveAdded.Format(name)); break; case ArchiveAddResult.InvalidName: infoBarService.Warning(SH.ViewModelAchievementArchiveInvalidName); break; case ArchiveAddResult.AlreadyExists: - infoBarService.Warning(string.Format(SH.ViewModelAchievementArchiveAlreadyExists, name)); + infoBarService.Warning(SH.ViewModelAchievementArchiveAlreadyExists.Format(name)); break; default: throw Must.NeverHappen(); @@ -223,7 +221,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav { if (Archives is not null && SelectedArchive is not null) { - string title = string.Format(SH.ViewModelAchievementRemoveArchiveTitle, SelectedArchive.Name); + string title = SH.ViewModelAchievementRemoveArchiveTitle.Format(SelectedArchive.Name); string content = SH.ViewModelAchievementRemoveArchiveContent; ContentDialogResult result = await contentDialogFactory .CreateForConfirmCancelAsync(title, content) @@ -260,8 +258,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav [SH.ViewModelAchievementExportFileType] = ".json".Enumerate().ToList(), }; - FileSavePicker picker = serviceProvider - .GetRequiredService() + FileSavePicker picker = pickerFactory .GetFileSavePicker(PickerLocationId.Desktop, fileName, SH.FilePickerExportCommit, fileTypes); (bool isPickerOk, ValueFile file) = await picker.TryPickSaveFileAsync().ConfigureAwait(false); @@ -343,8 +340,8 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav { if (IsUncompletedItemsFirst) { - Achievements.SortDescriptions.Add(UncompletedItemsFirstSortDescription); - Achievements.SortDescriptions.Add(CompletionTimeSortDescription); + Achievements.SortDescriptions.Add(uncompletedItemsFirstSortDescription); + Achievements.SortDescriptions.Add(completionTimeSortDescription); } else { @@ -387,7 +384,8 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav Achievements.Filter = obj => { AchievementView view = (AchievementView)obj; - return view.Inner.Title.Contains(search) || view.Inner.Description.Contains(search); + return view.Inner.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase) + || view.Inner.Description.Contains(search, StringComparison.CurrentCultureIgnoreCase); }; } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarProperty.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarProperty.cs index 57744a0d..83370f98 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarProperty.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarProperty.cs @@ -46,7 +46,7 @@ internal sealed class AvatarProperty : INameIcon { Name = name; Value = value; - Icon = PropertyIcons.GetValueOrDefault(property)!; + Icon = PropertyIcons.GetValueOrDefault(property); AddValue = addValue; } @@ -58,6 +58,7 @@ internal sealed class AvatarProperty : INameIcon /// /// 图标 /// + [AllowNull] public Uri Icon { get; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index e7f8d63a..135c2162 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -38,10 +38,16 @@ namespace Snap.Hutao.ViewModel.AvatarProperty; [Injection(InjectAs.Scoped)] internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipient { - private readonly IServiceProvider serviceProvider; + private readonly IContentDialogFactory contentDialogFactory; + private readonly IAppResourceProvider appResourceProvider; + private readonly ICultivationService cultivationService; + private readonly IAvatarInfoService avatarInfoService; + private readonly IClipboardInterop clipboardInterop; + private readonly CalculatorClient calculatorClient; private readonly ITaskContext taskContext; private readonly IUserService userService; private readonly IInfoBarService infoBarService; + private Summary? summary; private AvatarView? selectedAvatar; @@ -109,15 +115,13 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I ValueResult summaryResult; using (await EnterCriticalExecutionAsync().ConfigureAwait(false)) { - ContentDialog dialog = await serviceProvider - .GetRequiredService() + ContentDialog dialog = await contentDialogFactory .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyFetch) .ConfigureAwait(false); using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { - summaryResult = await serviceProvider - .GetRequiredService() + summaryResult = await avatarInfoService .GetSummaryAsync(userAndUid, option, token) .ConfigureAwait(false); } @@ -139,7 +143,8 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I break; case RefreshResult.StatusCodeNotSucceed: - infoBarService.Warning(summary!.Message); + ArgumentNullException.ThrowIfNull(summary); + infoBarService.Warning(summary.Message); break; case RefreshResult.ShowcaseNotOpen: @@ -167,49 +172,50 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I } // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); - CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance(options); + CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); - if (isOk) + if (!isOk) { - Response consumptionResponse = await serviceProvider - .GetRequiredService() - .ComputeAsync(userService.Current.Entity, delta) + return; + } + + Response consumptionResponse = await calculatorClient + .ComputeAsync(userService.Current.Entity, delta) + .ConfigureAwait(false); + + if (!consumptionResponse.IsOk()) + { + return; + } + + CalculatorConsumption consumption = consumptionResponse.Data; + + List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); + bool avatarSaved = await cultivationService + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) + .ConfigureAwait(false); + + try + { + // take a hot path if avatar is not saved. + bool avatarAndWeaponSaved = avatarSaved && await cultivationService + .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) .ConfigureAwait(false); - if (consumptionResponse.IsOk()) + if (avatarAndWeaponSaved) { - ICultivationService cultivationService = serviceProvider.GetRequiredService(); - CalculatorConsumption consumption = consumptionResponse.Data; - - List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); - bool avatarSaved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) - .ConfigureAwait(false); - - try - { - // take a hot path if avatar is not saved. - bool avatarAndWeaponSaved = avatarSaved && await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) - .ConfigureAwait(false); - - if (avatarAndWeaponSaved) - { - infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); - } - else - { - infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); - } - } - catch (Core.ExceptionService.UserdataCorruptedException ex) - { - infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); - } + infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); } + else + { + infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); + } + } + catch (Core.ExceptionService.UserdataCorruptedException ex) + { + infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); } } else @@ -231,7 +237,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I bool clipboardOpened = false; using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight)) { - Bgra32 tint = serviceProvider.GetRequiredService().GetResource("CompatBackgroundColor"); + Bgra32 tint = appResourceProvider.GetResource("CompatBackgroundColor"); softwareBitmap.NormalBlend(tint); using (InMemoryRandomAccessStream memory = new()) { @@ -239,7 +245,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I encoder.SetSoftwareBitmap(softwareBitmap); await encoder.FlushAsync(); - clipboardOpened = serviceProvider.GetRequiredService().SetBitmap(memory); + clipboardOpened = clipboardInterop.SetBitmap(memory); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/WeaponView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/WeaponView.cs index 50c3ebf3..40d9f156 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/WeaponView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/WeaponView.cs @@ -25,7 +25,7 @@ internal sealed class WeaponView : Equip, ICalculableSource /// /// 精炼属性 /// - public string AffixLevel { get => string.Format(SH.ModelBindingAvatarPropertyWeaponAffixFormat, AffixLevelNumber); } + public string AffixLevel { get => SH.ModelBindingAvatarPropertyWeaponAffixFormat.Format(AffixLevelNumber); } /// /// 精炼名称 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/Team.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/Team.cs index e3b7a5e2..457cf592 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/Team.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/Team.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Primitives; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Primitive; using Snap.Hutao.Web.Hutao.Model; +using System.Globalization; namespace Snap.Hutao.ViewModel.Complex; @@ -25,11 +26,11 @@ internal sealed class Team : List // TODO use Collection Literials foreach (StringSegment item in new StringTokenizer(team.Item, new char[] { ',' })) { - uint id = uint.Parse(item.AsSpan()); + uint id = uint.Parse(item.AsSpan(), CultureInfo.InvariantCulture); Add(new(idAvatarMap[id])); } - Rate = string.Format(SH.ModelBindingHutaoTeamUpCountFormat, team.Rate); + Rate = SH.ModelBindingHutaoTeamUpCountFormat.Format(team.Rate); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/TeamAppearanceView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/TeamAppearanceView.cs index d2f1acd9..be1eb0b8 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/TeamAppearanceView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/TeamAppearanceView.cs @@ -20,7 +20,7 @@ internal sealed class TeamAppearanceView /// 映射 public TeamAppearanceView(TeamAppearance teamRank, Dictionary idAvatarMap) { - Floor = string.Format(SH.ModelBindingHutaoComplexRankFloor, teamRank.Floor); + Floor = SH.ModelBindingHutaoComplexRankFloor.Format(teamRank.Floor); Up = teamRank.Up.SelectList(teamRate => new Team(teamRate, idAvatarMap)); Down = teamRank.Down.SelectList(teamRate => new Team(teamRate, idAvatarMap)); } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs index 3e172735..b75e0305 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Metadata.Item; using Snap.Hutao.Model.Primitive; @@ -23,10 +24,12 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel { private readonly ConcurrentCancellationTokenSource statisticsCancellationTokenSource = new(); + private readonly IContentDialogFactory contentDialogFactory; private readonly ICultivationService cultivationService; private readonly ILogger logger; - private readonly IServiceProvider serviceProvider; + private readonly INavigationService navigationService; private readonly IMetadataService metadataService; + private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; private ObservableCollection? projects; @@ -89,15 +92,12 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel [Command("AddProjectCommand")] private async Task AddProjectAsync() { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - CultivateProjectDialog dialog = serviceProvider.CreateInstance(); + CultivateProjectDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); (bool isOk, CultivateProject project) = await dialog.CreateProjectAsync().ConfigureAwait(false); if (isOk) { ProjectAddResult result = await cultivationService.TryAddProjectAsync(project).ConfigureAwait(false); - IInfoBarService infoBarService = serviceProvider.GetRequiredService(); switch (result) { @@ -214,10 +214,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel { Type? pageType = Type.GetType(typeString); ArgumentNullException.ThrowIfNull(pageType); - - serviceProvider - .GetRequiredService() - .Navigate(pageType, INavigationAwaiter.Default, true); + navigationService.Navigate(pageType, INavigationAwaiter.Default, true); } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs index 33d4233f..7e9f37c5 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.DailyNote; using Snap.Hutao.Service.Notification; @@ -20,8 +21,8 @@ namespace Snap.Hutao.ViewModel.DailyNote; [Injection(InjectAs.Scoped)] internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel { + private readonly IContentDialogFactory contentDialogFactory; private readonly IDailyNoteService dailyNoteService; - private readonly IServiceProvider serviceProvider; private readonly IInfoBarService infoBarService; private readonly DailyNoteOptions options; private readonly ITaskContext taskContext; @@ -113,7 +114,8 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel { // ContentDialog must be created by main thread. await taskContext.SwitchToMainThreadAsync(); - await serviceProvider.CreateInstance(entry).ShowAsync(); + DailyNoteNotificationDialog dialog = await contentDialogFactory.CreateInstanceAsync(entry).ConfigureAwait(true); + await dialog.ShowAsync(); await taskContext.SwitchToBackgroundAsync(); await dailyNoteService.UpdateDailyNoteAsync(entry).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs index a4a680d9..ceb29fbe 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs @@ -26,10 +26,10 @@ namespace Snap.Hutao.ViewModel.GachaLog; [Injection(InjectAs.Scoped)] internal sealed partial class GachaLogViewModel : Abstraction.ViewModel { + private readonly IGachaLogQueryProviderFactory gachaLogQueryProviderFactory; private readonly IContentDialogFactory contentDialogFactory; private readonly HutaoCloudStatisticsViewModel hutaoCloudStatisticsViewModel; private readonly HutaoCloudViewModel hutaoCloudViewModel; - private readonly IServiceProvider serviceProvider; private readonly IGachaLogService gachaLogService; private readonly IInfoBarService infoBarService; private readonly JsonSerializerOptions options; @@ -150,7 +150,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel private async ValueTask RefreshInternalAsync(RefreshOption option) { - IGachaLogQueryProvider provider = serviceProvider.GetRequiredService().Create(option); + IGachaLogQueryProvider provider = gachaLogQueryProviderFactory.Create(option); (bool isOk, GachaLogQuery query) = await provider.GetQueryAsync().ConfigureAwait(false); @@ -158,10 +158,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel { RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge; - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - - GachaLogRefreshProgressDialog dialog = serviceProvider.CreateInstance(); + GachaLogRefreshProgressDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); ContentDialogHideToken hideToken = await dialog.BlockAsync(taskContext).ConfigureAwait(false); Progress progress = new(dialog.OnReport); bool authkeyValid; @@ -268,7 +265,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel if (Archives is not null && SelectedArchive is not null) { ContentDialogResult result = await contentDialogFactory - .CreateForConfirmCancelAsync(string.Format(SH.ViewModelGachaLogRemoveArchiveTitle, SelectedArchive.Uid), SH.ViewModelGachaLogRemoveArchiveDescription) + .CreateForConfirmCancelAsync(SH.ViewModelGachaLogRemoveArchiveTitle.Format(SelectedArchive.Uid), SH.ViewModelGachaLogRemoveArchiveDescription) .ConfigureAwait(false); if (result == ContentDialogResult.Primary) @@ -356,9 +353,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel { if (uigf.IsCurrentVersionSupported(out UIGFVersion version)) { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - GachaLogImportDialog importDialog = serviceProvider.CreateInstance(uigf); + GachaLogImportDialog importDialog = await contentDialogFactory.CreateInstanceAsync(uigf).ConfigureAwait(false); if (await importDialog.GetShouldImportAsync().ConfigureAwait(false)) { if (CanImport(version, uigf)) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs index 2d06ffb3..e9eecf5d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs @@ -22,10 +22,10 @@ namespace Snap.Hutao.ViewModel.GachaLog; [Injection(InjectAs.Scoped)] internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel { + private readonly INavigationService navigationService; private readonly IContentDialogFactory contentDialogFactory; private readonly IGachaLogHutaoCloudService hutaoCloudService; private readonly IHutaoUserService hutaoUserService; - private readonly IServiceProvider serviceProvider; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; private readonly HutaoUserOptions options; @@ -131,9 +131,7 @@ internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel [Command("NavigateToSpiralAbyssRecordCommand")] private void NavigateToSpiralAbyssRecord() { - serviceProvider - .GetRequiredService() - .Navigate(INavigationAwaiter.Default); + navigationService.Navigate(INavigationAwaiter.Default); } private async ValueTask RefreshUidCollectionAsync() diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs index ed0c1381..4cdd8616 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs @@ -23,7 +23,7 @@ internal sealed partial class TypedWishSummary : Wish /// public string MaxOrangePullFormatted { - get => string.Format(SH.ModelBindingGachaTypedWishSummaryMaxOrangePullFormat, MaxOrangePull); + get => SH.ModelBindingGachaTypedWishSummaryMaxOrangePullFormat.Format(MaxOrangePull); } /// @@ -31,7 +31,7 @@ internal sealed partial class TypedWishSummary : Wish /// public string MinOrangePullFormatted { - get => string.Format(SH.ModelBindingGachaTypedWishSummaryMinOrangePullFormat, MinOrangePull); + get => SH.ModelBindingGachaTypedWishSummaryMinOrangePullFormat.Format(MinOrangePull); } /// @@ -83,7 +83,7 @@ internal sealed partial class TypedWishSummary : Wish /// public string AverageOrangePullFormatted { - get => string.Format(SH.ModelBindingGachaTypedWishSummaryAveragePullFormat, AverageOrangePull); + get => SH.ModelBindingGachaTypedWishSummaryAveragePullFormat.Format(AverageOrangePull); } /// @@ -96,7 +96,7 @@ internal sealed partial class TypedWishSummary : Wish /// public string AverageUpOrangePullFormatted { - get => string.Format(SH.ModelBindingGachaTypedWishSummaryAveragePullFormat, AverageUpOrangePull); + get => SH.ModelBindingGachaTypedWishSummaryAveragePullFormat.Format(AverageUpOrangePull); } /// @@ -104,7 +104,7 @@ internal sealed partial class TypedWishSummary : Wish /// public string PredictedPullLeftToOrangeFormatted { - get => string.Format(SH.ViewModelGachaLogPredictedPullLeftToOrange, PredictedPullLeftToOrange, ProbabilityOfPredictedPullLeftToOrange); + get => SH.ViewModelGachaLogPredictedPullLeftToOrange.Format(PredictedPullLeftToOrange, ProbabilityOfPredictedPullLeftToOrange); } /// @@ -112,7 +112,7 @@ internal sealed partial class TypedWishSummary : Wish /// public string ProbabilityOfNextPullIsOrangeFormatted { - get => string.Format(SH.ViewModelGachaLogProbabilityOfNextPullIsOrange, ProbabilityOfNextPullIsOrange); + get => SH.ViewModelGachaLogProbabilityOfNextPullIsOrange.Format(ProbabilityOfNextPullIsOrange); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs index 47b23e45..efaf1989 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs @@ -40,7 +40,7 @@ internal abstract class Wish /// public string TotalCountFormatted { - get => string.Format(SH.ModelBindingGachaWishBaseTotalCountFormat, TotalCount); + get => SH.ModelBindingGachaWishBaseTotalCountFormat.Format(TotalCount); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 86b5bfb3..1c8c3792 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -6,6 +6,7 @@ using Snap.Hutao.Control.Extension; using Snap.Hutao.Core; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.LifeCycle; +using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service; using Snap.Hutao.Service.Game; @@ -33,10 +34,12 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel /// public const string DesiredUid = nameof(DesiredUid); - private readonly IServiceProvider serviceProvider; + private readonly IContentDialogFactory contentDialogFactory; + private readonly INavigationService navigationService; private readonly IInfoBarService infoBarService; private readonly LaunchOptions launchOptions; private readonly RuntimeOptions hutaoOptions; + private readonly ResourceClient resourceClient; private readonly IUserService userService; private readonly ITaskContext taskContext; private readonly IGameService gameService; @@ -102,8 +105,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel protected override async ValueTask InitializeUIAsync() { - IInfoBarService infoBarService = serviceProvider.GetRequiredService(); - if (File.Exists(AppOptions.GamePath)) { try @@ -127,7 +128,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel } else { - infoBarService.Warning(string.Format(SH.ViewModelLaunchGameMultiChannelReadFail, options.ConfigFilePath)); + infoBarService.Warning(SH.ViewModelLaunchGameMultiChannelReadFail.Format(options.ConfigFilePath)); } ObservableCollection accounts = gameService.GameAccountCollection; @@ -153,7 +154,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel { infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid); await taskContext.SwitchToMainThreadAsync(); - await serviceProvider.GetRequiredService() + await navigationService .NavigateAsync(INavigationAwaiter.Default, true) .ConfigureAwait(false); } @@ -164,8 +165,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel private async ValueTask UpdateGameResourceAsync(LaunchScheme scheme) { await taskContext.SwitchToBackgroundAsync(); - Web.Response.Response response = await serviceProvider - .GetRequiredService() + Web.Response.Response response = await resourceClient .GetResourceAsync(scheme) .ConfigureAwait(false); @@ -179,8 +179,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel [Command("LaunchCommand", AllowConcurrentExecutions = true)] private async Task LaunchAsync() { - IInfoBarService infoBarService = serviceProvider.GetRequiredService(); - if (SelectedScheme is not null) { try @@ -188,8 +186,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel if (gameService.SetChannelOptions(SelectedScheme)) { // Channel changed, we need to change local file. - await taskContext.SwitchToMainThreadAsync(); - LaunchGamePackageConvertDialog dialog = serviceProvider.CreateInstance(); + LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); Progress progress = new(state => dialog.State = state.Clone()); using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { @@ -232,7 +229,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel } catch (UserdataCorruptedException ex) { - serviceProvider.GetRequiredService().Error(ex); + infoBarService.Error(ex); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/DownloadSummary.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/DownloadSummary.cs index 26aad1a1..c94c215a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/DownloadSummary.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/DownloadSummary.cs @@ -24,6 +24,7 @@ internal sealed class DownloadSummary : ObservableObject { private readonly IServiceProvider serviceProvider; private readonly ITaskContext taskContext; + private readonly IImageCache imageCache; private readonly HttpClient httpClient; private readonly string fileName; private readonly string fileUrl; @@ -41,6 +42,7 @@ internal sealed class DownloadSummary : ObservableObject { taskContext = serviceProvider.GetRequiredService(); httpClient = serviceProvider.GetRequiredService(); + imageCache = serviceProvider.GetRequiredService(); RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService(); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent); @@ -72,7 +74,7 @@ internal sealed class DownloadSummary : ObservableObject /// 异步下载并解压 /// /// 任务 - public async Task DownloadAndExtractAsync() + public async ValueTask DownloadAndExtractAsync() { ILogger logger = serviceProvider.GetRequiredService>(); try @@ -116,13 +118,14 @@ internal sealed class DownloadSummary : ObservableObject private void ExtractFiles(Stream stream) { - IImageCacheFilePathOperation imageCache = serviceProvider.GetRequiredService().As()!; + IImageCacheFilePathOperation? imageCacheFilePathOperation = imageCache.As(); + ArgumentNullException.ThrowIfNull(imageCacheFilePathOperation); using (ZipArchive archive = new(stream)) { foreach (ZipArchiveEntry entry in archive.Entries) { - string destPath = imageCache.GetFileFromCategoryAndName(fileName, entry.FullName); + string destPath = imageCacheFilePathOperation.GetFileFromCategoryAndName(fileName, entry.FullName); entry.ExtractToFile(destPath, true); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs index 3240eb6f..cac37f8c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs @@ -77,7 +77,7 @@ internal sealed partial class AnnouncementViewModel : Abstraction.ViewModel } else if (rand == 1) { - GreetingText = string.Format(SH.ViewPageHomeGreetingTextCommon2, LocalSetting.Get(SettingKeys.LaunchTimes, 0)); + GreetingText = SH.ViewPageHomeGreetingTextCommon2.Format(LocalSetting.Get(SettingKeys.LaunchTimes, 0)); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoPassportViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoPassportViewModel.cs index 3833ad71..92de3945 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoPassportViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoPassportViewModel.cs @@ -22,7 +22,6 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel private readonly HomaPassportClient homaPassportClient; private readonly INavigationService navigationService; private readonly HutaoUserOptions hutaoUserOptions; - private readonly IServiceProvider serviceProvider; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; @@ -142,7 +141,7 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel } Response response = await homaPassportClient.VerifyAsync(UserName, isResetPassword).ConfigureAwait(false); - serviceProvider.GetRequiredService().Information(response.Message); + infoBarService.Information(response.Message); } private void SaveUserNameAndPassword() diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/BattleView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/BattleView.cs index f4dabb70..efb8f465 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/BattleView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/BattleView.cs @@ -19,7 +19,7 @@ internal sealed class BattleView /// Id角色映射 public BattleView(Battle battle, Dictionary idAvatarMap) { - Time = DateTimeOffset.FromUnixTimeSeconds(battle.Timestamp).ToLocalTime().ToString("yyyy.MM.dd HH:mm:ss"); + Time = $"{DateTimeOffset.FromUnixTimeSeconds(battle.Timestamp).ToLocalTime():yyyy.MM.dd HH:mm:ss}"; Avatars = battle.Avatars.SelectList(a => AvatarView.From(idAvatarMap[a.Id])); } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/FloorView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/FloorView.cs index 697c4c88..369cc5f1 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/FloorView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/FloorView.cs @@ -18,7 +18,7 @@ internal sealed class FloorView /// Id角色映射 public FloorView(Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.Floor floor, Dictionary idAvatarMap) { - Index = string.Format(SH.ModelBindingHutaoComplexRankFloor, floor.Index); + Index = SH.ModelBindingHutaoComplexRankFloor.Format(floor.Index); SettleTime = $"{DateTimeOffset.FromUnixTimeSeconds(floor.SettleTime).ToLocalTime():yyyy.MM.dd HH:mm:ss}"; Star = floor.Star; Levels = floor.Levels.SelectList(l => new LevelView(l, idAvatarMap)); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/LevelView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/LevelView.cs index 1178f7d0..ce475b48 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/LevelView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/LevelView.cs @@ -18,7 +18,7 @@ internal sealed class LevelView /// Id角色映射 public LevelView(Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.Level level, Dictionary idAvatarMap) { - Index = string.Format(SH.ModelBindingHutaoComplexRankLevel, level.Index); + Index = SH.ModelBindingHutaoComplexRankLevel.Format(level.Index); Star = level.Star; Battles = level.Battles.SelectList(b => new BattleView(b, idAvatarMap)); } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs index c4d267e7..69c3cc1a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs @@ -26,7 +26,7 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss; internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient { private readonly ISpiralAbyssRecordService spiralAbyssRecordService; - private readonly IServiceProvider serviceProvider; + private readonly HomaSpiralAbyssClient spiralAbyssClient; private readonly IMetadataService metadataService; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; @@ -148,15 +148,12 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel [Command("UploadSpiralAbyssRecordCommand")] private async Task UploadSpiralAbyssRecordAsync() { - HomaSpiralAbyssClient homaClient = serviceProvider.GetRequiredService(); - IInfoBarService infoBarService = serviceProvider.GetRequiredService(); - if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) { - SimpleRecord? record = await homaClient.GetPlayerRecordAsync(userAndUid).ConfigureAwait(false); + SimpleRecord? record = await spiralAbyssClient.GetPlayerRecordAsync(userAndUid).ConfigureAwait(false); if (record is not null) { - Web.Response.Response response = await homaClient.UploadRecordAsync(record).ConfigureAwait(false); + Web.Response.Response response = await spiralAbyssClient.UploadRecordAsync(record).ConfigureAwait(false); if (response.IsOk()) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserAndUid.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserAndUid.cs index cef30cd0..2e8190d9 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserAndUid.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserAndUid.cs @@ -36,6 +36,7 @@ internal sealed class UserAndUid : IMappingFrom public PlayerUid Uid { get; private set; } + [SuppressMessage("", "SH002")] public static UserAndUid From(EntityUser user, PlayerUid role) { return new(user, role); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs index 2003ace6..71f81482 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs @@ -77,7 +77,7 @@ internal sealed partial class UserViewModel : ObservableObject SelectedUser = Users.Single(); } - infoBarService.Success(string.Format(SH.ViewModelUserAdded, uid)); + infoBarService.Success(SH.ViewModelUserAdded.Format(uid)); break; case UserOptionResult.Incomplete: infoBarService.Information(SH.ViewModelUserIncomplete); @@ -86,7 +86,7 @@ internal sealed partial class UserViewModel : ObservableObject infoBarService.Information(SH.ViewModelUserInvalid); break; case UserOptionResult.Updated: - infoBarService.Success(string.Format(SH.ViewModelUserUpdated, uid)); + infoBarService.Success(SH.ViewModelUserUpdated.Format(uid)); break; default: throw Must.NeverHappen(); @@ -173,7 +173,7 @@ internal sealed partial class UserViewModel : ObservableObject try { await userService.RemoveUserAsync(user).ConfigureAwait(false); - infoBarService.Success(string.Format(SH.ViewModelUserRemoved, user.UserInfo?.Nickname)); + infoBarService.Success(SH.ViewModelUserRemoved.Format(user.UserInfo?.Nickname)); } catch (UserdataCorruptedException ex) { @@ -198,7 +198,7 @@ internal sealed partial class UserViewModel : ObservableObject serviceProvider.GetRequiredService().SetText(cookieString); ArgumentNullException.ThrowIfNull(user.UserInfo); - infoBarService.Success(string.Format(SH.ViewModelUserCookieCopied, user.UserInfo.Nickname)); + infoBarService.Success(SH.ViewModelUserCookieCopied.Format(user.UserInfo.Nickname)); } catch (Exception ex) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs index d603d067..808abab5 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.WinUI.UI; +using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Model.Intrinsic; @@ -34,11 +35,13 @@ namespace Snap.Hutao.ViewModel.Wiki; [Injection(InjectAs.Scoped)] internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel { - private readonly IServiceProvider serviceProvider; + private readonly IContentDialogFactory contentDialogFactory; + private readonly ICultivationService cultivationService; private readonly IMetadataService metadataService; private readonly ITaskContext taskContext; private readonly IHutaoCache hutaoCache; private readonly IInfoBarService infoBarService; + private readonly CalculateClient calculateClient; private readonly IUserService userService; private AdvancedCollectionView? avatars; @@ -136,7 +139,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel // ContentDialog must be created by main thread. await taskContext.SwitchToMainThreadAsync(); CalculableOptions options = new(avatar.ToCalculable(), null); - CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance(options); + CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); (bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); if (!isOk) @@ -144,8 +147,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel return; } - Response consumptionResponse = await serviceProvider - .GetRequiredService() + Response consumptionResponse = await calculateClient .ComputeAsync(userService.Current.Entity, delta) .ConfigureAwait(false); @@ -158,8 +160,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel List items = CalculateItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); try { - bool saved = await serviceProvider - .GetRequiredService() + bool saved = await cultivationService .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) .ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs index 32a19dea..105114a1 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.WinUI.UI; +using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Model.Intrinsic; @@ -30,8 +31,10 @@ namespace Snap.Hutao.ViewModel.Wiki; [Injection(InjectAs.Scoped)] internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel { + private readonly IContentDialogFactory contentDialogFactory; + private readonly CalculateClient calculateClient; + private readonly ICultivationService cultivationService; private readonly ITaskContext taskContext; - private readonly IServiceProvider serviceProvider; private readonly IMetadataService metadataService; private readonly IHutaoCache hutaoCache; private readonly IInfoBarService infoBarService; @@ -120,10 +123,8 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel return; } - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); CalculableOptions options = new(null, weapon.ToCalculable()); - CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance(options); + CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); (bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); if (!isOk) @@ -131,8 +132,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel return; } - Response consumptionResponse = await serviceProvider - .GetRequiredService() + Response consumptionResponse = await calculateClient .ComputeAsync(userService.Current.Entity, delta) .ConfigureAwait(false); @@ -144,8 +144,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel CalculateConsumption consumption = consumptionResponse.Data; try { - bool saved = await serviceProvider - .GetRequiredService() + bool saved = await cultivationService .SaveConsumptionAsync(CultivateType.Weapon, weapon.Id, consumption.WeaponConsume.EmptyIfNull()) .ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs index 00c32196..2edfda4e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs @@ -47,17 +47,17 @@ internal static class CoreWebView2Extension { CoreWebView2CookieManager cookieManager = webView.CookieManager; - if (cookieToken != null) + if (cookieToken is not null) { cookieManager.AddMihoyoCookie(Cookie.ACCOUNT_ID, cookieToken, isOversea).AddMihoyoCookie(Cookie.COOKIE_TOKEN, cookieToken, isOversea); } - if (lToken != null) + if (lToken is not null) { cookieManager.AddMihoyoCookie(Cookie.LTUID, lToken, isOversea).AddMihoyoCookie(Cookie.LTOKEN, lToken, isOversea); } - if (sToken != null) + if (sToken is not null) { cookieManager.AddMihoyoCookie(Cookie.STUID, sToken, isOversea).AddMihoyoCookie(Cookie.STOKEN, sToken, isOversea); } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs index 44838cf6..78c6e9c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs @@ -20,7 +20,6 @@ namespace Snap.Hutao.Web.Bridge; /// [HighQuality] [SuppressMessage("", "CA1001")] -[SuppressMessage("", "SA1600")] internal class MiHoYoJSInterface { private const string InitializeJsInterfaceScript2 = """ @@ -65,7 +64,7 @@ internal class MiHoYoJSInterface /// /// 参数 /// 响应 - public virtual async Task GetActionTicketAsync(JsParam jsParam) + public virtual async ValueTask GetActionTicketAsync(JsParam jsParam) { return await serviceProvider .GetRequiredService() @@ -98,14 +97,13 @@ internal class MiHoYoJSInterface /// 响应 public virtual JsResult> GetCookieInfo(JsParam param) { - User user = serviceProvider.GetRequiredService().Current!; - + ArgumentNullException.ThrowIfNull(userAndUid.User.LToken); return new() { Data = new() { - [Cookie.LTUID] = user.LToken![Cookie.LTUID], - [Cookie.LTOKEN] = user.LToken[Cookie.LTOKEN], + [Cookie.LTUID] = userAndUid.User.LToken[Cookie.LTUID], + [Cookie.LTOKEN] = userAndUid.User.LToken[Cookie.LTOKEN], [Cookie.LOGIN_TICKET] = string.Empty, }, }; @@ -116,6 +114,7 @@ internal class MiHoYoJSInterface /// /// 参数 /// 响应 + [SuppressMessage("", "CA1308")] public virtual JsResult> GetDynamicSecrectV1(JsParam param) { string salt = HoyolabOptions.Salts[SaltType.LK2]; @@ -146,9 +145,9 @@ internal class MiHoYoJSInterface /// /// 参数 /// 响应 + [SuppressMessage("", "CA1308")] public virtual JsResult> GetDynamicSecrectV2(JsParam param) { - // TODO: Salt X4 for hoyolab user string salt = HoyolabOptions.Salts[SaltType.X4]; long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); int r = GetRandom(); @@ -193,7 +192,7 @@ internal class MiHoYoJSInterface /// /// 参数 /// 响应 - public virtual async Task>> GetCookieTokenAsync(JsParam param) + public virtual async ValueTask>> GetCookieTokenAsync(JsParam param) { IUserService userService = serviceProvider.GetRequiredService(); User user = userService.Current!; @@ -212,7 +211,7 @@ internal class MiHoYoJSInterface /// /// 参数 /// 响应 - public virtual async Task ClosePageAsync(JsParam param) + public virtual async ValueTask ClosePageAsync(JsParam param) { await taskContext.SwitchToMainThreadAsync(); if (webView.CanGoBack) @@ -247,7 +246,7 @@ internal class MiHoYoJSInterface return new() { Data = new() { ["statusBarHeight"] = 0 } }; } - public virtual async Task PushPageAsync(JsParam param) + public virtual async ValueTask PushPageAsync(JsParam param) { await taskContext.SwitchToMainThreadAsync(); webView.Navigate(param.Payload.Page); @@ -267,15 +266,16 @@ internal class MiHoYoJSInterface { Data = new() { + // TODO: replace with metadata options value ["language"] = appOptions.PreviousCulture.Name.ToLowerInvariant(), ["timeZone"] = "GMT+8", }, }; } - public virtual Task ShowAlertDialogAsync(JsParam param) + public virtual ValueTask ShowAlertDialogAsync(JsParam param) { - return Task.FromException(new NotImplementedException()); + return ValueTask.FromException(new NotSupportedException()); } public virtual IJsResult? StartRealPersonValidation(JsParam param) @@ -308,7 +308,7 @@ internal class MiHoYoJSInterface throw new NotImplementedException(); } - public virtual Task GetNotificationSettingsAsync(JsParam param) + public virtual ValueTask GetNotificationSettingsAsync(JsParam param) { throw new NotImplementedException(); } @@ -318,7 +318,7 @@ internal class MiHoYoJSInterface throw new NotImplementedException(); } - private async Task ExecuteCallbackScriptAsync(string callback, string? payload = null) + private async ValueTask ExecuteCallbackScriptAsync(string callback, string? payload = null) { if (string.IsNullOrEmpty(callback)) { @@ -377,7 +377,7 @@ internal class MiHoYoJSInterface return default; } - private async Task TryGetJsResultFromJsParamAsync(JsParam param) + private async ValueTask TryGetJsResultFromJsParamAsync(JsParam param) { try { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/DynamicSecrect2Playload.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/DynamicSecrect2Playload.cs index be1836e4..0cb8669b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/DynamicSecrect2Playload.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/DynamicSecrect2Playload.cs @@ -25,6 +25,7 @@ internal sealed class DynamicSecrect2Playload /// 获取排序后的的查询参数 /// /// 查询参数 + [SuppressMessage("", "CA1308")] public string GetQueryParam() { // TODO : improve here. diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/JsParam.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/JsParam.cs index 8a69814f..d03d92c8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/JsParam.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/JsParam.cs @@ -61,7 +61,7 @@ internal sealed class JsParam return new JsParam() { Method = jsParam.Method, - Payload = jsParam.Payload.HasValue ? jsParam.Payload.Value.Deserialize()! : default!, + Payload = JsonSerializer.Deserialize(jsParam.Payload ?? default) ?? default!, Callback = jsParam.Callback, }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs index 12208cb7..b442b5f0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs @@ -32,7 +32,7 @@ internal sealed partial class EnkaClient /// 玩家Uid /// 取消令牌 /// Enka API 响应 - public Task GetForwardDataAsync(in PlayerUid playerUid, CancellationToken token = default) + public ValueTask GetForwardDataAsync(in PlayerUid playerUid, CancellationToken token = default) { return TryGetEnkaResponseCoreAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), token); } @@ -43,12 +43,12 @@ internal sealed partial class EnkaClient /// 玩家Uid /// 取消令牌 /// Enka API 响应 - public Task GetDataAsync(in PlayerUid playerUid, CancellationToken token = default) + public ValueTask GetDataAsync(in PlayerUid playerUid, CancellationToken token = default) { - return TryGetEnkaResponseCoreAsync(string.Format(EnkaAPI, playerUid.Value), token); + return TryGetEnkaResponseCoreAsync(EnkaAPI.Format(playerUid.Value), token); } - private async Task TryGetEnkaResponseCoreAsync(string url, CancellationToken token = default) + private async ValueTask TryGetEnkaResponseCoreAsync(string url, CancellationToken token = default) { try { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs index 7c99346a..717b5200 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs @@ -34,7 +34,7 @@ internal sealed class EnkaResponse public bool IsValid { [MemberNotNullWhen(true, nameof(PlayerInfo), nameof(AvatarInfoList))] - get => PlayerInfo != null && AvatarInfoList != null; + get => PlayerInfo is not null && AvatarInfoList is not null; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Geetest/GeetestClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Geetest/GeetestClient.cs index 44bc5e0b..980a58e1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Geetest/GeetestClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Geetest/GeetestClient.cs @@ -23,7 +23,7 @@ internal sealed partial class GeetestClient /// /// gt /// 类型 - public async Task?> GetTypeAsync(string gt) + public async ValueTask?> GetTypeAsync(string gt) { string raw = await httpClient.GetStringAsync(ApiEndpoints.GeetestGetType(gt)).ConfigureAwait(false); raw = raw[0] == '(' ? raw[1..^1] : raw; // remove surrounded ( ) @@ -37,7 +37,7 @@ internal sealed partial class GeetestClient /// gt /// 验证流水号 /// 验证方式 - public async Task?> GetAjaxAsync(string gt, string challenge) + public async ValueTask?> GetAjaxAsync(string gt, string challenge) { string raw = await httpClient.GetStringAsync(ApiEndpoints.GeetestAjax(gt, challenge)).ConfigureAwait(false); raw = raw[0] == '(' ? raw[1..^1] : raw; // remove surrounded ( ) @@ -50,7 +50,7 @@ internal sealed partial class GeetestClient /// /// 验证注册 /// 验证方式 - public async Task?> GetAjaxAsync(VerificationRegistration registration) + public async ValueTask?> GetAjaxAsync(VerificationRegistration registration) { try { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs index 0e728633..7b956654 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs @@ -32,7 +32,7 @@ internal sealed partial class AccountClient /// 取消令牌 /// 用户角色信息 [ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)] - public async Task> GenerateAuthenticationKeyAsync(User user, GenAuthKeyData data, CancellationToken token = default) + public async ValueTask> GenerateAuthenticationKeyAsync(User user, GenAuthKeyData data, CancellationToken token = default) { Response? resp = await httpClient .SetUser(user, CookieType.SToken) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/IUserClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/IUserClient.cs index d3c8abb6..3024b485 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/IUserClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/IUserClient.cs @@ -16,5 +16,5 @@ internal interface IUserClient /// 用户 /// 取消令牌 /// 详细信息 - Task> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default); + ValueTask> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs index b8f65591..e369cea1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs @@ -29,15 +29,16 @@ internal sealed partial class UserClient : IUserClient /// 取消令牌 /// 详细信息 [ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)] - public async Task> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default) + public async ValueTask> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default) { + ArgumentException.ThrowIfNullOrEmpty(user.Aid); Response? resp = await httpClient // .SetUser(user, CookieType.SToken) .SetReferer(ApiEndpoints.BbsReferer) // .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true) - .TryCatchGetFromJsonAsync>(ApiEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token) + .TryCatchGetFromJsonAsync>(ApiEndpoints.UserFullInfoQuery(user.Aid), options, logger, token) .ConfigureAwait(false); return Response.Response.DefaultIfNull(resp); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs index feacd16c..e05008f4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs @@ -28,11 +28,12 @@ internal sealed partial class UserClientOversea : IUserClient /// 取消令牌 /// 详细信息 [ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.None)] - public async Task> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default) + public async ValueTask> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default) { + ArgumentException.ThrowIfNullOrEmpty(user.Aid); Response? resp = await httpClient .SetUser(user, CookieType.LToken) - .TryCatchGetFromJsonAsync>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token) + .TryCatchGetFromJsonAsync>(ApiOsEndpoints.UserFullInfoQuery(user.Aid), options, logger, token) .ConfigureAwait(false); return Response.Response.DefaultIfNull(resp); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Images.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Images.cs deleted file mode 100644 index 9e0d1fb6..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Images.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab; - -internal static class Images -{ - public const string UIItemIcon204 = $"https://smms.app/image/x9psnPrcbYoCl6U"; - public const string UIItemIcon210 = $"https://smms.app/image/n4gwxlFGPTX2j8p"; - public const string UIItemIcon220021 = $"https://smms.app/image/kbh1a2YVXpxWuez"; - public const string UIIconInteeExplore1 = $"https://smms.app/image/zJ4UYqKiD6uQlLc"; - public const string UIMarkQuestEventsProce = $"https://smms.app/image/DQyTF3rv4aA8MZV"; -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs index 7a76a380..4c8a860c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs @@ -55,7 +55,7 @@ internal sealed class HomaGachaLogClient /// 分布类型 /// 取消令牌 /// 祈愿分布 - public async Task> GetGachaDistributionAsync(GachaDistributionType distributionType, CancellationToken token = default) + public async ValueTask> GetGachaDistributionAsync(GachaDistributionType distributionType, CancellationToken token = default) { Response? resp = await httpClient .TryCatchGetFromJsonAsync>(HutaoEndpoints.GachaLogStatisticsDistribution(distributionType), 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 5b87e9eb..422c9289 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 @@ -21,7 +21,7 @@ internal sealed class ReliquarySetsConverter : JsonConverter List sets = new(); foreach (StringSegment segment in new StringTokenizer(source, Separator.ToArray())) { - if (segment.HasValue) + if (segment is { HasValue: true, Length: >0 }) { sets.Add(new(segment.Value)); } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs index e9cfc3e4..c9889a2f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs @@ -152,13 +152,7 @@ internal sealed class Response : Response, IJsResult } } - /// - /// 尝试获取数据 - /// - /// 数据 - /// 服务提供器 默认 Ioc.Default - /// 返回代码是否指示成功 - public bool TryGetData([NotNullWhen(true)] out TData? data, IServiceProvider? serviceProvider = null) + public bool TryGetData([NotNullWhen(true)] out TData? data, IInfoBarService? infoBarService = null, IServiceProvider? serviceProvider = null) { if (ReturnCode == 0) { @@ -169,7 +163,8 @@ internal sealed class Response : Response, IJsResult else { serviceProvider ??= Ioc.Default; - serviceProvider.GetRequiredService().Error(ToString()); + infoBarService ??= serviceProvider.GetRequiredService(); + infoBarService.Error(ToString()); data = default; return false; }