From 9a54df416344f72f9d5e3ecc031850d9f4def920 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Mon, 24 Apr 2023 19:34:58 +0800 Subject: [PATCH 1/4] remove CoreEnvironment --- .../CSharpLanguageFeatureTest.cs | 8 + src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 35 ++-- .../Snap.Hutao/Core/CoreEnvironment.cs | 167 ------------------ .../DependencyInjection.cs | 39 ++++ .../DependencyInjection/IocConfiguration.cs | 47 +++-- .../IocHttpClientConfiguration.cs | 20 ++- .../Core/ExceptionService/ExceptionFormat.cs | 40 +++++ .../ExceptionService/ExceptionRecorder.cs | 46 ++--- .../Snap.Hutao/Core/HoyolabOptions.cs | 73 ++++++++ .../Snap.Hutao/Core/HutaoOptions.cs | 102 +++++++++++ .../Snap.Hutao/Core/Json/JsonOptions.cs | 33 ++++ .../Core/Json/JsonTypeInfoResolvers.cs | 3 +- .../Snap.Hutao/Core/StringLiterals.cs | 2 + .../Snap.Hutao/Core/Threading/ITaskContext.cs | 30 ++++ .../Snap.Hutao/Core/Threading/TaskContext.cs | 51 ++++++ .../Snap.Hutao/Core/Threading/ThreadHelper.cs | 23 +-- .../Core/Windowing/ExtendedWindow.cs | 10 +- .../Configuration/JsonTextValueConverter.cs | 6 +- .../Model/InterChange/Achievement/UIAFInfo.cs | 8 +- .../Model/InterChange/GachaLog/UIGFInfo.cs | 7 +- .../Model/InterChange/Inventory/UIIFInfo.cs | 7 +- .../Model/Metadata/ParameterFormat.cs | 4 +- src/Snap.Hutao/Snap.Hutao/Program.cs | 59 ++----- .../Service/Achievement/AchievementService.cs | 7 +- .../Snap.Hutao/Service/AppOptions.cs | 1 + .../Service/GachaLog/GachaLogExportService.cs | 6 +- .../Service/Game/Package/PackageConverter.cs | 10 +- .../Service/Metadata/MetadataService.cs | 3 +- .../Snap.Hutao/View/Page/SettingPage.xaml | 4 +- .../Snap.Hutao/View/TitleView.xaml.cs | 11 +- .../ExperimentalFeaturesViewModel.cs | 7 +- .../Snap.Hutao/ViewModel/SettingViewModel.cs | 27 +-- .../Snap.Hutao/ViewModel/WelcomeViewModel.cs | 4 +- .../Web/Bridge/CoreWebView2Extension.cs | 10 +- .../Web/Bridge/MiHoYoJSInterface.cs | 8 +- .../Web/Bridge/SignInJSInterfaceOversea.cs | 4 +- .../Web/Bridge/SignInJsInterface.cs | 4 +- .../DynamicSecret/DynamicSecretHandler.cs | 2 +- .../Web/Hutao/HomaLogUploadClient.cs | 14 +- 39 files changed, 592 insertions(+), 350 deletions(-) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionFormat.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/HoyolabOptions.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Json/JsonOptions.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs index 43874f8a..3dce6ca8 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs @@ -61,4 +61,12 @@ public class CSharpLanguageFeatureTest ValueB = 2, ValueC = 3, } + + [TestMethod] + public void GetTwiceOnPropertyResultsSame() + { + Assert.AreEqual(UUID, UUID); + } + + public static Guid UUID { get => Guid.NewGuid(); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index e2fe90e5..f19a1c21 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -21,19 +21,22 @@ namespace Snap.Hutao; [SuppressMessage("", "SH001")] public sealed partial class App : Application { + private const string AppInstanceKey = "main"; private readonly ILogger logger; + private readonly IServiceProvider serviceProvider; /// /// Initializes the singleton application object. /// - /// 日志器 - public App(ILogger logger) + /// 服务提供器 + public App(IServiceProvider serviceProvider) { - // load app resource + // Load app resource InitializeComponent(); - this.logger = logger; - _ = new ExceptionRecorder(this, logger); + logger = serviceProvider.GetRequiredService>(); + serviceProvider.GetRequiredService().Record(this); + this.serviceProvider = serviceProvider; } /// @@ -42,7 +45,7 @@ public sealed partial class App : Application try { AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); - AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main"); + AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey); if (firstInstance.IsCurrent) { @@ -51,10 +54,8 @@ public sealed partial class App : Application firstInstance.Activated += Activation.Activate; ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate; - logger.LogInformation("Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version); - logger.LogInformation("Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path); - - JumpListHelper.ConfigureAsync().SafeForget(logger); + LogDiagnosticInformation(); + PostLaunchAsync().SafeForget(logger); } else { @@ -69,4 +70,18 @@ public sealed partial class App : Application Process.GetCurrentProcess().Kill(); } } + + private static async Task PostLaunchAsync() + { + await JumpListHelper.ConfigureAsync().ConfigureAwait(false); + } + + private void LogDiagnosticInformation() + { + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + + logger.LogInformation("Snap Hutao FamilyName: {name}", hutaoOptions.FamilyName); + logger.LogInformation("Snap Hutao Version: {version}", hutaoOptions.Version); + logger.LogInformation("Snap Hutao LocalCache: {folder}", hutaoOptions.LocalCache); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs deleted file mode 100644 index 169def0b..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.Win32; -using Snap.Hutao.Core.Json; -using Snap.Hutao.Core.Setting; -using Snap.Hutao.Web.Hoyolab.DynamicSecret; -using System.Collections.Immutable; -using System.IO; -using System.Text.Encodings.Web; -using System.Text.Json.Serialization.Metadata; -using Windows.ApplicationModel; - -namespace Snap.Hutao.Core; - -/// -/// 核心环境参数 -/// -[HighQuality] -internal static class CoreEnvironment -{ - /// - /// 米游社请求UA - /// - public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}"; - - /// - /// Hoyolab请求UA - /// - public const string HoyolabOsUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBSOversea/{HoyolabOsXrpcVersion}"; - - /// - /// 米游社移动端请求UA - /// - public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}"; - - /// - /// Hoyolab 移动端请求UA - /// - public const string HoyolabOsMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{HoyolabOsXrpcVersion}"; - - /// - /// 米游社 Rpc 版本 - /// - public const string HoyolabXrpcVersion = "2.49.1"; - - /// - /// Hoyolab Rpc 版本 - /// - public const string HoyolabOsXrpcVersion = "2.30.0"; - - /// - /// 盐 - /// - // https://github.com/UIGF-org/Hoyolab.Salt - public static readonly ImmutableDictionary DynamicSecretSalts = new Dictionary() - { - [SaltType.K2] = "egBrFMO1BPBG0UX5XOuuwMRLZKwTVKRV", - [SaltType.LK2] = "DG8lqMyc9gquwAUFc7zBS62ijQRX9XF7", - [SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs", - [SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v", - [SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS", - - // This SALT is not reliable - [SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s", - }.ToImmutableDictionary(); - - /// - /// 默认的Json序列化选项 - /// - public static readonly JsonSerializerOptions JsonOptions = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - PropertyNameCaseInsensitive = true, - WriteIndented = true, - TypeInfoResolver = new DefaultJsonTypeInfoResolver() - { - Modifiers = - { - JsonTypeInfoResolvers.ResolveEnumType, - }, - }, - }; - - /// - /// 当前版本 - /// - public static readonly Version Version; - - /// - /// 标准UA - /// - public static readonly string CommonUA; - - /// - /// 数据文件夹 - /// - public static readonly string DataFolder; - - /// - /// 包家族名称 - /// - public static readonly string FamilyName; - - /// - /// 米游社设备Id - /// - public static readonly string HoyolabDeviceId; - - /// - /// 胡桃设备Id - /// - public static readonly string HutaoDeviceId; - - /// - /// 安装位置 - /// - public static readonly string InstalledLocation; - - private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\"; - private const string MachineGuidValue = "MachineGuid"; - - static CoreEnvironment() - { - DataFolder = GetDatafolderPath(); - Version = Package.Current.Id.Version.ToVersion(); - FamilyName = Package.Current.Id.FamilyName; - InstalledLocation = Package.Current.InstalledLocation.Path; - CommonUA = $"Snap Hutao/{Version}"; - - // simply assign a random guid - HoyolabDeviceId = Guid.NewGuid().ToString(); - HutaoDeviceId = GetUniqueUserID(); - } - - private static string GetUniqueUserID() - { - string userName = Environment.UserName; - object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName); - return Convert.ToMd5HexString($"{userName}{machineGuid}"); - } - - private static string GetDatafolderPath() - { - string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty); - - if (string.IsNullOrEmpty(preferredPath)) - { - string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); -#if RELEASE - // 将测试版与正式版的文件目录分离 - string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao"; -#else - // 使得迁移能正常生成 - string folderName = "Hutao"; -#endif - string path = Path.GetFullPath(Path.Combine(myDocument, folderName)); - Directory.CreateDirectory(path); - return path; - } - else - { - return preferredPath; - } - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs new file mode 100644 index 00000000..7d0278c6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs @@ -0,0 +1,39 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.Messaging; + +namespace Snap.Hutao.Core.DependencyInjection; + +/// +/// 依赖注入 +/// +internal static class DependencyInjection +{ + /// + /// 初始化依赖注入 + /// + /// 服务提供器 + public static ServiceProvider Initialize() + { + ServiceProvider serviceProvider = new ServiceCollection() + + // Microsoft extension + .AddLogging(builder => builder.AddDebug()) + .AddMemoryCache() + + // Hutao extensions + .AddJsonOptions() + .AddDatabase() + .AddInjections() + .AddHttpClients() + + // Discrete services + .AddSingleton(WeakReferenceMessenger.Default) + + .BuildServiceProvider(true); + + Ioc.Default.ConfigureServices(serviceProvider); + return serviceProvider; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs index 76952034..350b165d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs @@ -2,8 +2,12 @@ // Licensed under the MIT license. using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Core.Json; using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Service; using System.Diagnostics; +using System.Globalization; +using Windows.Globalization; namespace Snap.Hutao.Core.DependencyInjection; @@ -20,7 +24,7 @@ internal static class IocConfiguration /// 可继续操作的集合 public static IServiceCollection AddJsonOptions(this IServiceCollection services) { - return services.AddSingleton(CoreEnvironment.JsonOptions); + return services.AddSingleton(JsonOptions.Default); } /// @@ -30,28 +34,51 @@ internal static class IocConfiguration /// 可继续操作的集合 public static IServiceCollection AddDatabase(this IServiceCollection services) { - string dbFile = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Userdata.db"); + return services.AddDbContext(AddDbContextCore); + } + + /// + /// 初始化语言 + /// + /// 服务提供器 + /// 服务提供器,用于链式调用 + public static IServiceProvider InitializeCulture(this IServiceProvider serviceProvider) + { + AppOptions appOptions = serviceProvider.GetRequiredService(); + appOptions.PreviousCulture = CultureInfo.CurrentCulture; + + CultureInfo cultureInfo = appOptions.CurrentCulture; + + CultureInfo.CurrentCulture = cultureInfo; + CultureInfo.CurrentUICulture = cultureInfo; + ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name; + + return serviceProvider; + } + + private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder) + { + HutaoOptions hutaoOptions = provider.GetRequiredService(); + string dbFile = System.IO.Path.Combine(hutaoOptions.DataFolder, "Userdata.db"); string sqlConnectionString = $"Data Source={dbFile}"; - // temporarily create a context + // Temporarily create a context using (AppDbContext context = AppDbContext.Create(sqlConnectionString)) { if (context.Database.GetPendingMigrations().Any()) { #if DEBUG - Debug.WriteLine("[Debug] Performing AppDbContext Migrations"); + Debug.WriteLine("[Database] Performing AppDbContext Migrations"); #endif context.Database.Migrate(); } } - return services.AddDbContext(builder => - { - builder + builder #if DEBUG - .EnableSensitiveDataLogging() + .EnableSensitiveDataLogging() #endif - .UseSqlite(sqlConnectionString); - }); + .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) + .UseSqlite(sqlConnectionString); } } \ 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 52e2f67f..07f0854d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -24,10 +24,12 @@ internal static partial class IocHttpClientConfiguration /// 默认配置 /// /// 配置后的客户端 - private static void DefaultConfiguration(HttpClient client) + private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client) { + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + client.Timeout = Timeout.InfiniteTimeSpan; - client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.CommonUA); + client.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent); } /// @@ -37,11 +39,11 @@ internal static partial class IocHttpClientConfiguration private static void XRpcConfiguration(HttpClient client) { client.Timeout = Timeout.InfiniteTimeSpan; - client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA); + client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgent); client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson); - client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion); + client.DefaultRequestHeaders.Add("x-rpc-app_version", HoyolabOptions.XrpcVersion); client.DefaultRequestHeaders.Add("x-rpc-client_type", "5"); - client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId); + client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId); } /// @@ -51,13 +53,13 @@ internal static partial class IocHttpClientConfiguration private static void XRpc2Configuration(HttpClient client) { client.Timeout = Timeout.InfiniteTimeSpan; - client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA); + client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgent); client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson); client.DefaultRequestHeaders.Add("x-rpc-aigis", string.Empty); client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8"); - client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion); + client.DefaultRequestHeaders.Add("x-rpc-app_version", HoyolabOptions.XrpcVersion); client.DefaultRequestHeaders.Add("x-rpc-client_type", "2"); - client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId); + client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId); client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn"); client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2"); } @@ -70,7 +72,7 @@ internal static partial class IocHttpClientConfiguration private static void XRpc3Configuration(HttpClient client) { client.Timeout = Timeout.InfiniteTimeSpan; - client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabOsUA); + client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgentOversea); client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson); client.DefaultRequestHeaders.Add("x-rpc-app_version", "1.5.0"); client.DefaultRequestHeaders.Add("x-rpc-client_type", "4"); diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionFormat.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionFormat.cs new file mode 100644 index 00000000..3b1ffab4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionFormat.cs @@ -0,0 +1,40 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Collections; +using System.Text; + +namespace Snap.Hutao.Core.ExceptionService; + +/// +/// 异常格式化 +/// +internal sealed class ExceptionFormat +{ + private const string SectionSeparator = "----------------------------------------"; + + /// + /// 格式化异常 + /// + /// 异常 + /// 格式化后的异常 + public static string Format(Exception exception) + { + StringBuilder builder = new(); + builder.AppendLine("Exception Data:"); + + foreach (DictionaryEntry entry in exception.Data) + { + builder + .Append(entry.Key) + .Append(':') + .Append(entry.Value) + .Append(StringLiterals.CRLF); + } + + builder.AppendLine(SectionSeparator); + builder.Append(exception.ToString()); + + return builder.ToString(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs index 306f54a2..bcfd9db8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. using Microsoft.UI.Xaml; -using System.Collections; -using System.Text; namespace Snap.Hutao.Core.ExceptionService; @@ -11,50 +9,56 @@ namespace Snap.Hutao.Core.ExceptionService; /// 异常记录器 /// [HighQuality] +[Injection(InjectAs.Singleton)] internal sealed class ExceptionRecorder { - private readonly ILogger logger; + private readonly ILogger logger; + private readonly IServiceProvider serviceProvider; /// /// 构造一个新的异常记录器 /// - /// 应用程序 + /// 服务提供器 /// 日志器 - public ExceptionRecorder(Application application, ILogger logger) + public ExceptionRecorder(IServiceProvider serviceProvider) { - this.logger = logger; + logger = serviceProvider.GetRequiredService>(); + this.serviceProvider = serviceProvider; + } - application.UnhandledException += OnAppUnhandledException; - application.DebugSettings.BindingFailed += OnXamlBindingFailed; - application.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed; + /// + /// 记录应用程序异常 + /// + /// 应用程序 + public void Record(Application app) + { + app.UnhandledException += OnAppUnhandledException; + app.DebugSettings.BindingFailed += OnXamlBindingFailed; + app.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed; } private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { #if RELEASE #pragma warning disable VSTHRD002 - Ioc.Default.GetRequiredService().UploadLogAsync(e.Exception).GetAwaiter().GetResult(); + serviceProvider + .GetRequiredService() + .UploadLogAsync(serviceProvider, e.Exception) + .GetAwaiter() + .GetResult(); #pragma warning restore VSTHRD002 #endif - StringBuilder dataDetailBuilder = new(); - foreach (DictionaryEntry entry in e.Exception.Data) - { - string key = $"{entry.Key}"; - string value = $"{entry.Value}"; - dataDetailBuilder.Append(key).Append(':').Append(value).Append("\r\n"); - } - - logger.LogError(e.Exception, "未经处理的异常\r\n{detail}", dataDetailBuilder.ToString()); + logger.LogError("未经处理的全局异常:\r\n{detail}", ExceptionFormat.Format(e.Exception)); } private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e) { - logger.LogCritical("XAML绑定失败: {message}", e.Message); + logger.LogCritical("XAML 绑定失败:{message}", e.Message); } private void OnXamlResourceReferenceFailed(DebugSettings sender, XamlResourceReferenceFailedEventArgs e) { - logger.LogCritical("XAML资源引用失败: {message}", e.Message); + logger.LogCritical("XAML 资源引用失败:{message}", e.Message); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/HoyolabOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/HoyolabOptions.cs new file mode 100644 index 00000000..57d20a74 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/HoyolabOptions.cs @@ -0,0 +1,73 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Extensions.Options; +using Snap.Hutao.Web.Hoyolab.DynamicSecret; +using System.Collections.Immutable; + +namespace Snap.Hutao.Core; + +/// +/// 米游社选项 +/// +[Injection(InjectAs.Singleton)] +internal sealed class HoyolabOptions : IOptions +{ + /// + /// 米游社请求UA + /// + public const string UserAgent = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{XrpcVersion}"; + + /// + /// Hoyolab 请求UA + /// + public const string UserAgentOversea = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBSOversea/{XrpcVersionOversea}"; + + /// + /// 米游社移动端请求UA + /// + public const string MobileUserAgent = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{XrpcVersion}"; + + /// + /// Hoyolab 移动端请求UA + /// + public const string MobileUserAgentOversea = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{XrpcVersionOversea}"; + + /// + /// 米游社 Rpc 版本 + /// + public const string XrpcVersion = "2.49.1"; + + /// + /// Hoyolab Rpc 版本 + /// + public const string XrpcVersionOversea = "2.30.0"; + + // https://github.com/UIGF-org/Hoyolab.Salt + private static readonly ImmutableDictionary SaltsInner = new Dictionary() + { + [SaltType.K2] = "egBrFMO1BPBG0UX5XOuuwMRLZKwTVKRV", + [SaltType.LK2] = "DG8lqMyc9gquwAUFc7zBS62ijQRX9XF7", + [SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs", + [SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v", + [SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS", + + // This SALT is not reliable + [SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s", + }.ToImmutableDictionary(); + + private static string? deviceId; + + /// + /// 米游社设备Id + /// + public static string DeviceId { get => deviceId ??= Guid.NewGuid().ToString(); } + + /// + /// 盐 + /// + public static ImmutableDictionary Salts { get => SaltsInner; } + + /// + public HoyolabOptions Value { get => this; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs new file mode 100644 index 00000000..cb5803fb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs @@ -0,0 +1,102 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Win32; +using Snap.Hutao.Core.Setting; +using System.IO; +using Windows.ApplicationModel; +using Windows.Storage; + +namespace Snap.Hutao.Core; + +/// +/// 胡桃选项 +/// 存储环境相关的选项 +/// +[Injection(InjectAs.Singleton)] +internal sealed class HutaoOptions : IOptions +{ + /// + /// 构造一个新的胡桃选项 + /// + public HutaoOptions() + { + DataFolder = GetDataFolderPath(); + LocalCache = ApplicationData.Current.LocalCacheFolder.Path; + InstalledLocation = Package.Current.InstalledLocation.Path; + FamilyName = Package.Current.Id.FamilyName; + + Version = Package.Current.Id.Version.ToVersion(); + UserAgent = $"Snap Hutao/{Version}"; + + DeviceId = GetUniqueUserId(); + } + + /// + /// 当前版本 + /// + public Version Version { get; } + + /// + /// 标准UA + /// + public string UserAgent { get; } + + /// + /// 数据文件夹路径 + /// + public string DataFolder { get; } + + /// + /// 安装位置 + /// + public string InstalledLocation { get; } + + /// + /// 本地缓存 + /// + public string LocalCache { get; } + + /// + /// 包家族名称 + /// + public string FamilyName { get; } + + /// + /// 设备Id + /// + public string DeviceId { get; } + + /// + public HutaoOptions Value { get => this; } + + private static string GetDataFolderPath() + { + string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty); + + if (!string.IsNullOrEmpty(preferredPath) && Directory.Exists(preferredPath)) + { + return preferredPath; + } + + string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); +#if RELEASE + // 将测试版与正式版的文件目录分离 + string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao"; +#else + // 使得迁移能正常生成 + string folderName = "Hutao"; +#endif + string path = Path.GetFullPath(Path.Combine(myDocument, folderName)); + Directory.CreateDirectory(path); + return path; + } + + private static string GetUniqueUserId() + { + string userName = Environment.UserName; + object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName); + return Convert.ToMd5HexString($"{userName}{machineGuid}"); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/JsonOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/JsonOptions.cs new file mode 100644 index 00000000..fe2e35ee --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Json/JsonOptions.cs @@ -0,0 +1,33 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Encodings.Web; +using System.Text.Json.Serialization.Metadata; + +namespace Snap.Hutao.Core.Json; + +/// +/// Json 选项 +/// +internal static class JsonOptions +{ + /// + /// 默认的Json序列化选项 + /// + public static readonly JsonSerializerOptions Default = new() + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + JsonTypeInfoResolvers.ResolveEnumType, + }, + }, + }; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/JsonTypeInfoResolvers.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/JsonTypeInfoResolvers.cs index c371d98a..9e3366e5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Json/JsonTypeInfoResolvers.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Json/JsonTypeInfoResolvers.cs @@ -32,9 +32,8 @@ internal static class JsonTypeInfoResolvers if (property.AttributeProvider is System.Reflection.ICustomAttributeProvider provider) { object[] attributes = provider.GetCustomAttributes(JsonEnumAttributeType, false); - if (attributes.Length == 1) + if (attributes.SingleOrDefault() is JsonEnumAttribute attr) { - JsonEnumAttribute attr = (JsonEnumAttribute)attributes[0]; property.CustomConverter = attr.CreateConverter(property); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/StringLiterals.cs b/src/Snap.Hutao/Snap.Hutao/Core/StringLiterals.cs index da733821..cf13ae58 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/StringLiterals.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/StringLiterals.cs @@ -28,4 +28,6 @@ internal static class StringLiterals /// False /// public const string False = "False"; + + public const string CRLF = "\r\n"; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs new file mode 100644 index 00000000..e2c7b8f0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Threading; + +/// +/// 任务上下文 +/// +internal interface ITaskContext +{ + /// + /// 在主线程上同步等待执行操作 + /// + /// 操作 + void InvokeOnMainThread(Action action); + + /// + /// 异步切换到 后台线程 + /// + /// 使用 异步切换到 主线程 + /// 等待体 + ThreadPoolSwitchOperation SwitchToBackgroundAsync(); + + /// + /// 异步切换到 主线程 + /// + /// 使用 异步切换到 后台线程 + /// 等待体 + DispatherQueueSwitchOperation SwitchToMainThreadAsync(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs new file mode 100644 index 00000000..dfeaacd0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs @@ -0,0 +1,51 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Dispatching; +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Core.Threading; + +/// +/// 任务上下文 +/// +[Injection(InjectAs.Singleton, typeof(ITaskContext))] +internal sealed class TaskContext : ITaskContext +{ + private readonly DispatcherQueue dispatcherQueue; + + /// + /// 构造一个新的任务上下文 + /// + public TaskContext() + { + dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + DispatcherQueueSynchronizationContext context = new(dispatcherQueue); + SynchronizationContext.SetSynchronizationContext(context); + } + + /// + public ThreadPoolSwitchOperation SwitchToBackgroundAsync() + { + return default; + } + + /// + public DispatherQueueSwitchOperation SwitchToMainThreadAsync() + { + return new(dispatcherQueue!); + } + + /// + public void InvokeOnMainThread(Action action) + { + if (dispatcherQueue!.HasThreadAccess) + { + action(); + } + else + { + dispatcherQueue.Invoke(action); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs index c0918ddb..ae17eefc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.UI.Dispatching; using System.Runtime.CompilerServices; namespace Snap.Hutao.Core.Threading; @@ -9,21 +8,20 @@ namespace Snap.Hutao.Core.Threading; /// /// 线程帮助类 /// +[Obsolete("Use TaskContext instead")] internal static class ThreadHelper { /// /// 主线程队列 /// - private static volatile DispatcherQueue? dispatcherQueue; + private static volatile ITaskContext taskContext; /// /// 初始化 /// - public static void Initialize() + public static void Initialize(ITaskContext taskContext) { - dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - DispatcherQueueSynchronizationContext context = new(dispatcherQueue); - SynchronizationContext.SetSynchronizationContext(context); + ThreadHelper.taskContext = taskContext; } /// @@ -34,7 +32,7 @@ internal static class ThreadHelper [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ThreadPoolSwitchOperation SwitchToBackgroundAsync() { - return default; + return taskContext.SwitchToBackgroundAsync(); } /// @@ -45,7 +43,7 @@ internal static class ThreadHelper [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DispatherQueueSwitchOperation SwitchToMainThreadAsync() { - return new(dispatcherQueue!); + return taskContext.SwitchToMainThreadAsync(); } /// @@ -55,13 +53,6 @@ internal static class ThreadHelper [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InvokeOnMainThread(Action action) { - if (dispatcherQueue!.HasThreadAccess) - { - action(); - } - else - { - dispatcherQueue.Invoke(action); - } + taskContext.InvokeOnMainThread(action); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs index 3e842baa..a94621af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs @@ -11,9 +11,9 @@ using Snap.Hutao.Message; using Snap.Hutao.Service; using Snap.Hutao.Win32; using System.IO; -using Windows.Win32.Foundation; using Windows.Graphics; using Windows.UI; +using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dwm; using static Windows.Win32.PInvoke; @@ -30,7 +30,6 @@ internal sealed class ExtendedWindow : IRecipient options; private readonly IServiceProvider serviceProvider; - private readonly ILogger> logger; private readonly WindowSubclass subclass; private ExtendedWindow(TWindow window, FrameworkElement titleBar, IServiceProvider serviceProvider) @@ -38,7 +37,6 @@ internal sealed class ExtendedWindow : IRecipient>>(); this.serviceProvider = serviceProvider; InitializeWindow(); @@ -63,8 +61,10 @@ internal sealed class ExtendedWindow : IRecipient(); + + options.AppWindow.Title = string.Format(SH.AppNameAndVersion, hutaoOptions.Version); + options.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico")); ExtendsContentIntoTitleBar(); Persistence.RecoverOrInit(options); diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Configuration/JsonTextValueConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Configuration/JsonTextValueConverter.cs index 97ea9c44..36620046 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Configuration/JsonTextValueConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Configuration/JsonTextValueConverter.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Snap.Hutao.Core; +using Snap.Hutao.Core.Json; namespace Snap.Hutao.Model.Entity.Configuration; @@ -18,8 +18,8 @@ internal sealed class JsonTextValueConverter : ValueConverter public JsonTextValueConverter() : base( - obj => JsonSerializer.Serialize(obj, CoreEnvironment.JsonOptions), - str => JsonSerializer.Deserialize(str, CoreEnvironment.JsonOptions)!) + obj => JsonSerializer.Serialize(obj, JsonOptions.Default), + str => JsonSerializer.Deserialize(str, JsonOptions.Default)!) { } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs index b331377c..8ef870a3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs @@ -47,15 +47,17 @@ internal sealed class UIAFInfo /// /// 构造一个新的专用 UIAF 信息 /// - /// uid + /// 服务提供器 /// 专用 UIAF 信息 - public static UIAFInfo Create() + public static UIAFInfo Create(IServiceProvider serviceProvider) { + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + return new() { ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(), ExportApp = SH.AppName, - ExportAppVersion = CoreEnvironment.Version.ToString(), + ExportAppVersion = hutaoOptions.Version.ToString(), UIAFVersion = UIAF.CurrentVersion, }; } 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 700cebe6..a8bacc4a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs @@ -60,17 +60,20 @@ internal sealed class UIGFInfo /// /// 构造一个新的专用 UIGF 信息 /// + /// 服务提供器 /// uid /// 专用 UIGF 信息 - public static UIGFInfo Create(string uid) + public static UIGFInfo Create(IServiceProvider serviceProvider, string uid) { + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + return new() { Uid = uid, Language = "zh-cn", ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(), ExportApp = SH.AppName, - ExportAppVersion = CoreEnvironment.Version.ToString(), + ExportAppVersion = hutaoOptions.Version.ToString(), UIGFVersion = UIGF.CurrentVersion, }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs index c541e5b3..00cfa0c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs @@ -60,17 +60,20 @@ internal sealed class UIIFInfo /// /// 构造一个新的专用 UIGF 信息 /// + /// 服务提供器 /// uid /// 专用 UIGF 信息 - public static UIIFInfo Create(string uid) + public static UIIFInfo Create(IServiceProvider serviceProvider, string uid) { + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + return new() { Uid = uid, Language = "zh-cn", ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(), ExportApp = "胡桃", - ExportAppVersion = CoreEnvironment.Version.ToString(), + ExportAppVersion = hutaoOptions.Version.ToString(), UIIFVersion = UIIF.CurrentVersion, }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs index 8a6fdde4..ab7ba6d6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs @@ -11,6 +11,8 @@ namespace Snap.Hutao.Model.Metadata; [HighQuality] internal sealed class ParameterFormat : IFormatProvider, ICustomFormatter { + private static readonly Lazy LazyFormat = new(); + /// /// 格式化 /// @@ -20,7 +22,7 @@ internal sealed class ParameterFormat : IFormatProvider, ICustomFormatter [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string Format(string str, object param) { - return string.Format(new ParameterFormat(), str, param); + return string.Format(LazyFormat.Value, str, param); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Program.cs b/src/Snap.Hutao/Snap.Hutao/Program.cs index ee3473d1..3ca4dba9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Program.cs +++ b/src/Snap.Hutao/Snap.Hutao/Program.cs @@ -1,16 +1,13 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.Mvvm.Messaging; using Microsoft.UI.Xaml; -using Snap.Hutao.Service; -using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Windows.Globalization; using WinRT; -[assembly:InternalsVisibleTo("Snap.Hutao.Test")] +// Visible to test project. +[assembly: InternalsVisibleTo("Snap.Hutao.Test")] namespace Snap.Hutao; @@ -20,67 +17,37 @@ namespace Snap.Hutao; [SuppressMessage("", "SH001")] public static partial class Program { + private static readonly ApplicationInitializationCallback AppInitializationCallback = InitializeApp; + [LibraryImport("Microsoft.ui.xaml.dll")] private static partial void XamlCheckProcessRequirements(); [STAThread] private static void Main(string[] args) { - _ = args; + System.Diagnostics.Debug.WriteLine($"[Arguments]:{args}"); XamlCheckProcessRequirements(); ComWrappersSupport.InitializeComWrappers(); - // by adding the using statement, we can dispose the injected services when we closing - using (ServiceProvider serviceProvider = InitializeDependencyInjection()) + // By adding the using statement, we can dispose the injected services when we closing + using (ServiceProvider serviceProvider = DependencyInjection.Initialize()) { - InitializeCulture(serviceProvider); + serviceProvider.InitializeCulture(); // In a Desktop app this runs a message pump internally, // and does not return until the application shuts down. - Application.Start(InitializeApp); + Application.Start(AppInitializationCallback); Control.ScopedPage.DisposePreviousScope(); } } private static void InitializeApp(ApplicationInitializationCallbackParams param) { - ThreadHelper.Initialize(); - _ = Ioc.Default.GetRequiredService(); - } + IServiceProvider serviceProvider = Ioc.Default; - private static void InitializeCulture(IServiceProvider serviceProvider) - { - AppOptions appOptions = serviceProvider.GetRequiredService(); - appOptions.PreviousCulture = CultureInfo.CurrentCulture; - - CultureInfo cultureInfo = appOptions.CurrentCulture; - - CultureInfo.CurrentCulture = cultureInfo; - CultureInfo.CurrentUICulture = cultureInfo; - ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name; - } - - private static ServiceProvider InitializeDependencyInjection() - { - ServiceProvider services = new ServiceCollection() - - // Microsoft extension - .AddLogging(builder => builder.AddDebug()) - .AddMemoryCache() - - // Hutao extensions - .AddJsonOptions() - .AddDatabase() - .AddInjections() - .AddHttpClients() - - // Discrete services - .AddSingleton(WeakReferenceMessenger.Default) - - .BuildServiceProvider(true); - - Ioc.Default.ConfigureServices(services); - return services; + ITaskContext taskContext = serviceProvider.GetRequiredService(); + ThreadHelper.Initialize(taskContext); + _ = serviceProvider.GetRequiredService(); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs index 4f132fdf..d6ca3fe5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs @@ -28,19 +28,22 @@ internal sealed class AchievementService : IAchievementService private readonly ILogger logger; private readonly DbCurrent dbCurrent; private readonly AchievementDbOperation achievementDbOperation; + private readonly IServiceProvider serviceProvider; private ObservableCollection? archiveCollection; /// /// 构造一个新的成就服务 /// + /// 服务提供器 /// 数据库上下文 /// 日志器 /// 消息器 - public AchievementService(AppDbContext appDbContext, ILogger logger, IMessenger messenger) + public AchievementService(IServiceProvider serviceProvider, AppDbContext appDbContext, ILogger logger, IMessenger messenger) { this.appDbContext = appDbContext; this.logger = logger; + this.serviceProvider = serviceProvider; dbCurrent = new(appDbContext.AchievementArchives, messenger); achievementDbOperation = new(appDbContext); @@ -174,7 +177,7 @@ internal sealed class AchievementService : IAchievementService UIAF uigf = new() { - Info = UIAFInfo.Create(), + Info = UIAFInfo.Create(serviceProvider), List = list, }; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs index 5ceced67..ac308585 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs @@ -9,6 +9,7 @@ namespace Snap.Hutao.Service; /// /// 应用程序选项 +/// 存储服务相关的选项 /// [Injection(InjectAs.Singleton)] internal sealed class AppOptions : DbStoreOptions diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs index 3070fc0d..353ea48e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs @@ -15,14 +15,16 @@ namespace Snap.Hutao.Service.GachaLog; internal sealed class GachaLogExportService : IGachaLogExportService { private readonly AppDbContext appDbContext; + private readonly IServiceProvider serviceProvider; /// /// 构造一个新的祈愿记录导出服务 /// /// 数据库上下文 - public GachaLogExportService(AppDbContext appDbContext) + public GachaLogExportService(IServiceProvider serviceProvider, AppDbContext appDbContext) { this.appDbContext = appDbContext; + this.serviceProvider = serviceProvider; } /// @@ -37,7 +39,7 @@ internal sealed class GachaLogExportService : IGachaLogExportService UIGF uigf = new() { - Info = UIGFInfo.Create(archive.Uid), + Info = UIGFInfo.Create(serviceProvider, archive.Uid), List = list, }; 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 aeea4eaa..8ce0967f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -21,16 +21,19 @@ internal sealed class PackageConverter { private readonly JsonSerializerOptions options; private readonly HttpClient httpClient; + private readonly IServiceProvider serviceProvider; /// /// 构造一个新的游戏文件转换器 /// + /// 服务提供器 /// Json序列化选项 /// http客户端 - public PackageConverter(JsonSerializerOptions options, HttpClient httpClient) + public PackageConverter(IServiceProvider serviceProvider, HttpClient httpClient) { - this.options = options; + options = serviceProvider.GetRequiredService(); this.httpClient = httpClient; + this.serviceProvider = serviceProvider; } /// @@ -228,7 +231,8 @@ internal sealed class PackageConverter } // Cache folder - string cacheFolder = Path.Combine(Core.CoreEnvironment.DataFolder, "ServerCache"); + Core.HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + string cacheFolder = Path.Combine(hutaoOptions.DataFolder, "ServerCache"); // 执行下载与移动操作 foreach (ItemOperationInfo info in operations) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index 46497ce2..dc32844f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -47,6 +47,7 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi /// 日志器 /// 内存缓存 public MetadataService( + Core.HutaoOptions hutaoOptions, IInfoBarService infoBarService, IHttpClientFactory httpClientFactory, JsonSerializerOptions options, @@ -60,7 +61,7 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi httpClient = httpClientFactory.CreateClient(nameof(MetadataService)); locale = GetTextMapLocale(); - metadataFolderPath = Path.Combine(Core.CoreEnvironment.DataFolder, "Metadata", locale); + metadataFolderPath = Path.Combine(hutaoOptions.DataFolder, "Metadata", locale); Directory.CreateDirectory(metadataFolderPath); } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index 5f8653a5..9351b397 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -69,7 +69,7 @@ TextWrapping="Wrap"/> diff --git a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs index 94a9ebee..ae5d380f 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs @@ -26,11 +26,18 @@ internal sealed partial class TitleView : UserControl [SuppressMessage("", "CA1822")] public string Title { + get + { + Core.HutaoOptions hutaoOptions = Ioc.Default.GetRequiredService(); + + string format = #if DEBUG - get => string.Format(SH.AppDevNameAndVersion, Core.CoreEnvironment.Version); + SH.AppDevNameAndVersion; #else - get => string.Format(SH.AppNameAndVersion, Core.CoreEnvironment.Version); + SH.AppNameAndVersion; #endif + return string.Format(format, hutaoOptions.Version); + } } /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/ExperimentalFeaturesViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/ExperimentalFeaturesViewModel.cs index 0b2388f0..2ebbaf08 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/ExperimentalFeaturesViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/ExperimentalFeaturesViewModel.cs @@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.EntityFrameworkCore; using Microsoft.UI.Xaml.Controls; using Microsoft.Windows.AppLifecycle; +using Snap.Hutao.Core; using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model.Entity.Database; using Windows.Storage; @@ -52,12 +53,14 @@ internal sealed class ExperimentalFeaturesViewModel : ObservableObject private Task OpenCacheFolderAsync() { - return Launcher.LaunchFolderAsync(ApplicationData.Current.LocalCacheFolder).AsTask(); + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + return Launcher.LaunchFolderPathAsync(hutaoOptions.LocalCache).AsTask(); } private Task OpenDataFolderAsync() { - return Launcher.LaunchFolderPathAsync(Core.CoreEnvironment.DataFolder).AsTask(); + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + return Launcher.LaunchFolderPathAsync(hutaoOptions.DataFolder).AsTask(); } private async Task DangerousDeleteUsersAsync() diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs index 9cde203e..c0acfbb7 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Windows.AppLifecycle; +using Snap.Hutao.Core; using Snap.Hutao.Core.IO; using Snap.Hutao.Core.IO.DataTransfer; using Snap.Hutao.Core.LifeCycle; @@ -67,6 +68,7 @@ internal sealed class SettingViewModel : Abstraction.ViewModel Experimental = serviceProvider.GetRequiredService(); Options = serviceProvider.GetRequiredService(); UserOptions = serviceProvider.GetRequiredService(); + HutaoOptions = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; selectedCulture = cultures.FirstOrDefault(c => c.Value == Options.CurrentCulture.Name); @@ -82,15 +84,6 @@ internal sealed class SettingViewModel : Abstraction.ViewModel NavigateToHutaoPassportCommand = new RelayCommand(NavigateToHutaoPassport); } - /// - /// 版本 - /// - [SuppressMessage("", "CA1822")] - public string AppVersion - { - get => Core.CoreEnvironment.Version.ToString(); - } - /// /// WebView2 版本 /// @@ -100,20 +93,16 @@ internal sealed class SettingViewModel : Abstraction.ViewModel get => Core.WebView2Helper.Version; } - /// - /// 设备Id - /// - [SuppressMessage("", "CA1822")] - public string DeviceId - { - get => Core.CoreEnvironment.HutaoDeviceId; - } - /// /// 应用程序设置 /// public AppOptions Options { get; } + /// + /// 胡桃选项 + /// + public HutaoOptions HutaoOptions { get; } + /// /// 胡桃用户选项 /// @@ -317,7 +306,7 @@ internal sealed class SettingViewModel : Abstraction.ViewModel try { - Clipboard.SetText(DeviceId); + Clipboard.SetText(HutaoOptions.DeviceId); infoBarService.Success(SH.ViewModelSettingCopyDeviceIdSuccess); } catch (COMException ex) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs index 40071f23..d226b73a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs @@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.WinUI.Notifications; +using Snap.Hutao.Core; using Snap.Hutao.Core.Caching; using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.IO; @@ -146,7 +147,8 @@ internal sealed class WelcomeViewModel : ObservableObject public DownloadSummary(IServiceProvider serviceProvider, string fileName) { httpClient = serviceProvider.GetRequiredService(); - httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Core.CoreEnvironment.CommonUA); + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent); this.serviceProvider = serviceProvider; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs index 4fb6f6e8..ae15c1ff 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs @@ -15,29 +15,29 @@ internal static class CoreWebView2Extension /// /// 设置 移动端UA /// - /// webview2 + /// webView2 /// 链式调用的WebView2 public static CoreWebView2 SetMobileUserAgent(this CoreWebView2 webView) { - webView.Settings.UserAgent = Core.CoreEnvironment.HoyolabMobileUA; + webView.Settings.UserAgent = Core.HoyolabOptions.MobileUserAgent; return webView; } /// /// 设置 移动端OsUA /// - /// webview2 + /// webView2 /// 链式调用的WebView2 public static CoreWebView2 SetMobileOverseaUserAgent(this CoreWebView2 webView) { - webView.Settings.UserAgent = Core.CoreEnvironment.HoyolabOsMobileUA; + webView.Settings.UserAgent = Core.HoyolabOptions.MobileUserAgentOversea; return webView; } /// /// 设置WebView2的Cookie /// - /// webview2 + /// webView2 /// CookieToken /// LToken /// SToken diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs index 52b35bd0..4bc55b5e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs @@ -87,8 +87,8 @@ internal class MiHoYoJSInterface Data = new Dictionary() { { "x-rpc-client_type", "5" }, - { "x-rpc-device_id", Core.CoreEnvironment.HoyolabDeviceId }, - { "x-rpc-app_version", Core.CoreEnvironment.HoyolabXrpcVersion }, + { "x-rpc-device_id", Core.HoyolabOptions.DeviceId }, + { "x-rpc-app_version", Core.HoyolabOptions.XrpcVersion }, }, }; } @@ -120,7 +120,7 @@ internal class MiHoYoJSInterface /// 响应 public virtual JsResult> GetDynamicSecrectV1(JsParam param) { - string salt = Core.CoreEnvironment.DynamicSecretSalts[SaltType.LK2]; + string salt = Core.HoyolabOptions.Salts[SaltType.LK2]; long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); string r = GetRandomString(); string check = Core.Convert.ToMd5HexString($"salt={salt}&t={t}&r={r}").ToLowerInvariant(); @@ -151,7 +151,7 @@ internal class MiHoYoJSInterface public virtual JsResult> GetDynamicSecrectV2(JsParam param) { // TODO: Salt X4 for hoyolab user - string salt = Core.CoreEnvironment.DynamicSecretSalts[SaltType.X4]; + string salt = Core.HoyolabOptions.Salts[SaltType.X4]; long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); int r = GetRandom(); string b = param.Payload.Body; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterfaceOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterfaceOversea.cs index b8ea6811..56d3c1c4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterfaceOversea.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterfaceOversea.cs @@ -35,8 +35,8 @@ internal sealed class SignInJSInterfaceOversea : MiHoYoJSInterface Data = new Dictionary() { { "x-rpc-client_type", "2" }, - { "x-rpc-device_id", Core.CoreEnvironment.HoyolabDeviceId }, - { "x-rpc-app_version", Core.CoreEnvironment.HoyolabOsXrpcVersion }, + { "x-rpc-device_id", Core.HoyolabOptions.DeviceId }, + { "x-rpc-app_version", Core.HoyolabOptions.XrpcVersionOversea }, }, }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJsInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJsInterface.cs index cd8a9fe4..46ae796c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJsInterface.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJsInterface.cs @@ -26,8 +26,8 @@ internal sealed class SignInJsInterface : MiHoYoJSInterface Data = new Dictionary() { { "x-rpc-client_type", "2" }, - { "x-rpc-device_id", Core.CoreEnvironment.HoyolabDeviceId }, - { "x-rpc-app_version", Core.CoreEnvironment.HoyolabXrpcVersion }, + { "x-rpc-device_id", Core.HoyolabOptions.DeviceId }, + { "x-rpc-app_version", Core.HoyolabOptions.XrpcVersion }, }, }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretHandler.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretHandler.cs index 0aaeb578..1d4814f4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretHandler.cs @@ -33,7 +33,7 @@ internal sealed class DynamicSecretHandler : DelegatingHandler private static async Task ProcessRequestWithOptionsAsync(HttpRequestMessage request, DynamicSecretCreationOptions options, CancellationToken token) { - string salt = Core.CoreEnvironment.DynamicSecretSalts[options.SaltType]; + string salt = Core.HoyolabOptions.Salts[options.SaltType]; long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaLogUploadClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaLogUploadClient.cs index 84ef3834..cc179859 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaLogUploadClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaLogUploadClient.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core; using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Web.Hutao.Log; using Snap.Hutao.Web.Response; @@ -29,11 +30,12 @@ internal sealed class HomaLogUploadClient /// /// 上传日志 /// + /// 服务提供器 /// 异常 /// 任务 - public async Task UploadLogAsync(Exception exception) + public async Task UploadLogAsync(IServiceProvider serviceProvider, Exception exception) { - HutaoLog log = BuildFromException(exception); + HutaoLog log = BuildFromException(serviceProvider, exception); Response? a = await httpClient .TryCatchPostAsJsonAsync>(HutaoEndpoints.HutaoLogUpload, log) @@ -41,13 +43,15 @@ internal sealed class HomaLogUploadClient return a?.Data; } - private static HutaoLog BuildFromException(Exception exception) + private static HutaoLog BuildFromException(IServiceProvider serviceProvider, Exception exception) { + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); + return new() { - Id = Core.CoreEnvironment.HutaoDeviceId, + Id = hutaoOptions.DeviceId, Time = DateTimeOffset.Now.ToUnixTimeMilliseconds(), - Info = exception.ToString(), + Info = Core.ExceptionService.ExceptionFormat.Format(exception), }; } } From 9657df36c24b83776b84c2fa1a7533a53baf8345 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 26 Apr 2023 21:43:21 +0800 Subject: [PATCH 2/4] Generate Enum Localization --- .../HttpClientGenerator.cs | 2 +- .../DependencyInjection/InjectionGenerator.cs | 12 +- .../Enum/LocalizedEnumGenerator.cs | 133 +++++++++++++++ .../Snap.Hutao.SourceGeneration/JsonParser.cs | 2 +- .../CSharpLanguageFeatureTest.cs | 23 ++- src/Snap.Hutao/Snap.Hutao.sln | 2 +- src/Snap.Hutao/Snap.Hutao/App.xaml | 4 +- src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 1 + .../Control/Behavior/AutoWidthBehavior.cs | 4 +- .../Snap.Hutao/Control/BindingProxy.cs | 1 + .../Extension/ContentDialogExtension.cs | 38 +++-- .../Snap.Hutao/Control/Image/CachedImage.cs | 5 +- .../Control/Image/CompositionImage.cs | 12 +- .../Snap.Hutao/Control/Image/GradientStop.cs | 8 +- .../Control/Media/SoftwareBitmapExtension.cs | 17 +- .../Control/Panel/PanelSelector.xaml.cs | 14 +- .../Control/Text/DescriptionTextBlock.cs | 1 + .../Snap.Hutao/Core/Caching/IImageCache.cs | 10 +- .../Snap.Hutao/Core/Caching/ImageCache.cs | 54 +++--- .../Snap.Hutao/Core/Database/DbCurrent.cs | 1 + .../Core/Database/DbSetExtension.cs | 65 ++++---- ...bleExtension.cs => SelectableExtension.cs} | 2 +- .../{ => Abstraction}/ICastableService.cs | 2 +- .../{ => Abstraction}/INamedService.cs | 2 +- .../{ => Abstraction}/IOverseaSupport.cs | 2 +- .../CastableServiceExtension.cs | 2 + .../EnumerableServiceExtension.cs | 1 + .../IocHttpClientConfiguration.cs | 1 + .../ServiceCollectionExtension.cs | 1 + .../Core/ExceptionService/ExceptionFormat.cs | 2 +- .../UserdataCorruptedException.cs | 2 +- .../Snap.Hutao/Core/HutaoOptions.cs | 29 ++++ .../Core/IO/DataTransfer/Clipboard.cs | 11 +- .../Core/Json/Annotation/JsonEnumAttribute.cs | 4 +- .../Core/Json/Annotation/JsonSerializeType.cs | 6 +- .../Converter/ConfigurableEnumConverter.cs | 63 ------- .../Json/Converter/UnsafeEnumConverter.cs | 156 ++++++++++++++++++ .../Snap.Hutao/Core/LifeCycle/Activation.cs | 27 ++- .../Snap.Hutao/Core/Setting/SettingKeys.cs | 3 + .../Core/Threading/SemaphoreSlimExtension.cs | 25 +-- .../{TaskExtensions.cs => TaskExtension.cs} | 6 +- .../Snap.Hutao/Core/WebView2Helper.cs | 1 + .../Core/Windowing/ExtendedWindow.cs | 50 +++--- ...indowSource.cs => IWindowOptionsSource.cs} | 18 +- .../Snap.Hutao/Core/Windowing/Persistence.cs | 25 +-- .../Core/Windowing/WindowOptions.cs | 33 ++-- .../Core/Windowing/WindowSubclass.cs | 19 ++- .../Snap.Hutao/Extension/EnumExtension.cs | 87 ---------- .../Snap.Hutao/Extension/ObjectExtension.cs | 4 +- .../Factory/ContentDialogFactory.cs | 15 +- .../Snap.Hutao/Factory/PickerFactory.cs | 4 +- .../Snap.Hutao/IdentityStructs.json | 30 +++- .../Snap.Hutao/LaunchGameWindow.xaml.cs | 14 +- src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs | 23 +-- .../Model/Calculable/CalculableAvatar.cs | 9 +- .../Model/Calculable/CalculableSkill.cs | 3 +- .../Model/Calculable/CalculableWeapon.cs | 7 +- .../Model/Calculable/ICalculable.cs | 2 +- .../Model/Calculable/ICalculableAvatar.cs | 2 +- .../Snap.Hutao/Model/Entity/Achievement.cs | 12 +- .../Snap.Hutao/Model/Entity/DailyNoteEntry.cs | 9 +- .../Model/Entity/Database/AppDbContext.cs | 1 - .../Snap.Hutao/Model/Entity/GachaArchive.cs | 43 +++-- .../Snap.Hutao/Model/Entity/GachaItem.cs | 8 +- .../Snap.Hutao/Model/Entity/GameAccount.cs | 10 +- .../Model/Entity/SettingEntry.Constant.cs | 95 +++++++++++ .../Snap.Hutao/Model/Entity/SettingEntry.cs | 90 +--------- .../Model/Entity/SpiralAbyssEntry.cs | 2 +- .../Snap.Hutao/Model/Entity/User.cs | 6 +- .../{Binding => }/IEntityWithMetadata.cs | 2 +- .../Model/{Binding => }/INameIcon.cs | 2 +- .../Model/{Binding => }/INameIconSide.cs | 2 +- .../Model/InterChange/GachaLog/UIGFItem.cs | 2 +- .../Model/Intrinsic/AssociationType.cs | 1 + .../Snap.Hutao/Model/Intrinsic/BodyType.cs | 1 + .../Snap.Hutao/Model/Intrinsic/ElementType.cs | 24 ++- .../Model/Intrinsic/FightProperty.cs | 1 + .../Snap.Hutao/Model/Intrinsic/ItemQuality.cs | 1 + .../Snap.Hutao/Model/Intrinsic/WeaponType.cs | 1 + .../Snap.Hutao/Model/{Binding => }/Item.cs | 2 +- .../Model/Metadata/Achievement/Achievement.cs | 2 +- .../Metadata/Achievement/AchievementGoal.cs | 4 +- .../Model/Metadata/Achievement/Reward.cs | 4 +- .../Metadata/Annotation/EnumExtension.cs | 1 + .../Metadata/Avatar/Avatar.Implementation.cs | 2 +- .../Model/Metadata/Avatar/Costume.cs | 4 +- .../Model/Metadata/Avatar/ProudableSkill.cs | 4 +- .../Snap.Hutao/Model/Metadata/Avatar/Skill.cs | 4 +- .../Model/Metadata/Avatar/SkillDepot.cs | 46 +++--- ...cs => DescriptionsParametersDescriptor.cs} | 73 +++----- .../Converter/ElementNameIconConverter.cs | 46 +++--- ...r.cs => PropertiesParametersDescriptor.cs} | 4 +- .../Metadata/Monster/MonsterBaseValue.cs | 16 +- .../Metadata/Weapon/Weapon.Implementation.cs | 4 +- .../Localization/LocalizationAttribute.cs | 12 ++ .../Localization}/LocalizationKeyAttribute.cs | 4 +- .../Resource/Localization/SH.Designer.cs | 9 + .../Service/AvatarInfo/AvatarInfoService.cs | 2 +- .../AvatarInfo/Factory/ISummaryFactory.cs | 2 +- .../Factory/SummaryAvatarFactory.cs | 15 +- .../AvatarInfo/Factory/SummaryFactory.cs | 2 +- .../Factory/SummaryFightPropertyMapHelper.cs | 20 +-- .../AvatarInfo/Factory/SummaryHelper.cs | 7 +- .../Factory/SummaryReliquaryFactory.cs | 8 +- .../Service/AvatarInfo/IAvatarInfoService.cs | 2 +- .../Service/Cultivation/CultivationService.cs | 2 +- .../Service/DailyNote/DailyNoteService.cs | 6 +- .../GachaArchiveInitializationContext.cs | 49 ++++++ .../Service/GachaLog/GachaArchives.cs | 14 -- .../Service/GachaLog/GachaLogFetchState.cs | 2 +- .../Service/GachaLog/GachaLogImportService.cs | 8 +- .../Service/GachaLog/GachaLogService.cs | 54 +++++- .../GachaLog/GachaLogServiceContext.cs | 2 +- .../QueryProvider/IGachaLogQueryProvider.cs | 2 + .../Snap.Hutao/Service/Game/LaunchScheme.cs | 10 ++ .../Service/Game/Locator/IGameLocator.cs | 2 + .../Service/Hutao/IHutaoUserService.cs | 2 + .../Service/Metadata/IMetadataService.cs | 1 + .../IToastNotificationLifeTime.cs | 11 ++ .../Notification/ToastNotificationLifeTime.cs | 25 +++ .../Snap.Hutao/Service/User/UserService.cs | 6 +- .../GachaLogRefreshProgressDialog.xaml.cs | 2 +- .../Snap.Hutao/View/Page/SettingPage.xaml | 2 +- .../Achievement/AchievementFinishPercent.cs | 3 +- .../Achievement/AchievementGoalView.cs | 5 +- .../Achievement/AchievementImporter.cs | 2 +- .../ViewModel/Achievement/AchievementView.cs | 2 +- .../Achievement/AchievementViewModel.cs | 3 +- .../AvatarProperty/AvatarProperty.cs | 3 +- .../AvatarProperty/AvatarPropertyViewModel.cs | 1 - .../ViewModel/AvatarProperty/AvatarView.cs | 3 +- .../AvatarProperty/ConstellationView.cs | 2 +- .../ViewModel/AvatarProperty/Equip.cs | 3 +- .../AvatarProperty/NameDescription.cs | 2 +- .../AvatarProperty/NameIconDescription.cs | 4 +- .../AvatarProperty/ReliquarySubProperty.cs | 2 +- .../ViewModel/AvatarProperty/ReliquaryView.cs | 2 +- .../ViewModel/AvatarProperty/SkillView.cs | 2 +- .../ViewModel/AvatarProperty/Summary.cs | 2 +- .../ViewModel/AvatarProperty/WeaponView.cs | 7 +- .../ViewModel/Complex/AvatarView.cs | 2 +- .../ViewModel/Complex/WeaponView.cs | 2 +- .../Cultivation/CultivateEntryView.cs | 2 +- .../Cultivation/CultivateItemView.cs | 2 +- .../Cultivation/InventoryItemView.cs | 2 +- .../ViewModel/GachaLog/StatisticsItem.cs | 2 +- .../ViewModel/GachaLog/SummaryItem.cs | 2 +- .../Snap.Hutao/ViewModel/SettingViewModel.cs | 9 - .../ViewModel/SpiralAbyss/AvatarView.cs | 2 +- .../ViewModel/Wiki/BaseValueInfo.cs | 2 +- .../Snap.Hutao/Web/Enka/Model/AvatarInfo.cs | 2 +- .../Web/Hoyolab/Bbs/User/IUserClient.cs | 1 + .../Snap.Hutao/Web/Hoyolab/Cookie.cs | 12 +- .../Hk4e/Event/GachaInfo/GachaConfigType.cs | 1 + .../Hk4e/Event/GachaInfo/GachaLogItem.cs | 4 +- .../Web/Hoyolab/Passport/IPassportClient.cs | 1 + .../Takumi/GameRecord/IGameRecordClient.cs | 1 + 157 files changed, 1211 insertions(+), 800 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao.SourceGeneration/Enum/LocalizedEnumGenerator.cs rename src/Snap.Hutao/Snap.Hutao/Core/Database/{EnumerableExtension.cs => SelectableExtension.cs} (94%) rename src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/{ => Abstraction}/ICastableService.cs (75%) rename src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/{ => Abstraction}/INamedService.cs (84%) rename src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/{ => Abstraction}/IOverseaSupport.cs (83%) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/ConfigurableEnumConverter.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/UnsafeEnumConverter.cs rename src/Snap.Hutao/Snap.Hutao/Core/Threading/{TaskExtensions.cs => TaskExtension.cs} (95%) rename src/Snap.Hutao/Snap.Hutao/Core/Windowing/{IExtendedWindowSource.cs => IWindowOptionsSource.cs} (62%) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Extension/EnumExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs rename src/Snap.Hutao/Snap.Hutao/Model/{Binding => }/IEntityWithMetadata.cs (93%) rename src/Snap.Hutao/Snap.Hutao/Model/{Binding => }/INameIcon.cs (90%) rename src/Snap.Hutao/Snap.Hutao/Model/{Binding => }/INameIconSide.cs (89%) rename src/Snap.Hutao/Snap.Hutao/Model/{Binding => }/Item.cs (94%) rename src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/{ParameterDescriptor.cs => DescriptionsParametersDescriptor.cs} (55%) rename src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/{PropertyDescriptor.cs => PropertiesParametersDescriptor.cs} (95%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Resource/Localization/LocalizationAttribute.cs rename src/Snap.Hutao/Snap.Hutao/{Core/Annotation => Resource/Localization}/LocalizationKeyAttribute.cs (81%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveInitializationContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Notification/IToastNotificationLifeTime.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Notification/ToastNotificationLifeTime.cs diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs index 6006c3b8..ea7a50a0 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs @@ -33,7 +33,7 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator { IncrementalValueProvider> injectionClasses = context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass) - .Where(GeneratorSyntaxContext2.NotNull)! + .Where(GeneratorSyntaxContext2.NotNull) .Collect(); context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddHttpClientsImplementation); diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs index 567fbd7c..1b5630ce 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs @@ -26,9 +26,9 @@ internal sealed class InjectionGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { - IncrementalValueProvider> injectionClasses = - context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass) - .Where(GeneratorSyntaxContext2.NotNull)! + IncrementalValueProvider> injectionClasses = context.SyntaxProvider + .CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass) + .Where(GeneratorSyntaxContext2.NotNull) .Collect(); context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddInjectionsImplementation); @@ -96,13 +96,13 @@ internal sealed class InjectionGenerator : IIncrementalGenerator switch (injectAsName) { case InjectAsSingletonName: - lineBuilder.Append(@" services.AddSingleton<"); + lineBuilder.Append(" services.AddSingleton<"); break; case InjectAsTransientName: - lineBuilder.Append(@" services.AddTransient<"); + lineBuilder.Append(" services.AddTransient<"); break; case InjectAsScopedName: - lineBuilder.Append(@" services.AddScoped<"); + lineBuilder.Append(" services.AddScoped<"); break; default: production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, null, injectAsName)); diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Enum/LocalizedEnumGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Enum/LocalizedEnumGenerator.cs new file mode 100644 index 00000000..4ab25d43 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Enum/LocalizedEnumGenerator.cs @@ -0,0 +1,133 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Snap.Hutao.SourceGeneration.Primitive; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; + +namespace Snap.Hutao.SourceGeneration.Enum; + +[Generator(LanguageNames.CSharp)] +internal class LocalizedEnumGenerator : IIncrementalGenerator +{ + private const string AttributeName = "Snap.Hutao.Resource.Localization.LocalizationAttribute"; + private const string LocalizationKeyName = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider localizationEnums = context.SyntaxProvider + .CreateSyntaxProvider(FilterAttributedEnums, LocalizationEnum) + .Where(GeneratorSyntaxContext2.NotNull); + + context.RegisterSourceOutput(localizationEnums, GenerateGetLocalizedDescriptionImplementation); + } + + private static bool FilterAttributedEnums(SyntaxNode node, CancellationToken token) + { + return node is EnumDeclarationSyntax enumDeclarationSyntax && enumDeclarationSyntax.AttributeLists.Count > 0; + } + + private static GeneratorSyntaxContext2 LocalizationEnum(GeneratorSyntaxContext context, CancellationToken token) + { + if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol enumSymbol) + { + ImmutableArray attributes = enumSymbol.GetAttributes(); + if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName)) + { + return new(context, enumSymbol, attributes); + } + } + + return default; + } + + private static void GenerateGetLocalizedDescriptionImplementation(SourceProductionContext context, GeneratorSyntaxContext2 context2) + { + StringBuilder sourceBuilder = new StringBuilder().Append($$""" + // Copyright (c) DGP Studio. All rights reserved. + // Licensed under the MIT license. + + namespace Snap.Hutao.Resource.Localization; + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}","1.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + internal static class {{context2.Symbol.Name}}Extension + { + /// + /// 获取本地化的描述 + /// + /// 枚举值 + /// 本地化的描述 + public static string GetLocalizedDescription(this {{context2.Symbol}} value) + { + string key = value switch + { + + """); + + FillUpWithSwitchBranches(sourceBuilder, context2); + + sourceBuilder.Append($$""" + _ => string.Empty, + }; + + if (string.IsNullOrEmpty(key)) + { + return Enum.GetName(value); + } + else + { + return SH.ResourceManager.GetString(key); + } + } + + /// + /// 获取本地化的描述 + /// + /// 枚举值 + /// 本地化的描述 + [return:MaybeNull] + public static string GetLocalizedDescriptionOrDefault(this {{context2.Symbol}} value) + { + string key = value switch + { + + """); + + FillUpWithSwitchBranches(sourceBuilder, context2); + + sourceBuilder.Append($$""" + _ => string.Empty, + }; + + return SH.ResourceManager.GetString(key); + } + } + """); + + context.AddSource($"{context2.Symbol.Name}Extension.g.cs", sourceBuilder.ToString()); + } + + private static void FillUpWithSwitchBranches(StringBuilder sourceBuilder, GeneratorSyntaxContext2 context) + { + EnumDeclarationSyntax enumSyntax = (EnumDeclarationSyntax)context.Context.Node; + + foreach(EnumMemberDeclarationSyntax enumMemberSyntax in enumSyntax.Members) + { + if (context.Context.SemanticModel.GetDeclaredSymbol(enumMemberSyntax) is IFieldSymbol fieldSymbol) + { + AttributeData? localizationKeyInfo = fieldSymbol.GetAttributes() + .SingleOrDefault(data => data.AttributeClass!.ToDisplayString() == LocalizationKeyName); + if (localizationKeyInfo != null) + { + sourceBuilder.Append(" ").Append(fieldSymbol).Append(" => \"").Append(localizationKeyInfo.ConstructorArguments[0].Value).AppendLine("\","); + } + } + } + + sourceBuilder.Append($""" + + """); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/JsonParser.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/JsonParser.cs index ce9ac3d6..1aed7884 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/JsonParser.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/JsonParser.cs @@ -205,7 +205,7 @@ public static class JsonParser try { - return Enum.Parse(type, json, false); + return System.Enum.Parse(type, json, false); } catch { diff --git a/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs index 3dce6ca8..85e8c873 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Snap.Hutao.Test; @@ -63,9 +64,27 @@ public class CSharpLanguageFeatureTest } [TestMethod] - public void GetTwiceOnPropertyResultsSame() + public void GetTwiceOnPropertyResultsNotSame() { - Assert.AreEqual(UUID, UUID); + Assert.AreNotEqual(UUID, UUID); + } + + [TestMethod] + public void ListOfStringCanEnumerateAsReadOnlySpanOfChar() + { + List strings = new() + { + "a", "b", "c" + }; + + int count = 0; + foreach (ReadOnlySpan chars in strings) + { + Assert.IsTrue(chars.Length == 1); + ++count; + } + + Assert.AreEqual(3, count); } public static Guid UUID { get => Guid.NewGuid(); } diff --git a/src/Snap.Hutao/Snap.Hutao.sln b/src/Snap.Hutao/Snap.Hutao.sln index 12a43506..69968c32 100644 --- a/src/Snap.Hutao/Snap.Hutao.sln +++ b/src/Snap.Hutao/Snap.Hutao.sln @@ -12,7 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml b/src/Snap.Hutao/Snap.Hutao/App.xaml index 5d2c689c..3a4da8b0 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml @@ -89,7 +89,7 @@ - + @@ -98,7 +98,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index f19a1c21..856135f6 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -36,6 +36,7 @@ public sealed partial class App : Application logger = serviceProvider.GetRequiredService>(); serviceProvider.GetRequiredService().Record(this); + this.serviceProvider = serviceProvider; } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/AutoWidthBehavior.cs b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/AutoWidthBehavior.cs index 6837f71f..8215a032 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/AutoWidthBehavior.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/AutoWidthBehavior.cs @@ -12,8 +12,8 @@ namespace Snap.Hutao.Control.Behavior; [HighQuality] internal sealed class AutoWidthBehavior : BehaviorBase { - private static readonly DependencyProperty TargetWidthProperty = Property.Depend(nameof(TargetWidth), 320D); - private static readonly DependencyProperty TargetHeightProperty = Property.Depend(nameof(TargetHeight), 1024D); + private static readonly DependencyProperty TargetWidthProperty = Property.DependBoxed(nameof(TargetWidth), BoxedValues.DoubleOne); + private static readonly DependencyProperty TargetHeightProperty = Property.DependBoxed(nameof(TargetHeight), BoxedValues.DoubleOne); /// /// 目标宽度 diff --git a/src/Snap.Hutao/Snap.Hutao/Control/BindingProxy.cs b/src/Snap.Hutao/Snap.Hutao/Control/BindingProxy.cs index 2065e1eb..873fe745 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/BindingProxy.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/BindingProxy.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; namespace Snap.Hutao.Control; diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtension.cs index e75bb321..4a89e01e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtension.cs @@ -15,30 +15,36 @@ internal static class ContentDialogExtension /// 阻止用户交互 /// /// 对话框 + /// 任务上下文 /// 用于恢复用户交互 - public static async ValueTask BlockAsync(this ContentDialog contentDialog) + public static async ValueTask BlockAsync(this ContentDialog contentDialog, ITaskContext taskContext) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); contentDialog.ShowAsync().AsTask().SafeForget(); // E_ASYNC_OPERATION_NOT_STARTED 0x80000019 // Only a single ContentDialog can be open at any time. - return new ContentDialogHider(contentDialog); + return new ContentDialogHider(contentDialog, taskContext); + } +} + +[SuppressMessage("", "SA1201")] +[SuppressMessage("", "SA1400")] +[SuppressMessage("", "SA1600")] +file readonly struct ContentDialogHider : IDisposable +{ + private readonly ContentDialog contentDialog; + private readonly ITaskContext taskContext; + + public ContentDialogHider(ContentDialog contentDialog, ITaskContext taskContext) + { + this.contentDialog = contentDialog; + this.taskContext = taskContext; } - private class ContentDialogHider : IDisposable + public void Dispose() { - private readonly ContentDialog contentDialog; - - public ContentDialogHider(ContentDialog contentDialog) - { - this.contentDialog = contentDialog; - } - - public void Dispose() - { - // Hide() must be called on main thread. - ThreadHelper.InvokeOnMainThread(contentDialog.Hide); - } + // Hide() must be called on main thread. + taskContext.InvokeOnMainThread(contentDialog.Hide); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs index 44f581d1..53dbfc50 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs @@ -27,8 +27,7 @@ internal sealed class CachedImage : ImageEx /// protected override async Task ProvideCachedResourceAsync(Uri imageUri, CancellationToken token) { - // We can only use Ioc to retrive IImageCache, - // no IServiceProvider is available. + // We can only use Ioc to retrieve IImageCache, no IServiceProvider is available. IImageCache imageCache = Ioc.Default.GetRequiredService(); try @@ -47,7 +46,7 @@ internal sealed class CachedImage : ImageEx catch (COMException) { // The image is corrupted, remove it. - imageCache.Remove(imageUri.Enumerate()); + imageCache.Remove(imageUri); return null; } catch (OperationCanceledException) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs index f8428b1b..dc3a4d1a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs @@ -35,7 +35,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control /// public CompositionImage() { - serviceProvider = Ioc.Default.GetRequiredService(); + serviceProvider = Ioc.Default; AllowFocusOnInteraction = false; IsDoubleTapEnabled = false; @@ -123,7 +123,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control private static void OnApplyImageFailed(IServiceProvider serviceProvider, Uri? uri, Exception exception) { - IInfoBarService infoBarService = Ioc.Default.GetRequiredService(); + IInfoBarService infoBarService = serviceProvider.GetRequiredService(); if (exception is HttpRequestException httpRequestException) { @@ -157,11 +157,11 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control } catch (COMException) { - imageCache.Remove(uri.Enumerate()); + imageCache.Remove(uri); } catch (IOException) { - imageCache.Remove(uri.Enumerate()); + imageCache.Remove(uri); } if (imageSurface != null) @@ -183,7 +183,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control if (EnableLazyLoading) { - await AnimationBuilder.Create().Opacity(1d, 0d).StartAsync(this, token).ConfigureAwait(true); + await AnimationBuilder.Create().Opacity(from: 0D, to: 1D).StartAsync(this, token).ConfigureAwait(true); } else { @@ -200,7 +200,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control if (EnableLazyLoading) { - await AnimationBuilder.Create().Opacity(0d, 1d).StartAsync(this, token).ConfigureAwait(true); + await AnimationBuilder.Create().Opacity(from: 1D, to: 0D).StartAsync(this, token).ConfigureAwait(true); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/GradientStop.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/GradientStop.cs index 6c54967a..2685ea36 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/GradientStop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/GradientStop.cs @@ -9,17 +9,17 @@ namespace Snap.Hutao.Control.Image; /// 便宜 /// 颜色 [HighQuality] -internal struct GradientStop +internal readonly struct GradientStop { /// - /// 便宜 + /// 偏移 /// - public float Offset; + public readonly float Offset; /// /// 颜色 /// - public Windows.UI.Color Color; + public readonly Windows.UI.Color Color; /// /// 构造一个新的渐变锚点 diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Media/SoftwareBitmapExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Media/SoftwareBitmapExtension.cs index df16eec0..c6f10f2d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Media/SoftwareBitmapExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Media/SoftwareBitmapExtension.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using System.Runtime.InteropServices; using Windows.Foundation; using Windows.Graphics.Imaging; using Windows.Win32; @@ -27,15 +28,15 @@ internal static class SoftwareBitmapExtension using (IMemoryBufferReference reference = buffer.CreateReference()) { reference.As().GetBuffer(out byte* data, out uint length); - - for (int i = 0; i < length; i += 4) + Span bytes = new(data, unchecked((int)length / (sizeof(Bgra8) / sizeof(uint)))); + foreach (ref Bgra8 pixel in bytes) { - Bgra8* pixel = (Bgra8*)(data + i); - byte baseAlpha = pixel->A; - pixel->B = (byte)(((pixel->B * baseAlpha) + (tint.B * (0xFF - baseAlpha))) / 0xFF); - pixel->G = (byte)(((pixel->G * baseAlpha) + (tint.G * (0xFF - baseAlpha))) / 0xFF); - pixel->R = (byte)(((pixel->R * baseAlpha) + (tint.R * (0xFF - baseAlpha))) / 0xFF); - pixel->A = 0xFF; + byte baseAlpha = pixel.A; + int opposite = 0xFF - baseAlpha; + pixel.B = (byte)(((pixel.B * baseAlpha) + (tint.B * opposite)) / 0xFF); + pixel.G = (byte)(((pixel.G * baseAlpha) + (tint.G * opposite)) / 0xFF); + pixel.R = (byte)(((pixel.R * baseAlpha) + (tint.R * opposite)) / 0xFF); + pixel.A = 0xFF; } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs b/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs index c2028e10..0d33234f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs @@ -33,6 +33,11 @@ internal sealed partial class PanelSelector : SplitButton set => SetValue(CurrentProperty, value); } + private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) + { + OnCurrentChanged((PanelSelector)obj, (string)args.NewValue); + } + private static void OnCurrentChanged(PanelSelector sender, string current) { MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout; @@ -43,21 +48,16 @@ internal sealed partial class PanelSelector : SplitButton sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph; } - private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) - { - OnCurrentChanged((PanelSelector)obj, (string)args.NewValue); - } - private void OnRootControlLoaded(object sender, RoutedEventArgs e) { // because the GroupName shares in global - // we have to impl a control scoped GroupName. + // we have to implement a control scoped GroupName. PanelSelector selector = (PanelSelector)sender; MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout; int hash = GetHashCode(); foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast()) { - item.GroupName = $"PanelSelector{hash}Group"; + item.GroupName = $"{nameof(PanelSelector)}GroupOf@{hash}"; } OnCurrentChanged(selector, Current); diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs index 952638c5..a33a5eb3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs @@ -153,6 +153,7 @@ internal sealed class DescriptionTextBlock : ContentControl private void OnActualThemeChanged(FrameworkElement sender, object args) { + // Simply re-apply texts ApplyDescription((TextBlock)Content, Description); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs index d05f4407..782d80e5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; + namespace Snap.Hutao.Core.Caching; /// @@ -21,7 +23,13 @@ internal interface IImageCache : ICastableService /// Removed items based on uri list passed /// /// Enumerable uri list - void Remove(IEnumerable uriForCachedItems); + void Remove(in ReadOnlySpan uriForCachedItems); + + /// + /// Removed item based on uri passed + /// + /// uri + void Remove(Uri uriForCachedItem); /// /// Removes invalid cached files diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs index 81181c6a..93ead897 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs @@ -36,6 +36,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation private readonly ILogger logger; private readonly HttpClient httpClient; + private readonly IServiceProvider serviceProvider; private readonly ConcurrentDictionary concurrentTasks = new(); @@ -45,12 +46,15 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation /// /// Initializes a new instance of the class. /// + /// 服务提供器 /// 日志器 /// http客户端工厂 - public ImageCache(ILogger logger, IHttpClientFactory httpClientFactory) + public ImageCache(IServiceProvider serviceProvider) { - this.logger = logger; - httpClient = httpClientFactory.CreateClient(nameof(ImageCache)); + logger = serviceProvider.GetRequiredService>(); + httpClient = serviceProvider.GetRequiredService().CreateClient(nameof(ImageCache)); + + this.serviceProvider = serviceProvider; } /// @@ -73,9 +77,15 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation } /// - public void Remove(IEnumerable uriForCachedItems) + public void Remove(Uri uriForCachedItem) { - if (uriForCachedItems == null || !uriForCachedItems.Any()) + Remove(new ReadOnlySpan(uriForCachedItem)); + } + + /// + public void Remove(in ReadOnlySpan uriForCachedItems) + { + if (uriForCachedItems == null || uriForCachedItems.Length <= 0) { return; } @@ -131,24 +141,10 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation /// public string GetFilePathFromCategoryAndFileName(string category, string fileName) { - Uri dummyUri = new(Web.HutaoEndpoints.StaticFile(category, fileName)); + Uri dummyUri = Web.HutaoEndpoints.StaticFile(category, fileName).ToUri(); return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri)); } - private static void RemoveInternal(IEnumerable filePaths) - { - foreach (string filePath in filePaths) - { - try - { - File.Delete(filePath); - } - catch - { - } - } - } - private static string GetCacheFileName(Uri uri) { string url = uri.ToString(); @@ -164,11 +160,25 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation return treatNullFileAsInvalid; } - // Get extended properties. FileInfo fileInfo = new(file); return fileInfo.Length == 0; } + private void RemoveInternal(IEnumerable filePaths) + { + foreach (string filePath in filePaths) + { + try + { + File.Delete(filePath); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Remove Cache Image Failed:{file}", filePath); + } + } + } + private async Task DownloadFileAsync(Uri uri, string baseFile) { logger.LogInformation("Begin downloading for {uri}", uri); @@ -218,7 +228,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation { if (cacheFolder == null) { - baseFolder ??= ApplicationData.Current.LocalCacheFolder.Path; + baseFolder ??= serviceProvider.GetRequiredService().LocalCache; DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName)); cacheFolder = info.FullName; } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs index f0c1e9a2..6b36c9af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs @@ -13,6 +13,7 @@ namespace Snap.Hutao.Core.Database; /// 实体的类型 /// 消息的类型 [HighQuality] +[Obsolete("Use ScopedDbCurrent instead")] internal sealed class DbCurrent where TEntity : class, ISelectable where TMessage : Message.ValueChangedMessage, new() diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs index 603b09c7..10787b35 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs @@ -24,10 +24,7 @@ internal static class DbSetExtension where TEntity : class { dbSet.Add(entity); - DbContext dbContext = dbSet.Context(); - int count = dbContext.SaveChanges(); - dbContext.ChangeTracker.Clear(); - return count; + return dbSet.SaveChangesAndClearChangeTracker(); } /// @@ -37,14 +34,11 @@ internal static class DbSetExtension /// 数据库集 /// 实体 /// 影响条数 - public static async ValueTask AddAndSaveAsync(this DbSet dbSet, TEntity entity) + public static ValueTask AddAndSaveAsync(this DbSet dbSet, TEntity entity) where TEntity : class { dbSet.Add(entity); - DbContext dbContext = dbSet.Context(); - int count = await dbContext.SaveChangesAsync().ConfigureAwait(false); - dbContext.ChangeTracker.Clear(); - return count; + return dbSet.SaveChangesAndClearChangeTrackerAsync(); } /// @@ -58,10 +52,7 @@ internal static class DbSetExtension where TEntity : class { dbSet.AddRange(entities); - DbContext dbContext = dbSet.Context(); - int count = dbSet.Context().SaveChanges(); - dbContext.ChangeTracker.Clear(); - return count; + return dbSet.SaveChangesAndClearChangeTracker(); } /// @@ -71,14 +62,11 @@ internal static class DbSetExtension /// 数据库集 /// 实体 /// 影响条数 - public static async ValueTask AddRangeAndSaveAsync(this DbSet dbSet, IEnumerable entities) + public static ValueTask AddRangeAndSaveAsync(this DbSet dbSet, IEnumerable entities) where TEntity : class { dbSet.AddRange(entities); - DbContext dbContext = dbSet.Context(); - int count = await dbContext.SaveChangesAsync().ConfigureAwait(false); - dbContext.ChangeTracker.Clear(); - return count; + return dbSet.SaveChangesAndClearChangeTrackerAsync(); } /// @@ -92,10 +80,7 @@ internal static class DbSetExtension where TEntity : class { dbSet.Remove(entity); - DbContext dbContext = dbSet.Context(); - int count = dbContext.SaveChanges(); - dbContext.ChangeTracker.Clear(); - return count; + return dbSet.SaveChangesAndClearChangeTracker(); } /// @@ -105,14 +90,11 @@ internal static class DbSetExtension /// 数据库集 /// 实体 /// 影响条数 - public static async ValueTask RemoveAndSaveAsync(this DbSet dbSet, TEntity entity) + public static ValueTask RemoveAndSaveAsync(this DbSet dbSet, TEntity entity) where TEntity : class { dbSet.Remove(entity); - DbContext dbContext = dbSet.Context(); - int count = await dbContext.SaveChangesAsync().ConfigureAwait(false); - dbContext.ChangeTracker.Clear(); - return count; + return dbSet.SaveChangesAndClearChangeTrackerAsync(); } /// @@ -126,10 +108,7 @@ internal static class DbSetExtension where TEntity : class { dbSet.Update(entity); - DbContext dbContext = dbSet.Context(); - int count = dbContext.SaveChanges(); - dbContext.ChangeTracker.Clear(); - return count; + return dbSet.SaveChangesAndClearChangeTracker(); } /// @@ -139,11 +118,31 @@ internal static class DbSetExtension /// 数据库集 /// 实体 /// 影响条数 - public static async ValueTask UpdateAndSaveAsync(this DbSet dbSet, TEntity entity) + public static ValueTask UpdateAndSaveAsync(this DbSet dbSet, TEntity entity) where TEntity : class { dbSet.Update(entity); - return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false); + return dbSet.SaveChangesAndClearChangeTrackerAsync(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int SaveChangesAndClearChangeTracker(this DbSet dbSet) + where TEntity : class + { + DbContext dbContext = dbSet.Context(); + int count = dbContext.SaveChanges(); + dbContext.ChangeTracker.Clear(); + return count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static async ValueTask SaveChangesAndClearChangeTrackerAsync(this DbSet dbSet) + where TEntity : class + { + DbContext dbContext = dbSet.Context(); + int count = await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.ChangeTracker.Clear(); + return count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/EnumerableExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/SelectableExtension.cs similarity index 94% rename from src/Snap.Hutao/Snap.Hutao/Core/Database/EnumerableExtension.cs rename to src/Snap.Hutao/Snap.Hutao/Core/Database/SelectableExtension.cs index 7fcc6a51..cf88e740 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Database/EnumerableExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/SelectableExtension.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Database; /// 可枚举扩展 /// [HighQuality] -internal static class EnumerableExtension +internal static class SelectableExtension { /// /// 获取选中的值或默认值 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ICastableService.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/ICastableService.cs similarity index 75% rename from src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ICastableService.cs rename to src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/ICastableService.cs index d1d150b7..8b86bfd2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ICastableService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/ICastableService.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Core.DependencyInjection; +namespace Snap.Hutao.Core.DependencyInjection.Abstraction; /// /// 可转换类型服务 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/INamedService.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/INamedService.cs similarity index 84% rename from src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/INamedService.cs rename to src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/INamedService.cs index e22ea9b8..556588e1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/INamedService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/INamedService.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Core.DependencyInjection; +namespace Snap.Hutao.Core.DependencyInjection.Abstraction; /// /// 有名称的对象 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IOverseaSupport.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/IOverseaSupport.cs similarity index 83% rename from src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IOverseaSupport.cs rename to src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/IOverseaSupport.cs index 44a57100..2ecc0fe9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IOverseaSupport.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Abstraction/IOverseaSupport.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Core.DependencyInjection; +namespace Snap.Hutao.Core.DependencyInjection.Abstraction; /// /// 海外服/Hoyolab 可区分 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/CastableServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/CastableServiceExtension.cs index e569a1c5..389b63e4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/CastableServiceExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/CastableServiceExtension.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; + namespace Snap.Hutao.Core.DependencyInjection; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/EnumerableServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/EnumerableServiceExtension.cs index 28a3396a..9abda922 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/EnumerableServiceExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/EnumerableServiceExtension.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Runtime.CompilerServices; +using Snap.Hutao.Core.DependencyInjection.Abstraction; namespace Snap.Hutao.Core.DependencyInjection; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs index 07f0854d..ebdfcd6d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -15,6 +15,7 @@ internal static partial class IocHttpClientConfiguration /// /// 添加 + /// 此方法将会自动生成 /// /// 集合 /// 可继续操作的集合 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceCollectionExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceCollectionExtension.cs index e3354734..cb663d18 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceCollectionExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceCollectionExtension.cs @@ -12,6 +12,7 @@ internal static partial class ServiceCollectionExtension { /// /// 向容器注册服务 + /// 此方法将会自动生成 /// /// 容器 /// 可继续操作的服务集合 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionFormat.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionFormat.cs index 3b1ffab4..24fb7776 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionFormat.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionFormat.cs @@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.ExceptionService; /// internal sealed class ExceptionFormat { - private const string SectionSeparator = "----------------------------------------"; + private static readonly string SectionSeparator = new('-', 40); /// /// 格式化异常 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/UserdataCorruptedException.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/UserdataCorruptedException.cs index d0860313..d0e18b58 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/UserdataCorruptedException.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/UserdataCorruptedException.cs @@ -15,7 +15,7 @@ internal sealed class UserdataCorruptedException : Exception /// 消息 /// 内部错误 public UserdataCorruptedException(string message, Exception innerException) - : base(string.Format(SH.CoreExceptionServiceUserdataCorruptedMessage, message), innerException) + : base(string.Format(SH.CoreExceptionServiceUserdataCorruptedMessage, $"{message}\n{innerException.Message}"), innerException) { } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs index cb5803fb..3e59e122 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.Extensions.Options; +using Microsoft.Web.WebView2.Core; using Microsoft.Win32; using Snap.Hutao.Core.Setting; using System.IO; @@ -17,6 +18,9 @@ namespace Snap.Hutao.Core; [Injection(InjectAs.Singleton)] internal sealed class HutaoOptions : IOptions { + private readonly bool isWebView2Supported; + private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected; + /// /// 构造一个新的胡桃选项 /// @@ -31,6 +35,7 @@ internal sealed class HutaoOptions : IOptions UserAgent = $"Snap Hutao/{Version}"; DeviceId = GetUniqueUserId(); + DetectWebView2Environment(ref webView2Version, ref isWebView2Supported); } /// @@ -68,6 +73,16 @@ internal sealed class HutaoOptions : IOptions /// public string DeviceId { get; } + /// + /// WebView2 版本 + /// + public string WebView2Version { get => webView2Version; } + + /// + /// 是否支持 WebView2 + /// + public bool IsWebView2Supported { get => isWebView2Supported; } + /// public HutaoOptions Value { get => this; } @@ -99,4 +114,18 @@ internal sealed class HutaoOptions : IOptions object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName); return Convert.ToMd5HexString($"{userName}{machineGuid}"); } + + private static void DetectWebView2Environment(ref string webView2Version, ref bool isWebView2Supported) + { + try + { + webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString(); + isWebView2Supported = true; + } + catch (FileNotFoundException ex) + { + ILogger logger = Ioc.Default.GetRequiredService>(); + logger.LogError(ex, "WebView2 Runtime not installed."); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs index c9f17637..2f910bb7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs @@ -17,20 +17,21 @@ internal static class Clipboard /// 从剪贴板文本中反序列化 /// /// 目标类型 - /// Json序列化选项 + /// 服务提供器 /// 实例 - public static async Task DeserializeTextAsync(JsonSerializerOptions options) + public static async Task DeserializeTextAsync(IServiceProvider serviceProvider) where T : class { - await ThreadHelper.SwitchToMainThreadAsync(); + ITaskContext taskContext = serviceProvider.GetRequiredService(); + await taskContext.SwitchToMainThreadAsync(); DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent(); if (view.Contains(StandardDataFormats.Text)) { string json = await view.GetTextAsync(); - await ThreadHelper.SwitchToBackgroundAsync(); - return JsonSerializer.Deserialize(json, options); + await taskContext.SwitchToBackgroundAsync(); + return JsonSerializer.Deserialize(json, serviceProvider.GetRequiredService()); } return null; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/Annotation/JsonEnumAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/Annotation/JsonEnumAttribute.cs index b08bd9f1..3be40473 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Json/Annotation/JsonEnumAttribute.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Json/Annotation/JsonEnumAttribute.cs @@ -12,7 +12,7 @@ namespace Snap.Hutao.Core.Json.Annotation; [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] internal class JsonEnumAttribute : Attribute { - private static readonly Type ConfigurableEnumConverterType = typeof(ConfigurableEnumConverter<>); + private static readonly Type UnsafeEnumConverterType = typeof(UnsafeEnumConverter<>); /// /// 构造一个新的Json枚举声明 @@ -52,7 +52,7 @@ internal class JsonEnumAttribute : Attribute /// Json转换器 internal JsonConverter CreateConverter(JsonPropertyInfo info) { - Type converterType = ConfigurableEnumConverterType.MakeGenericType(info.PropertyType); + Type converterType = UnsafeEnumConverterType.MakeGenericType(info.PropertyType); return (JsonConverter)Activator.CreateInstance(converterType, ReadAs, WriteAs)!; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/Annotation/JsonSerializeType.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/Annotation/JsonSerializeType.cs index 749bf561..a93dd62e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Json/Annotation/JsonSerializeType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Json/Annotation/JsonSerializeType.cs @@ -10,14 +10,14 @@ namespace Snap.Hutao.Core.Json.Annotation; internal enum JsonSerializeType { /// - /// Int32 + /// 数字 /// - Int32, + Number, /// /// 字符串包裹的数字 /// - Int32AsString, + NumberString, /// /// 名称字符串 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/ConfigurableEnumConverter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/ConfigurableEnumConverter.cs deleted file mode 100644 index b3d7c2f3..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/ConfigurableEnumConverter.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core.ExpressionService; -using Snap.Hutao.Core.Json.Annotation; - -namespace Snap.Hutao.Core.Json.Converter; - -/// -/// 枚举转换器 -/// -/// 枚举的类型 -[HighQuality] -internal sealed class ConfigurableEnumConverter : JsonConverter - where TEnum : struct, Enum -{ - private readonly JsonSerializeType readAs; - private readonly JsonSerializeType writeAs; - - /// - /// 构造一个新的枚举转换器 - /// - /// 读取 - /// 写入 - public ConfigurableEnumConverter(JsonSerializeType readAs, JsonSerializeType writeAs) - { - this.readAs = readAs; - this.writeAs = writeAs; - } - - /// - public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (readAs == JsonSerializeType.Int32) - { - return CastTo.From(reader.GetInt32()); - } - - if (reader.GetString() is string str) - { - return Enum.Parse(str); - } - - throw Must.NeverHappen(); - } - - /// - public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) - { - switch (writeAs) - { - case JsonSerializeType.Int32: - writer.WriteNumberValue(CastTo.From(value)); - break; - case JsonSerializeType.Int32AsString: - writer.WriteStringValue(value.ToString("D")); - break; - default: - writer.WriteStringValue(value.ToString()); - break; - } - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/UnsafeEnumConverter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/UnsafeEnumConverter.cs new file mode 100644 index 00000000..90764239 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/UnsafeEnumConverter.cs @@ -0,0 +1,156 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Json.Annotation; +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Core.Json.Converter; + +/// +/// 枚举转换器 +/// +/// 枚举的类型 +[HighQuality] +internal sealed class UnsafeEnumConverter : JsonConverter + where TEnum : struct, Enum +{ + private readonly TypeCode enumTypeCode = Type.GetTypeCode(typeof(TEnum)); + + private readonly JsonSerializeType readAs; + private readonly JsonSerializeType writeAs; + + /// + /// 构造一个新的枚举转换器 + /// + /// 读取 + /// 写入 + public UnsafeEnumConverter(JsonSerializeType readAs, JsonSerializeType writeAs) + { + this.readAs = readAs; + this.writeAs = writeAs; + } + + /// + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConverTEnum, JsonSerializerOptions options) + { + if (readAs == JsonSerializeType.Number) + { + return GetEnum(ref reader, enumTypeCode); + } + + if (reader.GetString() is string str) + { + return Enum.Parse(str); + } + + throw new JsonException(); + } + + /// + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + switch (writeAs) + { + case JsonSerializeType.Number: + WriteEnumValue(writer, value, enumTypeCode); + break; + case JsonSerializeType.NumberString: + writer.WriteStringValue(value.ToString("D")); + break; + default: + writer.WriteStringValue(value.ToString()); + break; + } + } + + private static TEnum GetEnum(ref Utf8JsonReader reader, TypeCode typeCode) + { + switch (typeCode) + { + case TypeCode.Int32: + if (reader.TryGetInt32(out int int32)) + { + return Unsafe.As(ref int32); + } + + break; + case TypeCode.UInt32: + if (reader.TryGetUInt32(out uint uint32)) + { + return Unsafe.As(ref uint32); + } + + break; + case TypeCode.UInt64: + if (reader.TryGetUInt64(out ulong uint64)) + { + return Unsafe.As(ref uint64); + } + + break; + case TypeCode.Int64: + if (reader.TryGetInt64(out long int64)) + { + return Unsafe.As(ref int64); + } + + break; + case TypeCode.Byte: + if (reader.TryGetByte(out byte byte8)) + { + return Unsafe.As(ref byte8); + } + + break; + case TypeCode.Int16: + if (reader.TryGetInt16(out short int16)) + { + return Unsafe.As(ref int16); + } + + break; + case TypeCode.UInt16: + if (reader.TryGetUInt16(out ushort uint16)) + { + return Unsafe.As(ref uint16); + } + + break; + } + + throw new JsonException(); + } + + private static void WriteEnumValue(Utf8JsonWriter writer, TEnum value, TypeCode typeCode) + { + switch (typeCode) + { + case TypeCode.Int32: + writer.WriteNumberValue(Unsafe.As(ref value)); + break; + case TypeCode.UInt32: + writer.WriteNumberValue(Unsafe.As(ref value)); + break; + case TypeCode.UInt64: + writer.WriteNumberValue(Unsafe.As(ref value)); + break; + case TypeCode.Int64: + writer.WriteNumberValue(Unsafe.As(ref value)); + break; + case TypeCode.Int16: + writer.WriteNumberValue(Unsafe.As(ref value)); + break; + case TypeCode.UInt16: + writer.WriteNumberValue(Unsafe.As(ref value)); + break; + case TypeCode.Byte: + writer.WriteNumberValue(Unsafe.As(ref value)); + break; + case TypeCode.SByte: + writer.WriteNumberValue(Unsafe.As(ref value)); + break; + default: + throw new JsonException(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index d8f34df4..fe76b3fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -46,6 +46,8 @@ internal static class Activation private const string CategoryDailyNote = "dailynote"; private const string UrlActionImport = "/import"; private const string UrlActionRefresh = "/refresh"; + + private static readonly WeakReference MainWindowReference = new(default!); private static readonly SemaphoreSlim ActivateSemaphore = new(1); /// @@ -72,7 +74,7 @@ internal static class Activation /// 任务 public static async ValueTask RestartAsElevatedAsync() { - if (GetElevated()) + if (!GetElevated()) { await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync(); Process.GetCurrentProcess().Kill(); @@ -182,12 +184,16 @@ internal static class Activation private static async Task WaitMainWindowAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); IServiceProvider serviceProvider = Ioc.Default; + ITaskContext taskContext = serviceProvider.GetRequiredService(); + await taskContext.SwitchToMainThreadAsync(); - serviceProvider.GetRequiredService().Activate(); + MainWindowReference.SetTarget(serviceProvider.GetRequiredService()); - await serviceProvider.GetRequiredService().WaitInitializationAsync().ConfigureAwait(false); + await serviceProvider + .GetRequiredService() + .WaitInitializationAsync() + .ConfigureAwait(false); serviceProvider .GetRequiredService() @@ -279,16 +285,19 @@ internal static class Activation private static async Task HandleLaunchGameActionAsync(string? uid = null) { - Ioc.Default.GetRequiredService().Set(ViewModel.Game.LaunchGameViewModel.DesiredUid, uid); - await ThreadHelper.SwitchToMainThreadAsync(); + IServiceProvider serviceProvider = Ioc.Default; + IMemoryCache memoryCache = serviceProvider.GetRequiredService(); + memoryCache.Set(ViewModel.Game.LaunchGameViewModel.DesiredUid, uid); + ITaskContext taskContext = serviceProvider.GetRequiredService(); + await taskContext.SwitchToMainThreadAsync(); - if (!MainWindow.IsPresent) + if (!MainWindowReference.TryGetTarget(out _)) { - _ = Ioc.Default.GetRequiredService(); + _ = serviceProvider.GetRequiredService(); } else { - await Ioc.Default + await serviceProvider .GetRequiredService() .NavigateAsync(INavigationAwaiter.Default, true) .ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs index 58d9be50..9eeb38e0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs @@ -39,6 +39,8 @@ internal static class SettingKeys /// public const string PassportPassword = "PassportPassword"; + #region StaticResource + /// /// 静态资源合约 /// 新增合约时 请注意 @@ -66,4 +68,5 @@ internal static class SettingKeys /// 静态资源合约V5 刷新 AvatarIcon /// public const string StaticResourceV5Contract = "StaticResourceV5Contract"; + #endregion } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SemaphoreSlimExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SemaphoreSlimExtension.cs index 2edd7af9..06d68f00 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SemaphoreSlimExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SemaphoreSlimExtension.cs @@ -29,19 +29,22 @@ internal static class SemaphoreSlimExtension return new SemaphoreSlimReleaser(semaphoreSlim); } +} - private readonly struct SemaphoreSlimReleaser : IDisposable +[SuppressMessage("", "SA1201")] +[SuppressMessage("", "SA1400")] +[SuppressMessage("", "SA1600")] +file readonly struct SemaphoreSlimReleaser : IDisposable +{ + private readonly SemaphoreSlim semaphoreSlim; + + public SemaphoreSlimReleaser(SemaphoreSlim semaphoreSlim) { - private readonly SemaphoreSlim semaphoreSlim; + this.semaphoreSlim = semaphoreSlim; + } - public SemaphoreSlimReleaser(SemaphoreSlim semaphoreSlim) - { - this.semaphoreSlim = semaphoreSlim; - } - - public void Dispose() - { - semaphoreSlim.Release(); - } + public void Dispose() + { + semaphoreSlim.Release(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskExtension.cs similarity index 95% rename from src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskExtensions.cs rename to src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskExtension.cs index c9640d2a..76b18e55 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskExtension.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Threading; [HighQuality] [SuppressMessage("", "VSTHRD003")] [SuppressMessage("", "VSTHRD100")] -internal static class TaskExtensions +internal static class TaskExtension { /// /// 安全的触发任务 @@ -21,6 +21,10 @@ internal static class TaskExtensions { await task.ConfigureAwait(false); } + catch (OperationCanceledException) + { + // Do nothing + } #if DEBUG catch (Exception ex) { diff --git a/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs b/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs index bb6c6436..64bf93d2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs @@ -12,6 +12,7 @@ namespace Snap.Hutao.Core; /// 必须为抽象类才能使用泛型日志器 /// [HighQuality] +[Obsolete("Use HutaoOptions instead")] internal abstract class WebView2Helper { private static bool hasEverDetected; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs index a94621af..64786886 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs @@ -25,20 +25,19 @@ namespace Snap.Hutao.Core.Windowing; /// 窗体类型 [SuppressMessage("", "CA1001")] internal sealed class ExtendedWindow : IRecipient - where TWindow : Window, IExtendedWindowSource + where TWindow : Window, IWindowOptionsSource { - private readonly WindowOptions options; - + private readonly TWindow window; private readonly IServiceProvider serviceProvider; private readonly WindowSubclass subclass; - private ExtendedWindow(TWindow window, FrameworkElement titleBar, IServiceProvider serviceProvider) + private ExtendedWindow(TWindow window, IServiceProvider serviceProvider) { - options = new(window, titleBar); - subclass = new(options); - + this.window = window; this.serviceProvider = serviceProvider; + subclass = new(window.WindowOptions); + InitializeWindow(); } @@ -50,7 +49,7 @@ internal sealed class ExtendedWindow : IRecipient实例 public static ExtendedWindow Initialize(TWindow window, IServiceProvider serviceProvider) { - return new(window, window.TitleBar, serviceProvider); + return new(window, serviceProvider); } /// @@ -63,16 +62,17 @@ internal sealed class ExtendedWindow : IRecipient(); - options.AppWindow.Title = string.Format(SH.AppNameAndVersion, hutaoOptions.Version); - options.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico")); + WindowOptions options = window.WindowOptions; + window.AppWindow.Title = string.Format(SH.AppNameAndVersion, hutaoOptions.Version); + window.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico")); ExtendsContentIntoTitleBar(); - Persistence.RecoverOrInit(options); + Persistence.RecoverOrInit(window); UpdateImmersiveDarkMode(options.TitleBar, default!); // appWindow.Show(true); // appWindow.Show can't bring window to top. - // options.Window.Activate(); + window.Activate(); Persistence.BringToForeground(options.Hwnd); AppOptions appOptions = serviceProvider.GetRequiredService(); @@ -83,7 +83,7 @@ internal sealed class ExtendedWindow : IRecipient().Register(this); - options.Window.Closed += OnWindowClosed; + window.Closed += OnWindowClosed; options.TitleBar.ActualThemeChanged += UpdateImmersiveDarkMode; } @@ -97,9 +97,9 @@ internal sealed class ExtendedWindow : IRecipient : IRecipient : IRecipient new MicaBackdrop() { Kind = MicaKind.BaseAlt }, BackdropType.Mica => new MicaBackdrop() { Kind = MicaKind.Base }, @@ -139,7 +140,7 @@ internal sealed class ExtendedWindow : IRecipient : IRecipient : IRecipient : IRecipient /// 窗体类型 -internal interface IExtendedWindowSource +internal interface IWindowOptionsSource { /// - /// 提供的标题栏 + /// 窗体选项 /// - FrameworkElement TitleBar { get; } - - /// - /// 是否持久化尺寸 - /// - bool PersistSize { get; } - - /// - /// 初始大小 - /// - SizeInt32 InitSize { get; } + WindowOptions WindowOptions { get; } /// /// 处理最大最小信息 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/Persistence.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/Persistence.cs index 4aa4c5b8..3f5b4097 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/Persistence.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/Persistence.cs @@ -22,44 +22,45 @@ internal static class Persistence /// /// 设置窗体位置 /// - /// 选项 + /// 选项窗口param> /// 窗体类型 - public static void RecoverOrInit(in WindowOptions options) - where TWindow : Window, IExtendedWindowSource + public static void RecoverOrInit(TWindow window) + where TWindow : Window, IWindowOptionsSource { + WindowOptions options = window.WindowOptions; // Set first launch size. double scale = GetScaleForWindowHandle(options.Hwnd); - SizeInt32 transformedSize = options.Window.InitSize.Scale(scale); + SizeInt32 transformedSize = options.InitSize.Scale(scale); RectInt32 rect = StructMarshal.RectInt32(transformedSize); - if (options.Window.PersistSize) + if (options.PersistSize) { RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect); - if (persistedRect.Size() >= options.Window.InitSize.Size()) + if (persistedRect.Size() >= options.InitSize.Size()) { rect = persistedRect; } } TransformToCenterScreen(ref rect); - options.AppWindow.MoveAndResize(rect); + window.AppWindow.MoveAndResize(rect); } /// /// 保存窗体的位置 /// - /// 选项 + /// 窗口 /// 窗体类型 - public static void Save(in WindowOptions options) - where TWindow : Window, IExtendedWindowSource + public static void Save(TWindow window) + where TWindow : Window, IWindowOptionsSource { WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT(); - GetWindowPlacement(options.Hwnd, ref windowPlacement); + GetWindowPlacement(window.WindowOptions.Hwnd, ref windowPlacement); // prevent save value when we are maximized. if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED)) { - LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)options.AppWindow.GetRect()); + LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect()); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs index 6d8bc291..6ecdb136 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs @@ -1,9 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.UI; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; +using Windows.Graphics; using Windows.Win32.Foundation; using WinRT.Interop; @@ -13,29 +13,28 @@ namespace Snap.Hutao.Core.Windowing; /// Window 选项 /// /// 窗体类型 -internal readonly struct WindowOptions - where TWindow : Window, IExtendedWindowSource +internal readonly struct WindowOptions { /// /// 窗体句柄 /// public readonly HWND Hwnd; - /// - /// AppWindow - /// - public readonly AppWindow AppWindow; - - /// - /// 窗体 - /// - public readonly TWindow Window; - /// /// 标题栏元素 /// public readonly FrameworkElement TitleBar; + /// + /// 初始大小 + /// + public readonly SizeInt32 InitSize; + + /// + /// 是否持久化尺寸 + /// + public readonly bool PersistSize; + /// /// 是否使用 Win UI 3 自带的拓展标题栏实现 /// @@ -46,11 +45,13 @@ internal readonly struct WindowOptions /// /// 窗体 /// 标题栏 - public WindowOptions(TWindow window, FrameworkElement titleBar) + /// 初始尺寸 + /// 持久化尺寸 + public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false) { - Window = window; Hwnd = (HWND)WindowNative.GetWindowHandle(window); - AppWindow = window.AppWindow; TitleBar = titleBar; + InitSize = initSize; + PersistSize = persistSize; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclass.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclass.cs index 1cdd31fe..5031d024 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclass.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclass.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.UI.Xaml; +using System.Runtime.CompilerServices; using Windows.Win32.Foundation; using Windows.Win32.UI.Shell; using Windows.Win32.UI.WindowsAndMessaging; @@ -15,12 +16,12 @@ namespace Snap.Hutao.Core.Windowing; /// 窗体类型 [HighQuality] internal sealed class WindowSubclass : IDisposable - where TWindow : Window, IExtendedWindowSource + where TWindow : Window, IWindowOptionsSource { private const int WindowSubclassId = 101; private const int DragBarSubclassId = 102; - private readonly WindowOptions options; + private readonly TWindow window; // We have to explicitly hold a reference to SUBCLASSPROC private SUBCLASSPROC? windowProc; @@ -30,9 +31,9 @@ internal sealed class WindowSubclass : IDisposable /// 构造一个新的窗体子类管理器 /// /// 选项 - public WindowSubclass(in WindowOptions options) + public WindowSubclass(TWindow window) { - this.options = options; + this.window = window; } /// @@ -41,6 +42,8 @@ internal sealed class WindowSubclass : IDisposable /// 是否设置成功 public bool Initialize() { + WindowOptions options = window.WindowOptions; + windowProc = new(OnSubclassProcedure); bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0); @@ -65,6 +68,8 @@ internal sealed class WindowSubclass : IDisposable /// public void Dispose() { + WindowOptions options = window.WindowOptions; + RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId); windowProc = null; @@ -83,14 +88,14 @@ internal sealed class WindowSubclass : IDisposable case WM_GETMINMAXINFO: { double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd); - options.Window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor); + window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor); break; } case WM_NCRBUTTONDOWN: case WM_NCRBUTTONUP: { - return (LRESULT)0; // WM_NULL + return (LRESULT)(nint)WM_NULL; } } @@ -105,7 +110,7 @@ internal sealed class WindowSubclass : IDisposable case WM_NCRBUTTONDOWN: case WM_NCRBUTTONUP: { - return (LRESULT)0; // WM_NULL + return (LRESULT)(nint)WM_NULL; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumExtension.cs deleted file mode 100644 index 4d7222d8..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumExtension.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.Reflection; - -namespace Snap.Hutao.Extension; - -/// -/// 枚举拓展 -/// -[HighQuality] -internal static class EnumExtension -{ - /// - /// 获取枚举的描述 - /// - /// 枚举的类型 - /// 枚举值 - /// 描述 - [Obsolete] - public static string GetDescription(this TEnum @enum) - where TEnum : struct, Enum - { - string enumName = Enum.GetName(@enum)!; - FieldInfo? field = @enum.GetType().GetField(enumName); - DescriptionAttribute? attr = field?.GetCustomAttribute(); - return attr?.Description ?? enumName; - } - - /// - /// 获取枚举的描述 - /// - /// 枚举的类型 - /// 枚举值 - /// 描述 - [Obsolete] - public static string? GetDescriptionOrNull(this TEnum @enum) - where TEnum : struct, Enum - { - string enumName = Enum.GetName(@enum)!; - FieldInfo? field = @enum.GetType().GetField(enumName); - DescriptionAttribute? attr = field?.GetCustomAttribute(); - return attr?.Description; - } - - /// - /// 获取本地化的描述 - /// - /// 枚举的类型 - /// 枚举值 - /// 本地化的描述 - public static string GetLocalizedDescription(this TEnum @enum) - where TEnum : struct, Enum - { - string enumName = Enum.GetName(@enum)!; - FieldInfo? field = @enum.GetType().GetField(enumName); - LocalizationKeyAttribute? attr = field?.GetCustomAttribute(); - string? result = null; - if (attr != null) - { - result = SH.ResourceManager.GetString(attr.Key); - } - - return result ?? enumName; - } - - /// - /// 获取本地化的描述 - /// - /// 枚举的类型 - /// 枚举值 - /// 本地化的描述 - public static string? GetLocalizedDescriptionOrDefault(this TEnum @enum) - where TEnum : struct, Enum - { - string enumName = Enum.GetName(@enum)!; - FieldInfo? field = @enum.GetType().GetField(enumName); - LocalizationKeyAttribute? attr = field?.GetCustomAttribute(); - string? result = null; - if (attr != null) - { - result = SH.ResourceManager.GetString(attr.Key); - } - - return result; - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs index b4ca30ae..1d97eb3b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs @@ -1,8 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using System.Runtime.CompilerServices; - namespace Snap.Hutao.Extension; /// @@ -18,6 +16,6 @@ internal static class ObjectExtension /// 数组 public static T[] ToArray(this T source) { - return new[] { source }; + return new T[] { source }; } } \ 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 1eb8e504..935fef52 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs @@ -12,13 +12,16 @@ namespace Snap.Hutao.Factory; internal sealed class ContentDialogFactory : IContentDialogFactory { private readonly MainWindow mainWindow; + private readonly ITaskContext taskContext; /// /// 构造一个新的内容对话框工厂 /// + /// 任务上下文 /// 主窗体 - public ContentDialogFactory(MainWindow mainWindow) + public ContentDialogFactory(ITaskContext taskContext, MainWindow mainWindow) { + this.taskContext = taskContext; this.mainWindow = mainWindow; } @@ -26,7 +29,7 @@ internal sealed class ContentDialogFactory : IContentDialogFactory public async ValueTask ConfirmAsync(string title, string content) { ContentDialog dialog = await CreateForConfirmAsync(title, content).ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); return await dialog.ShowAsync(); } @@ -34,14 +37,14 @@ internal sealed class ContentDialogFactory : IContentDialogFactory public async ValueTask ConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close) { ContentDialog dialog = await CreateForConfirmCancelAsync(title, content, defaultButton).ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); return await dialog.ShowAsync(); } /// public async ValueTask CreateForIndeterminateProgressAsync(string title) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialog dialog = new() { XamlRoot = mainWindow.Content.XamlRoot, @@ -54,7 +57,7 @@ internal sealed class ContentDialogFactory : IContentDialogFactory private async ValueTask CreateForConfirmAsync(string title, string content) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialog dialog = new() { XamlRoot = mainWindow.Content.XamlRoot, @@ -69,7 +72,7 @@ internal sealed class ContentDialogFactory : IContentDialogFactory private async ValueTask CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialog dialog = new() { XamlRoot = mainWindow.Content.XamlRoot, diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs index ac0b1d5c..4e772f53 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs @@ -75,9 +75,7 @@ internal class PickerFactory : IPickerFactory { // Create a folder picker. T picker = new(); - - IntPtr hWnd = WindowNative.GetWindowHandle(mainWindow); - InitializeWithWindow.Initialize(picker, hWnd); + InitializeWithWindow.Initialize(picker, mainWindow.WindowOptions.Hwnd); return picker; } diff --git a/src/Snap.Hutao/Snap.Hutao/IdentityStructs.json b/src/Snap.Hutao/Snap.Hutao/IdentityStructs.json index 860380c6..bb5ac9d8 100644 --- a/src/Snap.Hutao/Snap.Hutao/IdentityStructs.json +++ b/src/Snap.Hutao/Snap.Hutao/IdentityStructs.json @@ -1,9 +1,24 @@ [ + { + "Name": "AchievementGoalId", + "Type": "int", + "Documentation": "1-2位 成就分类Id" + }, + { + "Name": "AchievementId", + "Type": "int", + "Documentation": "5位 成就Id" + }, { "Name": "AvatarId", "Type": "int", "Documentation": "8位 角色Id" }, + { + "Name": "CostumeId", + "Type": "int", + "Documentation": "6位 角色装扮Id" + }, { "Name": "EquipAffixId", "Type": "int", @@ -39,14 +54,19 @@ "Type": "int", "Documentation": "5位 圣遗物主属性Id" }, + { + "Name": "SkillGroupId", + "Type": "int", + "Documentation": "3-4位 技能组Id" + }, + { + "Name": "SkillId", + "Type": "int", + "Documentation": "5-6位 技能Id" + }, { "Name": "WeaponId", "Type": "int", "Documentation": "5位 武器Id" - }, - { - "Name": "AchievementId", - "Type": "int", - "Documentation": "5位 成就Id" } ] \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml.cs index 001814d5..998c6815 100644 --- a/src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml.cs @@ -4,7 +4,6 @@ using Microsoft.UI.Xaml; using Snap.Hutao.Core.Windowing; using Snap.Hutao.ViewModel.Game; -using Windows.Graphics; using Windows.Win32.UI.WindowsAndMessaging; namespace Snap.Hutao; @@ -14,7 +13,7 @@ namespace Snap.Hutao; /// [HighQuality] [Injection(InjectAs.Singleton)] -internal sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedWindowSource +internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOptionsSource { private const int MinWidth = 240; private const int MinHeight = 240; @@ -22,6 +21,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedW private const int MaxWidth = 320; private const int MaxHeight = 320; + private readonly WindowOptions windowOptions; private readonly IServiceScope scope; /// @@ -33,19 +33,13 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedW InitializeComponent(); scope = scopeFactory.CreateScope(); + windowOptions = new(this, DragableGrid, new(320, 320)); ExtendedWindow.Initialize(this, scope.ServiceProvider); RootGrid.DataContext = scope.ServiceProvider.GetRequiredService(); - Closed += (s, e) => Dispose(); } /// - public FrameworkElement TitleBar { get => DragableGrid; } - - /// - public bool PersistSize { get => false; } - - /// - public SizeInt32 InitSize { get => new(320, 320); } + public WindowOptions WindowOptions { get => throw new NotImplementedException(); } /// public void Dispose() diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs index 488beae8..9835f223 100644 --- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs @@ -17,11 +17,13 @@ namespace Snap.Hutao; [HighQuality] [Injection(InjectAs.Singleton)] [SuppressMessage("", "CA1001")] -internal sealed partial class MainWindow : Window, IExtendedWindowSource, IRecipient +internal sealed partial class MainWindow : Window, IWindowOptionsSource, IRecipient { private const int MinWidth = 848; private const int MinHeight = 524; + private readonly WindowOptions windowOptions; + /// /// 构造一个新的主窗体 /// @@ -29,29 +31,16 @@ internal sealed partial class MainWindow : Window, IExtendedWindowSource, IRecip public MainWindow(IServiceProvider serviceProvider) { InitializeComponent(); + windowOptions = new(this, TitleBarView.DragArea, new(1200, 741), true); ExtendedWindow.Initialize(this, serviceProvider); - IsPresent = true; - Closed += (s, e) => IsPresent = false; - - Ioc.Default.GetRequiredService().Register(this); + serviceProvider.GetRequiredService().Register(this); // If not complete we should present the welcome view. ContentSwitchPresenter.Value = StaticResource.IsAnyUnfulfilledContractPresent(); } - /// - /// 是否打开 - /// - public static bool IsPresent { get; private set; } - /// - public FrameworkElement TitleBar { get => TitleBarView.DragArea; } - - /// - public bool PersistSize { get => true; } - - /// - public SizeInt32 InitSize { get => new(1200, 741); } + public WindowOptions WindowOptions { get => windowOptions; } /// public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs index 3388bf55..16444f23 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Primitive; +using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Model.Calculable; @@ -26,7 +27,7 @@ internal sealed class CalculableAvatar : ObservableObject, ICalculableAvatar AvatarId = avatar.Id; LevelMin = 1; LevelMax = 90; - Skills = avatar.SkillDepot.EnumerateCompositeSkillsNoInherents().Select(p => p.ToCalculable()).ToList(); + Skills = avatar.SkillDepot.CompositeSkillsNoInherents().SelectList(p => p.ToCalculable()); Name = avatar.Name; Icon = AvatarIconConverter.IconNameToUri(avatar.Icon); Quality = avatar.Quality; @@ -39,12 +40,12 @@ internal sealed class CalculableAvatar : ObservableObject, ICalculableAvatar /// 构造一个新的可计算角色 /// /// 角色 - public CalculableAvatar(Binding.AvatarProperty.AvatarView avatar) + public CalculableAvatar(AvatarView avatar) { AvatarId = avatar.Id; LevelMin = avatar.LevelNumber; LevelMax = 90; // hard coded 90 - Skills = avatar.Skills.Select(s => s.ToCalculable()).ToList(); + Skills = avatar.Skills.SelectList(s => s.ToCalculable()); Name = avatar.Name; Icon = avatar.Icon; Quality = avatar.Quality; @@ -63,7 +64,7 @@ internal sealed class CalculableAvatar : ObservableObject, ICalculableAvatar public int LevelMax { get; } /// - public IList Skills { get; } + public List Skills { get; } /// public string Name { get; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableSkill.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableSkill.cs index 8e1ab8d4..a1668b43 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableSkill.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableSkill.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Model.Calculable; @@ -38,7 +39,7 @@ internal sealed class CalculableSkill : ObservableObject, ICalculableSkill /// 构造一个新的可计算的技能 /// /// 技能 - public CalculableSkill(Binding.AvatarProperty.SkillView skill) + public CalculableSkill(SkillView skill) { GruopId = skill.GroupId; LevelMin = skill.LevelNumber; diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableWeapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableWeapon.cs index 1e8d483c..938a6d23 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableWeapon.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableWeapon.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Primitive; +using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Model.Calculable; @@ -25,7 +26,7 @@ internal class CalculableWeapon : ObservableObject, ICalculableWeapon { WeaponId = weapon.Id; LevelMin = 1; - LevelMax = (int)weapon.Quality >= 3 ? 90 : 70; + LevelMax = weapon.MaxLevel; Name = weapon.Name; Icon = EquipIconConverter.IconNameToUri(weapon.Icon); Quality = weapon.RankLevel; @@ -38,11 +39,11 @@ internal class CalculableWeapon : ObservableObject, ICalculableWeapon /// 构造一个新的可计算武器 /// /// 武器 - public CalculableWeapon(Binding.AvatarProperty.WeaponView weapon) + public CalculableWeapon(WeaponView weapon) { WeaponId = weapon.Id; LevelMin = weapon.LevelNumber; - LevelMax = (int)weapon.Quality >= 3 ? 90 : 70; + LevelMax = weapon.MaxLevel; Name = weapon.Name; Icon = weapon.Icon; Quality = weapon.Quality; diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculable.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculable.cs index 866641f0..b3cefa82 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculable.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculable.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Calculable; /// 可计算源 /// [HighQuality] -internal interface ICalculable : Binding.INameIcon +internal interface ICalculable : INameIcon { /// /// 星级 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableAvatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableAvatar.cs index a9d20b96..7f888290 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableAvatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableAvatar.cs @@ -29,5 +29,5 @@ internal interface ICalculableAvatar : ICalculable /// /// 技能组 /// - IList Skills { get; } + List Skills { get; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Achievement.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Achievement.cs index 4c3be852..8b5febbb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Achievement.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Achievement.cs @@ -23,17 +23,17 @@ internal sealed class Achievement : IEquatable [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid InnerId { get; set; } + /// + /// 存档 Id + /// + public Guid ArchiveId { get; set; } + /// /// 存档 /// [ForeignKey(nameof(ArchiveId))] public AchievementArchive Archive { get; set; } = default!; - /// - /// 存档Id - /// - public Guid ArchiveId { get; set; } - /// /// Id /// @@ -100,7 +100,7 @@ internal sealed class Achievement : IEquatable Id = Id, Current = Current, Status = Status, - Timestamp = Time.ToUniversalTime().ToUnixTimeSeconds(), + Timestamp = Time.ToUnixTimeSeconds(), }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/DailyNoteEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/DailyNoteEntry.cs index 1286e387..592a87d5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/DailyNoteEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/DailyNoteEntry.cs @@ -101,11 +101,6 @@ internal sealed class DailyNoteEntry : ObservableObject /// public bool ExpeditionNotifySuppressed { get; set; } - /// - /// 是否在主页显示小组件 - /// - public bool ShowInHomeWidget { get; set; } - /// /// 构造一个新的实时便笺 /// @@ -117,8 +112,8 @@ internal sealed class DailyNoteEntry : ObservableObject { UserId = userAndUid.User.InnerId, Uid = userAndUid.Uid.Value, - ResinNotifyThreshold = 160, - HomeCoinNotifyThreshold = 2400, + ResinNotifyThreshold = 120, + HomeCoinNotifyThreshold = 1800, }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs index 6e563b9c..c9f63aa9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs @@ -23,7 +23,6 @@ internal sealed class AppDbContext : DbContext public AppDbContext(DbContextOptions options) : base(options) { - ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs index c5aa76b7..9a3096ed 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs @@ -5,8 +5,8 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Service.GachaLog; using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; -using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -47,34 +47,31 @@ internal sealed class GachaArchive : ISelectable /// /// 初始化或跳过 /// + /// 上下文 /// 存档 - /// uid - /// 数据库集 - /// 集合 - public static void SkipOrInit([NotNull] ref GachaArchive? archive, string uid, DbSet gachaArchives, ObservableCollection collection) + public static void SkipOrInit(in GachaArchiveInitializationContext context, [NotNull] ref GachaArchive? archive) { if (archive == null) { - Init(out archive, uid, gachaArchives, collection); + Init(context, out archive); } } /// /// 初始化 /// + /// 上下文 /// 存档 - /// uid - /// 数据库集 - /// 集合 - public static void Init([NotNull] out GachaArchive? archive, string uid, DbSet gachaArchives, ObservableCollection collection) + [SuppressMessage("", "SH002")] + public static void Init(GachaArchiveInitializationContext context, [NotNull] out GachaArchive? archive) { - archive = collection.SingleOrDefault(a => a.Uid == uid); + archive = context.ArchiveCollection.SingleOrDefault(a => a.Uid == context.Uid); if (archive == null) { - GachaArchive created = Create(uid); - gachaArchives.AddAndSave(created); - ThreadHelper.InvokeOnMainThread(() => collection!.Add(created)); + GachaArchive created = Create(context.Uid); + context.GachaArchives.AddAndSave(created); + context.TaskContext.InvokeOnMainThread(() => context.ArchiveCollection.Add(created)); archive = created; } } @@ -82,24 +79,22 @@ internal sealed class GachaArchive : ISelectable /// /// 保存祈愿物品 /// - /// 待添加物品 - /// 是否懒惰 - /// 结尾Id - /// 数据集 - public void SaveItems(List itemsToAdd, bool isLazy, long endId, DbSet gachaItems) + /// 上下文 + [SuppressMessage("", "SH002")] + public void SaveItems(GachaItemSaveContext context) { - if (itemsToAdd.Count > 0) + if (context.ItemsToAdd.Count > 0) { // 全量刷新 - if (!isLazy) + if (!context.IsLazy) { - gachaItems + context.GachaItems .Where(i => i.ArchiveId == InnerId) - .Where(i => i.Id >= endId) + .Where(i => i.Id >= context.EndId) .ExecuteDelete(); } - gachaItems.AddRangeAndSave(itemsToAdd); + context.GachaItems.AddRangeAndSave(context.ItemsToAdd); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaItem.cs index 4e1b6149..f2bf6eda 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaItem.cs @@ -68,8 +68,12 @@ internal sealed class GachaItem /// 物品类型字符串 public static string GetItemTypeStringByItemId(int itemId) { - int idLength = itemId.Place(); - return idLength == 8 ? "角色" : idLength == 5 ? "武器" : "未知"; + return itemId.Place() switch + { + 8 => "角色", + 5 => "武器", + _ => "未知", + }; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs index 5907e39f..5b77aa5b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Model.Entity.Primitive; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -12,11 +13,8 @@ namespace Snap.Hutao.Model.Entity; /// [HighQuality] [Table("game_accounts")] -internal sealed class GameAccount : INotifyPropertyChanged +internal sealed class GameAccount : ObservableObject { - /// - public event PropertyChangedEventHandler? PropertyChanged; - /// /// 内部Id /// @@ -66,7 +64,7 @@ internal sealed class GameAccount : INotifyPropertyChanged public void UpdateAttachUid(string? uid) { AttachUid = uid; - PropertyChanged?.Invoke(this, new(nameof(AttachUid))); + OnPropertyChanged(nameof(AttachUid)); } /// @@ -76,6 +74,6 @@ internal sealed class GameAccount : INotifyPropertyChanged public void UpdateName(string name) { Name = name; - PropertyChanged?.Invoke(this, new(nameof(Name))); + OnPropertyChanged($"{nameof(Name)}"); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs new file mode 100644 index 00000000..d16b6074 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -0,0 +1,95 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Entity; + +/// +/// 键名 +/// +internal sealed partial class SettingEntry +{ + /// + /// 游戏路径 + /// + public const string GamePath = "GamePath"; + + /// + /// 空的历史记录卡池是否可见 + /// + public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible"; + + /// + /// 窗口背景类型 + /// + public const string SystemBackdropType = "SystemBackdropType"; + + /// + /// 启用高级功能 + /// + public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled"; + + /// + /// 实时便笺刷新时间 + /// + public const string DailyNoteRefreshSeconds = "DailyNote.RefreshSeconds"; + + /// + /// 实时便笺提醒式通知 + /// + public const string DailyNoteReminderNotify = "DailyNote.ReminderNotify"; + + /// + /// 实时便笺免打扰模式 + /// + public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame"; + + /// + /// 启动游戏 独占全屏 + /// + public const string LaunchIsExclusive = "Launch.IsExclusive"; + + /// + /// 启动游戏 全屏 + /// + public const string LaunchIsFullScreen = "Launch.IsFullScreen"; + + /// + /// 启动游戏 无边框 + /// + public const string LaunchIsBorderless = "Launch.IsBorderless"; + + /// + /// 启动游戏 宽度 + /// + public const string LaunchScreenWidth = "Launch.ScreenWidth"; + + /// + /// 启动游戏 高度 + /// + public const string LaunchScreenHeight = "Launch.ScreenHeight"; + + /// + /// 启动游戏 解锁帧率 + /// + public const string LaunchUnlockFps = "Launch.UnlockFps"; + + /// + /// 启动游戏 目标帧率 + /// + public const string LaunchTargetFps = "Launch.TargetFps"; + + /// + /// 启动游戏 显示器编号 + /// + public const string LaunchMonitor = "Launch.Monitor"; + + /// + /// 启动游戏 多倍启动 + /// + public const string MultipleInstances = "Launch.MultipleInstances"; + + /// + /// 语言 + /// + public const string Culture = "Culture"; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.cs index b57a6458..d281461b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.cs @@ -12,96 +12,8 @@ namespace Snap.Hutao.Model.Entity; [HighQuality] [Table("settings")] [SuppressMessage("", "SA1124")] -internal sealed class SettingEntry +internal sealed partial class SettingEntry { - #region EntryNames - - /// - /// 游戏路径 - /// - public const string GamePath = "GamePath"; - - /// - /// 空的历史记录卡池是否可见 - /// - public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible"; - - /// - /// 窗口背景类型 - /// - public const string SystemBackdropType = "SystemBackdropType"; - - /// - /// 启用高级功能 - /// - public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled"; - - /// - /// 实时便笺刷新时间 - /// - public const string DailyNoteRefreshSeconds = "DailyNote.RefreshSeconds"; - - /// - /// 实时便笺提醒式通知 - /// - public const string DailyNoteReminderNotify = "DailyNote.ReminderNotify"; - - /// - /// 实时便笺免打扰模式 - /// - public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame"; - - /// - /// 启动游戏 独占全屏 - /// - public const string LaunchIsExclusive = "Launch.IsExclusive"; - - /// - /// 启动游戏 全屏 - /// - public const string LaunchIsFullScreen = "Launch.IsFullScreen"; - - /// - /// 启动游戏 无边框 - /// - public const string LaunchIsBorderless = "Launch.IsBorderless"; - - /// - /// 启动游戏 宽度 - /// - public const string LaunchScreenWidth = "Launch.ScreenWidth"; - - /// - /// 启动游戏 高度 - /// - public const string LaunchScreenHeight = "Launch.ScreenHeight"; - - /// - /// 启动游戏 解锁帧率 - /// - public const string LaunchUnlockFps = "Launch.UnlockFps"; - - /// - /// 启动游戏 目标帧率 - /// - public const string LaunchTargetFps = "Launch.TargetFps"; - - /// - /// 启动游戏 显示器编号 - /// - public const string LaunchMonitor = "Launch.Monitor"; - - /// - /// 启动游戏 多倍启动 - /// - public const string MultipleInstances = "Launch.MultipleInstances"; - - /// - /// 语言 - /// - public const string Culture = "Culture"; - #endregion - /// /// 构造一个新的设置入口 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SpiralAbyssEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SpiralAbyssEntry.cs index 89c4d090..9ecf4261 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SpiralAbyssEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SpiralAbyssEntry.cs @@ -27,7 +27,7 @@ internal sealed class SpiralAbyssEntry : ObservableObject public int ScheduleId { get; set; } /// - /// 视图中使用的计划Id字符串 + /// 视图 中使用的计划 Id 字符串 /// [NotMapped] public string Schedule { get => string.Format(SH.ModelEntitySpiralAbyssScheduleFormat, ScheduleId); } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs index 124ba3d6..74054216 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs @@ -67,9 +67,9 @@ internal sealed class User : ISelectable /// 新创建的用户 public static User Create(Cookie cookie, bool isOversea) { - _ = cookie.TryGetAsSToken(isOversea, out Cookie? stoken); - _ = cookie.TryGetAsLToken(out Cookie? ltoken); - _ = cookie.TryGetAsCookieToken(out Cookie? cookieToken); + _ = cookie.TryGetSToken(isOversea, out Cookie? stoken); + _ = cookie.TryGetLToken(out Cookie? ltoken); + _ = cookie.TryGetCookieToken(out Cookie? cookieToken); return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/IEntityWithMetadata.cs b/src/Snap.Hutao/Snap.Hutao/Model/IEntityWithMetadata.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Model/Binding/IEntityWithMetadata.cs rename to src/Snap.Hutao/Snap.Hutao/Model/IEntityWithMetadata.cs index df6510b0..f894963e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/IEntityWithMetadata.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/IEntityWithMetadata.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding; +namespace Snap.Hutao.Model; /// /// 实体与元数据 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/INameIcon.cs b/src/Snap.Hutao/Snap.Hutao/Model/INameIcon.cs similarity index 90% rename from src/Snap.Hutao/Snap.Hutao/Model/Binding/INameIcon.cs rename to src/Snap.Hutao/Snap.Hutao/Model/INameIcon.cs index 2b9cb075..a6a49ff9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/INameIcon.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/INameIcon.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding; +namespace Snap.Hutao.Model; /// /// 名称与图标 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/INameIconSide.cs b/src/Snap.Hutao/Snap.Hutao/Model/INameIconSide.cs similarity index 89% rename from src/Snap.Hutao/Snap.Hutao/Model/Binding/INameIconSide.cs rename to src/Snap.Hutao/Snap.Hutao/Model/INameIconSide.cs index d547fc8b..55b80efb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/INameIconSide.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/INameIconSide.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding; +namespace Snap.Hutao.Model; /// /// 包括侧面图标的名称与图标 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFItem.cs index 1d159e8b..3c7ca2f8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFItem.cs @@ -16,6 +16,6 @@ internal sealed class UIGFItem : GachaLogItem /// 额外祈愿映射 /// [JsonPropertyName("uigf_gacha_type")] - [JsonEnum(JsonSerializeType.Int32AsString)] + [JsonEnum(JsonSerializeType.NumberString)] public GachaConfigType UIGFGachaType { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AssociationType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AssociationType.cs index 752e6f52..2ee18679 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AssociationType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AssociationType.cs @@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic; /// 从属地区 /// [HighQuality] +[Localization] internal enum AssociationType { /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/BodyType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/BodyType.cs index c774ef76..503801e4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/BodyType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/BodyType.cs @@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic; /// 体型 /// [HighQuality] +[Localization] internal enum BodyType { /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs index 6fa2d69d..f2bde7f6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs @@ -60,7 +60,27 @@ internal enum ElementType AntiFire = 9, /// - /// 默认 + /// 枫丹玩法 /// - Default = 255, + VehicleMuteIce = 10, + + /// + /// 弹弹菇 + /// + Mushroom = 11, + + /// + /// 激元素 + /// + Overdose = 12, + + /// + /// 木元素 + /// + Wood = 13, + + /// + /// 个数 + /// + Count = 14, } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs index ca97396d..06afb3af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs @@ -9,6 +9,7 @@ namespace Snap.Hutao.Model.Intrinsic; /// 战斗属性 /// [HighQuality] +[Localization] internal enum FightProperty { /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemQuality.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemQuality.cs index 0624b0d9..489a3f20 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemQuality.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemQuality.cs @@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic; /// 稀有度 /// [HighQuality] +[Localization] internal enum ItemQuality { /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs index 30e58aaf..107fd262 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs @@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic; /// 武器类型 /// [HighQuality] +[Localization] [SuppressMessage("", "SA1124")] internal enum WeaponType { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Item.cs b/src/Snap.Hutao/Snap.Hutao/Model/Item.cs similarity index 94% rename from src/Snap.Hutao/Snap.Hutao/Model/Binding/Item.cs rename to src/Snap.Hutao/Snap.Hutao/Model/Item.cs index 2567d08a..3ff05ba7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Item.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Item.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Model.Binding; +namespace Snap.Hutao.Model; /// /// 物品基类 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Achievement.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Achievement.cs index 57d00466..89f05571 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Achievement.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Achievement.cs @@ -19,7 +19,7 @@ internal sealed class Achievement /// /// 分类Id /// - public int Goal { get; set; } + public AchievementGoalId Goal { get; set; } /// /// 排序顺序 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/AchievementGoal.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/AchievementGoal.cs index 5b6bf2cd..9c943822 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/AchievementGoal.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/AchievementGoal.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Primitive; + namespace Snap.Hutao.Model.Metadata.Achievement; /// @@ -12,7 +14,7 @@ internal sealed class AchievementGoal /// /// Id /// - public int Id { get; set; } + public AchievementGoalId Id { get; set; } /// /// 排序顺序 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Reward.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Reward.cs index 3499c82a..a80a6ce0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Reward.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Reward.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Primitive; + namespace Snap.Hutao.Model.Metadata.Achievement; /// @@ -11,7 +13,7 @@ internal sealed class Reward /// /// Id /// - public int Id { get; set; } + public MaterialId Id { get; set; } /// /// 数量 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/EnumExtension.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/EnumExtension.cs index e0bb0212..1435707a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/EnumExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/EnumExtension.cs @@ -20,6 +20,7 @@ internal static class EnumExtension internal static FormatMethod GetFormatMethod(this TEnum @enum) where TEnum : struct, Enum { + // TODO: Not use Reflection string enumName = Must.NotNull(Enum.GetName(@enum)!); FieldInfo? field = @enum.GetType().GetField(enumName); FormatAttribute? attr = field?.GetCustomAttribute(); diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.Implementation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.Implementation.cs index 5ab66b62..0b90a050 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.Implementation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.Implementation.cs @@ -49,7 +49,7 @@ internal partial class Avatar : IStatisticsItemSource, ISummaryItemSource, IName /// 转换为基础物品 /// /// 基础物品 - public Binding.Item ToItemBase() + public Model.Item ToItemBase() { return new() { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs index cb55acea..b90ce93d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Primitive; + namespace Snap.Hutao.Model.Metadata.Avatar; /// @@ -12,7 +14,7 @@ internal sealed class Costume /// /// Id /// - public int Id { get; set; } + public CostumeId Id { get; set; } /// /// 名称 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/ProudableSkill.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/ProudableSkill.cs index 32cec14f..65648d7e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/ProudableSkill.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/ProudableSkill.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Primitive; + namespace Snap.Hutao.Model.Metadata.Avatar; /// @@ -12,7 +14,7 @@ internal sealed partial class ProudableSkill : Skill /// /// 组Id /// - public int GroupId { get; set; } + public SkillGroupId GroupId { get; set; } /// /// 提升属性 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Skill.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Skill.cs index c598d5b7..7f6774ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Skill.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Skill.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Primitive; + namespace Snap.Hutao.Model.Metadata.Avatar; /// @@ -13,7 +15,7 @@ internal class Skill /// /// Id /// - public int Id { get; set; } + public SkillId Id { get; set; } /// /// 名称 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs index 7b872db7..1d17d8b8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs @@ -9,6 +9,9 @@ namespace Snap.Hutao.Model.Metadata.Avatar; [HighQuality] internal sealed class SkillDepot { + private List? compositeSkills; + private List? compositeSkillsNoInherents; + /// /// 技能天赋 /// @@ -30,7 +33,18 @@ internal sealed class SkillDepot /// public List CompositeSkills { - get => EnumerateCompositeSkills().ToList(); + get + { + if (compositeSkills == null) + { + compositeSkills = new(Skills.Count + 1 + Inherents.Count); + compositeSkills.AddRange(Skills); + compositeSkills.Add(EnergySkill); + compositeSkills.AddRange(Inherents); + } + + return compositeSkills; + } } /// @@ -42,32 +56,16 @@ internal sealed class SkillDepot /// 获取无固有天赋的技能列表 /// /// 天赋列表 - public IEnumerable EnumerateCompositeSkillsNoInherents() + public List CompositeSkillsNoInherents() { - foreach (ProudableSkill skill in Skills) + if (compositeSkillsNoInherents == null) { - // skip skills like Mona's & Ayaka's shift - if (skill.Proud.Parameters.Count > 1) - { - yield return skill; - } + compositeSkillsNoInherents = new(Skills.Count + 1); + compositeSkillsNoInherents.AddRange(Skills); + compositeSkillsNoInherents.Add(EnergySkill); } - yield return EnergySkill; - } - - private IEnumerable EnumerateCompositeSkills() - { - foreach (ProudableSkill skill in Skills) - { - yield return skill; - } - - yield return EnergySkill; - - foreach (ProudableSkill skill in Inherents) - { - yield return skill; - } + // No Inherents + return compositeSkillsNoInherents; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ParameterDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescriptionsParametersDescriptor.cs similarity index 55% rename from src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ParameterDescriptor.cs rename to src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescriptionsParametersDescriptor.cs index 53108132..104d53dd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ParameterDescriptor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescriptionsParametersDescriptor.cs @@ -5,6 +5,7 @@ using Snap.Hutao.Control; using Snap.Hutao.Model.Metadata.Avatar; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using System.Text.RegularExpressions; namespace Snap.Hutao.Model.Metadata.Converter; @@ -13,7 +14,7 @@ namespace Snap.Hutao.Model.Metadata.Converter; /// 描述参数解析器 /// [HighQuality] -internal sealed partial class ParameterDescriptor : ValueConverter>> +internal sealed partial class DescriptionsParametersDescriptor : ValueConverter>> { /// /// 获取特定等级的解释 @@ -23,58 +24,48 @@ internal sealed partial class ParameterDescriptor : ValueConverter特定等级的解释 public static LevelParameters Convert(DescriptionsParameters from, int level) { - // DO NOT INLINE! - // Cache the formats - List formats = from.Descriptions - .Select(desc => new DescFormat(desc)) - .ToList(); - LevelParameters param = from.Parameters.Single(param => param.Level == level); - - return new LevelParameters($"Lv.{param.Level}", GetParameterInfos(formats, param.Parameters)); + return new LevelParameters($"Lv.{param.Level}", GetParameterDescription(from.Descriptions, param.Parameters)); } /// public override List> Convert(DescriptionsParameters from) { - // DO NOT INLINE! - // Cache the formats - List formats = from.Descriptions - .SelectList(desc => new DescFormat(desc)); - List> parameters = from.Parameters - .SelectList(param => new LevelParameters(param.Level.ToString(), GetParameterInfos(formats, param.Parameters))); + .SelectList(param => new LevelParameters($"Lv.{param.Level}", GetParameterDescription(from.Descriptions, param.Parameters))); return parameters; } - private static List GetParameterInfos(List formats, List param) - { - Span span = CollectionsMarshal.AsSpan(formats); - List results = new(span.Length); - - foreach (DescFormat descFormat in span) - { - string format = descFormat.Format; - string resultFormatted = ParamRegex().Replace(format, match => EvaluateMatch(match, param)); - results.Add(new ParameterDescription { Description = descFormat.Description, Parameter = resultFormatted }); - } - - return results; - } - [GeneratedRegex("{param[0-9]+.*?}")] private static partial Regex ParamRegex(); - private static string EvaluateMatch(Match match, IList param) + private static List GetParameterDescription(List descriptions, List param) + { + Span span = CollectionsMarshal.AsSpan(descriptions); + List results = new(span.Length); + + foreach (ReadOnlySpan desc in span) + { + int indexOfSeparator = desc.IndexOf('|'); + ReadOnlySpan description = desc[..indexOfSeparator]; + ReadOnlySpan format = desc[(indexOfSeparator + 1)..]; + + string resultFormatted = ParamRegex().Replace(format.ToString(), match => ReplaceParamInMatch(match, param)); + results.Add(new ParameterDescription { Description = description.ToString(), Parameter = resultFormatted }); + } + + return results; + } + + private static string ReplaceParamInMatch(Match match, List param) { if (match.Success) { - // remove parentheses and split by {value:format} + // remove parentheses and split by {value:format} like {param1:F} string[] parts = match.Value[1..^1].Split(':', 2); int index = int.Parse(parts[0]["param".Length..]) - 1; - return ParameterFormat.Format($"{{0:{parts[1]}}}", param[index]); } else @@ -82,20 +73,4 @@ internal sealed partial class ParameterDescriptor : ValueConverter { + private static readonly Dictionary LocalizedNameToElementIconName = new() + { + [SH.ModelIntrinsicElementNameElec] = "Electric", + [SH.ModelIntrinsicElementNameFire] = "Fire", + [SH.ModelIntrinsicElementNameGrass] = "Grass", + [SH.ModelIntrinsicElementNameIce] = "Ice", + [SH.ModelIntrinsicElementNameRock] = "Rock", + [SH.ModelIntrinsicElementNameWater] = "Water", + [SH.ModelIntrinsicElementNameWind] = "Wind", + }; + + private static readonly Dictionary LocalizedNameToElementType = new() + { + [SH.ModelIntrinsicElementNameElec] = ElementType.Electric, + [SH.ModelIntrinsicElementNameFire] = ElementType.Fire, + [SH.ModelIntrinsicElementNameGrass] = ElementType.Grass, + [SH.ModelIntrinsicElementNameIce] = ElementType.Ice, + [SH.ModelIntrinsicElementNameRock] = ElementType.Rock, + [SH.ModelIntrinsicElementNameWater] = ElementType.Water, + [SH.ModelIntrinsicElementNameWind] = ElementType.Wind, + }; + /// /// 将中文元素名称转换为图标链接 /// @@ -19,17 +41,7 @@ internal sealed class ElementNameIconConverter : ValueConverter /// 图标链接 public static Uri ElementNameToIconUri(string elementName) { - string element = elementName switch - { - "雷" => "Electric", - "火" => "Fire", - "草" => "Grass", - "冰" => "Ice", - "岩" => "Rock", - "水" => "Water", - "风" => "Wind", - _ => string.Empty, - }; + string? element = LocalizedNameToElementIconName.GetValueOrDefault(elementName); return string.IsNullOrEmpty(element) ? Web.HutaoEndpoints.UIIconNone @@ -45,17 +57,7 @@ internal sealed class ElementNameIconConverter : ValueConverter /// 元素类型 public static ElementType ElementNameToElementType(string elementName) { - return elementName switch - { - "雷" => ElementType.Electric, - "火" => ElementType.Fire, - "草" => ElementType.Grass, - "冰" => ElementType.Ice, - "岩" => ElementType.Rock, - "水" => ElementType.Water, - "风" => ElementType.Wind, - _ => ElementType.None, - }; + return LocalizedNameToElementType.GetValueOrDefault(elementName); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertiesParametersDescriptor.cs similarity index 95% rename from src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyDescriptor.cs rename to src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertiesParametersDescriptor.cs index fe5d9ba2..b3a000db 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyDescriptor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertiesParametersDescriptor.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. using Snap.Hutao.Control; -using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Annotation; +using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Model.Metadata.Converter; @@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Converter; /// 基础属性翻译器 /// [HighQuality] -internal sealed class PropertyDescriptor : ValueConverter>?> +internal sealed class PropertiesParametersDescriptor : ValueConverter>?> { /// /// 格式化名称与值 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Monster/MonsterBaseValue.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Monster/MonsterBaseValue.cs index d05735a6..787d176e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Monster/MonsterBaseValue.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Monster/MonsterBaseValue.cs @@ -57,14 +57,14 @@ internal sealed class MonsterBaseValue : BaseValue { return new() { - Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_FIRE_SUB_HURT, FireSubHurt), - Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WATER_SUB_HURT, WaterSubHurt), - Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_GRASS_SUB_HURT, GrassSubHurt), - Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ELEC_SUB_HURT, ElecSubHurt), - Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WIND_SUB_HURT, WindSubHurt), - Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ICE_SUB_HURT, IceSubHurt), - Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ROCK_SUB_HURT, RockSubHurt), - Converter.PropertyDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, PhysicalSubHurt), + Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_FIRE_SUB_HURT, FireSubHurt), + Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WATER_SUB_HURT, WaterSubHurt), + Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_GRASS_SUB_HURT, GrassSubHurt), + Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ELEC_SUB_HURT, ElecSubHurt), + Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WIND_SUB_HURT, WindSubHurt), + Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ICE_SUB_HURT, IceSubHurt), + Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ROCK_SUB_HURT, RockSubHurt), + Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, PhysicalSubHurt), }; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.Implementation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.Implementation.cs index 83c0a1d8..500fb366 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.Implementation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.Implementation.cs @@ -31,7 +31,7 @@ internal sealed partial class Weapon : IStatisticsItemSource, ISummaryItemSource /// /// 最大等级 /// - public int MaxLevel { get => ((int)Quality) >= 3 ? 90 : 70; } + internal int MaxLevel { get => ((int)Quality) >= 3 ? 90 : 70; } /// public ICalculableWeapon ToCalculable() @@ -43,7 +43,7 @@ internal sealed partial class Weapon : IStatisticsItemSource, ISummaryItemSource /// 转换为基础物品 /// /// 基础物品 - public Binding.Item ToItemBase() + public Model.Item ToItemBase() { return new() { diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/LocalizationAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/LocalizationAttribute.cs new file mode 100644 index 00000000..730794ee --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/LocalizationAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Resource.Localization; + +/// +/// 指示此枚举支持本地化 +/// +[AttributeUsage(AttributeTargets.Enum)] +internal sealed class LocalizationAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Annotation/LocalizationKeyAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/LocalizationKeyAttribute.cs similarity index 81% rename from src/Snap.Hutao/Snap.Hutao/Core/Annotation/LocalizationKeyAttribute.cs rename to src/Snap.Hutao/Snap.Hutao/Resource/Localization/LocalizationKeyAttribute.cs index 08370927..596797cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Annotation/LocalizationKeyAttribute.cs +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/LocalizationKeyAttribute.cs @@ -1,14 +1,14 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Core.Annotation; +namespace Snap.Hutao.Resource.Localization; /// /// 本地化键 /// [HighQuality] [AttributeUsage(AttributeTargets.Field)] -internal class LocalizationKeyAttribute : Attribute +internal sealed class LocalizationKeyAttribute : Attribute { /// /// 指定本地化键 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs index a3a4da98..5d1c8702 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs @@ -1536,6 +1536,15 @@ namespace Snap.Hutao.Resource.Localization { } } + /// + /// 查找类似 同步角色信息 的本地化字符串。 + /// + internal static string ViewAvatarPropertySyncDataButtonLabel { + get { + return ResourceManager.GetString("ViewAvatarPropertySyncDataButtonLabel", resourceCulture); + } + } + /// /// 查找类似 成就统计 的本地化字符串。 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs index 52d5b0f6..b84557d1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs @@ -2,10 +2,10 @@ // Licensed under the MIT license. using Snap.Hutao.Core.Diagnostics; -using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Service.AvatarInfo.Factory; using Snap.Hutao.Service.Metadata; +using Snap.Hutao.ViewModel.AvatarProperty; using Snap.Hutao.ViewModel.User; using Snap.Hutao.Web.Enka; using Snap.Hutao.Web.Enka.Model; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs index db4b4976..a1e3c6e3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Service.AvatarInfo.Factory; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs index b765bc46..c7c72328 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs @@ -1,16 +1,17 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Model.Primitive; +using Snap.Hutao.ViewModel.AvatarProperty; using Snap.Hutao.Web.Enka.Model; using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon; using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; -using PropertyAvatar = Snap.Hutao.Model.Binding.AvatarProperty.AvatarView; -using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.ReliquaryView; -using PropertyWeapon = Snap.Hutao.Model.Binding.AvatarProperty.WeaponView; +using PropertyAvatar = Snap.Hutao.ViewModel.AvatarProperty.AvatarView; +using PropertyReliquary = Snap.Hutao.ViewModel.AvatarProperty.ReliquaryView; +using PropertyWeapon = Snap.Hutao.ViewModel.AvatarProperty.WeaponView; namespace Snap.Hutao.Service.AvatarInfo.Factory; @@ -54,7 +55,7 @@ internal sealed class SummaryAvatarFactory // webinfo & metadata mixed part Constellations = SummaryHelper.CreateConstellations(avatar.SkillDepot.Talents, avatarInfo.TalentIdList), - Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.EnumerateCompositeSkillsNoInherents()), + Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.CompositeSkillsNoInherents()), // webinfo part FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0, @@ -76,7 +77,7 @@ internal sealed class SummaryAvatarFactory { if (avatarInfo.CostumeId.HasValue) { - int costumeId = avatarInfo.CostumeId.Value; + CostumeId costumeId = avatarInfo.CostumeId.Value; Model.Metadata.Avatar.Costume costume = avatar.Costumes.Single(c => c.Id == costumeId); // Set to costume icon @@ -132,7 +133,7 @@ internal sealed class SummaryAvatarFactory else { subStat.StatValue = subStat.StatValue - Math.Truncate(subStat.StatValue) > 0 ? subStat.StatValue / 100D : subStat.StatValue; - subProperty = Model.Metadata.Converter.PropertyDescriptor.FormatNameDescription(subStat.AppendPropId, subStat.StatValue); + subProperty = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatNameDescription(subStat.AppendPropId, subStat.StatValue); } return new() diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs index a4b54ac7..e67fab50 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs @@ -1,9 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Metadata; using Snap.Hutao.Service.Metadata; +using Snap.Hutao.ViewModel.AvatarProperty; using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; namespace Snap.Hutao.Service.AvatarInfo.Factory; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFightPropertyMapHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFightPropertyMapHelper.cs index 2b0f5cbd..2eefc148 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFightPropertyMapHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFightPropertyMapHelper.cs @@ -1,9 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Annotation; +using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Service.AvatarInfo.Factory; @@ -30,19 +30,19 @@ internal static class SummaryFightPropertyMapHelper AvatarProperty defProp = GetDefProperty(fightPropMap); double em = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28 - AvatarProperty emProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty emProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( FightProperty.FIGHT_PROP_ELEMENT_MASTERY, SH.ServiceAvatarInfoPropertyEM, FormatMethod.Integer, em); double critRate = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL); // 20 - AvatarProperty critRateProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty critRateProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( FightProperty.FIGHT_PROP_CRITICAL, SH.ServiceAvatarInfoPropertyCR, FormatMethod.Percent, critRate); double critDMG = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22 - AvatarProperty critDMGProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty critDMGProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( FightProperty.FIGHT_PROP_CRITICAL_HURT, SH.ServiceAvatarInfoPropertyCDmg, FormatMethod.Percent, critDMG); double chargeEff = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23 - AvatarProperty chargeEffProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty chargeEffProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, SH.ServiceAvatarInfoPropertyCE, FormatMethod.Percent, chargeEff); List properties = new(9) { hpProp, atkProp, defProp, emProp, critRateProp, critDMGProp, chargeEffProp }; @@ -54,7 +54,7 @@ internal static class SummaryFightPropertyMapHelper double value = fightPropMap[bonusProperty]; if (value > 0) { - AvatarProperty bonusProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty bonusProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( bonusProperty, bonusProperty.GetLocalizedDescription(), FormatMethod.Percent, value); properties.Add(bonusProp); } @@ -66,7 +66,7 @@ internal static class SummaryFightPropertyMapHelper if (addValue > 0) { string description = FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT.GetLocalizedDescription(); - AvatarProperty physicalBonusProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty physicalBonusProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, description, FormatMethod.Percent, addValue); properties.Add(physicalBonusProp); } @@ -81,7 +81,7 @@ internal static class SummaryFightPropertyMapHelper double hp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP); // 2 double hpPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP_PERCENT); // 3 double hpAdd = hp + (baseHp * hpPercent); - AvatarProperty hpProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty hpProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( FightProperty.FIGHT_PROP_MAX_HP, SH.ServiceAvatarInfoPropertyHp, FormatMethod.Integer, baseHp, hpAdd); return hpProp; } @@ -92,7 +92,7 @@ internal static class SummaryFightPropertyMapHelper double atk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK); // 5 double atkPrecent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK_PERCENT); // 6 double atkAdd = atk + (baseAtk * atkPrecent); - AvatarProperty atkProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty atkProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( FightProperty.FIGHT_PROP_CUR_ATTACK, SH.ServiceAvatarInfoPropertyAtk, FormatMethod.Integer, baseAtk, atkAdd); return atkProp; } @@ -103,7 +103,7 @@ internal static class SummaryFightPropertyMapHelper double def = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE); // 8 double defPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE_PERCENT); // 9 double defAdd = def + (baseDef * defPercent); - AvatarProperty defProp = Model.Metadata.Converter.PropertyDescriptor.FormatAvatarProperty( + AvatarProperty defProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( FightProperty.FIGHT_PROP_CUR_DEFENSE, SH.ServiceAvatarInfoPropertyDef, FormatMethod.Integer, baseDef, defAdd); return defProp; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs index dae9eace..8694c542 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs @@ -1,10 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Model.Primitive; +using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Service.AvatarInfo.Factory; @@ -20,7 +21,7 @@ internal static class SummaryHelper /// 全部命座 /// 激活的命座列表 /// 命之座 - public static List CreateConstellations(List talents, List? talentIds) + public static List CreateConstellations(List talents, List? talentIds) { return talents.SelectList(talent => new ConstellationView() { @@ -70,7 +71,7 @@ internal static class SummaryHelper GroupId = proudableSkill.GroupId, LevelNumber = skillLevelMap[proudableSkill.Id.ToString()], - Info = ParameterDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[proudableSkill.Id.ToString()]), + Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[proudableSkill.Id.ToString()]), }; skills.Add(skill); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs index 2fa8fe84..b852496a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs @@ -1,17 +1,17 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Annotation; using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Metadata.Reliquary; +using Snap.Hutao.ViewModel.AvatarProperty; using Snap.Hutao.Web.Enka.Model; using System.Runtime.InteropServices; using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary; using MetadataReliquaryAffix = Snap.Hutao.Model.Metadata.Reliquary.ReliquaryAffix; using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; -using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.ReliquaryView; +using PropertyReliquary = Snap.Hutao.ViewModel.AvatarProperty.ReliquaryView; namespace Snap.Hutao.Service.AvatarInfo.Factory; @@ -82,7 +82,7 @@ internal sealed class SummaryReliquaryFactory // EquipBase Level = $"+{equip.Reliquary.Level - 1}", Quality = reliquary.RankLevel, - MainProperty = new(property.GetLocalizedDescription(), Model.Metadata.Converter.PropertyDescriptor.FormatValue(property, relicLevel.Properties[property])), + MainProperty = new(property.GetLocalizedDescription(), Model.Metadata.Converter.PropertiesParametersDescriptor.FormatValue(property, relicLevel.Properties[property])), // Reliquary ComposedSubProperties = composed, @@ -171,7 +171,7 @@ internal sealed class SummaryReliquaryFactory FightProperty property = affix.Type; double score = ScoreSubAffix(appendPropId); - return new(property.GetLocalizedDescription(), Model.Metadata.Converter.PropertyDescriptor.FormatValue(property, affix.Value), score); + return new(property.GetLocalizedDescription(), Model.Metadata.Converter.PropertiesParametersDescriptor.FormatValue(property, affix.Value), score); } private double ScoreSubAffix(int appendId) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs index 3073d646..754b196d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.ViewModel.AvatarProperty; using Snap.Hutao.ViewModel.User; namespace Snap.Hutao.Service.AvatarInfo; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 281f03d5..835a7fe0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -5,7 +5,7 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.EntityFrameworkCore; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.Entity.Primitive; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs index 2e8686a9..88b696fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs @@ -72,7 +72,7 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient +/// 祈愿存档初始化上下文 +/// +internal readonly struct GachaArchiveInitializationContext +{ + /// + /// 任务上下文 + /// + public readonly ITaskContext TaskContext; + + /// + /// Uid + /// + public readonly string Uid; + + /// + /// 数据集 + /// + public readonly DbSet GachaArchives; + + /// + /// 存档集合 + /// + public readonly ObservableCollection ArchiveCollection; + + /// + /// 初始化一个新的祈愿存档初始化上下文 + /// + /// 任务上下文 + /// uid + /// 数据集 + /// 存档集合 + public GachaArchiveInitializationContext(ITaskContext taskContext, string uid, DbSet gachaArchives, ObservableCollection archiveCollection) + { + TaskContext = taskContext; + Uid = uid; + GachaArchives = gachaArchives; + ArchiveCollection = archiveCollection; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs index d6d94be1..ca314ecb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs @@ -1,25 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.Mvvm.Messaging; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; -using Snap.Hutao.Core.Database; -using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Model.Binding; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; -using Snap.Hutao.Model.InterChange.GachaLog; -using Snap.Hutao.Model.Metadata.Abstraction; -using Snap.Hutao.Model.Primitive; -using Snap.Hutao.Service.GachaLog.Factory; -using Snap.Hutao.Service.GachaLog.QueryProvider; -using Snap.Hutao.Service.Metadata; -using Snap.Hutao.ViewModel.GachaLog; -using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; -using Snap.Hutao.Web.Response; -using System.Collections.Immutable; using System.Collections.ObjectModel; namespace Snap.Hutao.Service.GachaLog; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs index 911d176b..ab7b49f4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; namespace Snap.Hutao.Service.GachaLog; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs index dd9691d4..70046b2d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs @@ -14,16 +14,19 @@ namespace Snap.Hutao.Service.GachaLog; [Injection(InjectAs.Scoped, typeof(IGachaLogImportService))] internal sealed class GachaLogImportService : IGachaLogImportService { + private readonly ITaskContext taskContext; private readonly AppDbContext appDbContext; private readonly ILogger logger; /// /// 构造一个新的祈愿记录导入服务 /// + /// 任务上下文 /// 数据库上下文 /// 日志器 - public GachaLogImportService(AppDbContext appDbContext, ILogger logger) + public GachaLogImportService(ITaskContext taskContext, AppDbContext appDbContext, ILogger logger) { + this.taskContext = taskContext; this.appDbContext = appDbContext; this.logger = logger; } @@ -31,7 +34,8 @@ internal sealed class GachaLogImportService : IGachaLogImportService /// public async Task ImportFromUIGFAsync(GachaLogServiceContext context, List list, string uid) { - GachaArchive.Init(out GachaArchive? archive, uid, appDbContext.GachaArchives, context.ArchiveCollection); + GachaArchiveInitializationContext initContext = new(taskContext, uid, appDbContext.GachaArchives, context.ArchiveCollection); + GachaArchive.Init(initContext, out GachaArchive? archive); await ThreadHelper.SwitchToBackgroundAsync(); Guid archiveId = archive.InnerId; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs index 6b326d5d..fa2cacaa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs @@ -2,16 +2,12 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.Messaging; -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Diagnostics; -using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Model.Binding; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.InterChange.GachaLog; -using Snap.Hutao.Model.Metadata.Abstraction; using Snap.Hutao.Model.Primitive; using Snap.Hutao.Service.GachaLog.Factory; using Snap.Hutao.Service.GachaLog.QueryProvider; @@ -19,7 +15,6 @@ using Snap.Hutao.Service.Metadata; using Snap.Hutao.ViewModel.GachaLog; using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; using Snap.Hutao.Web.Response; -using System.Collections.Immutable; using System.Collections.ObjectModel; namespace Snap.Hutao.Service.GachaLog; @@ -31,6 +26,7 @@ namespace Snap.Hutao.Service.GachaLog; [Injection(InjectAs.Scoped, typeof(IGachaLogService))] internal sealed class GachaLogService : IGachaLogService { + private readonly ITaskContext taskContext; private readonly AppDbContext appDbContext; private readonly IGachaLogExportService gachaLogExportService; private readonly IGachaLogImportService gachaLogImportService; @@ -50,6 +46,7 @@ internal sealed class GachaLogService : IGachaLogService /// 消息器 public GachaLogService(IServiceProvider serviceProvider, IMessenger messenger) { + taskContext = serviceProvider.GetRequiredService(); gachaLogExportService = serviceProvider.GetRequiredService(); gachaLogImportService = serviceProvider.GetRequiredService(); appDbContext = serviceProvider.GetRequiredService(); @@ -218,7 +215,8 @@ internal sealed class GachaLogService : IGachaLogService foreach (GachaLogItem item in items) { - GachaArchive.SkipOrInit(ref archive, item.Uid, appDbContext.GachaArchives, context.ArchiveCollection); + GachaArchiveInitializationContext initContext = new(taskContext, item.Uid, appDbContext.GachaArchives, context.ArchiveCollection); + GachaArchive.SkipOrInit(initContext, ref archive); dbEndId ??= archive.GetEndId(configType, appDbContext.GachaItems); if ((!isLazy) || item.Id > dbEndId) @@ -259,10 +257,52 @@ internal sealed class GachaLogService : IGachaLogService } token.ThrowIfCancellationRequested(); - archive?.SaveItems(itemsToAdd, isLazy, options.EndId, appDbContext.GachaItems); + GachaItemSaveContext saveContext = new(itemsToAdd, isLazy, options.EndId, appDbContext.GachaItems); + archive?.SaveItems(saveContext); await RandomDelayAsync(token).ConfigureAwait(false); } return new(!state.AuthKeyTimeout, archive); } +} + +/// +/// 祈愿物品 +/// +internal readonly struct GachaItemSaveContext +{ + /// + /// 待添加物品 + /// + public readonly List ItemsToAdd; + + /// + /// 是否懒惰 + /// + public readonly bool IsLazy; + + /// + /// 结尾 Id + /// + public readonly long EndId; + + /// + /// 数据集 + /// + public readonly DbSet GachaItems; + + /// + /// 构造一个新的祈愿物品 + /// + /// 待添加物品 + /// 是否懒惰 + /// 结尾 Id + /// 数据集 + public GachaItemSaveContext(List itemsToAdd, bool isLazy, long endId, DbSet gachaItems) + { + ItemsToAdd = itemsToAdd; + IsLazy = isLazy; + EndId = endId; + GachaItems = gachaItems; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs index eaec0c4b..2520a94d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Metadata.Abstraction; using Snap.Hutao.Model.Metadata.Avatar; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/IGachaLogQueryProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/IGachaLogQueryProvider.cs index c71a64e6..89f0a8f7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/IGachaLogQueryProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/IGachaLogQueryProvider.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; + namespace Snap.Hutao.Service.GachaLog.QueryProvider; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs index 509215e6..613e2c3e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs @@ -12,6 +12,8 @@ namespace Snap.Hutao.Service.Game; [HighQuality] internal sealed class LaunchScheme { + // TODO: fix detection + /// /// 已知的启动方案 /// @@ -26,6 +28,14 @@ internal sealed class LaunchScheme SubChannel = SubChannelType.Official, IsOversea = false, }, + new() + { + LauncherId = "18", + Key = "eYd89JmJ", + Channel = ChannelType.Official, + SubChannel = SubChannelType.NoTapTap, + IsOversea = false, + }, // 渠道服 new() diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/IGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/IGameLocator.cs index 4e9d6dde..a5d9a95b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/IGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/IGameLocator.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; + namespace Snap.Hutao.Service.Game.Locator; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/IHutaoUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/IHutaoUserService.cs index da497111..29efaa6e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/IHutaoUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/IHutaoUserService.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; + namespace Snap.Hutao.Service.Hutao; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs index 12671d59..d9bd0e4f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata; using Snap.Hutao.Model.Metadata.Achievement; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/IToastNotificationLifeTime.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/IToastNotificationLifeTime.cs new file mode 100644 index 00000000..ce759379 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/IToastNotificationLifeTime.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Notification; + +/// +/// 系统通知生命周期时间 +/// +internal interface IToastNotificationLifeTime : IDisposable +{ +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/ToastNotificationLifeTime.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/ToastNotificationLifeTime.cs new file mode 100644 index 00000000..5200453b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/ToastNotificationLifeTime.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.WinUI.Notifications; + +namespace Snap.Hutao.Service.Notification; + +/// +/// 系统通知生命周期时间 +/// +internal sealed class ToastNotificationLifeTime : IToastNotificationLifeTime +{ + /// + public void Dispose() + { + // 用于在程序退出时尝试清除所有的系统通知 + try + { + ToastNotificationManagerCompat.History.Clear(); + } + catch + { + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index ed4dee16..33ee0425 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -208,11 +208,11 @@ internal class UserService : IUserService { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - if (cookie.TryGetAsSToken(isOversea, out Cookie? stoken)) + if (cookie.TryGetSToken(isOversea, out Cookie? stoken)) { user.SToken = stoken; - user.LToken = cookie.TryGetAsLToken(out Cookie? ltoken) ? ltoken : user.LToken; - user.CookieToken = cookie.TryGetAsCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; + user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken; + user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; await appDbContext.Users.UpdateAndSaveAsync(user.Entity).ConfigureAwait(false); return new(UserOptionResult.Updated, mid); 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 a706a905..3ef73473 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs @@ -4,7 +4,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Control; -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Service.GachaLog; using Snap.Hutao.View.Control; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index 9351b397..13c464da 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -87,7 +87,7 @@ HeaderIcon="{shcm:FontIcon Glyph=}" IsClickEnabled="True"/> diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs index 5706317a..2256fd89 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.WinUI.UI; +using Snap.Hutao.Model.Primitive; using System.Runtime.InteropServices; namespace Snap.Hutao.ViewModel.Achievement; @@ -25,7 +26,7 @@ internal static class AchievementFinishPercent { if (viewModel.AchievementGoals is List achievementGoals) { - Dictionary counter = achievementGoals.ToDictionary(x => x.Id, x => new AchievementGoalStatistics(x)); + Dictionary counter = achievementGoals.ToDictionary(x => x.Id, x => new AchievementGoalStatistics(x)); foreach (AchievementView achievement in achievements.SourceCollection.Cast()) { // We want to make the state update as fast as possible, diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs index d9f3f1ab..5c50af26 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs @@ -2,9 +2,10 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.ComponentModel; -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Model.Primitive; namespace Snap.Hutao.ViewModel.Achievement; @@ -32,7 +33,7 @@ internal sealed class AchievementGoalView : ObservableObject, INameIcon /// /// Id /// - public int Id { get; } + public AchievementGoalId Id { get; } /// /// 排序顺序 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs index 3967a4d1..3e7e26cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs @@ -105,7 +105,7 @@ internal sealed class AchievementImporter { try { - return await Clipboard.DeserializeTextAsync(options).ConfigureAwait(false); + return await Clipboard.DeserializeTextAsync(serviceProvider).ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementView.cs index aebb3ee0..e7abef96 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementView.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.ComponentModel; -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Intrinsic; namespace Snap.Hutao.ViewModel.Achievement; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs index 8ec1e601..f6850729 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs @@ -423,8 +423,7 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR } else { - int goalId = goal.Id; - Achievements.Filter = (object o) => o is AchievementView view && view.Inner.Goal == goalId; + Achievements.Filter = (object o) => o is AchievementView view && view.Inner.Goal == goal.Id; } } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarProperty.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarProperty.cs index 6d1507f8..728c19e4 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarProperty.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarProperty.cs @@ -1,10 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model; using Snap.Hutao.Model.Intrinsic; using System.Collections.Immutable; -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 角色属性值 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index 6eaa68eb..bf8ac244 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -11,7 +11,6 @@ using Snap.Hutao.Control.Media; using Snap.Hutao.Core.IO.DataTransfer; using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Message; -using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.AvatarInfo; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarView.cs index f919e85a..97ebfdb2 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarView.cs @@ -1,11 +1,12 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model; using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Primitive; -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 角色信息 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ConstellationView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ConstellationView.cs index 765a2986..d72e4307 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ConstellationView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ConstellationView.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 命座信息 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Equip.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Equip.cs index 3cefa458..f55ccdc8 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Equip.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Equip.cs @@ -1,9 +1,10 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model; using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 装备基类 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/NameDescription.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/NameDescription.cs index 7be4144e..30cad25e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/NameDescription.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/NameDescription.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 名称与描述 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/NameIconDescription.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/NameIconDescription.cs index d31699fe..5c97af70 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/NameIconDescription.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/NameIconDescription.cs @@ -1,7 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Model; + +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 名称与描述与图标抽象 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquarySubProperty.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquarySubProperty.cs index 80d18cd2..568233c4 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquarySubProperty.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquarySubProperty.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 圣遗物副词条 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquaryView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquaryView.cs index 03a19357..7c6bd8e6 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquaryView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquaryView.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 圣遗物 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/SkillView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/SkillView.cs index 9d05ae0f..19c73b39 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/SkillView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/SkillView.cs @@ -4,7 +4,7 @@ using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Metadata; -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 天赋 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Summary.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Summary.cs index a02698c6..f4688498 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Summary.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Summary.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 玩家与角色列表的包装器 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/WeaponView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/WeaponView.cs index e92daa79..6374a0f1 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/WeaponView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/WeaponView.cs @@ -4,7 +4,7 @@ using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Primitive; -namespace Snap.Hutao.Model.Binding.AvatarProperty; +namespace Snap.Hutao.ViewModel.AvatarProperty; /// /// 武器 @@ -47,6 +47,11 @@ internal sealed class WeaponView : Equip, ICalculableSource /// internal int LevelNumber { get; set; } + /// + /// 最大等级 + /// + internal int MaxLevel { get => ((int)Quality) >= 3 ? 90 : 70; } + /// public ICalculableWeapon ToCalculable() { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/AvatarView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/AvatarView.cs index f16b7569..95eff5e3 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/AvatarView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/AvatarView.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Converter; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/WeaponView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/WeaponView.cs index 2c5e5763..9bae0908 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/WeaponView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/WeaponView.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Metadata.Weapon; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs index 1a9f411a..71cc5ced 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; namespace Snap.Hutao.ViewModel.Cultivation; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateItemView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateItemView.cs index 24ce847d..3a5f5b21 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateItemView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateItemView.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.ComponentModel; -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Metadata.Item; namespace Snap.Hutao.ViewModel.Cultivation; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/InventoryItemView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/InventoryItemView.cs index 807f0e82..622adbbf 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/InventoryItemView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/InventoryItemView.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.ComponentModel; -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Metadata.Item; namespace Snap.Hutao.ViewModel.Cultivation; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/StatisticsItem.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/StatisticsItem.cs index 3a14a1f4..06c67420 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/StatisticsItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/StatisticsItem.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; namespace Snap.Hutao.ViewModel.GachaLog; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/SummaryItem.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/SummaryItem.cs index 3a34e4df..27c4b46d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/SummaryItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/SummaryItem.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; namespace Snap.Hutao.ViewModel.GachaLog; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs index c0acfbb7..1c9db33c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs @@ -84,15 +84,6 @@ internal sealed class SettingViewModel : Abstraction.ViewModel NavigateToHutaoPassportCommand = new RelayCommand(NavigateToHutaoPassport); } - /// - /// WebView2 版本 - /// - [SuppressMessage("", "CA1822")] - public string WebView2Version - { - get => Core.WebView2Helper.Version; - } - /// /// 应用程序设置 /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/AvatarView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/AvatarView.cs index 0cc9189c..b8f4f718 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/AvatarView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/AvatarView.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Primitive; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/BaseValueInfo.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/BaseValueInfo.cs index f1b6704e..8e577929 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/BaseValueInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/BaseValueInfo.cs @@ -102,7 +102,7 @@ internal sealed class BaseValueInfo : ObservableObject value += addValue; } - values.Add(Model.Metadata.Converter.PropertyDescriptor.FormatNameValue(propValue.Property, value)); + values.Add(Model.Metadata.Converter.PropertiesParametersDescriptor.FormatNameValue(propValue.Property, value)); } Values = values; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs index d09c424b..e0ae5a5d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs @@ -79,7 +79,7 @@ internal sealed class AvatarInfo /// 皮肤 Id /// [JsonPropertyName("costumeId")] - public int? CostumeId { get; set; } + public CostumeId? CostumeId { get; set; } /// /// 命座额外技能等级 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 7c19f4cf..768dd9b9 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 @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; using Snap.Hutao.Web.Response; namespace Snap.Hutao.Web.Hoyolab.Bbs.User; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs index 638605b3..a32f487f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs @@ -108,12 +108,12 @@ internal sealed partial class Cookie return new(cookieMap); } - public bool TryGetAsSToken(bool isOversea, [NotNullWhen(true)] out Cookie? cookie) + public bool TryGetSToken(bool isOversea, [NotNullWhen(true)] out Cookie? cookie) { - return isOversea ? TryGetAsLegacySToken(out cookie) : TryGetAsSToken(out cookie); + return isOversea ? TryGetLegacySToken(out cookie) : TryGetSToken(out cookie); } - public bool TryGetAsLToken([NotNullWhen(true)] out Cookie? cookie) + public bool TryGetLToken([NotNullWhen(true)] out Cookie? cookie) { bool hasLtoken = TryGetValue(LTOKEN, out string? ltoken); bool hasStuid = TryGetValue(LTUID, out string? ltuid); @@ -133,7 +133,7 @@ internal sealed partial class Cookie return false; } - public bool TryGetAsCookieToken([NotNullWhen(true)] out Cookie? cookie) + public bool TryGetCookieToken([NotNullWhen(true)] out Cookie? cookie) { bool hasAccountId = TryGetValue(ACCOUNT_ID, out string? accountId); bool hasCookieToken = TryGetValue(COOKIE_TOKEN, out string? cookieToken); @@ -178,7 +178,7 @@ internal sealed partial class Cookie return string.Join(';', inner.Select(kvp => $"{kvp.Key}={kvp.Value}")); } - private bool TryGetAsSToken([NotNullWhen(true)] out Cookie? cookie) + private bool TryGetSToken([NotNullWhen(true)] out Cookie? cookie) { bool hasMid = TryGetValue(MID, out string? mid); bool hasSToken = TryGetValue(STOKEN, out string? stoken); @@ -200,7 +200,7 @@ internal sealed partial class Cookie return false; } - private bool TryGetAsLegacySToken([NotNullWhen(true)] out Cookie? cookie) + private bool TryGetLegacySToken([NotNullWhen(true)] out Cookie? cookie) { bool hasSToken = TryGetValue(STOKEN, out string? stoken); bool hasSTuid = TryGetValue(STUID, out string? stuid); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaConfigType.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaConfigType.cs index 6ea851b5..35bd15dd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaConfigType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaConfigType.cs @@ -7,6 +7,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; /// 祈愿配置类型 /// [HighQuality] +[Localization] internal enum GachaConfigType { /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogItem.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogItem.cs index 1ab40bd3..028cd8d7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogItem.cs @@ -22,7 +22,7 @@ internal class GachaLogItem /// 祈愿类型 /// [JsonPropertyName("gacha_type")] - [JsonEnum(JsonSerializeType.Int32AsString)] + [JsonEnum(JsonSerializeType.NumberString)] public GachaConfigType GachaType { get; set; } = default!; /// @@ -68,7 +68,7 @@ internal class GachaLogItem /// 物品稀有等级 /// [JsonPropertyName("rank_type")] - [JsonEnum(JsonSerializeType.Int32AsString)] + [JsonEnum(JsonSerializeType.NumberString)] public ItemQuality Rank { get; set; } = default!; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/IPassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/IPassportClient.cs index 6012590f..5b32fd78 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/IPassportClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/IPassportClient.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; using Snap.Hutao.Model.Entity; using Snap.Hutao.Web.Response; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/IGameRecordClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/IGameRecordClient.cs index 47203d87..5a8d80ba 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/IGameRecordClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/IGameRecordClient.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.DependencyInjection.Abstraction; using Snap.Hutao.ViewModel.User; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar; using Snap.Hutao.Web.Response; From 0891c3f521b6a2b0b99f5c81eb47558d4c58c19d Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Sat, 29 Apr 2023 20:54:17 +0800 Subject: [PATCH 3/4] fixup all services --- .../Identity/IdentityGenerator.cs | 14 +- .../Core/Database/ScopedDbCurrent.cs | 20 +- .../Core/ExpressionService/CastTo.cs | 44 --- .../Snap.Hutao/Core/IO/StreamCopyWorker.cs | 54 ++++ src/Snap.Hutao/Snap.Hutao/Core/IO/TempFile.cs | 2 +- .../Snap.Hutao/Core/Threading/TaskContext.cs | 4 +- .../Threading/ThreadPoolSwitchOperation.cs | 20 +- .../Extension/EnumerableExtension.cs | 16 ++ .../Snap.Hutao/IdentityStructs.json | 10 + .../Model/Intrinsic/FightProperty.cs | 36 +-- .../Format}/FormatMethod.cs | 2 +- .../Intrinsic/Format/FormatMethodExtension.cs | 64 +++++ .../Metadata/Annotation/EnumExtension.cs | 29 -- .../Metadata/Annotation/FormatAttribute.cs | 26 -- .../Metadata/Avatar/DescriptionsParameters.cs | 2 +- .../Model/Metadata/Avatar/FetterInfo.cs | 23 ++ .../DescriptionsParametersDescriptor.cs | 6 +- .../Metadata/Converter/FightPropertyFormat.cs | 111 ++++++++ .../PropertiesParametersDescriptor.cs | 98 +------ .../Metadata/Converter/QualityConverter.cs | 2 +- .../Metadata/Monster/MonsterBaseValue.cs | 19 +- .../Model/Metadata/ParameterFormat.cs | 33 +-- .../Model/Metadata/PropertiesParameters.cs | 2 +- .../Model/Metadata/Reliquary/Reliquary.cs | 5 +- .../Metadata/Reliquary/ReliquaryAffix.cs | 2 +- .../Metadata/Reliquary/ReliquaryLevel.cs | 2 +- .../Resource/Localization/SH.Designer.cs | 63 +++++ .../Snap.Hutao/Resource/Localization/SH.resx | 21 ++ .../Achievement/AchievementDbBulkOperation.cs | 214 ++++++++++++++ .../Achievement/AchievementDbOperation.cs | 203 -------------- .../AchievementService.Collection.cs | 93 +++++++ .../AchievementService.Interchange.cs | 71 +++++ .../Service/Achievement/AchievementService.cs | 234 +++++----------- .../Achievement/IAchievementService.cs | 11 +- .../Snap.Hutao/Service/AnnouncementService.cs | 7 +- .../AvatarInfo/AvatarInfoDbBulkOperation.cs | 263 ++++++++++++++++++ .../AvatarInfo/AvatarInfoDbOperation.cs | 228 --------------- .../Service/AvatarInfo/AvatarInfoService.cs | 41 +-- .../Service/AvatarInfo/Factory/AffixWeight.cs | 34 +-- .../AvatarInfo/Factory/ElementMastery.cs | 2 +- .../Factory/ElementMasteryCoefficient.cs | 8 +- .../Factory/SummaryAvatarFactory.cs | 38 +-- .../Factory/SummaryAvatarProperties.cs | 120 ++++++++ .../AvatarInfo/Factory/SummaryFactory.cs | 2 +- .../Factory/SummaryFightPropertyMapHelper.cs | 150 ---------- .../AvatarInfo/Factory/SummaryHelper.cs | 66 +++-- .../Factory/SummaryReliquaryFactory.cs | 80 +++--- .../Service/AvatarInfo/RefreshResult.cs | 5 + ...ulateAvatarDetailAvatarInfoTransformer.cs} | 9 +- ...meRecordCharacterAvatarInfoTransformer.cs} | 31 +-- .../IAvatarInfoTransformer.cs} | 9 +- .../CultivationService.Collection.cs | 100 +++++++ .../Service/Cultivation/CultivationService.cs | 139 ++------- .../Cultivation/ICultivationService.cs | 11 +- .../Service/DailyNote/DailyNoteNotifier.cs | 44 ++- .../Service/DailyNote/DailyNoteService.cs | 58 ++-- .../Service/DailyNote/IDailyNoteService.cs | 3 +- .../Factory/GachaConfigTypeComparar.cs | 10 +- .../Factory/GachaStatisticsFactory.cs | 3 +- .../Factory/GachaStatisticsSlimFactory.cs | 13 +- .../Service/GachaLog/GachaArchives.cs | 2 +- .../Service/GachaLog/GachaItemSaveContext.cs | 48 ++++ .../Service/GachaLog/GachaLogExportService.cs | 37 +-- .../Service/GachaLog/GachaLogFetchContext.cs | 171 ++++++++++++ ...ogFetchState.cs => GachaLogFetchStatus.cs} | 13 +- .../Service/GachaLog/GachaLogImportService.cs | 49 ++-- .../Service/GachaLog/GachaLogService.cs | 179 +++++------- .../Service/GachaLog/HutaoCloudService.cs | 108 ++++--- .../Service/GachaLog/IGachaLogService.cs | 13 +- .../GachaLog/QueryProvider/GachaLogQuery.cs | 15 +- .../GachaLogQueryManualInputProvider.cs | 17 +- .../GachaLogQueryStokenProvider.cs | 2 +- .../GachaLogQueryWebCacheProvider.cs | 4 +- .../Snap.Hutao/Service/Game/GameService.cs | 86 +++--- .../Snap.Hutao/Service/Game/IGameService.cs | 11 +- .../Service/Game/Locator/ManualGameLocator.cs | 7 +- .../Game/Locator/RegistryLauncherLocator.cs | 24 +- .../Game/Locator/UnityLogGameLocator.cs | 19 +- .../Service/Game/Package/ItemOperationInfo.cs | 52 ++-- .../Service/Game/Package/PackageConverter.cs | 71 ++--- .../Snap.Hutao/Service/Game/ProcessInterop.cs | 7 +- .../Service/Game/Unlocker/GameFpsUnlocker.cs | 23 +- .../Snap.Hutao/Service/Hutao/HutaoService.cs | 60 ++-- .../Service/Hutao/HutaoUserService.cs | 9 +- .../Snap.Hutao/Service/InfoBarService.cs | 14 +- .../Service/Metadata/MetadataService.cs | 24 +- .../Service/Navigation/NavigationService.cs | 6 +- .../SpiralAbyss/SpiralAbyssRecordService.cs | 54 ++-- .../Snap.Hutao/Service/User/UserService.cs | 41 +-- .../GachaLogRefreshProgressDialog.xaml.cs | 14 +- .../Achievement/AchievementViewModel.cs | 2 +- .../AvatarProperty/AvatarPropertyViewModel.cs | 5 + .../AvatarProperty/ReliquarySubProperty.cs | 4 +- .../ViewModel/AvatarProperty/ReliquaryView.cs | 2 +- .../ViewModel/AvatarProperty/Summary.cs | 5 + .../Cultivation/CultivationViewModel.cs | 2 +- .../ViewModel/GachaLog/GachaLogViewModel.cs | 4 +- .../ViewModel/Game/LaunchGameViewModel.cs | 2 +- .../ViewModel/Game/LaunchGameViewModelSlim.cs | 2 +- .../ViewModel/Wiki/BaseValueInfo.cs | 11 +- .../Snap.Hutao/Web/Enka/EnkaClient.cs | 63 ++++- .../Snap.Hutao/Web/Enka/Model/AvatarInfo.cs | 4 +- .../Snap.Hutao/Web/Enka/Model/EnkaResponse.cs | 6 + .../Snap.Hutao/Web/Enka/Model/TypeValue.cs | 18 +- .../Snap.Hutao/Web/Enka/Model/WeaponStat.cs | 2 +- .../Takumi/GameRecord/Avatar/Constellation.cs | 4 +- .../Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs | 34 +++ .../Snap.Hutao/Web/Response/Response.cs | 24 +- 108 files changed, 2515 insertions(+), 1889 deletions(-) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/ExpressionService/CastTo.cs rename src/Snap.Hutao/Snap.Hutao/Model/{Metadata/Annotation => Intrinsic/Format}/FormatMethod.cs (88%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Format/FormatMethodExtension.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/EnumExtension.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/FormatAttribute.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/FightPropertyFormat.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbBulkOperation.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbOperation.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Collection.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Interchange.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbBulkOperation.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarProperties.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFightPropertyMapHelper.cs rename src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/{Composer/CalculateAvatarDetailAvatarInfoComposer.cs => Transformer/CalculateAvatarDetailAvatarInfoTransformer.cs} (51%) rename src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/{Composer/GameRecordCharacterAvatarInfoComposer.cs => Transformer/GameRecordCharacterAvatarInfoTransformer.cs} (63%) rename src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/{Composer/IAvatarInfoComposer.cs => Transformer/IAvatarInfoTransformer.cs} (57%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.Collection.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaItemSaveContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs rename src/Snap.Hutao/Snap.Hutao/Service/GachaLog/{GachaLogFetchState.cs => GachaLogFetchStatus.cs} (64%) diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs index 910741e4..7b0313a8 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs @@ -57,19 +57,25 @@ internal sealed class IdentityGenerator : IIncrementalGenerator /// 包装类型 [global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(IdentityGenerator)}}","1.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - internal sealed class IdentityConverter : JsonConverter - where TWrapper : struct + internal unsafe sealed class IdentityConverter : JsonConverter + where TWrapper : unmanaged { /// public override TWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return CastTo.From(reader.GetInt32()); + if (reader.TokenType == JsonTokenType.Number) + { + int value = reader.GetInt32(); + return *(TWrapper*)&value; + } + + throw new JsonException(); } /// public override void Write(Utf8JsonWriter writer, TWrapper value, JsonSerializerOptions options) { - writer.WriteNumberValue(CastTo.From(value)); + writer.WriteNumberValue(*(int*)&value); } } """; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/ScopedDbCurrent.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/ScopedDbCurrent.cs index 1e12dda7..342cbe76 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Database/ScopedDbCurrent.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/ScopedDbCurrent.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Model.Entity.Database; namespace Snap.Hutao.Core.Database; @@ -16,8 +17,7 @@ internal sealed class ScopedDbCurrent where TEntity : class, ISelectable where TMessage : Message.ValueChangedMessage, new() { - private readonly IServiceScopeFactory scopeFactory; - private readonly Func> dbSetSelector; + private readonly IServiceProvider serviceProvider; private readonly IMessenger messenger; private TEntity? current; @@ -25,14 +25,11 @@ internal sealed class ScopedDbCurrent /// /// 构造一个新的数据库当前项 /// - /// 范围工厂 - /// 数据集选择器 - /// 消息器 - public ScopedDbCurrent(IServiceScopeFactory scopeFactory, Func> dbSetSelector, IMessenger messenger) + /// 服务提供器 + public ScopedDbCurrent(IServiceProvider serviceProvider) { - this.scopeFactory = scopeFactory; - this.dbSetSelector = dbSetSelector; - this.messenger = messenger; + messenger = serviceProvider.GetRequiredService(); + this.serviceProvider = serviceProvider; } /// @@ -49,9 +46,10 @@ internal sealed class ScopedDbCurrent return; } - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { - DbSet dbSet = dbSetSelector(scope.ServiceProvider); + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + DbSet dbSet = appDbContext.Set(); // only update when not processing a deletion if (value != null) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExpressionService/CastTo.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExpressionService/CastTo.cs deleted file mode 100644 index 4329b3ea..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExpressionService/CastTo.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.Linq.Expressions; - -namespace Snap.Hutao.Core.ExpressionService; - -/// -/// Class to cast to type -/// -/// Target type -[HighQuality] -internal static class CastTo -{ - /// - /// Casts to . - /// This does not cause boxing for value types. - /// Useful in generic methods. - /// - /// Source type to cast from. Usually a generic type. - /// from value - /// target value - public static TTo From(TFrom from) - { - return Cache.Caster(from); - } - - private static class Cache - { - public static readonly Func Caster = Get(); - - private static Func Get() - { - // 参数表达式,表示 传入源类型 - ParameterExpression param = Expression.Parameter(typeof(TCachedFrom)); - - // 一元转换 调用 相关类的显式或隐式转换运算符 - UnaryExpression convert = Expression.ConvertChecked(param, typeof(TTo)); - - // 生成一个源类型入,目标类型出的 lamdba - return Expression.Lambda>(convert, param).Compile(); - } - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/StreamCopyWorker.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/StreamCopyWorker.cs index 2aad5dc7..3afcdc06 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/StreamCopyWorker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/StreamCopyWorker.cs @@ -54,4 +54,58 @@ internal sealed class StreamCopyWorker } while (bytesRead > 0); } +} + +/// +/// 针对特定类型的流复制器 +/// +/// 进度类型 +[SuppressMessage("", "SA1402")] +internal sealed class StreamCopyWorker +{ + private readonly Stream source; + private readonly Stream destination; + private readonly int bufferSize; + private readonly Func statusFactory; + + /// + /// 创建一个新的流复制器 + /// + /// 源 + /// 目标 + /// 状态工厂 + /// 总字节 + /// 字节尺寸 + public StreamCopyWorker(Stream source, Stream destination, Func statusFactory, int bufferSize = 81920) + { + Verify.Operation(source.CanRead, "Source Stream can't read"); + Verify.Operation(destination.CanWrite, "Destination Stream can't write"); + + this.source = source; + this.destination = destination; + this.statusFactory = statusFactory; + this.bufferSize = bufferSize; + } + + /// + /// 异步复制 + /// + /// 进度 + /// 任务 + public async Task CopyAsync(IProgress progress) + { + long totalBytesRead = 0; + int bytesRead; + Memory buffer = new byte[bufferSize]; + + do + { + bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false); + await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false); + + totalBytesRead += bytesRead; + progress.Report(statusFactory(totalBytesRead)); + } + while (bytesRead > 0); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/TempFile.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/TempFile.cs index 051e0513..49a183de 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/TempFile.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/TempFile.cs @@ -43,7 +43,7 @@ internal sealed class TempFile : IDisposable /// /// 源文件 /// 临时文件 - public static TempFile? CreateCopyFrom(string file) + public static TempFile? CopyFrom(string file) { TempFile temporaryFile = new(); try diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs index dfeaacd0..9f3c9773 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs @@ -27,13 +27,13 @@ internal sealed class TaskContext : ITaskContext /// public ThreadPoolSwitchOperation SwitchToBackgroundAsync() { - return default; + return new(dispatcherQueue); } /// public DispatherQueueSwitchOperation SwitchToMainThreadAsync() { - return new(dispatcherQueue!); + return new(dispatcherQueue); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadPoolSwitchOperation.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadPoolSwitchOperation.cs index 0ad09184..4342086a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadPoolSwitchOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadPoolSwitchOperation.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.UI.Dispatching; using Snap.Hutao.Core.Threading.Abstraction; namespace Snap.Hutao.Core.Threading; @@ -12,9 +13,26 @@ namespace Snap.Hutao.Core.Threading; internal readonly struct ThreadPoolSwitchOperation : IAwaitable, IAwaiter, ICriticalAwaiter { private static readonly WaitCallback WaitCallbackRunAction = RunAction; + private readonly DispatcherQueue dispatherQueue; + + /// + /// 构造一个新的线程池切换操作 + /// + /// 主线程队列 + public ThreadPoolSwitchOperation(DispatcherQueue dispatherQueue) + { + this.dispatherQueue = dispatherQueue; + } /// - public bool IsCompleted { get => false; } + public bool IsCompleted + { + get + { + // 如果已经处于后台就不再切换新的线程 + return !dispatherQueue.HasThreadAccess; + } + } /// public ThreadPoolSwitchOperation GetAwaiter() diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs index 4cbbad99..4dbd588f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs @@ -61,4 +61,20 @@ internal static partial class EnumerableExtension { return new ObservableCollection(source); } + + /// + /// 转换为枚举对象 + /// + /// 类型 + /// 源 + /// 为枚举对象 + [SuppressMessage("", "SH002")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable ToEnumerable(this Span span) + { + for (int i = 0; i < span.Length; i++) + { + yield return span[i]; + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/IdentityStructs.json b/src/Snap.Hutao/Snap.Hutao/IdentityStructs.json index bb5ac9d8..e3a3baac 100644 --- a/src/Snap.Hutao/Snap.Hutao/IdentityStructs.json +++ b/src/Snap.Hutao/Snap.Hutao/IdentityStructs.json @@ -49,11 +49,21 @@ "Type": "int", "Documentation": "6位 圣遗物副词条Id" }, + { + "Name": "ReliquaryId", + "Type": "int", + "Documentation": "5位 圣遗物Id" + }, { "Name": "ReliquaryMainAffixId", "Type": "int", "Documentation": "5位 圣遗物主属性Id" }, + { + "Name": "ReliquarySetId", + "Type": "int", + "Documentation": "5位 圣遗物套装Id" + }, { "Name": "SkillGroupId", "Type": "int", diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs index 06afb3af..716b5d83 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Model.Metadata.Annotation; +using Snap.Hutao.Model.Intrinsic.Format; namespace Snap.Hutao.Model.Intrinsic; @@ -21,63 +21,54 @@ internal enum FightProperty /// 基础生命值 /// [LocalizationKey("ServiceAvatarInfoPropertyBaseHp")] - [Format(FormatMethod.Integer)] FIGHT_PROP_BASE_HP = 1, /// /// 小生命值加成 /// [LocalizationKey("ServiceAvatarInfoPropertyHp")] - [Format(FormatMethod.Integer)] FIGHT_PROP_HP = 2, /// /// 生命值加成百分比 /// [LocalizationKey("ServiceAvatarInfoPropertyHp")] - [Format(FormatMethod.Percent)] FIGHT_PROP_HP_PERCENT = 3, /// /// 基础攻击力 /// [LocalizationKey("ServiceAvatarInfoPropertyBaseAtk")] - [Format(FormatMethod.Integer)] FIGHT_PROP_BASE_ATTACK = 4, /// /// 攻击力加成 /// [LocalizationKey("ServiceAvatarInfoPropertyAtk")] - [Format(FormatMethod.Integer)] FIGHT_PROP_ATTACK = 5, /// /// 攻击力百分比 /// [LocalizationKey("ServiceAvatarInfoPropertyAtk")] - [Format(FormatMethod.Percent)] FIGHT_PROP_ATTACK_PERCENT = 6, /// /// 基础防御力 /// [LocalizationKey("ServiceAvatarInfoPropertyBaseDef")] - [Format(FormatMethod.Integer)] FIGHT_PROP_BASE_DEFENSE = 7, /// /// 防御力加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDef")] - [Format(FormatMethod.Integer)] FIGHT_PROP_DEFENSE = 8, /// /// 防御力百分比 /// [LocalizationKey("ServiceAvatarInfoPropertyDef")] - [Format(FormatMethod.Percent)] FIGHT_PROP_DEFENSE_PERCENT = 9, /// @@ -104,7 +95,6 @@ internal enum FightProperty /// 暴击率 /// [LocalizationKey("ServiceAvatarInfoPropertyCR")] - [Format(FormatMethod.Percent)] FIGHT_PROP_CRITICAL = 20, /// @@ -116,14 +106,12 @@ internal enum FightProperty /// 暴击伤害 /// [LocalizationKey("ServiceAvatarInfoPropertyCDmg")] - [Format(FormatMethod.Percent)] FIGHT_PROP_CRITICAL_HURT = 22, /// /// 元素充能效率 /// [LocalizationKey("ServiceAvatarInfoPropertyCE")] - [Format(FormatMethod.Percent)] FIGHT_PROP_CHARGE_EFFICIENCY = 23, /// @@ -140,7 +128,6 @@ internal enum FightProperty /// 治疗提升 /// [LocalizationKey("ServiceAvatarInfoPropertyHB")] - [Format(FormatMethod.Percent)] FIGHT_PROP_HEAL_ADD = 26, /// @@ -152,21 +139,18 @@ internal enum FightProperty /// 元素精通 /// [LocalizationKey("ServiceAvatarInfoPropertyEM")] - [Format(FormatMethod.Integer)] FIGHT_PROP_ELEMENT_MASTERY = 28, /// /// 物理抗性提升 /// [LocalizationKey("ServiceAvatarInfoPropertyRESPhysical")] - [Format(FormatMethod.Percent)] FIGHT_PROP_PHYSICAL_SUB_HURT = 29, /// /// 物理伤害加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDBPhyiscal")] - [Format(FormatMethod.Percent)] FIGHT_PROP_PHYSICAL_ADD_HURT = 30, /// @@ -183,49 +167,42 @@ internal enum FightProperty /// 火元素伤害加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDBFire")] - [Format(FormatMethod.Percent)] FIGHT_PROP_FIRE_ADD_HURT = 40, /// /// 雷元素伤害加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDBElec")] - [Format(FormatMethod.Percent)] FIGHT_PROP_ELEC_ADD_HURT = 41, /// /// 水元素伤害加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDBWater")] - [Format(FormatMethod.Percent)] FIGHT_PROP_WATER_ADD_HURT = 42, /// /// 草元素伤害加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDBGrass")] - [Format(FormatMethod.Percent)] FIGHT_PROP_GRASS_ADD_HURT = 43, /// /// 风元素伤害加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDBWind")] - [Format(FormatMethod.Percent)] FIGHT_PROP_WIND_ADD_HURT = 44, /// /// 岩元素伤害加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDBRock")] - [Format(FormatMethod.Percent)] FIGHT_PROP_ROCK_ADD_HURT = 45, /// /// 冰元素伤害加成 /// [LocalizationKey("ServiceAvatarInfoPropertyDBIce")] - [Format(FormatMethod.Percent)] FIGHT_PROP_ICE_ADD_HURT = 46, /// @@ -237,49 +214,42 @@ internal enum FightProperty /// 火元素抗性提升 /// [LocalizationKey("ServiceAvatarInfoPropertyRESFire")] - [Format(FormatMethod.Percent)] FIGHT_PROP_FIRE_SUB_HURT = 50, /// /// 雷元素抗性提升 /// [LocalizationKey("ServiceAvatarInfoPropertyRESElec")] - [Format(FormatMethod.Percent)] FIGHT_PROP_ELEC_SUB_HURT = 51, /// /// 雷元素抗性提升 /// [LocalizationKey("ServiceAvatarInfoPropertyRESWater")] - [Format(FormatMethod.Percent)] FIGHT_PROP_WATER_SUB_HURT = 52, /// /// 草元素抗性提升 /// [LocalizationKey("ServiceAvatarInfoPropertyRESGrass")] - [Format(FormatMethod.Percent)] FIGHT_PROP_GRASS_SUB_HURT = 53, /// /// 风元素抗性提升 /// [LocalizationKey("ServiceAvatarInfoPropertyRESWind")] - [Format(FormatMethod.Percent)] FIGHT_PROP_WIND_SUB_HURT = 54, /// /// 岩元素抗性提升 /// [LocalizationKey("ServiceAvatarInfoPropertyRESRock")] - [Format(FormatMethod.Percent)] FIGHT_PROP_ROCK_SUB_HURT = 55, /// /// 冰元素抗性提升 /// [LocalizationKey("ServiceAvatarInfoPropertyRESIce")] - [Format(FormatMethod.Percent)] FIGHT_PROP_ICE_SUB_HURT = 56, /// @@ -410,19 +380,19 @@ internal enum FightProperty /// /// 最大生命值 /// - [Description("生命值")] [LocalizationKey("ServiceAvatarInfoPropertyHp")] - [Format(FormatMethod.Integer)] FIGHT_PROP_MAX_HP = 2000, /// /// 当前攻击力 /// + [LocalizationKey("ServiceAvatarInfoPropertyAtk")] FIGHT_PROP_CUR_ATTACK = 2001, /// /// 当前防御力 /// + [LocalizationKey("ServiceAvatarInfoPropertyDef")] FIGHT_PROP_CUR_DEFENSE = 2002, /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/FormatMethod.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Format/FormatMethod.cs similarity index 88% rename from src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/FormatMethod.cs rename to src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Format/FormatMethod.cs index 647766ed..f8d4b1f1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/FormatMethod.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Format/FormatMethod.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Model.Metadata.Annotation; +namespace Snap.Hutao.Model.Intrinsic.Format; /// /// 格式化方法 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Format/FormatMethodExtension.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Format/FormatMethodExtension.cs new file mode 100644 index 00000000..c2ec99a1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Format/FormatMethodExtension.cs @@ -0,0 +1,64 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Intrinsic.Format; + +/// +/// 枚举拓展 +/// +[HighQuality] +internal static class FormatMethodExtension +{ + /// + /// 获取枚举的格式化方法 + /// + /// 枚举值 + /// 描述 + internal static FormatMethod GetFormatMethod(this FightProperty value) + { + return value switch + { + FightProperty.FIGHT_PROP_BASE_HP => FormatMethod.Integer, + FightProperty.FIGHT_PROP_HP => FormatMethod.Integer, + FightProperty.FIGHT_PROP_HP_PERCENT => FormatMethod.Percent, + + FightProperty.FIGHT_PROP_BASE_ATTACK => FormatMethod.Integer, + FightProperty.FIGHT_PROP_ATTACK => FormatMethod.Integer, + FightProperty.FIGHT_PROP_ATTACK_PERCENT => FormatMethod.Percent, + + FightProperty.FIGHT_PROP_BASE_DEFENSE => FormatMethod.Integer, + FightProperty.FIGHT_PROP_DEFENSE => FormatMethod.Integer, + FightProperty.FIGHT_PROP_DEFENSE_PERCENT => FormatMethod.Percent, + + FightProperty.FIGHT_PROP_CRITICAL => FormatMethod.Percent, + FightProperty.FIGHT_PROP_CRITICAL_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY => FormatMethod.Percent, + FightProperty.FIGHT_PROP_HEAL_ADD => FormatMethod.Percent, + FightProperty.FIGHT_PROP_ELEMENT_MASTERY => FormatMethod.Integer, + + FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT => FormatMethod.Percent, + + FightProperty.FIGHT_PROP_FIRE_ADD_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_ELEC_ADD_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_WATER_ADD_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_GRASS_ADD_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_WIND_ADD_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_ROCK_ADD_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_ICE_ADD_HURT => FormatMethod.Percent, + + FightProperty.FIGHT_PROP_FIRE_SUB_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_ELEC_SUB_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_WATER_SUB_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_GRASS_SUB_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_WIND_SUB_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_ROCK_SUB_HURT => FormatMethod.Percent, + FightProperty.FIGHT_PROP_ICE_SUB_HURT => FormatMethod.Percent, + + FightProperty.FIGHT_PROP_MAX_HP => FormatMethod.Integer, + FightProperty.FIGHT_PROP_CUR_ATTACK => FormatMethod.Integer, + FightProperty.FIGHT_PROP_CUR_DEFENSE => FormatMethod.Integer, + _ => FormatMethod.None, + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/EnumExtension.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/EnumExtension.cs deleted file mode 100644 index 1435707a..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/EnumExtension.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.Reflection; - -namespace Snap.Hutao.Model.Metadata.Annotation; - -/// -/// 枚举拓展 -/// -[HighQuality] -internal static class EnumExtension -{ - /// - /// 获取枚举的描述 - /// - /// 枚举的类型 - /// 枚举值 - /// 描述 - internal static FormatMethod GetFormatMethod(this TEnum @enum) - where TEnum : struct, Enum - { - // TODO: Not use Reflection - string enumName = Must.NotNull(Enum.GetName(@enum)!); - FieldInfo? field = @enum.GetType().GetField(enumName); - FormatAttribute? attr = field?.GetCustomAttribute(); - return attr?.Method ?? FormatMethod.None; - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/FormatAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/FormatAttribute.cs deleted file mode 100644 index 0773620e..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Annotation/FormatAttribute.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Model.Metadata.Annotation; - -/// -/// 格式特性 -/// -[HighQuality] -[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] -internal sealed class FormatAttribute : Attribute -{ - /// - /// 指示该字段应用的格式化方法 - /// - /// 格式化方法 - public FormatAttribute(FormatMethod method) - { - Method = method; - } - - /// - /// 格式化方法 - /// - public FormatMethod Method { get; init; } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/DescriptionsParameters.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/DescriptionsParameters.cs index 56cccbc5..828bc3a5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/DescriptionsParameters.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/DescriptionsParameters.cs @@ -17,5 +17,5 @@ internal sealed class DescriptionsParameters /// /// 参数 /// - public List> Parameters { get; set; } = default!; + public List> Parameters { get; set; } = default!; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/FetterInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/FetterInfo.cs index b603be21..5ae8899f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/FetterInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/FetterInfo.cs @@ -61,6 +61,29 @@ internal sealed class FetterInfo /// public string ConstellationBefore { get; set; } = default!; + /// + /// 命座-后 + /// + public string ConstellationAfter { get; set; } = default!; + + /// + /// 命座 + /// + public string Constellation + { + get + { + if (string.IsNullOrEmpty(ConstellationAfter)) + { + return ConstellationBefore; + } + else + { + return ConstellationAfter; + } + } + } + /// /// 中文CV /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescriptionsParametersDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescriptionsParametersDescriptor.cs index 104d53dd..98602643 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescriptionsParametersDescriptor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescriptionsParametersDescriptor.cs @@ -24,7 +24,7 @@ internal sealed partial class DescriptionsParametersDescriptor : ValueConverter< /// 特定等级的解释 public static LevelParameters Convert(DescriptionsParameters from, int level) { - LevelParameters param = from.Parameters.Single(param => param.Level == level); + LevelParameters param = from.Parameters.Single(param => param.Level == level); return new LevelParameters($"Lv.{param.Level}", GetParameterDescription(from.Descriptions, param.Parameters)); } @@ -40,7 +40,7 @@ internal sealed partial class DescriptionsParametersDescriptor : ValueConverter< [GeneratedRegex("{param[0-9]+.*?}")] private static partial Regex ParamRegex(); - private static List GetParameterDescription(List descriptions, List param) + private static List GetParameterDescription(List descriptions, List param) { Span span = CollectionsMarshal.AsSpan(descriptions); List results = new(span.Length); @@ -58,7 +58,7 @@ internal sealed partial class DescriptionsParametersDescriptor : ValueConverter< return results; } - private static string ReplaceParamInMatch(Match match, List param) + private static string ReplaceParamInMatch(Match match, List param) { if (match.Success) { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/FightPropertyFormat.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/FightPropertyFormat.cs new file mode 100644 index 00000000..daec8abe --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/FightPropertyFormat.cs @@ -0,0 +1,111 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Intrinsic.Format; +using Snap.Hutao.ViewModel.AvatarProperty; + +namespace Snap.Hutao.Model.Metadata.Converter; + +/// +/// 战斗属性格式化 +/// +internal static class FightPropertyFormat +{ + /// + /// 格式化名称与值 + /// + /// 属性 + /// 值 + /// + public static NameValue ToNameValue(FightProperty property, float value) + { + return new(property.GetLocalizedDescription(), FormatValue(property, value)); + } + + /// + /// 格式化名称与描述 + /// + /// 属性 + /// 值 + /// + public static NameDescription ToNameDescription(FightProperty property, float value) + { + return new(property.GetLocalizedDescription(), FormatValue(property, value)); + } + + /// + /// 格式化有绿字的角色属性 + /// + /// 战斗属性 + /// 白字 + /// 绿字 + /// 对2 + public static AvatarProperty ToAvatarProperty(FightProperty property, float baseValue, float addValue) + { + string name = property.GetLocalizedDescription(); + FormatMethod method = property.GetFormatMethod(); + + string value = FormatValue(method, baseValue + addValue); + string addedValue = $"[{FormatValue(method, baseValue)}+{FormatValue(method, addValue)}]"; + + return new(property, name, value, addedValue); + } + + /// + /// 格式化无绿字的角色属性 + /// + /// 战斗属性 + /// 值 + /// 对2 + public static AvatarProperty ToAvatarProperty(FightProperty property, float value) + { + string name = property.GetLocalizedDescription(); + FormatMethod method = property.GetFormatMethod(); + + return new(property, name, FormatValue(method, value)); + } + + /// + /// 格式化无绿字的角色属性 + /// + /// 战斗属性 + /// 战斗属性映射 + /// 对2 + public static AvatarProperty ToAvatarProperty(FightProperty property, Dictionary fightPropMap) + { + string name = property.GetLocalizedDescription(); + FormatMethod method = property.GetFormatMethod(); + + float value = fightPropMap.GetValueOrDefault(property); + + return new(property, name, FormatValue(method, value)); + } + + /// + /// 格式化战斗属性 + /// + /// 战斗属性 + /// 值 + /// 格式化的值 + public static string FormatValue(FightProperty property, float value) + { + return FormatValue(property.GetFormatMethod(), value); + } + + /// + /// 格式化战斗属性 + /// + /// 格式化方法 + /// 值 + /// 格式化的值 + public static string FormatValue(FormatMethod method, float value) + { + return method switch + { + FormatMethod.Integer => $"{Math.Round((double)value, MidpointRounding.AwayFromZero)}", + FormatMethod.Percent => string.Format("{0:P1}", value), + _ => value.ToString(), + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertiesParametersDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertiesParametersDescriptor.cs index b3a000db..08ee2041 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertiesParametersDescriptor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertiesParametersDescriptor.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Control; using Snap.Hutao.Model.Intrinsic; -using Snap.Hutao.Model.Metadata.Annotation; +using Snap.Hutao.Model.Intrinsic.Format; using Snap.Hutao.ViewModel.AvatarProperty; namespace Snap.Hutao.Model.Metadata.Converter; @@ -14,109 +14,29 @@ namespace Snap.Hutao.Model.Metadata.Converter; [HighQuality] internal sealed class PropertiesParametersDescriptor : ValueConverter>?> { - /// - /// 格式化名称与值 - /// - /// 属性 - /// 值 - /// - public static NameValue FormatNameValue(FightProperty property, float value) - { - return new(property.GetLocalizedDescription(), FormatValue(property, value)); - } - - /// - /// 格式化名称与描述 - /// - /// 属性 - /// 值 - /// - public static NameDescription FormatNameDescription(FightProperty property, double value) - { - return new(property.GetLocalizedDescription(), FormatValue(property, value)); - } - - /// - /// 格式化有绿字的角色属性 - /// - /// 战斗属性 - /// 属性名称 - /// 方法 - /// 值1 - /// 值2 - /// 对2 - public static AvatarProperty FormatAvatarProperty(FightProperty property, string name, FormatMethod method, double baseValue, double addValue) - { - return new(property, name, FormatValue(method, baseValue + addValue), $"[{FormatValue(method, baseValue)}+{FormatValue(method, addValue)}]"); - } - - /// - /// 格式化无绿字的角色属性 - /// - /// 战斗属性 - /// 属性名称 - /// 方法 - /// 值 - /// 对2 - public static AvatarProperty FormatAvatarProperty(FightProperty property, string name, FormatMethod method, double value) - { - return new(property, name, FormatValue(method, value)); - } - - /// - /// 格式化战斗属性 - /// - /// 战斗属性 - /// 值 - /// 格式化的值 - public static string FormatValue(FightProperty property, double value) - { - return FormatValue(property.GetFormatMethod(), value); - } - - /// - /// 格式化战斗属性 - /// - /// 格式化方法 - /// 值 - /// 格式化的值 - public static string FormatValue(FormatMethod method, double value) - { - string valueFormatted = method switch - { - FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(), - FormatMethod.Percent => string.Format("{0:P1}", value), - _ => value.ToString(), - }; - return valueFormatted; - } - /// public override List> Convert(PropertiesParameters from) { - return from.Parameters.SelectList(param => + return from.Parameters.SelectList(param => new LevelParameters() { - List parameters = GetParameterInfos(param.Parameters, from.Properties); - return new LevelParameters() { Level = param.Level, Parameters = parameters }; + Level = param.Level, + Parameters = GetParameterDescriptions(param.Parameters, from.Properties), }); } - private static List GetParameterInfos(List parameters, List properties) + private static List GetParameterDescriptions(List parameters, List properties) { List results = new(); - for (int index = 0; index < parameters.Count; index++) + foreach ((float param, FightProperty property) in parameters.Zip(properties)) { - double param = parameters[index]; - string valueFormatted = FormatValue(properties[index], param); - results.Add(new ParameterDescription { - Description = properties[index].GetLocalizedDescription(), - Parameter = valueFormatted, + Description = property.GetLocalizedDescription(), + Parameter = FightPropertyFormat.FormatValue(property, param), }); } return results; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityConverter.cs index 344aa845..5306edef 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityConverter.cs @@ -15,7 +15,7 @@ internal sealed class QualityConverter : ValueConverter /// public override Uri Convert(ItemQuality from) { - string name = from.ToString(); + string name = Enum.GetName(from) ?? from.ToString(); if (name == nameof(ItemQuality.QUALITY_ORANGE_SP)) { name = "QUALITY_RED"; diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Monster/MonsterBaseValue.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Monster/MonsterBaseValue.cs index 787d176e..49be55d8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Monster/MonsterBaseValue.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Monster/MonsterBaseValue.cs @@ -1,6 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Converter; + namespace Snap.Hutao.Model.Metadata.Monster; /// @@ -57,14 +60,14 @@ internal sealed class MonsterBaseValue : BaseValue { return new() { - Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_FIRE_SUB_HURT, FireSubHurt), - Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WATER_SUB_HURT, WaterSubHurt), - Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_GRASS_SUB_HURT, GrassSubHurt), - Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ELEC_SUB_HURT, ElecSubHurt), - Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_WIND_SUB_HURT, WindSubHurt), - Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ICE_SUB_HURT, IceSubHurt), - Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_ROCK_SUB_HURT, RockSubHurt), - Converter.PropertiesParametersDescriptor.FormatNameValue(Intrinsic.FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, PhysicalSubHurt), + FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_FIRE_SUB_HURT, FireSubHurt), + FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_WATER_SUB_HURT, WaterSubHurt), + FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_GRASS_SUB_HURT, GrassSubHurt), + FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_ELEC_SUB_HURT, ElecSubHurt), + FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_WIND_SUB_HURT, WindSubHurt), + FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_ICE_SUB_HURT, IceSubHurt), + FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_ROCK_SUB_HURT, RockSubHurt), + FightPropertyFormat.ToNameValue(FightProperty.FIGHT_PROP_PHYSICAL_SUB_HURT, PhysicalSubHurt), }; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs index ab7ba6d6..4bfc9543 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs @@ -19,7 +19,6 @@ internal sealed class ParameterFormat : IFormatProvider, ICustomFormatter /// 字符串 /// 参数 /// 格式化的字符串 - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string Format(string str, object param) { return string.Format(LazyFormat.Value, str, param); @@ -28,25 +27,23 @@ internal sealed class ParameterFormat : IFormatProvider, ICustomFormatter /// public string Format(string? fmt, object? arg, IFormatProvider? formatProvider) { - if (fmt != null) + ReadOnlySpan fmtSpan = fmt; + switch (fmtSpan.Length) { - switch (fmt.Length) - { - case 3: // FnP - return string.Format($"{{0:P{fmt[1]}}}", arg); - case 2: // Fn - return string.Format($"{{0:{fmt}}}", arg); - case 1: // P I - switch (fmt[0]) - { - case 'P': - return string.Format($"{{0:P0}}", arg); - case 'I': - return arg == null ? "0" : ((IConvertible)arg).ToInt32(null).ToString(); - } + case 3: // FnP + return string.Format($"{{0:P{fmtSpan[1]}}}", arg); + case 2: // Fn + return string.Format($"{{0:{fmtSpan}}}", arg); + case 1: // P I + switch (fmtSpan[0]) + { + case 'P': + return string.Format($"{{0:P0}}", arg); + case 'I': + return arg == null ? "0" : ((IConvertible)arg).ToInt32(default).ToString(); + } - break; - } + break; } return arg?.ToString() ?? string.Empty; diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/PropertiesParameters.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/PropertiesParameters.cs index 489767ea..07303343 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/PropertiesParameters.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/PropertiesParameters.cs @@ -19,5 +19,5 @@ internal sealed class PropertiesParameters /// /// 参数 /// - public List> Parameters { get; set; } = default!; + public List> Parameters { get; set; } = default!; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs index eb8ee3d8..4bb61753 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Primitive; namespace Snap.Hutao.Model.Metadata.Reliquary; @@ -14,7 +15,7 @@ internal sealed class Reliquary /// /// 表示同种类的Id /// - public List Ids { get; set; } = default!; + public List Ids { get; set; } = default!; /// /// 允许出现的等级 @@ -24,7 +25,7 @@ internal sealed class Reliquary /// /// 套装Id /// - public int SetId { get; set; } + public ReliquarySetId SetId { get; set; } /// /// 装备类型 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffix.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffix.cs index 8568076c..34c1bb2c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffix.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffix.cs @@ -19,5 +19,5 @@ internal sealed class ReliquaryAffix : ReliquaryMainAffix /// /// 值 /// - public double Value { get; set; } + public float Value { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs index 803ff490..740fece8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs @@ -24,5 +24,5 @@ internal sealed class ReliquaryLevel /// /// 属性 /// - public Dictionary Properties { get; set; } = default!; + public Dictionary Properties { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs index 5d1c8702..d1673a10 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs @@ -5568,6 +5568,69 @@ namespace Snap.Hutao.Resource.Localization { } } + /// + /// 查找类似 错误的 UID 格式 的本地化字符串。 + /// + internal static string WebEnkaResponseStatusCode400 { + get { + return ResourceManager.GetString("WebEnkaResponseStatusCode400", resourceCulture); + } + } + + /// + /// 查找类似 角色 UID 不存在 的本地化字符串。 + /// + internal static string WebEnkaResponseStatusCode404 { + get { + return ResourceManager.GetString("WebEnkaResponseStatusCode404", resourceCulture); + } + } + + /// + /// 查找类似 游戏维护中 的本地化字符串。 + /// + internal static string WebEnkaResponseStatusCode424 { + get { + return ResourceManager.GetString("WebEnkaResponseStatusCode424", resourceCulture); + } + } + + /// + /// 查找类似 请求过快,请稍后再试 的本地化字符串。 + /// + internal static string WebEnkaResponseStatusCode429 { + get { + return ResourceManager.GetString("WebEnkaResponseStatusCode429", resourceCulture); + } + } + + /// + /// 查找类似 服务器偶发错误 的本地化字符串。 + /// + internal static string WebEnkaResponseStatusCode500 { + get { + return ResourceManager.GetString("WebEnkaResponseStatusCode500", resourceCulture); + } + } + + /// + /// 查找类似 服务器严重错误 的本地化字符串。 + /// + internal static string WebEnkaResponseStatusCode503 { + get { + return ResourceManager.GetString("WebEnkaResponseStatusCode503", resourceCulture); + } + } + + /// + /// 查找类似 未知的服务器错误 的本地化字符串。 + /// + internal static string WebEnkaResponseStatusCodeUnknown { + get { + return ResourceManager.GetString("WebEnkaResponseStatusCodeUnknown", resourceCulture); + } + } + /// /// 查找类似 角色活动祈愿 的本地化字符串。 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 238d6427..a6ff20a8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1971,4 +1971,25 @@ 同步角色信息 + + 错误的 UID 格式 + + + 角色 UID 不存在 + + + 游戏维护中 + + + 请求过快,请稍后再试 + + + 服务器偶发错误 + + + 服务器严重错误 + + + 未知的服务器错误 + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbBulkOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbBulkOperation.cs new file mode 100644 index 00000000..d74b8e0f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbBulkOperation.cs @@ -0,0 +1,214 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Database; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Model.InterChange.Achievement; +using EntityAchievement = Snap.Hutao.Model.Entity.Achievement; + +namespace Snap.Hutao.Service.Achievement; + +/// +/// 成就数据库操作 +/// 双指针操作 +/// +[HighQuality] +[Injection(InjectAs.Scoped)] +internal sealed class AchievementDbBulkOperation +{ + private readonly IServiceProvider serviceProvider; + + /// + /// 构造一个新的成就数据库操作 + /// + /// 服务提供器 + public AchievementDbBulkOperation(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + /// + /// 合并 + /// + /// 成就id + /// 待合并的项 + /// 是否贪婪 + /// 导入结果 + [SuppressMessage("", "SH002")] + public ImportResult Merge(Guid archiveId, IEnumerable items, bool aggressive) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + IOrderedQueryable oldData = appDbContext.Achievements + .Where(a => a.ArchiveId == archiveId) + .OrderBy(a => a.Id); + + int add = 0; + int update = 0; + + using (IEnumerator entityEnumerator = oldData.GetEnumerator()) + { + using (IEnumerator uiafEnumerator = items.GetEnumerator()) + { + bool moveEntity = true; + bool moveUIAF = true; + + while (true) + { + bool moveEntityResult = moveEntity && entityEnumerator.MoveNext(); + bool moveUIAFResult = moveUIAF && uiafEnumerator.MoveNext(); + + if (!(moveEntityResult || moveUIAFResult)) + { + break; + } + else + { + EntityAchievement? entity = entityEnumerator.Current; + UIAFItem? uiaf = uiafEnumerator.Current; + + if (entity == null && uiaf != null) + { + appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf)); + add++; + continue; + } + else if (entity != null && uiaf == null) + { + // skip + continue; + } + + if (entity!.Id < uiaf!.Id) + { + moveEntity = true; + moveUIAF = false; + } + else if (entity.Id == uiaf.Id) + { + moveEntity = true; + moveUIAF = true; + + if (aggressive) + { + appDbContext.Achievements.RemoveAndSave(entity); + appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf)); + update++; + } + } + else + { + // entity.Id > uiaf.Id + moveEntity = false; + moveUIAF = true; + + appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf)); + add++; + } + } + } + } + } + + return new(add, update, 0); + } + } + + /// + /// 覆盖 + /// + /// 成就id + /// 待覆盖的项 + /// 导入结果 + [SuppressMessage("", "SH002")] + public ImportResult Overwrite(Guid archiveId, IEnumerable items) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + IOrderedQueryable oldData = appDbContext.Achievements + .Where(a => a.ArchiveId == archiveId) + .OrderBy(a => a.Id); + + int add = 0; + int update = 0; + int remove = 0; + + using (IEnumerator oldDataEnumerator = oldData.GetEnumerator()) + { + using (IEnumerator newDataEnumerator = items.GetEnumerator()) + { + bool moveOld = true; + bool moveNew = true; + + while (true) + { + bool moveOldResult = moveOld && oldDataEnumerator.MoveNext(); + bool moveNewResult = moveNew && newDataEnumerator.MoveNext(); + + if (moveOldResult || moveNewResult) + { + EntityAchievement? oldEntity = oldDataEnumerator.Current; + EntityAchievement? newEntity = newDataEnumerator.Current; + + if (oldEntity == null && newEntity != null) + { + appDbContext.Achievements.AddAndSave(newEntity); + add++; + continue; + } + else if (oldEntity != null && newEntity == null) + { + appDbContext.Achievements.RemoveAndSave(oldEntity); + remove++; + continue; + } + + if (oldEntity!.Id < newEntity!.Id) + { + moveOld = true; + moveNew = false; + appDbContext.Achievements.RemoveAndSave(oldEntity); + remove++; + } + else if (oldEntity.Id == newEntity.Id) + { + moveOld = true; + moveNew = true; + + if (oldEntity.Equals(newEntity)) + { + // skip same entry. + continue; + } + else + { + appDbContext.Achievements.RemoveAndSave(oldEntity); + appDbContext.Achievements.AddAndSave(newEntity); + update++; + } + } + else + { + // entity.Id > uiaf.Id + moveOld = false; + moveNew = true; + appDbContext.Achievements.AddAndSave(newEntity); + add++; + } + } + else + { + break; + } + } + } + } + + return new(add, update, remove); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbOperation.cs deleted file mode 100644 index 758ca4f3..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbOperation.cs +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core.Database; -using Snap.Hutao.Model.Entity.Database; -using Snap.Hutao.Model.InterChange.Achievement; -using EntityAchievement = Snap.Hutao.Model.Entity.Achievement; - -namespace Snap.Hutao.Service.Achievement; - -/// -/// 成就数据库操作 -/// 双指针操作 -/// -[HighQuality] -internal sealed class AchievementDbOperation -{ - private readonly AppDbContext appDbContext; - - /// - /// 构造一个新的成就数据库操作 - /// - /// 数据库上下文 - public AchievementDbOperation(AppDbContext appDbContext) - { - this.appDbContext = appDbContext; - } - - /// - /// 合并 - /// - /// 成就id - /// 待合并的项 - /// 是否贪婪 - /// 导入结果 - [SuppressMessage("", "SH002")] - public ImportResult Merge(Guid archiveId, IEnumerable items, bool aggressive) - { - IOrderedQueryable oldData = appDbContext.Achievements - .Where(a => a.ArchiveId == archiveId) - .OrderBy(a => a.Id); - - int add = 0; - int update = 0; - - using (IEnumerator entityEnumerator = oldData.GetEnumerator()) - { - using (IEnumerator uiafEnumerator = items.GetEnumerator()) - { - bool moveEntity = true; - bool moveUIAF = true; - - while (true) - { - bool moveEntityResult = moveEntity && entityEnumerator.MoveNext(); - bool moveUIAFResult = moveUIAF && uiafEnumerator.MoveNext(); - - if (!(moveEntityResult || moveUIAFResult)) - { - break; - } - else - { - EntityAchievement? entity = entityEnumerator.Current; - UIAFItem? uiaf = uiafEnumerator.Current; - - if (entity == null && uiaf != null) - { - appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf)); - add++; - continue; - } - else if (entity != null && uiaf == null) - { - // skip - continue; - } - - if (entity!.Id < uiaf!.Id) - { - moveEntity = true; - moveUIAF = false; - } - else if (entity.Id == uiaf.Id) - { - moveEntity = true; - moveUIAF = true; - - if (aggressive) - { - appDbContext.Achievements.RemoveAndSave(entity); - appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf)); - update++; - } - } - else - { - // entity.Id > uiaf.Id - moveEntity = false; - moveUIAF = true; - - appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf)); - add++; - } - } - } - } - } - - return new(add, update, 0); - } - - /// - /// 覆盖 - /// - /// 成就id - /// 待覆盖的项 - /// 导入结果 - [SuppressMessage("", "SH002")] - public ImportResult Overwrite(Guid archiveId, IEnumerable items) - { - IOrderedQueryable oldData = appDbContext.Achievements - .Where(a => a.ArchiveId == archiveId) - .OrderBy(a => a.Id); - - int add = 0; - int update = 0; - int remove = 0; - - using (IEnumerator oldDataEnumerator = oldData.GetEnumerator()) - { - using (IEnumerator newDataEnumerator = items.GetEnumerator()) - { - bool moveOld = true; - bool moveNew = true; - - while (true) - { - bool moveOldResult = moveOld && oldDataEnumerator.MoveNext(); - bool moveNewResult = moveNew && newDataEnumerator.MoveNext(); - - if (moveOldResult || moveNewResult) - { - EntityAchievement? oldEntity = oldDataEnumerator.Current; - EntityAchievement? newEntity = newDataEnumerator.Current; - - if (oldEntity == null && newEntity != null) - { - appDbContext.Achievements.AddAndSave(newEntity); - add++; - continue; - } - else if (oldEntity != null && newEntity == null) - { - appDbContext.Achievements.RemoveAndSave(oldEntity); - remove++; - continue; - } - - if (oldEntity!.Id < newEntity!.Id) - { - moveOld = true; - moveNew = false; - appDbContext.Achievements.RemoveAndSave(oldEntity); - remove++; - } - else if (oldEntity.Id == newEntity.Id) - { - moveOld = true; - moveNew = true; - - if (oldEntity.Equals(newEntity)) - { - // skip same entry. - continue; - } - else - { - appDbContext.Achievements.RemoveAndSave(oldEntity); - appDbContext.Achievements.AddAndSave(newEntity); - update++; - } - } - else - { - // entity.Id > uiaf.Id - moveOld = false; - moveNew = true; - appDbContext.Achievements.AddAndSave(newEntity); - add++; - } - } - else - { - break; - } - } - } - } - - return new(add, update, remove); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Collection.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Collection.cs new file mode 100644 index 00000000..f4c07b67 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Collection.cs @@ -0,0 +1,93 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Database; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.Achievement; + +/// +/// 集合部分 +/// +internal sealed partial class AchievementService +{ + /// + public AchievementArchive? CurrentArchive + { + get => dbCurrent.Current; + set => dbCurrent.Current = value; + } + + /// + public ObservableCollection ArchiveCollection + { + get + { + if (archiveCollection == null) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + archiveCollection = appDbContext.AchievementArchives.ToObservableCollection(); + } + + CurrentArchive = archiveCollection.SelectedOrDefault(); + } + + return archiveCollection; + } + } + + /// + public async Task TryAddArchiveAsync(AchievementArchive newArchive) + { + if (string.IsNullOrWhiteSpace(newArchive.Name)) + { + return ArchiveAddResult.InvalidName; + } + + // 查找是否有相同的名称 + if (archiveCollection!.SingleOrDefault(a => a.Name == newArchive.Name) != null) + { + return ArchiveAddResult.AlreadyExists; + } + else + { + // Sync cache + await taskContext.SwitchToMainThreadAsync(); + archiveCollection!.Add(newArchive); + + // Sync database + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.AchievementArchives.AddAndSaveAsync(newArchive).ConfigureAwait(false); + } + + return ArchiveAddResult.Added; + } + } + + /// + public async Task RemoveArchiveAsync(AchievementArchive archive) + { + // Sync cache + await taskContext.SwitchToMainThreadAsync(); + archiveCollection!.Remove(archive); + + // Sync database + await taskContext.SwitchToBackgroundAsync(); + + // Cascade deleted the achievements. + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.AchievementArchives + .ExecuteDeleteWhereAsync(a => a.InnerId == archive.InnerId) + .ConfigureAwait(false); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Interchange.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Interchange.cs new file mode 100644 index 00000000..887e4931 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Interchange.cs @@ -0,0 +1,71 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Model.InterChange.Achievement; +using EntityAchievement = Snap.Hutao.Model.Entity.Achievement; + +namespace Snap.Hutao.Service.Achievement; + +/// +/// 数据交换部分 +/// +internal sealed partial class AchievementService +{ + /// + public async Task ImportFromUIAFAsync(AchievementArchive archive, List list, ImportStrategy strategy) + { + await taskContext.SwitchToBackgroundAsync(); + + Guid archiveId = archive.InnerId; + + switch (strategy) + { + case ImportStrategy.AggressiveMerge: + { + IOrderedEnumerable orederedUIAF = list.OrderBy(a => a.Id); + return achievementDbBulkOperation.Merge(archiveId, orederedUIAF, true); + } + + case ImportStrategy.LazyMerge: + { + IOrderedEnumerable orederedUIAF = list.OrderBy(a => a.Id); + return achievementDbBulkOperation.Merge(archiveId, orederedUIAF, false); + } + + case ImportStrategy.Overwrite: + { + IEnumerable orederedUIAF = list + .Select(uiaf => EntityAchievement.Create(archiveId, uiaf)) + .OrderBy(a => a.Id); + return achievementDbBulkOperation.Overwrite(archiveId, orederedUIAF); + } + + default: + throw Must.NeverHappen(); + } + } + + /// + public async Task ExportToUIAFAsync(AchievementArchive archive) + { + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + List list = appDbContext.Achievements + .Where(i => i.ArchiveId == archive.InnerId) + .AsEnumerable() + .Select(i => i.ToUIAFItem()) + .ToList(); + + return new() + { + Info = UIAFInfo.Create(serviceProvider), + List = list, + }; + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs index d6ca3fe5..e1a91bcd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs @@ -1,14 +1,12 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.Mvvm.Messaging; using Microsoft.EntityFrameworkCore; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; -using Snap.Hutao.Model.InterChange.Achievement; using Snap.Hutao.Model.Primitive; using Snap.Hutao.ViewModel.Achievement; using System.Collections.ObjectModel; @@ -22,12 +20,12 @@ namespace Snap.Hutao.Service.Achievement; /// [HighQuality] [Injection(InjectAs.Scoped, typeof(IAchievementService))] -internal sealed class AchievementService : IAchievementService +internal sealed partial class AchievementService : IAchievementService { - private readonly AppDbContext appDbContext; + private readonly ScopedDbCurrent dbCurrent; + private readonly AchievementDbBulkOperation achievementDbBulkOperation; private readonly ILogger logger; - private readonly DbCurrent dbCurrent; - private readonly AchievementDbOperation achievementDbOperation; + private readonly ITaskContext taskContext; private readonly IServiceProvider serviceProvider; private ObservableCollection? archiveCollection; @@ -36,74 +34,14 @@ internal sealed class AchievementService : IAchievementService /// 构造一个新的成就服务 /// /// 服务提供器 - /// 数据库上下文 - /// 日志器 - /// 消息器 - public AchievementService(IServiceProvider serviceProvider, AppDbContext appDbContext, ILogger logger, IMessenger messenger) + public AchievementService(IServiceProvider serviceProvider) { - this.appDbContext = appDbContext; - this.logger = logger; + achievementDbBulkOperation = serviceProvider.GetRequiredService(); + logger = serviceProvider.GetRequiredService>(); + taskContext = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; - dbCurrent = new(appDbContext.AchievementArchives, messenger); - achievementDbOperation = new(appDbContext); - } - - /// - public AchievementArchive? CurrentArchive - { - get => dbCurrent.Current; - set => dbCurrent.Current = value; - } - - /// - public async Task> GetArchiveCollectionAsync() - { - await ThreadHelper.SwitchToMainThreadAsync(); - return archiveCollection ??= appDbContext.AchievementArchives.AsNoTracking().ToObservableCollection(); - } - - /// - public async Task RemoveArchiveAsync(AchievementArchive archive) - { - // Sync cache - await ThreadHelper.SwitchToMainThreadAsync(); - archiveCollection!.Remove(archive); - - // Sync database - await ThreadHelper.SwitchToBackgroundAsync(); - - // Cascade deleted the achievements. - await appDbContext.AchievementArchives - .ExecuteDeleteWhereAsync(a => a.InnerId == archive.InnerId) - .ConfigureAwait(false); - } - - /// - public async Task TryAddArchiveAsync(AchievementArchive newArchive) - { - if (string.IsNullOrWhiteSpace(newArchive.Name)) - { - return ArchiveAddResult.InvalidName; - } - - // 查找是否有相同的名称 - if (archiveCollection!.SingleOrDefault(a => a.Name == newArchive.Name) != null) - { - return ArchiveAddResult.AlreadyExists; - } - else - { - // Sync cache - await ThreadHelper.SwitchToMainThreadAsync(); - archiveCollection!.Add(newArchive); - - // Sync database - await ThreadHelper.SwitchToBackgroundAsync(); - await appDbContext.AchievementArchives.AddAndSaveAsync(newArchive).ConfigureAwait(false); - - return ArchiveAddResult.Added; - } + dbCurrent = new(serviceProvider); } /// @@ -112,140 +50,94 @@ internal sealed class AchievementService : IAchievementService Dictionary entityMap; try { - entityMap = appDbContext.Achievements - .Where(a => a.ArchiveId == archive.InnerId) - .AsEnumerable() - .ToDictionary(a => a.Id); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + entityMap = appDbContext.Achievements + .Where(a => a.ArchiveId == archive.InnerId) + .AsEnumerable() + .ToDictionary(a => a.Id); + } } catch (ArgumentException ex) { throw ThrowHelper.UserdataCorrupted(SH.ServiceAchievementUserdataCorruptedInnerIdNotUnique, ex); } - List results = new(); - foreach (MetadataAchievement meta in metadata) + return metadata.SelectList(meta => { EntityAchievement? entity = entityMap.GetValueOrDefault(meta.Id) ?? EntityAchievement.Create(archive.InnerId, meta.Id); - results.Add(new(entity, meta)); - } - - return results; + return new AchievementView(entity, meta); + }); } /// public async Task> GetAchievementStatisticsAsync(Dictionary achievementMap) { - await ThreadHelper.SwitchToBackgroundAsync(); - - List results = new(); - foreach (AchievementArchive archive in appDbContext.AchievementArchives) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { - int finished = await appDbContext.Achievements - .Where(a => a.ArchiveId == archive.InnerId) - .Where(a => (int)a.Status >= (int)Model.Intrinsic.AchievementStatus.STATUS_FINISHED) - .CountAsync() - .ConfigureAwait(false); - int count = achievementMap.Count; + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - List achievements = await appDbContext.Achievements - .Where(a => a.ArchiveId == archive.InnerId) - .OrderByDescending(a => a.Time.ToString()) - .Take(2) - .ToListAsync() - .ConfigureAwait(false); - - results.Add(new() + List results = new(); + foreach (AchievementArchive archive in appDbContext.AchievementArchives) { - DisplayName = archive.Name, - FinishDescription = $"{finished}/{count} - {(double)finished / count:P2}", - Achievements = achievements.SelectList(entity => new AchievementView(entity, achievementMap[entity.Id])), - }); - } + int finished = await appDbContext.Achievements + .Where(a => a.ArchiveId == archive.InnerId) - return results; - } + // We already filtered out incompleted achievements when save them. + // .Where(a => (int)a.Status >= (int)Model.Intrinsic.AchievementStatus.STATUS_FINISHED) + .CountAsync() + .ConfigureAwait(false); + int totalCount = achievementMap.Count; - /// - public async Task ExportToUIAFAsync(AchievementArchive archive) - { - await ThreadHelper.SwitchToBackgroundAsync(); - List list = appDbContext.Achievements - .Where(i => i.ArchiveId == archive.InnerId) - .AsEnumerable() - .Select(i => i.ToUIAFItem()) - .ToList(); + List achievements = await appDbContext.Achievements + .Where(a => a.ArchiveId == archive.InnerId) + .OrderByDescending(a => a.Time.ToString()) + .Take(2) + .ToListAsync() + .ConfigureAwait(false); - UIAF uigf = new() - { - Info = UIAFInfo.Create(serviceProvider), - List = list, - }; - - return uigf; - } - - /// - public async Task ImportFromUIAFAsync(AchievementArchive archive, List list, ImportStrategy strategy) - { - await ThreadHelper.SwitchToBackgroundAsync(); - - Guid archiveId = archive.InnerId; - - switch (strategy) - { - case ImportStrategy.AggressiveMerge: + results.Add(new() { - IOrderedEnumerable orederedUIAF = list.OrderBy(a => a.Id); - return achievementDbOperation.Merge(archiveId, orederedUIAF, true); - } + DisplayName = archive.Name, + FinishDescription = $"{finished}/{totalCount} - {(double)finished / totalCount:P2}", + Achievements = achievements.SelectList(entity => new AchievementView(entity, achievementMap[entity.Id])), + }); + } - case ImportStrategy.LazyMerge: - { - IOrderedEnumerable orederedUIAF = list.OrderBy(a => a.Id); - return achievementDbOperation.Merge(archiveId, orederedUIAF, false); - } - - case ImportStrategy.Overwrite: - { - IEnumerable orederedUIAF = list - .Select(uiaf => EntityAchievement.Create(archiveId, uiaf)) - .OrderBy(a => a.Id); - return achievementDbOperation.Overwrite(archiveId, orederedUIAF); - } - - default: - throw Must.NeverHappen(); + return results; } } /// public void SaveAchievements(AchievementArchive archive, List achievements) { - string name = archive.Name; - logger.LogInformation("Begin saving achievements for [{name}]", name); - ValueStopwatch stopwatch = ValueStopwatch.StartNew(); + using (ValueStopwatch.MeasureExecution(logger)) + { + IEnumerable data = achievements + .Where(a => a.IsChecked) + .Select(a => a.Entity) + .OrderBy(a => a.Id); // Overwrite operation requires ordered achievements. + ImportResult result = achievementDbBulkOperation.Overwrite(archive.InnerId, data); - IEnumerable newData = achievements - .Where(a => a.IsChecked) - .Select(a => a.Entity) - .OrderBy(a => a.Id); - ImportResult result = achievementDbOperation.Overwrite(archive.InnerId, newData); - - double time = stopwatch.GetElapsedTime().TotalMilliseconds; - logger.LogInformation("{add} added, {update} updated, {remove} removed", result.Add, result.Update, result.Remove); - logger.LogInformation("Save achievements for [{name}] completed in {time}ms", name, time); + logger.LogInformation("SaveAchievements: {result}", result); + } } /// public void SaveAchievement(AchievementView achievement) { - // Delete exists one. - appDbContext.Achievements.ExecuteDeleteWhere(e => e.InnerId == achievement.Entity.InnerId); - if (achievement.IsChecked) + using (IServiceScope scope = serviceProvider.CreateScope()) { - // treat as new created. - achievement.Entity.InnerId = default; - appDbContext.Achievements.AddAndSave(achievement.Entity); + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + // Delete exists one. + appDbContext.Achievements.ExecuteDeleteWhere(e => e.InnerId == achievement.Entity.InnerId); + if (achievement.IsChecked) + { + appDbContext.Achievements.AddAndSave(achievement.Entity); + } } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs index 29755581..f6442d2a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs @@ -21,6 +21,11 @@ internal interface IAchievementService /// EntityArchive? CurrentArchive { get; set; } + /// + /// 获取用于绑定的成就存档集合 + /// + ObservableCollection ArchiveCollection { get; } + /// /// 异步导出到UIAF /// @@ -43,12 +48,6 @@ internal interface IAchievementService /// 成就统计列表 Task> GetAchievementStatisticsAsync(Dictionary achievementMap); - /// - /// 异步获取用于绑定的成就存档集合 - /// - /// 成就存档集合 - Task> GetArchiveCollectionAsync(); - /// /// 异步导入UIAF数据 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs index 504a11fd..db70c1c2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs @@ -16,16 +16,19 @@ internal sealed partial class AnnouncementService : IAnnouncementService { private static readonly string CacheKey = $"{nameof(AnnouncementService)}.Cache.{nameof(AnnouncementWrapper)}"; + private readonly ITaskContext taskContext; private readonly AnnouncementClient announcementClient; private readonly IMemoryCache memoryCache; /// /// 构造一个新的公告服务 /// + /// 任务上下文 /// 公告提供器 /// 缓存 - public AnnouncementService(AnnouncementClient announcementClient, IMemoryCache memoryCache) + public AnnouncementService(ITaskContext taskContext, AnnouncementClient announcementClient, IMemoryCache memoryCache) { + this.taskContext = taskContext; this.announcementClient = announcementClient; this.memoryCache = memoryCache; } @@ -39,7 +42,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService return (AnnouncementWrapper)cache!; } - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); Response announcementWrapperResponse = await announcementClient .GetAnnouncementsAsync(cancellationToken) .ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbBulkOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbBulkOperation.cs new file mode 100644 index 00000000..490f08fc --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbBulkOperation.cs @@ -0,0 +1,263 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Database; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Model.Metadata; +using Snap.Hutao.Service.AvatarInfo.Transformer; +using Snap.Hutao.Service.Metadata; +using Snap.Hutao.ViewModel.User; +using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; +using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; +using Snap.Hutao.Web.Response; +using System.Runtime.CompilerServices; +using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar; +using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; +using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo; +using RecordCharacter = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar.Character; +using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo; + +namespace Snap.Hutao.Service.AvatarInfo; + +/// +/// 角色信息数据库操作 +/// +[HighQuality] +[Injection(InjectAs.Scoped)] +internal sealed class AvatarInfoDbBulkOperation +{ + private readonly IServiceProvider serviceProvider; + + /// + /// 构造一个新的角色信息数据库操作 + /// + /// 服务提供器 + public AvatarInfoDbBulkOperation(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + /// + /// 更新数据库角色信息 + /// + /// uid + /// Enka信息 + /// 取消令牌 + /// 角色列表 + public List UpdateDbAvatarInfos(string uid, IEnumerable webInfos, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + List dbInfos = appDbContext.AvatarInfos + .Where(i => i.Uid == uid) + .ToList(); + EnsureItemsAvatarIdDistinct(ref dbInfos, uid); + + foreach (EnkaAvatarInfo webInfo in webInfos) + { + if (AvatarIds.IsPlayer(webInfo.AvatarId)) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId); + AddOrUpdateAvatarInfo(entity, uid, appDbContext, webInfo); + } + + token.ThrowIfCancellationRequested(); + return GetDbAvatarInfos(uid); + } + } + + /// + /// 米游社我的角色方式 更新数据库角色信息 + /// + /// 用户与角色 + /// 取消令牌 + /// 角色列表 + public async Task> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + string uid = userAndUid.Uid.Value; + + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + List dbInfos = appDbContext.AvatarInfos + .Where(i => i.Uid == uid) + .ToList(); + EnsureItemsAvatarIdDistinct(ref dbInfos, uid); + + IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService(userAndUid.User.IsOversea); + Response playerInfoResponse = await gameRecordClient + .GetPlayerInfoAsync(userAndUid, token) + .ConfigureAwait(false); + + if (playerInfoResponse.IsOk()) + { + Response charactersResponse = await gameRecordClient + .GetCharactersAsync(userAndUid, playerInfoResponse.Data, token) + .ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); + + if (charactersResponse.IsOk()) + { + List characters = charactersResponse.Data.Avatars; + + GameRecordCharacterAvatarInfoTransformer transformer = serviceProvider.GetRequiredService(); + transformer.IdAvatarMap = await serviceProvider + .GetRequiredService() + .GetIdToAvatarMapAsync(token) + .ConfigureAwait(false); + + foreach (RecordCharacter character in characters) + { + if (AvatarIds.IsPlayer(character.Id)) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id); + AddOrUpdateAvatarInfo(entity, character.Id, uid, appDbContext, transformer, character); + } + } + } + } + + return GetDbAvatarInfos(uid); + } + + /// + /// 米游社养成计算方式 更新数据库角色信息 + /// + /// 用户与角色 + /// 取消令牌 + /// 角色列表 + public async Task> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + string uid = userAndUid.Uid.Value; + + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + List dbInfos = appDbContext.AvatarInfos + .Where(i => i.Uid == uid) + .ToList(); + EnsureItemsAvatarIdDistinct(ref dbInfos, uid); + + CalculateClient calculateClient = scope.ServiceProvider.GetRequiredService(); + List avatars = await calculateClient + .GetAvatarsAsync(userAndUid, token) + .ConfigureAwait(false); + + CalculateAvatarDetailAvatarInfoTransformer transformer = scope.ServiceProvider.GetRequiredService(); + + foreach (CalculateAvatar avatar in avatars) + { + if (AvatarIds.IsPlayer(avatar.Id)) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + Response detailAvatarResponse = await calculateClient + .GetAvatarDetailAsync(userAndUid, avatar, token) + .ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); + + if (!detailAvatarResponse.IsOk()) + { + continue; + } + + ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id); + AvatarDetail detailAvatar = detailAvatarResponse.Data; + AddOrUpdateAvatarInfo(entity, avatar.Id, uid, appDbContext, transformer, detailAvatar); + } + } + + return GetDbAvatarInfos(uid); + } + + /// + /// 获取数据库角色信息 + /// + /// Uid + /// 角色列表 + public List GetDbAvatarInfos(string uid) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + return appDbContext.AvatarInfos + .Where(i => i.Uid == uid) + .Select(i => i.Info) + .ToList(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddOrUpdateAvatarInfo(ModelAvatarInfo? entity, int avatarId, string uid, AppDbContext appDbContext, IAvatarInfoTransformer transformer, TSource source) + { + if (entity == null) + { + EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId }; + transformer.Transform(ref avatarInfo, source); + entity = ModelAvatarInfo.Create(uid, avatarInfo); + appDbContext.AvatarInfos.AddAndSave(entity); + } + else + { + EnkaAvatarInfo avatarInfo = entity.Info; + transformer.Transform(ref avatarInfo, source); + entity.Info = avatarInfo; + appDbContext.AvatarInfos.UpdateAndSave(entity); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddOrUpdateAvatarInfo(ModelAvatarInfo? entity, string uid, AppDbContext appDbContext, EnkaAvatarInfo webInfo) + { + if (entity == null) + { + entity = ModelAvatarInfo.Create(uid, webInfo); + appDbContext.AvatarInfos.AddAndSave(entity); + } + else + { + entity.Info = webInfo; + appDbContext.AvatarInfos.UpdateAndSave(entity); + } + } + + private void EnsureItemsAvatarIdDistinct(ref List dbInfos, string uid) + { + int distinctCount = dbInfos.Select(info => info.Info.AvatarId).ToHashSet().Count; + + // Avatars are actually less than the list told us. + // This means that there are duplicate items. + if (distinctCount < dbInfos.Count) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.AvatarInfos.ExecuteDeleteWhere(i => i.Uid == uid); + } + + dbInfos = new(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs deleted file mode 100644 index a6a9f11d..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core.Database; -using Snap.Hutao.Model.Entity.Database; -using Snap.Hutao.Model.Metadata; -using Snap.Hutao.Service.AvatarInfo.Composer; -using Snap.Hutao.ViewModel.User; -using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; -using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; -using Snap.Hutao.Web.Response; -using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar; -using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; -using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo; -using RecordCharacter = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar.Character; -using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo; - -namespace Snap.Hutao.Service.AvatarInfo; - -/// -/// 角色信息数据库操作 -/// -[HighQuality] -internal sealed class AvatarInfoDbOperation -{ - private readonly AppDbContext appDbContext; - private readonly IServiceProvider serviceProvider; - - /// - /// 构造一个新的角色信息数据库操作 - /// - /// 数据库上下文 - /// 服务提供器 - public AvatarInfoDbOperation(AppDbContext appDbContext, IServiceProvider serviceProvider) - { - this.appDbContext = appDbContext; - this.serviceProvider = serviceProvider; - } - - /// - /// 更新数据库角色信息 - /// - /// uid - /// Enka信息 - /// 取消令牌 - /// 角色列表 - public List UpdateDbAvatarInfos(string uid, IEnumerable webInfos, CancellationToken token) - { - token.ThrowIfCancellationRequested(); - List dbInfos = appDbContext.AvatarInfos - .Where(i => i.Uid == uid) - .ToList(); - EnsureItemsAvatarIdDistinct(ref dbInfos, uid); - - foreach (EnkaAvatarInfo webInfo in webInfos) - { - if (AvatarIds.IsPlayer(webInfo.AvatarId)) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId); - if (entity == null) - { - entity = ModelAvatarInfo.Create(uid, webInfo); - appDbContext.AvatarInfos.AddAndSave(entity); - } - else - { - entity.Info = webInfo; - appDbContext.AvatarInfos.UpdateAndSave(entity); - } - } - - token.ThrowIfCancellationRequested(); - return GetDbAvatarInfos(uid); - } - - /// - /// 米游社我的角色方式 更新数据库角色信息 - /// - /// 用户与角色 - /// 取消令牌 - /// 角色列表 - public async Task> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token) - { - token.ThrowIfCancellationRequested(); - string uid = userAndUid.Uid.Value; - List dbInfos = appDbContext.AvatarInfos - .Where(i => i.Uid == uid) - .ToList(); - EnsureItemsAvatarIdDistinct(ref dbInfos, uid); - - IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService(userAndUid.User.IsOversea); - Response playerInfoResponse = await gameRecordClient - .GetPlayerInfoAsync(userAndUid, token) - .ConfigureAwait(false); - - if (playerInfoResponse.IsOk()) - { - Response charactersResponse = await gameRecordClient - .GetCharactersAsync(userAndUid, playerInfoResponse.Data, token) - .ConfigureAwait(false); - - token.ThrowIfCancellationRequested(); - - if (charactersResponse.IsOk()) - { - List characters = charactersResponse.Data.Avatars; - - GameRecordCharacterAvatarInfoComposer composer = serviceProvider.GetRequiredService(); - - foreach (RecordCharacter character in characters) - { - if (AvatarIds.IsPlayer(character.Id)) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id); - - if (entity == null) - { - EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id }; - avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false); - entity = ModelAvatarInfo.Create(uid, avatarInfo); - appDbContext.AvatarInfos.AddAndSave(entity); - } - else - { - entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false); - appDbContext.AvatarInfos.UpdateAndSave(entity); - } - } - } - } - - return GetDbAvatarInfos(uid); - } - - /// - /// 米游社养成计算方式 更新数据库角色信息 - /// - /// 用户与角色 - /// 取消令牌 - /// 角色列表 - public async Task> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token) - { - token.ThrowIfCancellationRequested(); - string uid = userAndUid.Uid.Value; - List dbInfos = appDbContext.AvatarInfos - .Where(i => i.Uid == uid) - .ToList(); - EnsureItemsAvatarIdDistinct(ref dbInfos, uid); - - CalculateClient calculateClient = Ioc.Default.GetRequiredService(); - List avatars = await calculateClient.GetAvatarsAsync(userAndUid, token).ConfigureAwait(false); - - CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService(); - - foreach (CalculateAvatar avatar in avatars) - { - if (AvatarIds.IsPlayer(avatar.Id)) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - Response detailAvatarResponse = await calculateClient.GetAvatarDetailAsync(userAndUid, avatar, token).ConfigureAwait(false); - - token.ThrowIfCancellationRequested(); - - if (!detailAvatarResponse.IsOk()) - { - continue; - } - - ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id); - AvatarDetail detailAvatar = detailAvatarResponse.Data; - - if (entity == null) - { - EnkaAvatarInfo avatarInfo = new() { AvatarId = avatar.Id }; - avatarInfo = await composer.ComposeAsync(avatarInfo, detailAvatar).ConfigureAwait(false); - entity = ModelAvatarInfo.Create(uid, avatarInfo); - appDbContext.AvatarInfos.AddAndSave(entity); - } - else - { - entity.Info = await composer.ComposeAsync(entity.Info, detailAvatar).ConfigureAwait(false); - appDbContext.AvatarInfos.UpdateAndSave(entity); - } - } - - return GetDbAvatarInfos(uid); - } - - /// - /// 获取数据库角色信息 - /// - /// Uid - /// 角色列表 - public List GetDbAvatarInfos(string uid) - { - return appDbContext.AvatarInfos - .Where(i => i.Uid == uid) - .Select(i => i.Info) - .ToList(); - } - - private void EnsureItemsAvatarIdDistinct(ref List dbInfos, string uid) - { - int distinctCount = dbInfos.Select(info => info.Info.AvatarId).ToHashSet().Count; - - // Avatars are actually less than the list told us. - // This means that there are duplicate items. - if (distinctCount < dbInfos.Count) - { - appDbContext.AvatarInfos.ExecuteDeleteWhere(i => i.Uid == uid); - dbInfos = new(); - } - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs index b84557d1..0fde6f20 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs @@ -21,26 +21,25 @@ namespace Snap.Hutao.Service.AvatarInfo; [Injection(InjectAs.Scoped, typeof(IAvatarInfoService))] internal sealed class AvatarInfoService : IAvatarInfoService { + private readonly AvatarInfoDbBulkOperation avatarInfoDbBulkOperation; private readonly ISummaryFactory summaryFactory; private readonly IMetadataService metadataService; private readonly ILogger logger; - - private readonly AvatarInfoDbOperation avatarInfoDbOperation; + private readonly IServiceProvider serviceProvider; /// /// 构造一个新的角色信息服务 /// /// 数据库上下文 /// 服务提供器 - public AvatarInfoService( - AppDbContext appDbContext, - IServiceProvider serviceProvider) + public AvatarInfoService(IServiceProvider serviceProvider) { + avatarInfoDbBulkOperation = serviceProvider.GetRequiredService(); metadataService = serviceProvider.GetRequiredService(); summaryFactory = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); - avatarInfoDbOperation = new(appDbContext, serviceProvider); + this.serviceProvider = serviceProvider; } /// @@ -58,36 +57,41 @@ internal sealed class AvatarInfoService : IAvatarInfoService token.ThrowIfCancellationRequested(); if (resp == null) { - return new(RefreshResult.APIUnavailable, null); + return new(RefreshResult.APIUnavailable, default); + } + + if (!string.IsNullOrEmpty(resp.Message)) + { + return new(RefreshResult.StatusCodeNotSucceed, new Summary { Message = resp.Message }); } if (!resp.IsValid) { - return new(RefreshResult.ShowcaseNotOpen, null); + return new(RefreshResult.ShowcaseNotOpen, default); } - List list = avatarInfoDbOperation.UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token); + List list = avatarInfoDbBulkOperation.UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token); Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false); return new(RefreshResult.Ok, summary); } case RefreshOption.RequestFromHoyolabGameRecord: { - List list = await avatarInfoDbOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false); + List list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false); Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false); return new(RefreshResult.Ok, summary); } case RefreshOption.RequestFromHoyolabCalculate: { - List list = await avatarInfoDbOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false); + List list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false); Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false); return new(RefreshResult.Ok, summary); } default: { - List list = avatarInfoDbOperation.GetDbAvatarInfos(userAndUid.Uid.Value); + List list = avatarInfoDbBulkOperation.GetDbAvatarInfos(userAndUid.Uid.Value); Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); return new(RefreshResult.Ok, summary.Avatars.Count == 0 ? null : summary); @@ -100,9 +104,9 @@ internal sealed class AvatarInfoService : IAvatarInfoService } } - private static async Task GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default) + private async Task GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default) { - EnkaClient enkaClient = Ioc.Default.GetRequiredService(); + EnkaClient enkaClient = serviceProvider.GetRequiredService(); return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false) ?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false); @@ -110,10 +114,9 @@ internal sealed class AvatarInfoService : IAvatarInfoService private async Task GetSummaryCoreAsync(IEnumerable avatarInfos, CancellationToken token) { - ValueStopwatch stopwatch = ValueStopwatch.StartNew(); - Summary summary = await summaryFactory.CreateAsync(avatarInfos, token).ConfigureAwait(false); - logger.LogInformation("AvatarInfoSummary Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds); - - return summary; + using (ValueStopwatch.MeasureExecution(logger)) + { + return await summaryFactory.CreateAsync(avatarInfos, token).ConfigureAwait(false); + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/AffixWeight.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/AffixWeight.cs index 08adb469..f6f6476c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/AffixWeight.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/AffixWeight.cs @@ -10,7 +10,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory; /// 词条权重 /// [HighQuality] -internal sealed class AffixWeight : Dictionary +internal sealed class AffixWeight : Dictionary { /// /// 构造一个新的词条权重 @@ -27,14 +27,14 @@ internal sealed class AffixWeight : Dictionary /// 名称 public AffixWeight( int avatarId, - double hp, - double atk, - double def, - double crit, - double critHurt, - double mastery, - double charge, - double heal, + float hp, + float atk, + float def, + float crit, + float critHurt, + float mastery, + float charge, + float heal, string name = "通用") { AvatarId = avatarId; @@ -65,7 +65,7 @@ internal sealed class AffixWeight : Dictionary /// /// 值 /// 链式调用对象 - public AffixWeight Anemo(double value = 100) + public AffixWeight Anemo(float value = 100) { this[FightProperty.FIGHT_PROP_WIND_ADD_HURT] = value; return this; @@ -76,7 +76,7 @@ internal sealed class AffixWeight : Dictionary /// /// 值 /// 链式调用对象 - public AffixWeight Cryo(double value = 100) + public AffixWeight Cryo(float value = 100) { this[FightProperty.FIGHT_PROP_ICE_ADD_HURT] = value; return this; @@ -87,7 +87,7 @@ internal sealed class AffixWeight : Dictionary /// /// 值 /// 链式调用对象 - public AffixWeight Dendro(double value = 100) + public AffixWeight Dendro(float value = 100) { this[FightProperty.FIGHT_PROP_GRASS_ADD_HURT] = value; return this; @@ -98,7 +98,7 @@ internal sealed class AffixWeight : Dictionary /// /// 值 /// 链式调用对象 - public AffixWeight Electro(double value = 100) + public AffixWeight Electro(float value = 100) { this[FightProperty.FIGHT_PROP_ELEC_ADD_HURT] = value; return this; @@ -109,7 +109,7 @@ internal sealed class AffixWeight : Dictionary /// /// 值 /// 链式调用对象 - public AffixWeight Geo(double value = 100) + public AffixWeight Geo(float value = 100) { this[FightProperty.FIGHT_PROP_ROCK_ADD_HURT] = value; return this; @@ -120,7 +120,7 @@ internal sealed class AffixWeight : Dictionary /// /// 值 /// 链式调用对象 - public AffixWeight Hydro(double value = 100) + public AffixWeight Hydro(float value = 100) { this[FightProperty.FIGHT_PROP_WATER_ADD_HURT] = value; return this; @@ -131,7 +131,7 @@ internal sealed class AffixWeight : Dictionary /// /// 值 /// 链式调用对象 - public AffixWeight Pyro(double value = 100) + public AffixWeight Pyro(float value = 100) { this[FightProperty.FIGHT_PROP_FIRE_ADD_HURT] = value; return this; @@ -142,7 +142,7 @@ internal sealed class AffixWeight : Dictionary /// /// 值 /// 链式调用对象 - public AffixWeight Physical(double value = 100) + public AffixWeight Physical(float value = 100) { this[FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT] = value; return this; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ElementMastery.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ElementMastery.cs index a8d65bf7..96b0020f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ElementMastery.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ElementMastery.cs @@ -37,6 +37,6 @@ internal static class ElementMastery /// 差异 public static float GetDelta(float mastery, in ElementMasteryCoefficient coeff) { - return mastery + coeff.P2 == 0 ? 0 : MathF.Max(mastery * coeff.P1 / (mastery + coeff.P2), 0); + return mastery + coeff.Param2 == 0 ? 0 : MathF.Max(mastery * coeff.Param1 / (mastery + coeff.Param2), 0); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ElementMasteryCoefficient.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ElementMasteryCoefficient.cs index 05fb0ea1..2b32c05c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ElementMasteryCoefficient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ElementMasteryCoefficient.cs @@ -12,12 +12,12 @@ internal readonly struct ElementMasteryCoefficient /// /// 参数1 /// - public readonly float P1; + public readonly float Param1; /// /// 参数2 /// - public readonly float P2; + public readonly float Param2; /// /// 构造一个新的元素精通系数 @@ -26,7 +26,7 @@ internal readonly struct ElementMasteryCoefficient /// 参数2 public ElementMasteryCoefficient(float p1, float p2) { - P1 = p1; - P2 = p2; + Param1 = p1; + Param2 = p2; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs index c7c72328..9980e23d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs @@ -16,7 +16,7 @@ using PropertyWeapon = Snap.Hutao.ViewModel.AvatarProperty.WeaponView; namespace Snap.Hutao.Service.AvatarInfo.Factory; /// -/// 简述角色工厂 +/// 单个角色工厂 /// [HighQuality] internal sealed class SummaryAvatarFactory @@ -39,7 +39,7 @@ internal sealed class SummaryAvatarFactory /// 创建角色 /// /// 角色 - public PropertyAvatar CreateAvatar() + public PropertyAvatar Create() { ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull()); MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId]; @@ -59,26 +59,25 @@ internal sealed class SummaryAvatarFactory // webinfo part FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0, - Properties = SummaryFightPropertyMapHelper.CreateAvatarProperties(avatarInfo.FightPropMap), + Properties = SummaryAvatarProperties.Create(avatarInfo.FightPropMap), CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}", - LevelNumber = avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].ValueInt32 ?? 0, + LevelNumber = avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? 0, // processed webinfo part Weapon = reliquaryAndWeapon.Weapon, Reliquaries = reliquaryAndWeapon.Reliquaries, - Score = reliquaryAndWeapon.Reliquaries.Sum(r => r.Score).ToString("F2"), + Score = $"{reliquaryAndWeapon.Reliquaries.Sum(r => r.Score):F2}", }; - TryApplyCostumeIconToAvatar(ref propertyAvatar, avatar); + TryApplyCostumeIcon(ref propertyAvatar, avatar); return propertyAvatar; } - private void TryApplyCostumeIconToAvatar(ref PropertyAvatar propertyAvatar, MetadataAvatar avatar) + private void TryApplyCostumeIcon(ref PropertyAvatar propertyAvatar, MetadataAvatar avatar) { if (avatarInfo.CostumeId.HasValue) { - CostumeId costumeId = avatarInfo.CostumeId.Value; - Model.Metadata.Avatar.Costume costume = avatar.Costumes.Single(c => c.Id == costumeId); + Model.Metadata.Avatar.Costume costume = avatar.Costumes.Single(c => c.Id == avatarInfo.CostumeId.Value); // Set to costume icon propertyAvatar.Icon = AvatarIconConverter.IconNameToUri(costume.Icon); @@ -120,10 +119,10 @@ internal sealed class SummaryAvatarFactory // AffixMap can be empty when it's a white weapon. KeyValuePair? idLevel = equip.Weapon!.AffixMap?.Single(); - int affixLevel = idLevel.HasValue ? idLevel.Value.Value : 0; + int affixLevel = idLevel?.Value ?? 0; - WeaponStat? mainStat = equip.Flat.WeaponStats?[0]; - WeaponStat? subStat = equip.Flat.WeaponStats?.Count > 1 ? equip.Flat.WeaponStats![1] : null; + WeaponStat? mainStat = equip.Flat.WeaponStats?.ElementAtOrDefault(0); + WeaponStat? subStat = equip.Flat.WeaponStats?.ElementAtOrDefault(1); NameDescription subProperty; if (subStat == null) @@ -132,8 +131,11 @@ internal sealed class SummaryAvatarFactory } else { - subStat.StatValue = subStat.StatValue - Math.Truncate(subStat.StatValue) > 0 ? subStat.StatValue / 100D : subStat.StatValue; - subProperty = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatNameDescription(subStat.AppendPropId, subStat.StatValue); + // 是否为整数 + subStat.StatValue = subStat.StatValue == MathF.Truncate(subStat.StatValue) + ? subStat.StatValue / 100F + : subStat.StatValue; + subProperty = FightPropertyFormat.ToNameDescription(subStat.AppendPropId, subStat.StatValue); } return new() @@ -146,7 +148,7 @@ internal sealed class SummaryAvatarFactory // EquipBase Level = $"Lv.{equip.Weapon!.Level}", Quality = weapon.Quality, - MainProperty = mainStat != null ? new(mainStat.AppendPropId.GetLocalizedDescription(), mainStat.StatValue.ToString()) : default!, + MainProperty = mainStat != null ? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue) : default!, // Weapon Id = weapon.Id, @@ -158,10 +160,10 @@ internal sealed class SummaryAvatarFactory }; } - private struct ReliquaryAndWeapon + private readonly struct ReliquaryAndWeapon { - public List Reliquaries; - public PropertyWeapon? Weapon; + public readonly List Reliquaries; + public readonly PropertyWeapon? Weapon; public ReliquaryAndWeapon(List reliquaries, PropertyWeapon? weapon) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarProperties.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarProperties.cs new file mode 100644 index 00000000..b7a9c749 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarProperties.cs @@ -0,0 +1,120 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.ViewModel.AvatarProperty; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 简述战斗属性帮助类 +/// +[HighQuality] +internal static class SummaryAvatarProperties +{ + /// + /// 创建角色属性 + /// + /// 属性映射 + /// 列表 + public static List Create(Dictionary fightPropMap) + { + if (fightPropMap == null) + { + return new(); + } + + AvatarProperty hpProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_HP, fightPropMap); + AvatarProperty atkProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, fightPropMap); + AvatarProperty defProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, fightPropMap); + AvatarProperty emProp = ToAvatarProperty(FightProperty.FIGHT_PROP_ELEMENT_MASTERY, fightPropMap); + AvatarProperty critRateProp = ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL, fightPropMap); + AvatarProperty critDMGProp = ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, fightPropMap); + AvatarProperty chargeEffProp = ToAvatarProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, fightPropMap); + + List properties = new(9) { hpProp, atkProp, defProp, emProp, critRateProp, critDMGProp, chargeEffProp }; + + // 元素伤害 + if (TryGetBonusFightProperty(fightPropMap, out FightProperty bonusProperty)) + { + float value = fightPropMap[bonusProperty]; + if (value > 0) + { + properties.Add(FightPropertyFormat.ToAvatarProperty(bonusProperty, value)); + } + } + + // 物伤 可以和其他元素伤害并存,所以分别判断 + if (fightPropMap.TryGetValue(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, out float addValue)) + { + if (addValue > 0) + { + properties.Add(FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, addValue)); + } + } + + return properties; + } + + private static AvatarProperty ToAvatarProperty(FightProperty baseProp, Dictionary fightPropMap) + { + // 1 2 3 2000 + // 4 5 6 2001 + // 7 8 9 2002 + float baseHp = fightPropMap.GetValueOrDefault(baseProp); + float hp = fightPropMap.GetValueOrDefault(baseProp + 1); + float hpPercent = fightPropMap.GetValueOrDefault(baseProp + 2); + float hpAdd = hp + (baseHp * hpPercent); + + return FightPropertyFormat.ToAvatarProperty(baseProp + 1999, baseHp, hpAdd); + } + + private static bool TryGetBonusFightProperty(Dictionary fightPropMap, out FightProperty value) + { + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY)) + { + value = FightProperty.FIGHT_PROP_FIRE_ADD_HURT; // 70 40 + return true; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY)) + { + value = FightProperty.FIGHT_PROP_ELEC_ADD_HURT; // 71 41 + return true; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_WATER_ENERGY)) + { + value = FightProperty.FIGHT_PROP_WATER_ADD_HURT; // 72 42 + return true; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY)) + { + value = FightProperty.FIGHT_PROP_GRASS_ADD_HURT; // 73 43 + return true; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_WIND_ENERGY)) + { + value = FightProperty.FIGHT_PROP_WIND_ADD_HURT; // 74 44 + return true; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ICE_ENERGY)) + { + value = FightProperty.FIGHT_PROP_ICE_ADD_HURT; // 75 46 + return true; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY)) + { + value = FightProperty.FIGHT_PROP_ROCK_ADD_HURT; // 76 45 + return true; + } + + value = FightProperty.FIGHT_PROP_NONE; + return false; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs index e67fab50..ca47493c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs @@ -43,7 +43,7 @@ internal sealed class SummaryFactory : ISummaryFactory { Avatars = avatarInfos .Where(a => !AvatarIds.IsPlayer(a.AvatarId)) - .Select(a => new SummaryAvatarFactory(metadataContext, a).CreateAvatar()) + .Select(a => new SummaryAvatarFactory(metadataContext, a).Create()) .OrderByDescending(a => (int)a.Quality) .ThenByDescending(a => a.ActivatedConstellationCount) .ToList(), diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFightPropertyMapHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFightPropertyMapHelper.cs deleted file mode 100644 index 2eefc148..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFightPropertyMapHelper.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Model.Intrinsic; -using Snap.Hutao.Model.Metadata.Annotation; -using Snap.Hutao.ViewModel.AvatarProperty; - -namespace Snap.Hutao.Service.AvatarInfo.Factory; - -/// -/// 简述战斗属性帮助类 -/// -[HighQuality] -internal static class SummaryFightPropertyMapHelper -{ - /// - /// 创建角色属性 - /// - /// 属性映射 - /// 列表 - public static List CreateAvatarProperties(Dictionary fightPropMap) - { - if (fightPropMap == null) - { - return new(); - } - - AvatarProperty hpProp = GetHpProperty(fightPropMap); - AvatarProperty atkProp = GetAtkProperty(fightPropMap); - AvatarProperty defProp = GetDefProperty(fightPropMap); - - double em = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28 - AvatarProperty emProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - FightProperty.FIGHT_PROP_ELEMENT_MASTERY, SH.ServiceAvatarInfoPropertyEM, FormatMethod.Integer, em); - - double critRate = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL); // 20 - AvatarProperty critRateProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - FightProperty.FIGHT_PROP_CRITICAL, SH.ServiceAvatarInfoPropertyCR, FormatMethod.Percent, critRate); - - double critDMG = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22 - AvatarProperty critDMGProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - FightProperty.FIGHT_PROP_CRITICAL_HURT, SH.ServiceAvatarInfoPropertyCDmg, FormatMethod.Percent, critDMG); - - double chargeEff = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23 - AvatarProperty chargeEffProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, SH.ServiceAvatarInfoPropertyCE, FormatMethod.Percent, chargeEff); - - List properties = new(9) { hpProp, atkProp, defProp, emProp, critRateProp, critDMGProp, chargeEffProp }; - - // 元素伤害 - FightProperty bonusProperty = GetBonusFightProperty(fightPropMap); - if (bonusProperty != FightProperty.FIGHT_PROP_NONE) - { - double value = fightPropMap[bonusProperty]; - if (value > 0) - { - AvatarProperty bonusProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - bonusProperty, bonusProperty.GetLocalizedDescription(), FormatMethod.Percent, value); - properties.Add(bonusProp); - } - } - - // 物伤 - if (fightPropMap.TryGetValue(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, out double addValue)) - { - if (addValue > 0) - { - string description = FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT.GetLocalizedDescription(); - AvatarProperty physicalBonusProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, description, FormatMethod.Percent, addValue); - properties.Add(physicalBonusProp); - } - } - - return properties; - } - - private static AvatarProperty GetHpProperty(Dictionary fightPropMap) - { - double baseHp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_HP); // 1 - double hp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP); // 2 - double hpPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP_PERCENT); // 3 - double hpAdd = hp + (baseHp * hpPercent); - AvatarProperty hpProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - FightProperty.FIGHT_PROP_MAX_HP, SH.ServiceAvatarInfoPropertyHp, FormatMethod.Integer, baseHp, hpAdd); - return hpProp; - } - - private static AvatarProperty GetAtkProperty(Dictionary fightPropMap) - { - double baseAtk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_ATTACK); // 4 - double atk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK); // 5 - double atkPrecent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK_PERCENT); // 6 - double atkAdd = atk + (baseAtk * atkPrecent); - AvatarProperty atkProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - FightProperty.FIGHT_PROP_CUR_ATTACK, SH.ServiceAvatarInfoPropertyAtk, FormatMethod.Integer, baseAtk, atkAdd); - return atkProp; - } - - private static AvatarProperty GetDefProperty(Dictionary fightPropMap) - { - double baseDef = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_DEFENSE); // 7 - double def = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE); // 8 - double defPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE_PERCENT); // 9 - double defAdd = def + (baseDef * defPercent); - AvatarProperty defProp = Model.Metadata.Converter.PropertiesParametersDescriptor.FormatAvatarProperty( - FightProperty.FIGHT_PROP_CUR_DEFENSE, SH.ServiceAvatarInfoPropertyDef, FormatMethod.Integer, baseDef, defAdd); - return defProp; - } - - private static FightProperty GetBonusFightProperty(IDictionary fightPropMap) - { - if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY)) - { - return FightProperty.FIGHT_PROP_FIRE_ADD_HURT; // 70 40 - } - - if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY)) - { - return FightProperty.FIGHT_PROP_ELEC_ADD_HURT; // 71 41 - } - - if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_WATER_ENERGY)) - { - return FightProperty.FIGHT_PROP_WATER_ADD_HURT; // 72 42 - } - - if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY)) - { - return FightProperty.FIGHT_PROP_GRASS_ADD_HURT; // 73 43 - } - - if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_WIND_ENERGY)) - { - return FightProperty.FIGHT_PROP_WIND_ADD_HURT; // 74 44 - } - - if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ICE_ENERGY)) - { - return FightProperty.FIGHT_PROP_ICE_ADD_HURT; // 75 46 - } - - if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY)) - { - return FightProperty.FIGHT_PROP_ROCK_ADD_HURT; // 76 45 - } - - return FightProperty.FIGHT_PROP_NONE; - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs index 8694c542..8c254c99 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs @@ -39,7 +39,7 @@ internal static class SummaryHelper /// 额外提升等级映射 /// 技能列表 /// 技能 - public static List CreateSkills(Dictionary skillLevelMap, Dictionary? proudSkillExtraLevelMap, IEnumerable proudSkills) + public static List CreateSkills(Dictionary skillLevelMap, Dictionary? proudSkillExtraLevelMap, List proudSkills) { if (skillLevelMap == null) { @@ -50,34 +50,30 @@ internal static class SummaryHelper if (proudSkillExtraLevelMap != null) { - foreach ((string skillGroupId, int extraLevel) in proudSkillExtraLevelMap) + foreach ((string skillGroupIdString, int extraLevel) in proudSkillExtraLevelMap) { - int skillGroupIdInt32 = int.Parse(skillGroupId); - int skillId = proudSkills.Single(p => p.GroupId == skillGroupIdInt32).Id; + SkillGroupId skillGroupId = int.Parse(skillGroupIdString); + SkillId skillId = proudSkills.Single(p => p.GroupId == skillGroupId).Id; - skillExtraLeveledMap.Increase(skillId.ToString(), extraLevel); + skillExtraLeveledMap.Increase($"{skillId.Value}", extraLevel); } } - List skills = new(); - - foreach (ProudableSkill proudableSkill in proudSkills) + return proudSkills.SelectList(proudableSkill => { - SkillView skill = new() + string skillId = $"{proudableSkill.Id.Value}"; + + return new SkillView() { Name = proudableSkill.Name, Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon), Description = proudableSkill.Description, GroupId = proudableSkill.GroupId, - LevelNumber = skillLevelMap[proudableSkill.Id.ToString()], - Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[proudableSkill.Id.ToString()]), + LevelNumber = skillLevelMap[skillId], + Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[skillId]), }; - - skills.Add(skill); - } - - return skills; + }); } /// @@ -104,32 +100,32 @@ internal static class SummaryHelper /// /// id /// 分数 - public static double GetPercentSubAffixScore(int appendId) + public static float GetPercentSubAffixScore(int appendId) { int maxId = GetAffixMaxId(appendId); int delta = maxId - appendId; return (maxId / 100000, delta) switch { - (5, 0) => 100, - (5, 1) => 90, - (5, 2) => 80, - (5, 3) => 70, + (5, 0) => 100F, + (5, 1) => 90F, + (5, 2) => 80F, + (5, 3) => 70F, - (4, 0) => 100, - (4, 1) => 90, - (4, 2) => 80, - (4, 3) => 70, + (4, 0) => 100F, + (4, 1) => 90F, + (4, 2) => 80F, + (4, 3) => 70F, - (3, 0) => 100, - (3, 1) => 85, - (3, 2) => 70, + (3, 0) => 100F, + (3, 1) => 85F, + (3, 2) => 70F, - (2, 0) => 100, - (2, 1) => 80, + (2, 0) => 100F, + (2, 1) => 80F, // TODO: Not quite sure why can we hit this branch. - _ => 0, + _ => 0F, }; } @@ -138,15 +134,15 @@ internal static class SummaryHelper /// /// 属性 /// 评分 - public static double ScoreCrit(IDictionary fightPropMap) + public static float ScoreCrit(Dictionary fightPropMap) { if (fightPropMap == null) { - return 0.0; + return 0F; } - double cr = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL]; - double cd = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL_HURT]; + float cr = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL]; + float cd = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL_HURT]; return 100 * ((cr * 2) + cd); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs index b852496a..25e79003 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Intrinsic; -using Snap.Hutao.Model.Metadata.Annotation; +using Snap.Hutao.Model.Intrinsic.Format; using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Metadata.Reliquary; using Snap.Hutao.ViewModel.AvatarProperty; @@ -48,31 +48,8 @@ internal sealed class SummaryReliquaryFactory List subProperty = equip.Reliquary!.AppendPropIdList.EmptyIfNull().SelectList(CreateSubProperty); int affixCount = GetSecondaryAffixCount(reliquary); - if (subProperty.Count == 0) - { - return new() - { - // NameIconDescription - Name = reliquary.Name, - Icon = RelicIconConverter.IconNameToUri(reliquary.Icon), - Description = reliquary.Description, - // EquipBase - Level = $"+{equip.Reliquary.Level - 1}", - Quality = reliquary.RankLevel, - }; - } - - Span span = CollectionsMarshal.AsSpan(subProperty); - List primary = new(span[..^affixCount].ToArray()); - List secondary = new(span[^affixCount..].ToArray()); - - List composed = equip.Flat.ReliquarySubstats!.SelectList(CreateComposedSubProperty); - - ReliquaryLevel relicLevel = metadataContext.ReliqueryLevels.Single(r => r.Level == equip.Reliquary!.Level && r.Quality == reliquary.RankLevel); - FightProperty property = metadataContext.IdRelicMainPropMap[equip.Reliquary.MainPropId]; - - return new() + PropertyReliquary result = new() { // NameIconDescription Name = reliquary.Name, @@ -82,14 +59,23 @@ internal sealed class SummaryReliquaryFactory // EquipBase Level = $"+{equip.Reliquary.Level - 1}", Quality = reliquary.RankLevel, - MainProperty = new(property.GetLocalizedDescription(), Model.Metadata.Converter.PropertiesParametersDescriptor.FormatValue(property, relicLevel.Properties[property])), - - // Reliquary - ComposedSubProperties = composed, - PrimarySubProperties = primary, - SecondarySubProperties = secondary, - Score = ScoreReliquary(property, reliquary, relicLevel, subProperty), }; + + if (subProperty.Count > 0) + { + Span span = CollectionsMarshal.AsSpan(subProperty); + result.PrimarySubProperties = new(span[..^affixCount].ToEnumerable()); + result.SecondarySubProperties = new(span[^affixCount..].ToEnumerable()); + result.ComposedSubProperties = equip.Flat.ReliquarySubstats!.SelectList(CreateComposedSubProperty); + + ReliquaryLevel relicLevel = metadataContext.ReliqueryLevels.Single(r => r.Level == equip.Reliquary!.Level && r.Quality == reliquary.RankLevel); + FightProperty property = metadataContext.IdRelicMainPropMap[equip.Reliquary.MainPropId]; + + result.MainProperty = FightPropertyFormat.ToNameValue(property, relicLevel.Properties[property]); + result.Score = ScoreReliquary(property, reliquary, relicLevel, subProperty); + } + + return result; } private int GetSecondaryAffixCount(MetadataReliquary reliquary) @@ -125,30 +111,30 @@ internal sealed class SummaryReliquaryFactory }; } - private double ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryLevel relicLevel, List subProperties) + private float ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryLevel relicLevel, List subProperties) { // 沙 杯 头 - if (equip.Flat.EquipType is EquipType.EQUIP_SHOES or EquipType.EQUIP_RING or EquipType.EQUIP_DRESS) + // equip.Flat.EquipType is EquipType.EQUIP_SHOES or EquipType.EQUIP_RING or EquipType.EQUIP_DRESS + if ((int)equip.Flat.EquipType > 3) { AffixWeight weightConfig = GetAffixWeightForAvatarId(); ReliquaryLevel maxRelicLevel = metadataContext.ReliqueryLevels.Where(r => r.Quality == reliquary.RankLevel).MaxBy(r => r.Level)!; - double percent = relicLevel.Properties[property] / maxRelicLevel.Properties[property]; - double baseScore = 8 * percent * weightConfig.GetValueOrDefault(property, 0); + float percent = relicLevel.Properties[property] / maxRelicLevel.Properties[property]; + float baseScore = 8 * percent * weightConfig.GetValueOrDefault(property); - double score = subProperties.Sum(p => p.Score); + float score = subProperties.Sum(p => p.Score); return ((score + baseScore) / 1700) * 66; } else { - double score = subProperties.Sum(p => p.Score); + float score = subProperties.Sum(p => p.Score); return (score / 900) * 66; } } private AffixWeight GetAffixWeightForAvatarId() { - // TODO: more score support return ReliquaryWeightConfiguration.AffixWeights.FirstOrDefault(w => w.AvatarId == avatarInfo.AvatarId, ReliquaryWeightConfiguration.Default); } @@ -170,29 +156,31 @@ internal sealed class SummaryReliquaryFactory MetadataReliquaryAffix affix = metadataContext.IdReliquaryAffixMap[appendPropId]; FightProperty property = affix.Type; - double score = ScoreSubAffix(appendPropId); - return new(property.GetLocalizedDescription(), Model.Metadata.Converter.PropertiesParametersDescriptor.FormatValue(property, affix.Value), score); + return new( + property.GetLocalizedDescription(), + FightPropertyFormat.FormatValue(property, affix.Value), + ScoreSubAffix(appendPropId)); } - private double ScoreSubAffix(int appendId) + private float ScoreSubAffix(int appendId) { MetadataReliquaryAffix affix = metadataContext.IdReliquaryAffixMap[appendId]; AffixWeight weightConfig = GetAffixWeightForAvatarId(); - double weight = weightConfig.GetValueOrDefault(affix.Type) / 100D; + float weight = weightConfig.GetValueOrDefault(affix.Type) / 100F; // 小字词条,转换到等效百分比计算 if (affix.Type is FightProperty.FIGHT_PROP_HP or FightProperty.FIGHT_PROP_ATTACK or FightProperty.FIGHT_PROP_DEFENSE) { // 等效百分比 [ 当前小字词条 / 角色基本属性 ] - double equalPercent = affix.Value / avatarInfo.FightPropMap[affix.Type - 1]; + float equalPercent = affix.Value / avatarInfo.FightPropMap[affix.Type - 1]; // 获取对应百分比词条权重 - weight = weightConfig.GetValueOrDefault(affix.Type + 1) / 100D; + weight = weightConfig.GetValueOrDefault(affix.Type + 1) / 100F; // 最大同属性百分比数值 最大同属性百分比Id 第四五位是战斗属性位 MetadataReliquaryAffix maxPercentAffix = metadataContext.IdReliquaryAffixMap[SummaryHelper.GetAffixMaxId(appendId + 10)]; - double equalScore = equalPercent / maxPercentAffix.Value; + float equalScore = equalPercent / maxPercentAffix.Value; return weight * equalScore * 100; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs index b4480d63..f68f6449 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs @@ -24,6 +24,11 @@ internal enum RefreshResult /// APIUnavailable, + /// + /// 状态码异常 + /// + StatusCodeNotSucceed, + /// /// 角色橱窗未对外开放 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/CalculateAvatarDetailAvatarInfoComposer.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/CalculateAvatarDetailAvatarInfoTransformer.cs similarity index 51% rename from src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/CalculateAvatarDetailAvatarInfoComposer.cs rename to src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/CalculateAvatarDetailAvatarInfoTransformer.cs index d3db1440..1041ad72 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/CalculateAvatarDetailAvatarInfoComposer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/CalculateAvatarDetailAvatarInfoTransformer.cs @@ -3,20 +3,19 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; -namespace Snap.Hutao.Service.AvatarInfo.Composer; +namespace Snap.Hutao.Service.AvatarInfo.Transformer; /// /// 计数器角色详情转角色信息 /// [HighQuality] [Injection(InjectAs.Transient)] -internal sealed class CalculateAvatarDetailAvatarInfoComposer : IAvatarInfoComposer +internal sealed class CalculateAvatarDetailAvatarInfoTransformer : IAvatarInfoTransformer { /// - public ValueTask ComposeAsync(Web.Enka.Model.AvatarInfo avatarInfo, AvatarDetail source) + public void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, AvatarDetail source) { // update skills - avatarInfo.SkillLevelMap = source.SkillList.ToDictionary(s => s.Id.ToString(), s => s.LevelCurrent); - return ValueTask.FromResult(avatarInfo); + avatarInfo.SkillLevelMap = source.SkillList.ToDictionary(s => $"{s.Id}", s => s.LevelCurrent); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/GameRecordCharacterAvatarInfoComposer.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs similarity index 63% rename from src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/GameRecordCharacterAvatarInfoComposer.cs rename to src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs index 288940af..e44d37fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/GameRecordCharacterAvatarInfoComposer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs @@ -3,35 +3,34 @@ using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Primitive; -using Snap.Hutao.Service.Metadata; using Snap.Hutao.Web.Enka.Model; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar; -namespace Snap.Hutao.Service.AvatarInfo.Composer; +namespace Snap.Hutao.Service.AvatarInfo.Transformer; /// /// 游戏记录角色转角色详情转换器 /// [HighQuality] [Injection(InjectAs.Transient)] -internal sealed class GameRecordCharacterAvatarInfoComposer : IAvatarInfoComposer +internal sealed class GameRecordCharacterAvatarInfoTransformer : IAvatarInfoTransformer { - private readonly IMetadataService metadataService; - /// /// 构造一个新的游戏记录角色转角色详情转换器 /// - /// 元数据服务 - public GameRecordCharacterAvatarInfoComposer(IMetadataService metadataService) + public GameRecordCharacterAvatarInfoTransformer() { - this.metadataService = metadataService; } + /// + /// Id 角色映射 + /// + public Dictionary? IdAvatarMap { get; set; } + /// - public async ValueTask ComposeAsync(Web.Enka.Model.AvatarInfo avatarInfo, Character source) + public void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, Character source) { - Dictionary map = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); - Model.Metadata.Avatar.Avatar avatar = map[source.Id]; + Model.Metadata.Avatar.Avatar avatar = Must.NotNull(IdAvatarMap!)[source.Id]; // update fetter avatarInfo.FetterInfo ??= new(); @@ -39,7 +38,7 @@ internal sealed class GameRecordCharacterAvatarInfoComposer : IAvatarInfoCompose // update level avatarInfo.PropMap ??= new Dictionary(); - avatarInfo.PropMap[PlayerProperty.PROP_LEVEL] = new(PlayerProperty.PROP_LEVEL, source.Level.ToString()); + avatarInfo.PropMap[PlayerProperty.PROP_LEVEL] = new(PlayerProperty.PROP_LEVEL, $"{source.Level}"); // update constellations avatarInfo.TalentIdList = source.Constellations.Where(t => t.IsActived).Select(t => t.Id).ToList(); @@ -52,14 +51,14 @@ internal sealed class GameRecordCharacterAvatarInfoComposer : IAvatarInfoCompose Flat = new() { ItemType = ItemType.ITEM_RELIQUARY, EquipType = r.Position, }, }); - Equip? equip = avatarInfo.EquipList.LastOrDefault(); - if (equip == null || equip.Weapon == null) + Equip? equipTest = avatarInfo.EquipList.LastOrDefault(); + if (equipTest == null || equipTest.Weapon == null) { // 不存在武器则添加 avatarInfo.EquipList.Add(new()); } - equip = avatarInfo.EquipList.Last(); + Equip equip = avatarInfo.EquipList.Last(); equip.ItemId = source.Weapon.Id; equip.Weapon = new() @@ -70,7 +69,5 @@ internal sealed class GameRecordCharacterAvatarInfoComposer : IAvatarInfoCompose // Special case here, don't set EQUIP_WEAPON equip.Flat = new() { ItemType = ItemType.ITEM_WEAPON, }; - - return avatarInfo; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/IAvatarInfoComposer.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/IAvatarInfoTransformer.cs similarity index 57% rename from src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/IAvatarInfoComposer.cs rename to src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/IAvatarInfoTransformer.cs index e26f4641..dd798b79 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Composer/IAvatarInfoComposer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/IAvatarInfoTransformer.cs @@ -1,20 +1,19 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Service.AvatarInfo.Composer; +namespace Snap.Hutao.Service.AvatarInfo.Transformer; /// -/// 角色信息合并器 +/// 角色信息转换器 /// /// 源类型 [HighQuality] -internal interface IAvatarInfoComposer +internal interface IAvatarInfoTransformer { /// /// 合并到角色信息 /// /// 基底,角色Id必定存在 /// 源 - /// 任务 - ValueTask ComposeAsync(Web.Enka.Model.AvatarInfo avatarInfo, TSource source); + void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, TSource source); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.Collection.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.Collection.cs new file mode 100644 index 00000000..feaf4336 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.Collection.cs @@ -0,0 +1,100 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Model; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Model.Entity.Primitive; +using Snap.Hutao.Model.Metadata.Item; +using Snap.Hutao.Model.Primitive; +using Snap.Hutao.Service.Metadata; +using Snap.Hutao.ViewModel.Cultivation; +using System.Collections.ObjectModel; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.Service.Cultivation; + +/// +/// 集合部分 +/// +internal sealed partial class CultivationService +{ + /// + public CultivateProject? Current + { + get => dbCurrent.Current; + set => dbCurrent.Current = value; + } + + /// + public ObservableCollection ProjectCollection + { + get + { + if (projects == null) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + projects = appDbContext.CultivateProjects.ToObservableCollection(); + } + + Current ??= projects.SelectedOrDefault(); + } + + return projects; + } + } + + /// + public async Task TryAddProjectAsync(CultivateProject project) + { + if (string.IsNullOrWhiteSpace(project.Name)) + { + return ProjectAddResult.InvalidName; + } + + if (projects!.SingleOrDefault(a => a.Name == project.Name) != null) + { + return ProjectAddResult.AlreadyExists; + } + else + { + // Sync cache + await taskContext.SwitchToMainThreadAsync(); + projects!.Add(project); + + // Sync database + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.CultivateProjects.AddAndSaveAsync(project).ConfigureAwait(false); + } + + return ProjectAddResult.Added; + } + } + + /// + public async Task RemoveProjectAsync(CultivateProject project) + { + // Sync cache + // Keep this on main thread. + await taskContext.SwitchToMainThreadAsync(); + projects!.Remove(project); + + // Sync database + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.CultivateProjects + .ExecuteDeleteWhereAsync(p => p.InnerId == project.InnerId) + .ConfigureAwait(false); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 835a7fe0..7cdf7a3e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.Mvvm.Messaging; using Microsoft.EntityFrameworkCore; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.ExceptionService; @@ -14,6 +13,7 @@ using Snap.Hutao.Model.Primitive; using Snap.Hutao.Service.Metadata; using Snap.Hutao.ViewModel.Cultivation; using System.Collections.ObjectModel; +using System.Runtime.InteropServices; namespace Snap.Hutao.Service.Cultivation; @@ -22,9 +22,10 @@ namespace Snap.Hutao.Service.Cultivation; /// [HighQuality] [Injection(InjectAs.Singleton, typeof(ICultivationService))] -internal sealed class CultivationService : ICultivationService +internal sealed partial class CultivationService : ICultivationService { - private readonly IServiceScopeFactory scopeFactory; + private readonly ITaskContext taskContext; + private readonly IServiceProvider serviceProvider; private readonly ScopedDbCurrent dbCurrent; private ObservableCollection? projects; @@ -32,105 +33,24 @@ internal sealed class CultivationService : ICultivationService /// /// 构造一个新的养成计算服务 /// - /// 范围工厂 + /// 范围工厂 /// 消息器 - public CultivationService(IServiceScopeFactory scopeFactory, IMessenger messenger) + public CultivationService(IServiceProvider serviceProvider) { - this.scopeFactory = scopeFactory; - dbCurrent = new(scopeFactory, provider => provider.GetRequiredService().CultivateProjects, messenger); - } + taskContext = serviceProvider.GetRequiredService(); + dbCurrent = new(serviceProvider); - /// - public CultivateProject? Current - { - get => dbCurrent.Current; - set => dbCurrent.Current = value; - } - - /// - public ObservableCollection GetProjectCollection() - { - if (projects == null) - { - using (IServiceScope scope = scopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - projects = appDbContext.CultivateProjects.AsNoTracking().ToObservableCollection(); - } - - try - { - Current ??= projects.SelectedOrDefault(); - } - catch (DbUpdateConcurrencyException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceCultivationProjectCurrentUserdataCourrpted, ex); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceCultivationProjectCurrentUserdataCourrpted2, ex); - } - } - - return projects; - } - - /// - public async Task TryAddProjectAsync(CultivateProject project) - { - if (string.IsNullOrWhiteSpace(project.Name)) - { - return ProjectAddResult.InvalidName; - } - - if (projects!.SingleOrDefault(a => a.Name == project.Name) != null) - { - return ProjectAddResult.AlreadyExists; - } - else - { - // Sync cache - await ThreadHelper.SwitchToMainThreadAsync(); - projects!.Add(project); - - // Sync database - await ThreadHelper.SwitchToBackgroundAsync(); - using (IServiceScope scope = scopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - await appDbContext.CultivateProjects.AddAndSaveAsync(project).ConfigureAwait(false); - } - - return ProjectAddResult.Added; - } - } - - /// - public async Task RemoveProjectAsync(CultivateProject project) - { - // Sync cache - // Keep this on main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - projects!.Remove(project); - - // Sync database - await ThreadHelper.SwitchToBackgroundAsync(); - using (IServiceScope scope = scopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - await appDbContext.CultivateProjects - .ExecuteDeleteWhereAsync(p => p.InnerId == project.InnerId) - .ConfigureAwait(false); - } + this.serviceProvider = serviceProvider; } /// public List GetInventoryItems(CultivateProject cultivateProject, List metadata, ICommand saveCommand) { - Guid projectId = cultivateProject.InnerId; - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + Guid projectId = cultivateProject.InnerId; List entities = appDbContext.InventoryItems .Where(a => a.ProjectId == projectId) .ToList(); @@ -149,8 +69,8 @@ internal sealed class CultivationService : ICultivationService /// public async Task> GetCultivateEntriesAsync(CultivateProject cultivateProject) { - await ThreadHelper.SwitchToBackgroundAsync(); - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); IMetadataService metadataService = scope.ServiceProvider.GetRequiredService(); @@ -159,12 +79,12 @@ internal sealed class CultivationService : ICultivationService Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false); - List results = new(); List entries = await appDbContext.CultivateEntries .Where(e => e.ProjectId == cultivateProject.InnerId) .ToListAsync() .ConfigureAwait(false); + List results = new(entries.Count); foreach (CultivateEntry entry in entries) { Guid entryId = entry.InnerId; @@ -195,7 +115,8 @@ internal sealed class CultivationService : ICultivationService /// public async Task> GetStatisticsCultivateItemCollectionAsync(CultivateProject cultivateProject, CancellationToken token) { - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { List resultItems = new(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); @@ -241,28 +162,28 @@ internal sealed class CultivationService : ICultivationService token.ThrowIfCancellationRequested(); - await ThreadHelper.SwitchToMainThreadAsync(); - return resultItems.OrderByDescending(i => i.Count).ToObservableCollection(); + await taskContext.SwitchToMainThreadAsync(); + return resultItems.OrderByDescending(i => i.TotalCount).ToObservableCollection(); } } /// public async Task RemoveCultivateEntryAsync(Guid entryId) { - await ThreadHelper.SwitchToBackgroundAsync(); - IEnumerable removed; - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - removed = await GetEntryItemsAsync(appDbContext, entryId).ConfigureAwait(false); - await appDbContext.CultivateEntries.Where(i => i.InnerId == entryId).ExecuteDeleteAsync().ConfigureAwait(false); + await appDbContext.CultivateEntries + .ExecuteDeleteWhereAsync(i => i.InnerId == entryId) + .ConfigureAwait(false); } } /// public void SaveInventoryItem(InventoryItemView item) { - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { scope.ServiceProvider.GetRequiredService().InventoryItems.UpdateAndSave(item.Entity); } @@ -271,7 +192,7 @@ internal sealed class CultivationService : ICultivationService /// public void SaveCultivateItem(CultivateItem item) { - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { scope.ServiceProvider.GetRequiredService().CultivateItems.UpdateAndSave(item); } @@ -285,13 +206,14 @@ internal sealed class CultivationService : ICultivationService return true; } - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); try { - Current ??= appDbContext.CultivateProjects.AsNoTracking().SelectedOrDefault(); + Current ??= appDbContext.CultivateProjects.SelectedOrDefault(); } catch (InvalidOperationException ex) { @@ -328,7 +250,6 @@ internal sealed class CultivationService : ICultivationService private static Task> GetProjectInventoryAsync(AppDbContext appDbContext, Guid projectId) { return appDbContext.InventoryItems - .AsNoTracking() .Where(e => e.ProjectId == projectId) .ToListAsync(); } @@ -337,7 +258,6 @@ internal sealed class CultivationService : ICultivationService private static Task> GetProjectEntriesAsync(AppDbContext appDbContext, Guid projectId) { return appDbContext.CultivateEntries - .AsNoTracking() .Where(e => e.ProjectId == projectId) .ToListAsync(); } @@ -346,7 +266,6 @@ internal sealed class CultivationService : ICultivationService private static Task> GetEntryItemsAsync(AppDbContext appDbContext, Guid entryId) { return appDbContext.CultivateItems - .AsNoTracking() .Where(i => i.EntryId == entryId) .OrderBy(i => i.ItemId) .ToListAsync(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs index b7fbc887..d0c4f0cc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs @@ -21,6 +21,11 @@ internal interface ICultivationService /// CultivateProject? Current { get; set; } + /// + /// 获取用于绑定的项目集合 + /// + ObservableCollection ProjectCollection { get; } + /// /// 获取绑定用的养成列表 /// @@ -37,12 +42,6 @@ internal interface ICultivationService /// 物品列表 List GetInventoryItems(CultivateProject cultivateProject, List metadata, ICommand saveCommand); - /// - /// 获取用于绑定的项目集合 - /// - /// 项目集合 - ObservableCollection GetProjectCollection(); - /// /// 异步获取统计物品列表 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs index 74667bf7..b9e1c183 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs @@ -2,9 +2,8 @@ // Licensed under the MIT license. using CommunityToolkit.WinUI.Notifications; -using Snap.Hutao.Core.Database; using Snap.Hutao.Model.Entity; -using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Service.Game; using Snap.Hutao.Web.Hoyolab.Takumi.Auth; using Snap.Hutao.Web.Hoyolab.Takumi.Binding; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote; @@ -19,17 +18,19 @@ namespace Snap.Hutao.Service.DailyNote; [HighQuality] internal sealed class DailyNoteNotifier { - private readonly IServiceScopeFactory scopeFactory; + private readonly ITaskContext taskContext; + private readonly IServiceProvider serviceProvider; private readonly DailyNoteEntry entry; /// /// 构造一个新的实时便笺通知器 /// - /// 范围工厂 + /// 服务提供器 /// 实时便笺入口 - public DailyNoteNotifier(IServiceScopeFactory scopeFactory, DailyNoteEntry entry) + public DailyNoteNotifier(IServiceProvider serviceProvider, DailyNoteEntry entry) { - this.scopeFactory = scopeFactory; + taskContext = serviceProvider.GetRequiredService(); + this.serviceProvider = serviceProvider; this.entry = entry; } @@ -53,7 +54,7 @@ internal sealed class DailyNoteNotifier return; } - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { DailyNoteOptions options = scope.ServiceProvider.GetRequiredService(); BindingClient bindingClient = scope.ServiceProvider.GetRequiredService(); @@ -121,8 +122,8 @@ internal sealed class DailyNoteNotifier } } - await ThreadHelper.SwitchToMainThreadAsync(); - builder.Show(); + await taskContext.SwitchToMainThreadAsync(); + builder.Show(toast => toast.SuppressPopup = ShouldSuppressPopup(options)); } } @@ -215,12 +216,27 @@ internal sealed class DailyNoteNotifier } } - private struct NotifyInfo + private bool ShouldSuppressPopup(DailyNoteOptions options) { - public string Title; - public string AdaptiveIcon; - public string AdaptiveHint; - public string Hint; + bool isGameRunning = serviceProvider.GetRequiredService().IsGameRunning(); + + if (options.IsSilentWhenPlayingGame && isGameRunning) + { + // Prevent notify when we are in game && silent mode. + return true; + } + else + { + return false; + } + } + + 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) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs index 88b696fa..5f645cb9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs @@ -25,39 +25,36 @@ namespace Snap.Hutao.Service.DailyNote; [Injection(InjectAs.Singleton, typeof(IDailyNoteService))] internal sealed class DailyNoteService : IDailyNoteService, IRecipient { - private readonly IServiceScopeFactory scopeFactory; + private readonly IServiceProvider serviceProvider; private readonly IUserService userService; + private readonly ITaskContext taskContext; private ObservableCollection? entries; /// /// 构造一个新的实时便笺服务 /// - /// 范围工厂 - /// 用户服务 - /// 消息器 - public DailyNoteService(IServiceScopeFactory scopeFactory, IUserService userService, IMessenger messenger) + /// 服务提供器 + public DailyNoteService(IServiceProvider serviceProvider) { - this.scopeFactory = scopeFactory; - this.userService = userService; + userService = serviceProvider.GetRequiredService(); + taskContext = serviceProvider.GetRequiredService(); + this.serviceProvider = serviceProvider; - messenger.Register(this); + serviceProvider.GetRequiredService().Register(this); } /// public void Receive(UserRemovedMessage message) { - ThreadHelper.InvokeOnMainThread(() => - { - // Database items have been deleted by cascade deleting. - entries?.RemoveWhere(n => n.UserId == message.RemovedUserId); - }); + // Database items have been deleted by cascade deleting. + taskContext.InvokeOnMainThread(() => entries?.RemoveWhere(n => n.UserId == message.RemovedUserId)); } /// public async Task AddDailyNoteAsync(UserAndUid role) { string roleUid = role.Uid.Value; - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); @@ -78,7 +75,7 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient(); - List entryList = appDbContext.DailyNotes.AsNoTracking().ToList(); + List entryList = appDbContext.DailyNotes.ToList(); entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); }); entries = new(entryList); } @@ -104,20 +101,11 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient - public async ValueTask RefreshDailyNotesAsync(bool notify) + public async ValueTask RefreshDailyNotesAsync() { - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - DailyNoteOptions options = scope.ServiceProvider.GetRequiredService(); - - bool isGameRunning = scope.ServiceProvider.GetRequiredService().IsGameRunning(); - - if (options.IsSilentWhenPlayingGame && isGameRunning) - { - // Prevent notify when we are in game && silent mode. - notify = false; - } foreach (DailyNoteEntry entry in appDbContext.DailyNotes.Include(n => n.User)) { @@ -134,14 +122,10 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote); - if (notify) - { - await new DailyNoteNotifier(scopeFactory, entry).NotifyAsync().ConfigureAwait(false); - } - + await new DailyNoteNotifier(serviceProvider, entry).NotifyAsync().ConfigureAwait(false); await appDbContext.DailyNotes.UpdateAndSaveAsync(entry).ConfigureAwait(false); } else @@ -165,9 +149,11 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient public async Task RemoveDailyNoteAsync(DailyNoteEntry entry) { + await taskContext.SwitchToMainThreadAsync(); entries!.Remove(entry); - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); await appDbContext.DailyNotes.ExecuteDeleteWhereAsync(d => d.InnerId == entry.InnerId).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/IDailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/IDailyNoteService.cs index c0dd5039..f8753e86 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/IDailyNoteService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/IDailyNoteService.cs @@ -29,9 +29,8 @@ internal interface IDailyNoteService /// /// 异步刷新实时便笺 /// - /// 是否通知 /// 任务 - ValueTask RefreshDailyNotesAsync(bool notify); + ValueTask RefreshDailyNotesAsync(); /// /// 移除指定的实时便笺 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaConfigTypeComparar.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaConfigTypeComparar.cs index ebd0a536..fb1a1b77 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaConfigTypeComparar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaConfigTypeComparar.cs @@ -15,11 +15,11 @@ internal sealed class GachaConfigTypeComparar : IComparer private static readonly GachaConfigTypeComparar InnerShared = new(); private static readonly ImmutableDictionary OrderMap = new Dictionary() { - { GachaConfigType.AvatarEventWish, 0 }, - { GachaConfigType.AvatarEventWish2, 1 }, - { GachaConfigType.WeaponEventWish, 2 }, - { GachaConfigType.StandardWish, 3 }, - { GachaConfigType.NoviceWish, 4 }, + [GachaConfigType.AvatarEventWish] = 0, + [GachaConfigType.AvatarEventWish2] = 1, + [GachaConfigType.WeaponEventWish] = 2, + [GachaConfigType.StandardWish] = 3, + [GachaConfigType.NoviceWish] = 4, }.ToImmutableDictionary(); /// 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 dddb0748..54df2063 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs @@ -38,8 +38,7 @@ internal sealed class GachaStatisticsFactory : IGachaStatisticsFactory public async Task CreateAsync(IOrderedQueryable items, GachaLogServiceContext context) { List gachaEvents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false); - - List historyWishBuilders = gachaEvents.Select(g => new HistoryWishBuilder(g, context)).ToList(); + List historyWishBuilders = gachaEvents.SelectList(g => new HistoryWishBuilder(g, context)); return CreateCore(items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsSlimFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsSlimFactory.cs index d3660c5c..9353abc0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsSlimFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsSlimFactory.cs @@ -15,10 +15,21 @@ namespace Snap.Hutao.Service.GachaLog.Factory; [Injection(InjectAs.Scoped, typeof(IGachaStatisticsSlimFactory))] internal sealed class GachaStatisticsSlimFactory : IGachaStatisticsSlimFactory { + private readonly ITaskContext taskContext; + + /// + /// 构造一个新的简化的祈愿统计工厂 + /// + /// 任务上下文 + public GachaStatisticsSlimFactory(ITaskContext taskContext) + { + this.taskContext = taskContext; + } + /// public async Task CreateAsync(IOrderedQueryable items, GachaLogServiceContext context) { - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); int standardOrangeTracker = 0; int standardPurpleTracker = 0; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs index ca314ecb..1517828d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs @@ -24,7 +24,7 @@ internal static class GachaArchives { try { - collection = appDbContext.GachaArchives.AsNoTracking().ToObservableCollection(); + collection = appDbContext.GachaArchives.ToObservableCollection(); } catch (SqliteException ex) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaItemSaveContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaItemSaveContext.cs new file mode 100644 index 00000000..0258fb8a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaItemSaveContext.cs @@ -0,0 +1,48 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Model.Entity; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 祈愿物品保存上下文 +/// +internal readonly struct GachaItemSaveContext +{ + /// + /// 待添加物品 + /// + public readonly List ItemsToAdd; + + /// + /// 是否懒惰 + /// + public readonly bool IsLazy; + + /// + /// 结尾 Id + /// + public readonly long EndId; + + /// + /// 数据集 + /// + public readonly DbSet GachaItems; + + /// + /// 构造一个新的祈愿物品 + /// + /// 待添加物品 + /// 是否懒惰 + /// 结尾 Id + /// 数据集 + public GachaItemSaveContext(List itemsToAdd, bool isLazy, long endId, DbSet gachaItems) + { + ItemsToAdd = itemsToAdd; + IsLazy = isLazy; + EndId = endId; + GachaItems = gachaItems; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs index 353ea48e..5c8bb4e6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs @@ -14,35 +14,40 @@ namespace Snap.Hutao.Service.GachaLog; [Injection(InjectAs.Scoped, typeof(IGachaLogExportService))] internal sealed class GachaLogExportService : IGachaLogExportService { - private readonly AppDbContext appDbContext; + private readonly ITaskContext taskContext; private readonly IServiceProvider serviceProvider; /// /// 构造一个新的祈愿记录导出服务 /// - /// 数据库上下文 - public GachaLogExportService(IServiceProvider serviceProvider, AppDbContext appDbContext) + /// 服务提供器 + public GachaLogExportService(IServiceProvider serviceProvider) { - this.appDbContext = appDbContext; + taskContext = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; } /// public async Task ExportToUIGFAsync(GachaLogServiceContext context, GachaArchive archive) { - await ThreadHelper.SwitchToBackgroundAsync(); - List list = appDbContext.GachaItems - .Where(i => i.ArchiveId == archive.InnerId) - .AsEnumerable() - .Select(i => i.ToUIGFItem(context.GetNameQualityByItemId(i.ItemId))) - .ToList(); - - UIGF uigf = new() + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { - Info = UIGFInfo.Create(serviceProvider, archive.Uid), - List = list, - }; + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - return uigf; + List list = appDbContext.GachaItems + .Where(i => i.ArchiveId == archive.InnerId) + .AsEnumerable() + .Select(i => i.ToUIGFItem(context.GetNameQualityByItemId(i.ItemId))) + .ToList(); + + UIGF uigf = new() + { + Info = UIGFInfo.Create(serviceProvider, archive.Uid), + List = list, + }; + + return uigf; + } } } \ 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 new file mode 100644 index 00000000..1b05746b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs @@ -0,0 +1,171 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Service.GachaLog.QueryProvider; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 祈愿记录获取上下文 +/// +internal struct GachaLogFetchContext +{ + /// + /// 当前处理的存档 + /// + public GachaArchive? TargetArchive; + + /// + /// 当前的获取状态 + /// + public GachaLogFetchStatus FetchStatus = default!; + + /// + /// 当前的数据库 End Id + /// + public long? DbEndId; + + /// + /// 查询选项 + /// + public GachaLogQueryOptions QueryOptions; + + /// + /// 待加入数据库的物品 + /// + public List ItemsToAdd = default!; + + /// + /// 当前类型增加物品是否结束 + /// + public bool CurrentTypeAddingCompleted; + + /// + /// 当前类型 + /// + public GachaConfigType CurrentType; + + private readonly IServiceProvider serviceProvider; + private readonly GachaLogServiceContext serviceContext; + private readonly bool isLazy; + + /// + /// 构造一个新的祈愿记录获取上下文 + /// + /// 服务提供器 + /// 祈愿服务上下文 + /// 是否为懒惰模式 + public GachaLogFetchContext(IServiceProvider serviceProvider, in GachaLogServiceContext serviceContext, bool isLazy) + { + this.serviceProvider = serviceProvider; + this.serviceContext = serviceContext; + this.isLazy = isLazy; + } + + /// + /// 为下一个卡池类型重置 + /// + /// 卡池类型 + /// 查询 + public void ResetForProcessingType(GachaConfigType configType, in GachaLogQuery query) + { + DbEndId = null; + CurrentType = configType; + ItemsToAdd = new(); + FetchStatus = new(configType); + QueryOptions = new(query, configType); + } + + /// + /// 为下一个物品页面重置 + /// + /// 卡池类型 + public void ResetForProcessingPage() + { + FetchStatus = new(CurrentType); + CurrentTypeAddingCompleted = false; + } + + /// + /// 确保 存档 与 EndId 不为空 + /// + /// 物品 + public void EnsureArchiveAndEndId(GachaLogItem item) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + ITaskContext taskContext = scope.ServiceProvider.GetRequiredService(); + + GachaArchiveInitializationContext initContext = new(taskContext, item.Uid, appDbContext.GachaArchives, serviceContext.ArchiveCollection); + GachaArchive.SkipOrInit(initContext, ref TargetArchive); + DbEndId ??= TargetArchive.GetEndId(CurrentType, appDbContext.GachaItems); + } + } + + /// + /// 判断是否应添加 + /// + /// 物品 + /// 是否应添加 + public bool ShouldAdd(GachaLogItem item) + { + return !isLazy || item.Id > DbEndId; + } + + /// + /// 判断当前类型已经处理完成 + /// + /// 物品集合 + /// 当前类型已经处理完成 + public bool ItemsHaveReachEnd(List items) + { + return CurrentTypeAddingCompleted || items.Count < GachaLogQueryOptions.Size; + } + + /// + /// 添加物品 + /// + /// 物品 + public void AddItem(GachaLogItem item) + { + ItemsToAdd.Add(GachaItem.Create(TargetArchive!.InnerId, item, serviceContext.GetItemId(item))); + FetchStatus.Items.Add(serviceContext.GetItemByNameAndType(item.Name, item.ItemType)); + QueryOptions.EndId = item.Id; + } + + /// + /// 保存物品 + /// + public void SaveItems() + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + GachaItemSaveContext saveContext = new(ItemsToAdd, isLazy, QueryOptions.EndId, appDbContext.GachaItems); + TargetArchive!.SaveItems(saveContext); + } + } + + /// + /// 完成添加 + /// + public void CompleteAdding() + { + CurrentTypeAddingCompleted = true; + } + + /// + /// 反馈进度 + /// + /// 进度 + /// 验证密钥是否过期 + public void Report(IProgress progress, bool isAuthKeyTimeout = false) + { + FetchStatus.AuthKeyTimeout = isAuthKeyTimeout; + progress.Report(FetchStatus); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchStatus.cs similarity index 64% rename from src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs rename to src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchStatus.cs index ab7b49f4..118f5221 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchStatus.cs @@ -7,10 +7,19 @@ using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; namespace Snap.Hutao.Service.GachaLog; /// -/// 获取状态 +/// 祈愿记录获取状态 /// -internal sealed class GachaLogFetchState +internal sealed class GachaLogFetchStatus { + /// + /// 构造一个新的祈愿记录获取状态 + /// + /// 卡池类型 + public GachaLogFetchStatus(GachaConfigType configType) + { + ConfigType = configType; + } + /// /// 验证密钥是否过期 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs index 70046b2d..541f239a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs @@ -15,43 +15,46 @@ namespace Snap.Hutao.Service.GachaLog; internal sealed class GachaLogImportService : IGachaLogImportService { private readonly ITaskContext taskContext; - private readonly AppDbContext appDbContext; private readonly ILogger logger; + private readonly IServiceProvider serviceProvider; /// /// 构造一个新的祈愿记录导入服务 /// - /// 任务上下文 - /// 数据库上下文 - /// 日志器 - public GachaLogImportService(ITaskContext taskContext, AppDbContext appDbContext, ILogger logger) + /// 服务提供器 + public GachaLogImportService(IServiceProvider serviceProvider) { - this.taskContext = taskContext; - this.appDbContext = appDbContext; - this.logger = logger; + taskContext = serviceProvider.GetRequiredService(); + logger = serviceProvider.GetRequiredService>(); + this.serviceProvider = serviceProvider; } /// public async Task ImportFromUIGFAsync(GachaLogServiceContext context, List list, string uid) { - GachaArchiveInitializationContext initContext = new(taskContext, uid, appDbContext.GachaArchives, context.ArchiveCollection); - GachaArchive.Init(initContext, out GachaArchive? archive); - await ThreadHelper.SwitchToBackgroundAsync(); - Guid archiveId = archive.InnerId; + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - long trimId = appDbContext.GachaItems - .Where(i => i.ArchiveId == archiveId) - .OrderBy(i => i.Id) - .FirstOrDefault()?.Id ?? long.MaxValue; + GachaArchiveInitializationContext initContext = new(taskContext, uid, appDbContext.GachaArchives, context.ArchiveCollection); + GachaArchive.Init(initContext, out GachaArchive? archive); + Guid archiveId = archive.InnerId; - logger.LogInformation("Last Id to trim with [{id}]", trimId); + long trimId = appDbContext.GachaItems + .Where(i => i.ArchiveId == archiveId) + .OrderBy(i => i.Id) + .FirstOrDefault()?.Id ?? long.MaxValue; - IEnumerable toAdd = list - .OrderByDescending(i => i.Id) - .Where(i => i.Id < trimId) - .Select(i => GachaItem.Create(archiveId, i, context.GetItemId(i))); + logger.LogInformation("Last Id to trim with: [{id}]", trimId); - await appDbContext.GachaItems.AddRangeAndSaveAsync(toAdd).ConfigureAwait(false); - return archive; + IEnumerable toAdd = list + .OrderByDescending(i => i.Id) + .Where(i => i.Id < trimId) + .Select(i => GachaItem.Create(archiveId, i, context.GetItemId(i))); + + await appDbContext.GachaItems.AddRangeAndSaveAsync(toAdd).ConfigureAwait(false); + return archive; + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs index fa2cacaa..6d0c95e4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs @@ -1,8 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.Mvvm.Messaging; -using Microsoft.EntityFrameworkCore; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Model.Entity; @@ -27,7 +25,6 @@ namespace Snap.Hutao.Service.GachaLog; internal sealed class GachaLogService : IGachaLogService { private readonly ITaskContext taskContext; - private readonly AppDbContext appDbContext; private readonly IGachaLogExportService gachaLogExportService; private readonly IGachaLogImportService gachaLogImportService; private readonly GachaInfoClient gachaInfoClient; @@ -35,7 +32,9 @@ internal sealed class GachaLogService : IGachaLogService private readonly IGachaStatisticsFactory gachaStatisticsFactory; private readonly IGachaStatisticsSlimFactory gachaStatisticsSlimFactory; private readonly ILogger logger; - private readonly DbCurrent dbCurrent; + private readonly IServiceProvider serviceProvider; + + private readonly ScopedDbCurrent dbCurrent; private GachaLogServiceContext context; @@ -43,20 +42,20 @@ internal sealed class GachaLogService : IGachaLogService /// 构造一个新的祈愿记录服务 /// /// 服务提供器 - /// 消息器 - public GachaLogService(IServiceProvider serviceProvider, IMessenger messenger) + public GachaLogService(IServiceProvider serviceProvider) { taskContext = serviceProvider.GetRequiredService(); gachaLogExportService = serviceProvider.GetRequiredService(); gachaLogImportService = serviceProvider.GetRequiredService(); - appDbContext = serviceProvider.GetRequiredService(); gachaInfoClient = serviceProvider.GetRequiredService(); metadataService = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); gachaStatisticsFactory = serviceProvider.GetRequiredService(); gachaStatisticsSlimFactory = serviceProvider.GetRequiredService(); - dbCurrent = new(appDbContext.GachaArchives, messenger); + dbCurrent = new(serviceProvider); + + this.serviceProvider = serviceProvider; } /// @@ -66,6 +65,12 @@ internal sealed class GachaLogService : IGachaLogService set => dbCurrent.Current = value; } + /// + public ObservableCollection ArchiveCollection + { + get => context.ArchiveCollection; + } + /// public async ValueTask InitializeAsync(CancellationToken token) { @@ -82,9 +87,14 @@ internal sealed class GachaLogService : IGachaLogService Dictionary nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false); Dictionary nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false); - GachaArchives.Initialize(appDbContext, out ObservableCollection collection); - context = new(idAvatarMap, idWeaponMap, nameAvatarMap, nameWeaponMap, collection); - return true; + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + GachaArchives.Initialize(appDbContext, out ObservableCollection collection); + + context = new(idAvatarMap, idWeaponMap, nameAvatarMap, nameWeaponMap, collection); + return true; + } } else { @@ -92,12 +102,6 @@ internal sealed class GachaLogService : IGachaLogService } } - /// - public ObservableCollection GetArchiveCollection() - { - return context.ArchiveCollection; - } - /// public async Task GetStatisticsAsync(GachaArchive? archive) { @@ -108,8 +112,15 @@ internal sealed class GachaLogService : IGachaLogService { using (ValueStopwatch.MeasureExecution(logger)) { - IOrderedQueryable items = appDbContext.GachaItems.Where(i => i.ArchiveId == archive.InnerId).OrderBy(i => i.Id); - return await gachaStatisticsFactory.CreateAsync(items, context).ConfigureAwait(false); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + IOrderedQueryable items = appDbContext.GachaItems + .Where(i => i.ArchiveId == archive.InnerId) + .OrderBy(i => i.Id); + + return await gachaStatisticsFactory.CreateAsync(items, context).ConfigureAwait(false); + } } } else @@ -121,19 +132,23 @@ internal sealed class GachaLogService : IGachaLogService /// public async Task> GetStatisticsSlimsAsync() { - List statistics = new(); - foreach (GachaArchive archive in appDbContext.GachaArchives) + using (IServiceScope scope = serviceProvider.CreateScope()) { - IOrderedQueryable items = appDbContext.GachaItems - .Where(i => i.ArchiveId == archive.InnerId) - .OrderBy(i => i.Id); + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + List statistics = new(); + foreach (GachaArchive archive in appDbContext.GachaArchives) + { + IOrderedQueryable items = appDbContext.GachaItems + .Where(i => i.ArchiveId == archive.InnerId) + .OrderBy(i => i.Id); - GachaStatisticsSlim slim = await gachaStatisticsSlimFactory.CreateAsync(items, context).ConfigureAwait(false); - slim.Uid = archive.Uid; - statistics.Add(slim); + GachaStatisticsSlim slim = await gachaStatisticsSlimFactory.CreateAsync(items, context).ConfigureAwait(false); + slim.Uid = archive.Uid; + statistics.Add(slim); + } + + return statistics; } - - return statistics; } /// @@ -149,7 +164,7 @@ internal sealed class GachaLogService : IGachaLogService } /// - public async Task RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token) + public async Task RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token) { bool isLazy = strategy switch { @@ -172,15 +187,18 @@ internal sealed class GachaLogService : IGachaLogService public async Task RemoveArchiveAsync(GachaArchive archive) { // Sync cache - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); context.ArchiveCollection.Remove(archive); // Sync database - await ThreadHelper.SwitchToBackgroundAsync(); - await appDbContext.GachaArchives - .Where(a => a.InnerId == archive.InnerId) - .ExecuteDeleteAsync() - .ConfigureAwait(false); + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.GachaArchives + .ExecuteDeleteWhereAsync(a => a.InnerId == archive.InnerId) + .ConfigureAwait(false); + } } private static Task RandomDelayAsync(CancellationToken token) @@ -188,53 +206,43 @@ internal sealed class GachaLogService : IGachaLogService return Task.Delay(TimeSpan.FromSeconds(Random.Shared.NextDouble() + 1), token); } - private async Task> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress progress, CancellationToken token) + private async Task> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress progress, CancellationToken token) { - GachaArchive? archive = null; - GachaLogFetchState state = new(); + GachaLogFetchContext fetchContext = new(serviceProvider, context, isLazy); foreach (GachaConfigType configType in GachaLog.QueryTypes) { - // 每个卡池类型重置 - long? dbEndId = null; - state.ConfigType = configType; - GachaLogQueryOptions options = new(query, configType); - List itemsToAdd = new(); + fetchContext.ResetForProcessingType(configType, query); do { - Response response = await gachaInfoClient.GetGachaLogPageAsync(options, token).ConfigureAwait(false); + Response response = await gachaInfoClient + .GetGachaLogPageAsync(fetchContext.QueryOptions, token) + .ConfigureAwait(false); - if (response.IsOk()) + if (response.TryGetData(out GachaLogPage? page, serviceProvider)) { - GachaLogPage page = response.Data; - - state.Items.Clear(); List items = page.List; - bool currentTypeAddingCompleted = false; + fetchContext.ResetForProcessingPage(); foreach (GachaLogItem item in items) { - GachaArchiveInitializationContext initContext = new(taskContext, item.Uid, appDbContext.GachaArchives, context.ArchiveCollection); - GachaArchive.SkipOrInit(initContext, ref archive); - dbEndId ??= archive.GetEndId(configType, appDbContext.GachaItems); + fetchContext.EnsureArchiveAndEndId(item); - if ((!isLazy) || item.Id > dbEndId) + if (fetchContext.ShouldAdd(item)) { - itemsToAdd.Add(GachaItem.Create(archive.InnerId, item, context.GetItemId(item))); - state.Items.Add(context.GetItemByNameAndType(item.Name, item.ItemType)); - options.EndId = item.Id; + fetchContext.AddItem(item); } else { - currentTypeAddingCompleted = true; + fetchContext.CompleteAdding(); break; } } - progress.Report(state); + fetchContext.Report(progress); - if (currentTypeAddingCompleted || items.Count < GachaLogQueryOptions.Size) + if (fetchContext.ItemsHaveReachEnd(items)) { // exit current type fetch loop break; @@ -242,8 +250,7 @@ internal sealed class GachaLogService : IGachaLogService } else { - state.AuthKeyTimeout = true; - progress.Report(state); + fetchContext.Report(progress, true); break; } @@ -251,58 +258,16 @@ internal sealed class GachaLogService : IGachaLogService } while (true); - if (state.AuthKeyTimeout) + if (fetchContext.FetchStatus.AuthKeyTimeout) { break; } token.ThrowIfCancellationRequested(); - GachaItemSaveContext saveContext = new(itemsToAdd, isLazy, options.EndId, appDbContext.GachaItems); - archive?.SaveItems(saveContext); + fetchContext.SaveItems(); await RandomDelayAsync(token).ConfigureAwait(false); } - return new(!state.AuthKeyTimeout, archive); - } -} - -/// -/// 祈愿物品 -/// -internal readonly struct GachaItemSaveContext -{ - /// - /// 待添加物品 - /// - public readonly List ItemsToAdd; - - /// - /// 是否懒惰 - /// - public readonly bool IsLazy; - - /// - /// 结尾 Id - /// - public readonly long EndId; - - /// - /// 数据集 - /// - public readonly DbSet GachaItems; - - /// - /// 构造一个新的祈愿物品 - /// - /// 待添加物品 - /// 是否懒惰 - /// 结尾 Id - /// 数据集 - public GachaItemSaveContext(List itemsToAdd, bool isLazy, long endId, DbSet gachaItems) - { - ItemsToAdd = itemsToAdd; - IsLazy = isLazy; - EndId = endId; - GachaItems = gachaItems; + return new(!fetchContext.FetchStatus.AuthKeyTimeout, fetchContext.TargetArchive); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/HutaoCloudService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/HutaoCloudService.cs index c0a59e59..29715ef2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/HutaoCloudService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/HutaoCloudService.cs @@ -8,6 +8,7 @@ using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao.GachaLog; using Snap.Hutao.Web.Response; +using System.Runtime.CompilerServices; namespace Snap.Hutao.Service.GachaLog; @@ -18,17 +19,16 @@ namespace Snap.Hutao.Service.GachaLog; internal sealed class HutaoCloudService : IHutaoCloudService { private readonly HomaGachaLogClient homaGachaLogClient; - private readonly AppDbContext appDbContext; + private readonly IServiceProvider serviceProvider; /// /// 构造一个新的胡桃云服务 /// - /// 胡桃祈愿记录客户端 - /// 数据库上下文 - public HutaoCloudService(HomaGachaLogClient homaGachaLogClient, AppDbContext appDbContext) + /// 服务提供器 + public HutaoCloudService(IServiceProvider serviceProvider) { - this.homaGachaLogClient = homaGachaLogClient; - this.appDbContext = appDbContext; + homaGachaLogClient = serviceProvider.GetRequiredService(); + this.serviceProvider = serviceProvider; } /// @@ -45,26 +45,14 @@ internal sealed class HutaoCloudService : IHutaoCloudService if (endIds != null) { List items = new(); - foreach ((GachaConfigType type, long endId) in endIds) + using (IServiceScope scope = serviceProvider.CreateScope()) { - IEnumerable part = appDbContext.GachaItems - .AsNoTracking() - .Where(i => i.ArchiveId == gachaArchive.InnerId) - .Where(i => i.QueryType == type) - .OrderByDescending(i => i.Id) - .Where(i => i.Id > endId) - - // Keep this to make SQL generates correctly - .Select(i => new GachaItem() - { - GachaType = i.GachaType, - QueryType = i.QueryType, - ItemId = i.ItemId, - Time = i.Time, - Id = i.Id, - }); - - items.AddRange(part); + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + foreach ((GachaConfigType type, long endId) in endIds) + { + IEnumerable part = QueryArchiveGachaItemsByTypeAndEndId(appDbContext, gachaArchive, type, endId); + items.AddRange(part); + } } return await homaGachaLogClient.UploadGachaItemsAsync(uid, items, token).ConfigureAwait(false); @@ -76,45 +64,31 @@ internal sealed class HutaoCloudService : IHutaoCloudService /// public async Task> RetrieveGachaItemsAsync(string uid, CancellationToken token = default) { - Model.Entity.GachaArchive? archive = await appDbContext.GachaArchives - .AsNoTracking() - .SingleOrDefaultAsync(a => a.Uid == uid, token) - .ConfigureAwait(false); - - EndIds endIds = new(); - foreach (GachaConfigType type in GachaLog.QueryTypes) + using (IServiceScope scope = serviceProvider.CreateScope()) { - if (archive != null) - { - Model.Entity.GachaItem? item = appDbContext.GachaItems - .AsNoTracking() - .Where(i => i.ArchiveId == archive.InnerId) - .Where(i => i.QueryType == type) - .OrderBy(i => i.Id) - .FirstOrDefault(); + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - if (item != null) + Model.Entity.GachaArchive? archive = await appDbContext.GachaArchives + .SingleOrDefaultAsync(a => a.Uid == uid, token) + .ConfigureAwait(false); + + EndIds endIds = await EndIds.CreateAsync(appDbContext, archive, token).ConfigureAwait(false); + Response> resp = await homaGachaLogClient.RetrieveGachaItemsAsync(uid, endIds, token).ConfigureAwait(false); + + if (resp.IsOk()) + { + if (archive == null) { - endIds[type] = item.Id; + archive = Model.Entity.GachaArchive.Create(uid); + await appDbContext.GachaArchives.AddAndSaveAsync(archive).ConfigureAwait(false); } + + List gachaItems = resp.Data.SelectList(i => Model.Entity.GachaItem.Create(archive.InnerId, i)); + await appDbContext.GachaItems.AddRangeAndSaveAsync(gachaItems).ConfigureAwait(false); + return new(true, archive); } } - Response> resp = await homaGachaLogClient.RetrieveGachaItemsAsync(uid, endIds, token).ConfigureAwait(false); - - if (resp.IsOk()) - { - if (archive == null) - { - archive = Model.Entity.GachaArchive.Create(uid); - await appDbContext.GachaArchives.AddAndSaveAsync(archive).ConfigureAwait(false); - } - - List gachaItems = resp.Data.SelectList(i => Model.Entity.GachaItem.Create(archive.InnerId, i)); - await appDbContext.GachaItems.AddRangeAndSaveAsync(gachaItems).ConfigureAwait(false); - return new(true, archive); - } - return new(false, null); } @@ -124,6 +98,26 @@ internal sealed class HutaoCloudService : IHutaoCloudService return await homaGachaLogClient.DeleteGachaItemsAsync(uid, token).ConfigureAwait(false); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IEnumerable QueryArchiveGachaItemsByTypeAndEndId(AppDbContext appDbContext, Model.Entity.GachaArchive gachaArchive, GachaConfigType type, long endId) + { + return appDbContext.GachaItems + .Where(i => i.ArchiveId == gachaArchive.InnerId) + .Where(i => i.QueryType == type) + .OrderByDescending(i => i.Id) + .Where(i => i.Id > endId) + + // Keep this to make SQL generates correctly + .Select(i => new GachaItem() + { + GachaType = i.GachaType, + QueryType = i.QueryType, + ItemId = i.ItemId, + Time = i.Time, + Id = i.Id, + }); + } + private async Task GetEndIdsFromCloudAsync(string uid, CancellationToken token = default) { Response resp = await homaGachaLogClient.GetEndIdsAsync(uid, token).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs index 7385494c..8fd2ec20 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs @@ -20,6 +20,11 @@ internal interface IGachaLogService /// GachaArchive? CurrentArchive { get; set; } + /// + /// 获取可用于绑定的存档集合 + /// + ObservableCollection ArchiveCollection { get; } + /// /// 导出为一个新的UIGF对象 /// @@ -27,12 +32,6 @@ internal interface IGachaLogService /// UIGF对象 Task ExportToUIGFAsync(GachaArchive archive); - /// - /// 获取可用于绑定的存档集合 - /// - /// 存档集合 - ObservableCollection GetArchiveCollection(); - /// /// 获得对应的祈愿统计 /// @@ -70,7 +69,7 @@ internal interface IGachaLogService /// 进度 /// 取消令牌 /// 验证密钥是否可用 - Task RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token); + Task RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token); /// /// 删除存档 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 c8b0af61..0808c2f6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs @@ -28,26 +28,21 @@ internal readonly struct GachaLogQuery /// 构造一个新的祈愿记录query /// /// query - /// 是否为国际服 - public GachaLogQuery(string query, bool isOversea) + public GachaLogQuery(string query) { Query = query; - IsOversea = isOversea; + IsOversea = query.Contains("hoyoverse.com"); Message = string.Empty; } - /// - /// 构造一个新的失败的祈愿记录query - /// - /// 失败原因 - public GachaLogQuery(string message) + private GachaLogQuery(string query, string message) { + Query = query; Message = message; - Query = string.Empty; } public static implicit operator GachaLogQuery(string message) { - return new(message); + return new(default!, message); } } \ No newline at end of file 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 3ac0a3a4..a38e8831 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -6,12 +6,23 @@ using Snap.Hutao.View.Dialog; namespace Snap.Hutao.Service.GachaLog.QueryProvider; /// -/// 手动输入方法 +/// 手动输入方法提供器 /// [HighQuality] [Injection(InjectAs.Transient, typeof(IGachaLogQueryProvider))] internal sealed class GachaLogQueryManualInputProvider : IGachaLogQueryProvider { + private readonly ITaskContext taskContext; + + /// + /// 构造一个新的手动输入方法提供器 + /// + /// 任务上下文 + public GachaLogQueryManualInputProvider(ITaskContext taskContext) + { + this.taskContext = taskContext; + } + /// public string Name { get => nameof(GachaLogQueryManualInputProvider); } @@ -19,14 +30,14 @@ internal sealed class GachaLogQueryManualInputProvider : IGachaLogQueryProvider public async Task> GetQueryAsync() { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); (bool isOk, string query) = await new GachaLogUrlDialog().GetInputUrlAsync().ConfigureAwait(false); if (isOk) { if (query.Contains("&auth_appid=webview_gacha")) { - return new(true, new(query, query.Contains("hoyoverse.com"))); + return new(true, new(query)); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs index db161f4d..3e9a916c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs @@ -48,7 +48,7 @@ internal sealed class GachaLogQuerySTokenProvider : IGachaLogQueryProvider if (authkeyResponse.IsOk()) { - return new(true, new(GachaLogQueryOptions.AsQuery(data, authkeyResponse.Data), false)); + return new(true, new(GachaLogQueryOptions.AsQuery(data, authkeyResponse.Data))); } else { 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 72428861..f98e02b4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs @@ -53,7 +53,7 @@ internal sealed class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider { string cacheFile = GetCacheFile(path); - using (TempFile? tempFile = TempFile.CreateCopyFrom(cacheFile)) + using (TempFile? tempFile = TempFile.CopyFrom(cacheFile)) { if (tempFile == null) { @@ -69,7 +69,7 @@ internal sealed class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider if (!string.IsNullOrEmpty(result)) { - return new(true, new(result, result.Contains("hoyoverse.com"))); + return new(true, new(result)); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs index 5aeba103..fcf07de8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs @@ -1,7 +1,6 @@ // 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; @@ -30,11 +29,12 @@ internal sealed class GameService : IGameService { private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}"; - private readonly IServiceScopeFactory scopeFactory; + private readonly ITaskContext taskContext; private readonly IMemoryCache memoryCache; private readonly PackageConverter packageConverter; private readonly LaunchOptions launchOptions; private readonly AppOptions appOptions; + private readonly IServiceProvider serviceProvider; private volatile int runningGamesCounter; private ObservableCollection? gameAccounts; @@ -42,24 +42,40 @@ internal sealed class GameService : IGameService /// /// 构造一个新的游戏服务 /// - /// 范围工厂 - /// 内存缓存 - /// 游戏文件包转换器 - /// 启动游戏选项 - /// 应用选项 - public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache, PackageConverter packageConverter, LaunchOptions launchOptions, AppOptions appOptions) + /// 服务提供器 + public GameService(IServiceProvider serviceProvider) { - this.scopeFactory = scopeFactory; - this.memoryCache = memoryCache; - this.packageConverter = packageConverter; - this.launchOptions = launchOptions; - this.appOptions = appOptions; + memoryCache = serviceProvider.GetRequiredService(); + packageConverter = serviceProvider.GetRequiredService(); + launchOptions = serviceProvider.GetRequiredService(); + appOptions = serviceProvider.GetRequiredService(); + taskContext = serviceProvider.GetRequiredService(); + + this.serviceProvider = serviceProvider; + } + + /// + public ObservableCollection GameAccountCollection + { + get + { + if (gameAccounts == null) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + gameAccounts = appDbContext.GameAccounts.ToObservableCollection(); + } + } + + return gameAccounts; + } } /// public async ValueTask> GetGamePathAsync() { - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { // Cannot find in setting if (string.IsNullOrEmpty(appOptions.GamePath)) @@ -84,7 +100,7 @@ internal sealed class GameService : IGameService if (result.IsOk) { // Save result. - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); appOptions.GamePath = result.Value; } else @@ -200,7 +216,7 @@ internal sealed class GameService : IGameService progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); Response response; - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { response = await scope.ServiceProvider .GetRequiredService() @@ -223,7 +239,7 @@ internal sealed class GameService : IGameService // We need to change the gamePath if we switched. string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); appOptions.GamePath = Path.Combine(gameFolder, exeName); } else @@ -263,22 +279,6 @@ internal sealed class GameService : IGameService || Process.GetProcessesByName(GenshinImpactProcessName).Any(); } - /// - public async Task> GetGameAccountCollectionAsync() - { - if (gameAccounts == null) - { - using (IServiceScope scope = scopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - await ThreadHelper.SwitchToMainThreadAsync(); - gameAccounts = appDbContext.GameAccounts.AsNoTracking().ToObservableCollection(); - } - } - - return gameAccounts; - } - /// public async ValueTask LaunchAsync() { @@ -344,7 +344,7 @@ internal sealed class GameService : IGameService if (account == null) { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); (bool isOk, string name) = await new LaunchGameAccountNameDialog().GetInputNameAsync().ConfigureAwait(false); if (isOk) @@ -352,8 +352,8 @@ internal sealed class GameService : IGameService account = GameAccount.Create(name, registrySdk); // sync database - await ThreadHelper.SwitchToBackgroundAsync(); - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { await scope.ServiceProvider .GetRequiredService() @@ -363,7 +363,7 @@ internal sealed class GameService : IGameService } // sync cache - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); gameAccounts.Add(account); } } @@ -401,7 +401,7 @@ internal sealed class GameService : IGameService /// public void AttachGameAccountToUid(GameAccount gameAccount, string uid) { - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { gameAccount.UpdateAttachUid(uid); scope.ServiceProvider.GetRequiredService().GameAccounts.UpdateAndSave(gameAccount); @@ -418,8 +418,8 @@ internal sealed class GameService : IGameService gameAccount.UpdateName(name); // sync database - await ThreadHelper.SwitchToBackgroundAsync(); - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); await appDbContext.GameAccounts.UpdateAndSaveAsync(gameAccount).ConfigureAwait(false); @@ -430,11 +430,11 @@ internal sealed class GameService : IGameService /// public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); gameAccounts!.Remove(gameAccount); - await ThreadHelper.SwitchToBackgroundAsync(); - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { await scope.ServiceProvider.GetRequiredService().GameAccounts.RemoveAndSaveAsync(gameAccount).ConfigureAwait(false); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs index 2ee3a81a..883d251b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs @@ -13,6 +13,11 @@ namespace Snap.Hutao.Service.Game; [HighQuality] internal interface IGameService { + /// + /// 游戏内账号集合 + /// + ObservableCollection GameAccountCollection { get; } + /// /// 将账号绑定到对应的Uid /// 清除老账号的绑定状态 @@ -27,12 +32,6 @@ internal interface IGameService /// 任务 ValueTask DetectGameAccountAsync(); - /// - /// 异步获取游戏内账号集合 - /// - /// 游戏内账号集合 - Task> GetGameAccountCollectionAsync(); - /// /// 异步获取游戏路径 /// 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 f38c34c6..5dc47a13 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs @@ -14,14 +14,17 @@ namespace Snap.Hutao.Service.Game.Locator; [Injection(InjectAs.Transient, typeof(IGameLocator))] internal sealed class ManualGameLocator : IGameLocator { + private readonly ITaskContext taskContext; private readonly IPickerFactory pickerFactory; /// /// 构造一个新的手动模式提供器 /// + /// 任务上下文 /// 选择器工厂 - public ManualGameLocator(IPickerFactory pickerFactory) + public ManualGameLocator(ITaskContext taskContext, IPickerFactory pickerFactory) { + this.taskContext = taskContext; this.pickerFactory = pickerFactory; } @@ -31,6 +34,8 @@ internal sealed class ManualGameLocator : IGameLocator /// public async Task> LocateGamePathAsync() { + await taskContext.SwitchToMainThreadAsync(); + FileOpenPicker picker = pickerFactory.GetFileOpenPicker( PickerLocationId.Desktop, SH.ServiceGameLocatorFileOpenPickerCommitText, 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 a7de8def..a580f756 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs @@ -15,17 +15,30 @@ namespace Snap.Hutao.Service.Game.Locator; [Injection(InjectAs.Transient, typeof(IGameLocator))] internal sealed partial class RegistryLauncherLocator : IGameLocator { + private readonly ITaskContext taskContext; + + /// + /// 构造一个新的注册表启动器位置定位器 + /// + /// 任务上下文 + public RegistryLauncherLocator(ITaskContext taskContext) + { + this.taskContext = taskContext; + } + /// public string Name { get => nameof(RegistryLauncherLocator); } /// - public Task> LocateGamePathAsync() + public async Task> LocateGamePathAsync() { + await taskContext.SwitchToBackgroundAsync(); + ValueResult result = LocateInternal("DisplayIcon"); if (result.IsOk == false) { - return Task.FromResult(result); + return result; } else { @@ -43,11 +56,11 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator if (escapedPath != null) { string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName); - return Task.FromResult>(new(true, gamePath)); + return new(true, gamePath); } } - return Task.FromResult>(new(false, null!)); + return new(false, string.Empty); } private static ValueResult LocateInternal(string key) @@ -77,7 +90,8 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator string? hex4Result = UTF16Regex().Replace(str, @"\u$1"); // 不包含中文 - if (!hex4Result.Contains(@"\u")) + // Some one's folder might begin with 'u' + if (!hex4Result.Contains(@"\u")) { // fix path with \ hex4Result = hex4Result.Replace(@"\", @"\\"); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs index bd88d513..d9619df5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs @@ -8,28 +8,39 @@ using System.Text.RegularExpressions; namespace Snap.Hutao.Service.Game.Locator; /// -/// Unity日志游戏定位器 +/// Unity 日志游戏定位器 /// [HighQuality] [Injection(InjectAs.Transient, typeof(IGameLocator))] internal sealed partial class UnityLogGameLocator : IGameLocator { + private readonly ITaskContext taskContext; + + /// + /// 构造一个新的 Unity 日志游戏定位器 + /// + /// 任务上下文 + public UnityLogGameLocator(ITaskContext taskContext) + { + this.taskContext = taskContext; + } + /// public string Name { get => nameof(UnityLogGameLocator); } /// public async Task> LocateGamePathAsync() { - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); string logFilePathChinese = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\原神\output_log.txt"); string logFilePathOvsesea = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\Genshin Impact\output_log.txt"); - // We need to fallback the the cn server rather than os server. + // Fallback to the CN server. string logFilePathFinal = File.Exists(logFilePathOvsesea) ? logFilePathOvsesea : logFilePathChinese; - using (TempFile? tempFile = TempFile.CreateCopyFrom(logFilePathFinal)) + using (TempFile? tempFile = TempFile.CopyFrom(logFilePathFinal)) { if (tempFile != null) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs index 1217369b..d9c29fe4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs @@ -10,8 +10,33 @@ namespace Snap.Hutao.Service.Game.Package; /// [HighQuality] [DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")] -internal sealed class ItemOperationInfo +internal readonly struct ItemOperationInfo { + /// + /// 操作的类型 + /// + public readonly ItemOperationType Type; + + /// + /// 目标文件 + /// + public readonly string Target; + + /// + /// 移动至中时的名称 + /// + public readonly string MoveTo; + + /// + /// 文件的目标Md5 + /// + public readonly string Md5; + + /// + /// 文件的目标大小 Byte + /// + public readonly long TotalBytes; + /// /// 构造一个新的包操作 /// @@ -26,29 +51,4 @@ internal sealed class ItemOperationInfo Md5 = target.Md5; TotalBytes = target.FileSize; } - - /// - /// 操作的类型 - /// - public ItemOperationType Type { get; } - - /// - /// 目标文件 - /// - public string Target { get; } - - /// - /// 移动至中时的名称 - /// - public string MoveTo { get; } - - /// - /// 文件的目标Md5 - /// - public string Md5 { get; } - - /// - /// 文件的目标大小 Byte - /// - public long TotalBytes { get; } } \ No newline at end of file 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 8ce0967f..2ccccaad 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -4,6 +4,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.IO; +using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; using System.IO; using System.IO.Compression; @@ -52,24 +53,8 @@ internal sealed class PackageConverter ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese; progress.Report(new(SH.ServiceGamePackageRequestPackageVerion)); - Dictionary remoteItems; - try - { - using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false)) - { - remoteItems = await GetRemoteVersionItemsAsync(remoteSteam).ConfigureAwait(false); - } - } - catch (IOException ex) - { - throw ThrowHelper.PackageConvert(SH.ServiceGamePackageRequestPackageVerionFailed, ex); - } - - Dictionary localItems; - using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, "pkg_version"))) - { - localItems = await GetLocalVersionItemsAsync(localSteam, direction).ConfigureAwait(false); - } + Dictionary remoteItems = await TryGetRemoteItemsAsync(pkgVersionUri).ConfigureAwait(false); + Dictionary localItems = await TryGetLocalItemsAsync(gameFolder, direction).ConfigureAwait(false); IEnumerable diffOperations = GetItemOperationInfos(remoteItems, localItems).OrderBy(i => (int)i.Type); return await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false); @@ -86,6 +71,7 @@ internal sealed class PackageConverter { string sdkDllBackup = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll.backup"); string sdkDll = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll"); + string sdkVersionBackup = Path.Combine(gameFolder, "sdk_pkg_version.backup"); string sdkVersion = Path.Combine(gameFolder, "sdk_pkg_version"); @@ -106,7 +92,8 @@ internal sealed class PackageConverter { foreach (ZipArchiveEntry entry in zip.Entries) { - if (entry.CompressedLength != 0) + // skip folder entry. + if (entry.Length != 0) { string targetPath = Path.Combine(gameFolder, entry.FullName); Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); @@ -155,9 +142,7 @@ internal sealed class PackageConverter } } - IEnumerable removes = local.Select(kvp => new ItemOperationInfo(ItemOperationType.Remove, kvp.Value, kvp.Value)); - - foreach (ItemOperationInfo item in removes) + foreach (ItemOperationInfo item in local.Select(kvp => new ItemOperationInfo(ItemOperationType.Remove, kvp.Value, kvp.Value))) { yield return item; } @@ -168,8 +153,6 @@ internal sealed class PackageConverter string yuanShenData = Path.Combine(gameFolder, YuanShenData); string genshinImpactData = Path.Combine(gameFolder, GenshinImpactData); - // We have check the exe path previously - // so we assume the data folder is present if (direction == ConvertDirection.ChineseToOversea) { if (Directory.Exists(yuanShenData)) @@ -192,28 +175,27 @@ internal sealed class PackageConverter File.Move(targetFullPath, cacheFilePath, true); } - private static async Task CopyToWithProgressAsync(Stream source, Stream target, string name, long totalBytes, IProgress progress) + private async Task> TryGetLocalItemsAsync(string gameFolder, ConvertDirection direction) { - const int bufferSize = 81920; - - int reportCounter = 0; - long totalBytesRead = 0; - int bytesRead; - Memory buffer = new byte[bufferSize]; - - do + using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, "pkg_version"))) { - bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false); - await target.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false); + return await GetLocalVersionItemsAsync(localSteam, direction).ConfigureAwait(false); + } + } - totalBytesRead += bytesRead; - - if ((++reportCounter) % 10 == 0) + private async Task> TryGetRemoteItemsAsync(Uri pkgVersionUri) + { + try + { + using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false)) { - progress.Report(new(name, totalBytesRead, totalBytes)); + return await GetRemoteVersionItemsAsync(remoteSteam).ConfigureAwait(false); } } - while (bytesRead > 0); + catch (IOException ex) + { + throw ThrowHelper.PackageConvert(SH.ServiceGamePackageRequestPackageVerionFailed, ex); + } } private async Task ReplaceGameResourceAsync(IEnumerable operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress progress) @@ -296,20 +278,23 @@ internal sealed class PackageConverter { try { - await CopyToWithProgressAsync(webStream, fileStream, info.Target, totalBytes, progress).ConfigureAwait(false); + StreamCopyWorker streamCopyWorker = new(webStream, fileStream, bytesRead => new(info.Target, bytesRead, totalBytes)); + await streamCopyWorker.CopyAsync(progress).ConfigureAwait(false); fileStream.Seek(0, SeekOrigin.Begin); string remoteMd5 = await Digest.GetStreamMD5Async(fileStream).ConfigureAwait(false); - if (info.Md5 == remoteMd5.ToLowerInvariant()) + if (string.Equals(info.Md5, remoteMd5, StringComparison.OrdinalIgnoreCase)) { return; } } - catch + catch (Exception ex) { // System.IO.IOException: The response ended prematurely. // System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream. // We want to retry forever. + serviceProvider.GetRequiredService().Error(ex); + await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs index a01101ac..7e35e71a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs @@ -56,7 +56,7 @@ internal static class ProcessInterop TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100); TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000); - TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000); + TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(3000); return unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay); } @@ -70,9 +70,10 @@ internal static class ProcessInterop public static bool DisableProtection(Process game, string gamePath) { string? gameFolder = Path.GetDirectoryName(gamePath); - if (!string.IsNullOrEmpty(gameFolder)) + string mhypbaseDll = Path.Combine(gameFolder ?? string.Empty, "mhypbase.dll"); + + if (File.Exists(mhypbaseDll)) { - string mhypbaseDll = Path.Combine(gameFolder, "mhypbase.dll"); using (File.OpenHandle(mhypbaseDll, share: FileShare.None)) { SpinWait.SpinUntil(() => game.MainWindowHandle != 0); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs index 781d1a67..bc1c19d5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs @@ -122,20 +122,21 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay) { - while (true) + using (PeriodicTimer timer = new(adjustFpsDelay)) { - if (!gameProcess.HasExited && fpsAddress != 0) + while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { - UnsafeWriteModuleMemory(gameProcess, fpsAddress, TargetFps); + if (!gameProcess.HasExited && fpsAddress != 0) + { + UnsafeWriteModuleMemory(gameProcess, fpsAddress, TargetFps); + } + else + { + isValid = false; + fpsAddress = 0; + return; + } } - else - { - isValid = false; - fpsAddress = 0; - return; - } - - await Task.Delay(adjustFpsDelay).ConfigureAwait(false); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs index 0360bc72..23b1db97 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs @@ -18,24 +18,23 @@ namespace Snap.Hutao.Service.Hutao; [Injection(InjectAs.Scoped, typeof(IHutaoService))] internal sealed class HutaoService : IHutaoService { + private static readonly TimeSpan CacheExpireTime = TimeSpan.FromHours(4); private readonly HomaSpiralAbyssClient homaClient; private readonly IMemoryCache memoryCache; - private readonly AppDbContext appDbContext; private readonly JsonSerializerOptions options; + private readonly IServiceProvider serviceProvider; /// /// 构造一个新的胡桃 API 服务 /// - /// 胡桃 API 客户端 - /// 内存缓存 - /// 数据库上下文 - /// Json序列化选项 - public HutaoService(HomaSpiralAbyssClient homaClient, IMemoryCache memoryCache, AppDbContext appDbContext, JsonSerializerOptions options) + /// 服务提供器 + public HutaoService(IServiceProvider serviceProvider) { - this.homaClient = homaClient; - this.memoryCache = memoryCache; - this.appDbContext = appDbContext; - this.options = options; + homaClient = serviceProvider.GetRequiredService(); + memoryCache = serviceProvider.GetRequiredService(); + options = serviceProvider.GetRequiredService(); + + this.serviceProvider = serviceProvider; } /// @@ -88,17 +87,21 @@ internal sealed class HutaoService : IHutaoService { return (T)cache!; } - - if (appDbContext.ObjectCache.SingleOrDefault(e => e.Key == key) is ObjectCacheEntry entry) + using (IServiceScope scope = serviceProvider.CreateScope()) { - if (entry.IsExpired) + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + if (appDbContext.ObjectCache.SingleOrDefault(e => e.Key == key) is ObjectCacheEntry entry) { - await appDbContext.ObjectCache.RemoveAndSaveAsync(entry).ConfigureAwait(false); - } - else - { - T value = JsonSerializer.Deserialize(entry.Value!, options)!; - return memoryCache.Set(key, value, TimeSpan.FromMinutes(30)); + 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)); + } } } @@ -109,14 +112,19 @@ internal sealed class HutaoService : IHutaoService { if (data != null) { - appDbContext.ObjectCache.AddAndSave(new() + using (IServiceScope scope = serviceProvider.CreateScope()) { - Key = key, + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - // we hold the cache for 4 hours, then just expire it. - ExpireTime = DateTimeOffset.Now.AddHours(4), - Value = JsonSerializer.Serialize(data, options), - }); + appDbContext.ObjectCache.AddAndSave(new() + { + Key = key, + + // We hold the cache for 4 hours + ExpireTime = DateTimeOffset.Now.Add(CacheExpireTime), + Value = JsonSerializer.Serialize(data, options), + }); + } } } catch (Microsoft.EntityFrameworkCore.DbUpdateException) @@ -125,6 +133,6 @@ internal sealed class HutaoService : IHutaoService // TODO: Not ignore it. } - return memoryCache.Set(key, data ?? new(), TimeSpan.FromHours(4)); + return memoryCache.Set(key, data ?? new(), CacheExpireTime); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs index 12480546..363267ac 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs @@ -14,6 +14,7 @@ internal sealed class HutaoUserService : IHutaoUserService, IHutaoUserServiceIni { private readonly TaskCompletionSource initializeCompletionSource = new(); + private readonly ITaskContext taskContext; private readonly HomaPassportClient passportClient; private readonly HutaoUserOptions options; @@ -22,10 +23,12 @@ internal sealed class HutaoUserService : IHutaoUserService, IHutaoUserServiceIni /// /// 构造一个新的胡桃用户服务 /// + /// 任务上下文 /// 通行证客户端 /// 选项 - public HutaoUserService(HomaPassportClient passportClient, HutaoUserOptions options) + public HutaoUserService(ITaskContext taskContext, HomaPassportClient passportClient, HutaoUserOptions options) { + this.taskContext = taskContext; this.passportClient = passportClient; this.options = options; } @@ -49,13 +52,13 @@ internal sealed class HutaoUserService : IHutaoUserService, IHutaoUserServiceIni if (response.IsOk()) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); options.LoginSucceed(userName, response.Data); isInitialized = true; } else { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); options.LoginFailed(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs index 44ee3f82..d43a0aa6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs @@ -12,9 +12,19 @@ namespace Snap.Hutao.Service; [Injection(InjectAs.Singleton, typeof(IInfoBarService))] internal sealed class InfoBarService : IInfoBarService { + private readonly ITaskContext taskContext; private readonly TaskCompletionSource initializaionCompletionSource = new(); private StackPanel? infoBarStack; + /// + /// 构造一个新的消息条服务 + /// + /// 任务上下文 + public InfoBarService(ITaskContext taskContext) + { + this.taskContext = taskContext; + } + /// public void Initialize(StackPanel container) { @@ -111,7 +121,7 @@ internal sealed class InfoBarService : IInfoBarService /// 关闭延迟 private async Task PrepareInfoBarAndShowInternalAsync(InfoBarSeverity severity, string? title, string? message, int delay) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); InfoBar infoBar = new() { @@ -136,7 +146,7 @@ internal sealed class InfoBarService : IInfoBarService [SuppressMessage("", "VSTHRD100")] private async void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); infoBarStack!.Children.Remove(sender); sender.Closed -= OnInfoBarClosed; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index dc32844f..f922cb34 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.Extensions.Caching.Memory; +using Snap.Hutao.Core; using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.ExceptionService; @@ -24,6 +25,7 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi { private const string MetaFileName = "Meta.json"; private readonly string metadataFolderPath; + private readonly IInfoBarService infoBarService; private readonly HttpClient httpClient; private readonly JsonSerializerOptions options; @@ -41,26 +43,18 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi /// /// 构造一个新的元数据服务 /// - /// 信息条服务 + /// 服务提供器 /// http客户端工厂 - /// json序列化选项 - /// 日志器 - /// 内存缓存 - public MetadataService( - Core.HutaoOptions hutaoOptions, - IInfoBarService infoBarService, - IHttpClientFactory httpClientFactory, - JsonSerializerOptions options, - ILogger logger, - IMemoryCache memoryCache) + public MetadataService(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory) { - this.infoBarService = infoBarService; - this.options = options; - this.logger = logger; - this.memoryCache = memoryCache; + infoBarService = serviceProvider.GetRequiredService(); + options = serviceProvider.GetRequiredService(); + logger = serviceProvider.GetRequiredService>(); + memoryCache = serviceProvider.GetRequiredService(); httpClient = httpClientFactory.CreateClient(nameof(MetadataService)); locale = GetTextMapLocale(); + HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); metadataFolderPath = Path.Combine(hutaoOptions.DataFolder, "Metadata", locale); Directory.CreateDirectory(metadataFolderPath); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs index 650fb677..d186b379 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs @@ -17,6 +17,7 @@ namespace Snap.Hutao.Service.Navigation; [Injection(InjectAs.Singleton, typeof(INavigationService))] internal sealed class NavigationService : INavigationService { + private readonly ITaskContext taskContext; private readonly IInfoBarService infoBarService; private readonly ILogger logger; @@ -27,8 +28,9 @@ internal sealed class NavigationService : INavigationService /// /// 信息条服务 /// 日志器 - public NavigationService(IInfoBarService infoBarService, ILogger logger) + public NavigationService(ITaskContext taskContext, IInfoBarService infoBarService, ILogger logger) { + this.taskContext = taskContext; this.infoBarService = infoBarService; this.logger = logger; } @@ -181,7 +183,7 @@ internal sealed class NavigationService : INavigationService /// public void GoBack() { - ThreadHelper.InvokeOnMainThread(() => + taskContext.InvokeOnMainThread(() => { bool canGoBack = Frame?.CanGoBack ?? false; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs index dcae2e4b..a892c635 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs @@ -20,7 +20,6 @@ namespace Snap.Hutao.Service.SpiralAbyss; internal class SpiralAbyssRecordService : ISpiralAbyssRecordService { private readonly IServiceProvider serviceProvider; - private readonly AppDbContext appDbContext; private string? uid; private ObservableCollection? spiralAbysses; @@ -31,7 +30,6 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService /// 服务提供器 public SpiralAbyssRecordService(IServiceProvider serviceProvider) { - appDbContext = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; } @@ -46,15 +44,18 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService uid = userAndUid.Uid.Value; if (spiralAbysses == null) { - List entries = await appDbContext.SpiralAbysses - .AsNoTracking() - .Where(s => s.Uid == userAndUid.Uid.Value) - .OrderByDescending(s => s.ScheduleId) - .ToListAsync() - .ConfigureAwait(false); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - await ThreadHelper.SwitchToMainThreadAsync(); - spiralAbysses = new(entries); + List entries = await appDbContext.SpiralAbysses + .Where(s => s.Uid == userAndUid.Uid.Value) + .OrderByDescending(s => s.ScheduleId) + .ToListAsync() + .ConfigureAwait(false); + + spiralAbysses = entries.ToObservableCollection(); + } } return spiralAbysses; @@ -79,23 +80,30 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data; SpiralAbyssEntry? existEntry = spiralAbysses!.SingleOrDefault(s => s.ScheduleId == webSpiralAbyss.ScheduleId); - if (existEntry != null) + + using (IServiceScope scope = serviceProvider.CreateScope()) { - await ThreadHelper.SwitchToMainThreadAsync(); - existEntry.UpdateSpiralAbyss(webSpiralAbyss); + ITaskContext taskContext = scope.ServiceProvider.GetRequiredService(); + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - await ThreadHelper.SwitchToBackgroundAsync(); - await appDbContext.SpiralAbysses.UpdateAndSaveAsync(existEntry).ConfigureAwait(false); - } - else - { - SpiralAbyssEntry newEntry = SpiralAbyssEntry.Create(userAndUid.Uid.Value, webSpiralAbyss); + if (existEntry != null) + { + await taskContext.SwitchToMainThreadAsync(); + existEntry.UpdateSpiralAbyss(webSpiralAbyss); - await ThreadHelper.SwitchToMainThreadAsync(); - spiralAbysses!.Insert(0, newEntry); + await taskContext.SwitchToBackgroundAsync(); + await appDbContext.SpiralAbysses.UpdateAndSaveAsync(existEntry).ConfigureAwait(false); + } + else + { + SpiralAbyssEntry newEntry = SpiralAbyssEntry.Create(userAndUid.Uid.Value, webSpiralAbyss); - await ThreadHelper.SwitchToBackgroundAsync(); - await appDbContext.SpiralAbysses.AddAndSaveAsync(newEntry).ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + spiralAbysses!.Insert(0, newEntry); + + await taskContext.SwitchToBackgroundAsync(); + await appDbContext.SpiralAbysses.AddAndSaveAsync(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 33ee0425..33af91e0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -24,7 +24,8 @@ namespace Snap.Hutao.Service.User; [Injection(InjectAs.Singleton, typeof(IUserService))] internal class UserService : IUserService { - private readonly IServiceScopeFactory scopeFactory; + private readonly ITaskContext taskContext; + private readonly IServiceProvider serviceProvider; private readonly IMessenger messenger; private readonly object currentUserLocker = new(); @@ -35,11 +36,13 @@ internal class UserService : IUserService /// /// 构造一个新的用户服务 /// - /// 范围工厂 + /// 服务提供器 /// 消息器 - public UserService(IServiceScopeFactory scopeFactory, IMessenger messenger) + public UserService(IServiceProvider serviceProvider, IMessenger messenger) { - this.scopeFactory = scopeFactory; + taskContext = serviceProvider.GetRequiredService(); + + this.serviceProvider = serviceProvider; this.messenger = messenger; } @@ -56,7 +59,7 @@ internal class UserService : IUserService lock (currentUserLocker) { - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); @@ -98,13 +101,13 @@ internal class UserService : IUserService public async Task RemoveUserAsync(BindingUser user) { // Sync cache - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); userCollection!.Remove(user); roleCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid); // Sync database - await ThreadHelper.SwitchToBackgroundAsync(); - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); await appDbContext.Users @@ -118,16 +121,16 @@ internal class UserService : IUserService /// public async Task> GetUserCollectionAsync() { - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); if (userCollection == null) { List users = new(); - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - foreach (Model.Entity.User entity in appDbContext.Users.AsNoTracking()) + foreach (Model.Entity.User entity in appDbContext.Users) { BindingUser initialized = await BindingUser.ResumeAsync(entity).ConfigureAwait(false); users.Add(initialized); @@ -151,7 +154,7 @@ internal class UserService : IUserService /// public async Task> GetRoleCollectionAsync() { - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); if (roleCollection == null) { List userAndUids = new(); @@ -193,7 +196,7 @@ internal class UserService : IUserService /// public async Task> ProcessInputCookieAsync(Cookie cookie, bool isOversea) { - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); string? mid = cookie.GetValueOrDefault(isOversea ? Cookie.STUID : Cookie.MID); if (mid == null) @@ -204,7 +207,7 @@ internal class UserService : IUserService // 检查 mid 对应用户是否存在 if (TryGetUser(userCollection!, mid, out BindingUser? user)) { - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); @@ -232,7 +235,7 @@ internal class UserService : IUserService /// public async Task RefreshCookieTokenAsync(BindingUser user) { - using (IServiceScope scope = scopeFactory.CreateScope()) + using (IServiceScope scope = serviceProvider.CreateScope()) { Response cookieTokenResponse = await scope.ServiceProvider .PickRequiredService(user.Entity.IsOversea) @@ -267,8 +270,8 @@ internal class UserService : IUserService private async Task> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea) { - await ThreadHelper.SwitchToBackgroundAsync(); - using (IServiceScope scope = scopeFactory.CreateScope()) + await taskContext.SwitchToBackgroundAsync(); + using (IServiceScope scope = serviceProvider.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); BindingUser? newUser = await BindingUser.CreateAsync(cookie, isOversea).ConfigureAwait(false); @@ -278,7 +281,7 @@ internal class UserService : IUserService // Sync cache if (userCollection != null) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); { userCollection!.Add(newUser); @@ -293,7 +296,7 @@ internal class UserService : IUserService } // Sync database - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); await appDbContext.Users.AddAndSaveAsync(newUser.Entity).ConfigureAwait(false); return new(UserOptionResult.Added, newUser.UserInfo!.Uid); } 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 3ef73473..4cfb9fef 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs @@ -16,7 +16,7 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog { - private static readonly DependencyProperty StateProperty = Property.Depend(nameof(State)); + private static readonly DependencyProperty StatusProperty = Property.Depend(nameof(Status)); /// /// 构造一个新的对话框 @@ -31,19 +31,21 @@ internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog /// /// 刷新状态 /// - public GachaLogFetchState State + public GachaLogFetchStatus Status { - get => (GachaLogFetchState)GetValue(StateProperty); - set => SetValue(StateProperty, value); + get => (GachaLogFetchStatus)GetValue(StatusProperty); + set => SetValue(StatusProperty, value); } /// /// 接收进度更新 /// /// 状态 - public void OnReport(GachaLogFetchState state) + public void OnReport(GachaLogFetchStatus state) { - State = state; + Status = state; + + // TODO: test new binding approach GachaItemsPresenter.Header = state.AuthKeyTimeout ? SH.ViewDialogGachaLogRefreshProgressAuthkeyTimeout : string.Format(SH.ViewDialogGachaLogRefreshProgressDescription, state.ConfigType.GetLocalizedDescription()); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs index f6850729..f8f757b8 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs @@ -233,7 +233,7 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR .OrderBy(goal => goal.Order) .Select(goal => new AchievementGoalView(goal)) .ToList(); - archives = await achievementService.GetArchiveCollectionAsync().ConfigureAwait(false); + archives = achievementService.ArchiveCollection; } await ThreadHelper.SwitchToMainThreadAsync(); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index bf8ac244..b4af21c2 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -184,6 +184,11 @@ internal sealed class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipien case RefreshResult.APIUnavailable: infoBarService.Warning(SH.ViewModelAvatarPropertyEnkaApiUnavailable); break; + + case RefreshResult.StatusCodeNotSucceed: + infoBarService.Warning(summary!.Message); + break; + case RefreshResult.ShowcaseNotOpen: infoBarService.Warning(SH.ViewModelAvatarPropertyShowcaseNotOpen); break; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquarySubProperty.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquarySubProperty.cs index 568233c4..9caf7e72 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquarySubProperty.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquarySubProperty.cs @@ -15,7 +15,7 @@ internal sealed class ReliquarySubProperty /// 名称 /// 值 /// 评分 - public ReliquarySubProperty(string name, string value, double score) + public ReliquarySubProperty(string name, string value, float score) { Name = name; Value = value; @@ -43,5 +43,5 @@ internal sealed class ReliquarySubProperty /// /// 评分 /// - internal double Score { get; } + internal float Score { get; } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquaryView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquaryView.cs index 7c6bd8e6..4ead1f70 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquaryView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/ReliquaryView.cs @@ -32,5 +32,5 @@ internal sealed class ReliquaryView : Equip /// /// 评分 /// - internal double Score { get; set; } + internal float Score { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Summary.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Summary.cs index f4688498..74cf67b1 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Summary.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/Summary.cs @@ -13,4 +13,9 @@ internal sealed class Summary /// 角色列表 /// public List Avatars { get; set; } = default!; + + /// + /// 服务器消息 + /// + public string Message { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs index 009c36e6..0aa4d936 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs @@ -123,7 +123,7 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel bool metaInitialized = await metadataService.InitializeAsync().ConfigureAwait(true); if (metaInitialized) { - Projects = cultivationService.GetProjectCollection(); + Projects = cultivationService.ProjectCollection(); SelectedProject = cultivationService.Current; } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs index 9858440c..8cf9adca 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs @@ -151,7 +151,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(false)) { await ThreadHelper.SwitchToMainThreadAsync(); - Archives = gachaLogService.GetArchiveCollection(); + Archives = gachaLogService.ArchiveCollection(); SetSelectedArchiveAndUpdateStatistics(Archives.SelectedOrDefault(), true); } } @@ -191,7 +191,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel await ThreadHelper.SwitchToMainThreadAsync(); GachaLogRefreshProgressDialog dialog = new(); IDisposable dialogHider = await dialog.BlockAsync().ConfigureAwait(false); - Progress progress = new(dialog.OnReport); + Progress progress = new(dialog.OnReport); bool authkeyValid; try diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 1c900b71..19f0295e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -175,7 +175,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel infoBarService.Warning(string.Format(SH.ViewModelLaunchGameMultiChannelReadFail, multi.ConfigFilePath)); } - ObservableCollection accounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(false); + ObservableCollection accounts = await gameService.GameAccountCollection().ConfigureAwait(false); await ThreadHelper.SwitchToMainThreadAsync(); GameAccounts = accounts; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs index aaa4948c..fec86d29 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs @@ -50,7 +50,7 @@ internal sealed class LaunchGameViewModelSlim : Abstraction.ViewModelSlim protected override async Task OpenUIAsync() { - ObservableCollection accounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(false); + ObservableCollection accounts = await gameService.GameAccountCollection().ConfigureAwait(false); await ThreadHelper.SwitchToMainThreadAsync(); GameAccounts = accounts; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/BaseValueInfo.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/BaseValueInfo.cs index 8e577929..724ba27e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/BaseValueInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/BaseValueInfo.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Model; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata; +using Snap.Hutao.Model.Metadata.Converter; namespace Snap.Hutao.ViewModel.Wiki; @@ -89,9 +90,7 @@ internal sealed class BaseValueInfo : ObservableObject private void UpdateValues(int level, bool promoted) { - List> values = new(propValues.Count); - - foreach (PropertyCurveValue propValue in propValues) + Values = propValues.SelectList(propValue => { float value = propValue.Value * growCurveMap[level].GetValueOrDefault(propValue.Type); if (promoteMap != null) @@ -102,10 +101,8 @@ internal sealed class BaseValueInfo : ObservableObject value += addValue; } - values.Add(Model.Metadata.Converter.PropertiesParametersDescriptor.FormatNameValue(propValue.Property, value)); - } - - Values = values; + return FightPropertyFormat.ToNameValue(propValue.Property, value); + }); } private int GetPromoteLevel(int level, bool promoted) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs index 89821d9c..8efdcc27 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs @@ -4,7 +4,11 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Web.Enka.Model; using Snap.Hutao.Web.Hoyolab; +using System.IO; +using System.Net; using System.Net.Http; +using System.Net.Http.Json; +using System.Net.Sockets; namespace Snap.Hutao.Web.Enka; @@ -20,7 +24,6 @@ internal sealed class EnkaClient private readonly HttpClient httpClient; private readonly JsonSerializerOptions options; - private readonly ILogger logger; /// /// 构造一个新的 Enka API 客户端 @@ -28,11 +31,10 @@ internal sealed class EnkaClient /// http客户端 /// 序列化选项 /// 日志器 - public EnkaClient(HttpClient httpClient, JsonSerializerOptions options, ILogger logger) + public EnkaClient(HttpClient httpClient, JsonSerializerOptions options) { this.httpClient = httpClient; this.options = options; - this.logger = logger; } /// @@ -43,7 +45,7 @@ internal sealed class EnkaClient /// Enka API 响应 public Task GetForwardDataAsync(in PlayerUid playerUid, CancellationToken token = default) { - return httpClient.TryCatchGetFromJsonAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), options, logger, token); + return TryGetEnkaResponseCoreAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), token); } /// @@ -54,6 +56,57 @@ internal sealed class EnkaClient /// Enka API 响应 public Task GetDataAsync(in PlayerUid playerUid, CancellationToken token = default) { - return httpClient.TryCatchGetFromJsonAsync(string.Format(EnkaAPI, playerUid.Value), options, logger, token); + return TryGetEnkaResponseCoreAsync(string.Format(EnkaAPI, playerUid.Value), token); + } + + private async Task TryGetEnkaResponseCoreAsync(string url, CancellationToken token = default) + { + try + { + HttpResponseMessage response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(options, token).ConfigureAwait(false); + } + else + { + // https://github.com/yoimiya-kokomi/miao-plugin/pull/441 + // Additionally, HTTP codes for UID requests: + // 400 = wrong UID format + // 404 = player does not exist(MHY server told that) + // 429 = rate - limit + // 424 = game maintenance / everything is broken after the update + // 500 = general server error + // 503 = I screwed up massively + string message = response.StatusCode switch + { + HttpStatusCode.BadRequest => SH.WebEnkaResponseStatusCode400, + HttpStatusCode.NotFound => SH.WebEnkaResponseStatusCode404, + HttpStatusCode.FailedDependency => SH.WebEnkaResponseStatusCode424, + HttpStatusCode.TooManyRequests => SH.WebEnkaResponseStatusCode429, + HttpStatusCode.InternalServerError => SH.WebEnkaResponseStatusCode500, + HttpStatusCode.ServiceUnavailable => SH.WebEnkaResponseStatusCode503, + _ => SH.WebEnkaResponseStatusCodeUnknown, + }; + + return new() { Message = message, }; + } + } + catch (HttpRequestException) + { + return null; + } + catch (IOException) + { + return null; + } + catch (JsonException) + { + return null; + } + catch (SocketException) + { + return null; + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs index e0ae5a5d..d867cba9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs @@ -31,14 +31,14 @@ internal sealed class AvatarInfo /// 命座 Id /// [JsonPropertyName("talentIdList")] - public List? TalentIdList { get; set; } + public List? TalentIdList { get; set; } /// /// 属性 Map /// Map of Character's Combat Properties. /// [JsonPropertyName("fightPropMap")] - public Dictionary FightPropMap { get; set; } = default!; + public Dictionary FightPropMap { get; set; } = default!; /// /// 技能组Id 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 42126712..8f331f66 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs @@ -36,4 +36,10 @@ internal sealed class EnkaResponse [MemberNotNullWhen(true, nameof(PlayerInfo), nameof(AvatarInfoList))] get => PlayerInfo != null && AvatarInfoList != null; } + + /// + /// 消息 + /// + [JsonIgnore] + public string Message { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/TypeValue.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/TypeValue.cs index a8be6350..d0fb21d6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/TypeValue.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/TypeValue.cs @@ -24,7 +24,7 @@ internal sealed class TypeValue /// /// 类型 /// 值 - public TypeValue(PlayerProperty type, string? value) + public TypeValue(PlayerProperty type, int value) { Type = type; Value = value; @@ -40,18 +40,6 @@ internal sealed class TypeValue /// 值 /// [JsonPropertyName("val")] - public string? Value { get; set; } - - /// - /// 值 Int32 - /// - [JsonIgnore] - public int ValueInt32 - { - get - { - _ = int.TryParse(Value, out int result); - return result; - } - } + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public int Value { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/WeaponStat.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/WeaponStat.cs index 66ac0976..840f19da 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/WeaponStat.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/WeaponStat.cs @@ -22,5 +22,5 @@ internal sealed class WeaponStat /// 值 /// [JsonPropertyName("statValue")] - public double StatValue { get; set; } + public float StatValue { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/Avatar/Constellation.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/Avatar/Constellation.cs index 0923fa6a..e5e5797a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/Avatar/Constellation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/Avatar/Constellation.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Primitive; + namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar; /// @@ -13,7 +15,7 @@ internal sealed class Constellation /// Id /// [JsonPropertyName("id")] - public int Id { get; set; } + public SkillId Id { get; set; } /// /// 名称 diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs index 01b0e1bd..fa681035 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs @@ -1,6 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; using System.Collections; @@ -74,6 +77,37 @@ internal sealed class EndIds } } + /// + /// 异步创建一个新的 End Id集合 + /// + /// 数据库上下文 + /// 存档 + /// 取消令牌 + /// 新的 End Id集合 + public static async Task CreateAsync(AppDbContext appDbContext, GachaArchive? archive, CancellationToken token) + { + EndIds endIds = new(); + foreach (GachaConfigType type in Service.GachaLog.GachaLog.QueryTypes) + { + if (archive != null) + { + Snap.Hutao.Model.Entity.GachaItem? item = await appDbContext.GachaItems + .Where(i => i.ArchiveId == archive.InnerId) + .Where(i => i.QueryType == type) + .OrderBy(i => i.Id) + .FirstOrDefaultAsync(token) + .ConfigureAwait(false); + + if (item != null) + { + endIds[type] = item.Id; + } + } + } + + return endIds; + } + /// /// 获取枚举器 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs index 5f155a66..3faf54dc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs @@ -148,8 +148,30 @@ internal sealed class Response : Response, IJsResult } } + /// + /// 尝试获取数据 + /// + /// 数据 + /// 服务提供器 默认 Ioc.Default + /// 返回代码是否指示成功 + public bool TryGetData([NotNullWhen(true)] out TData? data, IServiceProvider? serviceProvider = null) + { + if (ReturnCode == 0) + { + data = Data!; + return true; + } + else + { + serviceProvider ??= Ioc.Default; + serviceProvider.GetRequiredService().Error(ToString()); + data = default; + return false; + } + } + /// - string IJsResult.ToJson() + public string ToJson() { return JsonSerializer.Serialize(this); } From 7b3b1f1317c29a7fd48b0263861fbb94ccf54bf4 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Sun, 30 Apr 2023 20:26:07 +0800 Subject: [PATCH 4/4] remove all ThreadHelper usage --- .../Identity/IdentityGenerator.cs | 2 - .../DependencyInjectionTest.cs | 30 +++++++++ .../Snap.Hutao/AppResourceProvider.cs | 28 ++++++++ .../DependencyInjection/IocConfiguration.cs | 4 +- .../ServiceProviderExtension.cs | 19 ++++++ .../Snap.Hutao/Core/HutaoOptions.cs | 10 ++- .../Core/IO/DataTransfer/Clipboard.cs | 2 +- .../Core/IO/DataTransfer/ClipboardInterop.cs | 45 +++++++++++++ .../Core/IO/DataTransfer/IClipboardInterop.cs | 27 ++++++++ .../Snap.Hutao/Core/LifeCycle/Activation.cs | 11 +-- .../ConcurrentCancellationTokenSource.cs | 2 +- .../Snap.Hutao/Core/Threading/ThreadHelper.cs | 58 ---------------- .../Snap.Hutao/Core/Threading/ValueResult.cs | 1 - .../Core/Threading/ValueResultExtension.cs | 23 +++++++ .../Core/Windowing/ExtendedWindow.cs | 10 +-- .../Extension/EnumerableExtension.cs | 16 ----- .../Factory/Abstraction/IPickerFactory.cs | 11 +++ .../Snap.Hutao/Factory/PickerFactory.cs | 17 +++++ .../Snap.Hutao/IAppResourceProvider.cs | 18 +++++ src/Snap.Hutao/Snap.Hutao/Program.cs | 1 - .../AchievementService.Collection.cs | 1 + .../Service/Achievement/AchievementService.cs | 2 +- .../Factory/SummaryReliquaryFactory.cs | 4 +- ...ameRecordCharacterAvatarInfoTransformer.cs | 2 +- .../GachaLogQueryManualInputProvider.cs | 9 ++- .../Snap.Hutao/Service/Game/GameService.cs | 7 +- .../Snap.Hutao/Service/Hutao/HutaoCache.cs | 2 +- .../AchievementArchiveCreateDialog.xaml.cs | 12 ++-- .../Dialog/AchievementImportDialog.xaml.cs | 11 ++- .../View/Dialog/AdoptCalculatorDialog.xaml.cs | 13 ++-- .../Dialog/CommunityGameRecordDialog.xaml.cs | 13 ++-- .../Dialog/CultivateProjectDialog.xaml.cs | 15 +++-- .../CultivatePromotionDeltaDialog.xaml.cs | 43 ++++++++++-- .../DailyNoteNotificationDialog.xaml.cs | 6 +- .../DailyNoteVerificationDialog.xaml.cs | 17 +++-- .../View/Dialog/GachaLogImportDialog.xaml.cs | 11 ++- .../GachaLogRefreshProgressDialog.xaml.cs | 6 +- .../View/Dialog/GachaLogUrlDialog.xaml.cs | 12 ++-- .../LaunchGameAccountNameDialog.xaml.cs | 12 ++-- .../LaunchGamePackageConvertDialog.xaml.cs | 6 +- .../View/Dialog/SignInWebViewDialog.xaml.cs | 8 ++- .../Snap.Hutao/View/Dialog/UserDialog.xaml.cs | 12 ++-- .../ViewModel/Abstraction/ViewModelSlim.cs | 6 +- .../Achievement/AchievementFinishPercent.cs | 17 +++-- .../Achievement/AchievementGoalStatistics.cs | 12 +++- .../Achievement/AchievementGoalView.cs | 30 +++++++-- .../Achievement/AchievementImporter.cs | 34 ++++++---- .../Achievement/AchievementStatistics.cs | 13 ++++ .../ViewModel/Achievement/AchievementView.cs | 2 +- .../Achievement/AchievementViewModel.cs | 67 +++++++++++-------- .../Achievement/AchievementViewModelSlim.cs | 3 +- .../AvatarProperty/AvatarPropertyViewModel.cs | 25 +++---- .../Complex/AvatarConstellationInfoView.cs | 4 +- .../ViewModel/Complex/ReliquarySetView.cs | 3 +- .../Snap.Hutao/ViewModel/Complex/Team.cs | 6 +- .../ViewModel/Complex/TeamAppearanceView.cs | 4 +- .../Cultivation/CultivationViewModel.cs | 29 +++++--- .../ViewModel/DailyNote/DailyNoteViewModel.cs | 24 ++++--- .../DailyNote/DailyNoteViewModelSlim.cs | 6 +- .../ViewModel/GachaLog/GachaLogViewModel.cs | 50 ++++++++------ .../GachaLog/GachaLogViewModelSlim.cs | 3 +- .../ViewModel/GachaLog/HutaoCloudViewModel.cs | 10 +-- .../ViewModel/Game/LaunchGameViewModel.cs | 18 ++--- .../ViewModel/Game/LaunchGameViewModelSlim.cs | 6 +- .../ViewModel/HutaoPassportViewModel.cs | 8 ++- .../Snap.Hutao/ViewModel/SettingViewModel.cs | 9 ++- .../SpiralAbyss/SpiralAbyssRecordViewModel.cs | 8 ++- .../Snap.Hutao/ViewModel/TestViewModel.cs | 10 +-- .../ViewModel/User/UserViewModel.cs | 23 +++++-- .../Snap.Hutao/ViewModel/WelcomeViewModel.cs | 11 ++- .../ViewModel/Wiki/WikiAvatarViewModel.cs | 11 +-- .../ViewModel/Wiki/WikiMonsterViewModel.cs | 4 +- .../ViewModel/Wiki/WikiWeaponViewModel.cs | 11 +-- .../Web/Bridge/MiHoYoJSInterface.cs | 10 +-- .../Snap.Hutao/Web/Hoyolab/PlayerUid.cs | 15 +++++ 75 files changed, 708 insertions(+), 343 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/AppResourceProvider.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceProviderExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/ClipboardInterop.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/IClipboardInterop.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Threading/ValueResultExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/IAppResourceProvider.cs diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs index 7b0313a8..78ba54a2 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Identity/IdentityGenerator.cs @@ -47,8 +47,6 @@ internal sealed class IdentityGenerator : IIncrementalGenerator // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. - using Snap.Hutao.Core.ExpressionService; - namespace Snap.Hutao.Model.Primitive.Converter; /// diff --git a/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs index bf0abb9c..57b834e8 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs @@ -34,6 +34,16 @@ public class DependencyInjectionTest } } + [TestMethod] + public void GenericServicesCanBeResolved() + { + IServiceProvider services = new ServiceCollection() + .AddTransient(typeof(IGenericService<>),typeof(GenericService<>)) + .BuildServiceProvider(); + + Assert.IsNotNull(services.GetService>()); + } + private interface IService { Guid Id { get; } @@ -54,4 +64,24 @@ public class DependencyInjectionTest get => throw new NotImplementedException(); } } + + private interface IGenericService + { + } + + private sealed class GenericService : IGenericService + { + } + + private sealed class NonInjectedServiceA + { + } + + private sealed class NonInjectedServiceB + { + [ActivatorUtilitiesConstructor] + public NonInjectedServiceB(NonInjectedServiceA? serviceA) + { + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/AppResourceProvider.cs b/src/Snap.Hutao/Snap.Hutao/AppResourceProvider.cs new file mode 100644 index 00000000..b2891c6c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/AppResourceProvider.cs @@ -0,0 +1,28 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao; + +/// +/// 应用程序资源提供器 +/// +[Injection(InjectAs.Transient, typeof(IAppResourceProvider))] +internal sealed class AppResourceProvider : IAppResourceProvider +{ + private readonly App app; + + /// + /// 构造一个新的应用程序资源提供器 + /// + /// 应用 + public AppResourceProvider(App app) + { + this.app = app; + } + + /// + public T GetResource(string name) + { + return (T)app.Resources[name]; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs index 350b165d..00d575e3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs @@ -34,7 +34,9 @@ internal static class IocConfiguration /// 可继续操作的集合 public static IServiceCollection AddDatabase(this IServiceCollection services) { - return services.AddDbContext(AddDbContextCore); + return services + .AddTransient(typeof(Database.ScopedDbCurrent<,>)) + .AddDbContext(AddDbContextCore); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceProviderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceProviderExtension.cs new file mode 100644 index 00000000..f7d15aa4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceProviderExtension.cs @@ -0,0 +1,19 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Core.DependencyInjection; + +/// +/// 服务提供器扩展 +/// +internal static class ServiceProviderExtension +{ + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T CreateInstance(this IServiceProvider serviceProvider, params object[] parameters) + { + return ActivatorUtilities.CreateInstance(serviceProvider, parameters); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs index 3e59e122..8116938f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/HutaoOptions.cs @@ -18,14 +18,19 @@ namespace Snap.Hutao.Core; [Injection(InjectAs.Singleton)] internal sealed class HutaoOptions : IOptions { + private readonly ILogger logger; + private readonly bool isWebView2Supported; private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected; /// /// 构造一个新的胡桃选项 /// - public HutaoOptions() + /// 日志器 + public HutaoOptions(ILogger logger) { + this.logger = logger; + DataFolder = GetDataFolderPath(); LocalCache = ApplicationData.Current.LocalCacheFolder.Path; InstalledLocation = Package.Current.InstalledLocation.Path; @@ -115,7 +120,7 @@ internal sealed class HutaoOptions : IOptions return Convert.ToMd5HexString($"{userName}{machineGuid}"); } - private static void DetectWebView2Environment(ref string webView2Version, ref bool isWebView2Supported) + private void DetectWebView2Environment(ref string webView2Version, ref bool isWebView2Supported) { try { @@ -124,7 +129,6 @@ internal sealed class HutaoOptions : IOptions } catch (FileNotFoundException ex) { - ILogger logger = Ioc.Default.GetRequiredService>(); logger.LogError(ex, "WebView2 Runtime not installed."); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs index 2f910bb7..9c8e409a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs @@ -73,4 +73,4 @@ internal static class Clipboard SetBitmap(stream); } } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/ClipboardInterop.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/ClipboardInterop.cs new file mode 100644 index 00000000..a8baa44c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/ClipboardInterop.cs @@ -0,0 +1,45 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Windows.Storage.Streams; + +namespace Snap.Hutao.Core.IO.DataTransfer; + +/// +/// 剪贴板互操作 +/// +[Injection(InjectAs.Transient, typeof(IClipboardInterop))] +internal sealed class ClipboardInterop : IClipboardInterop +{ + private readonly IServiceProvider serviceProvider; + + /// + /// 构造一个新的剪贴板互操作对象 + /// + /// 服务提供器 + public ClipboardInterop(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + /// + public Task DeserializeTextAsync() + where T : class + { + return Clipboard.DeserializeTextAsync(serviceProvider); + } + + /// + public bool SetBitmap(IRandomAccessStream stream) + { + try + { + Clipboard.SetBitmap(stream); + return true; + } + catch (Exception) + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/IClipboardInterop.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/IClipboardInterop.cs new file mode 100644 index 00000000..47edd99e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/IClipboardInterop.cs @@ -0,0 +1,27 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Windows.Storage.Streams; + +namespace Snap.Hutao.Core.IO.DataTransfer; + +/// +/// 剪贴板互操作 +/// +internal interface IClipboardInterop +{ + /// + /// 从剪贴板文本中反序列化 + /// + /// 目标类型 + /// 实例 + Task DeserializeTextAsync() + where T : class; + + /// + /// 设置位图 + /// + /// 图片流 + /// 是否设置成功 + bool SetBitmap(IRandomAccessStream stream); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index fe76b3fa..8dd4c0c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -22,6 +22,8 @@ namespace Snap.Hutao.Core.LifeCycle; [HighQuality] internal static class Activation { + // TODO: make this class a dependency + /// /// 操作 /// @@ -40,7 +42,7 @@ internal static class Activation /// /// 从剪贴板导入成就 /// - public const string ImportUIAFFromClipBoard = nameof(ImportUIAFFromClipBoard); + public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard); private const string CategoryAchievement = "achievement"; private const string CategoryDailyNote = "dailynote"; @@ -247,9 +249,10 @@ internal static class Activation { case UrlActionImport: { - await ThreadHelper.SwitchToMainThreadAsync(); + ITaskContext taskContext = Ioc.Default.GetRequiredService(); + await taskContext.SwitchToMainThreadAsync(); - INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipBoard); + INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipboard); await Ioc.Default .GetRequiredService() .NavigateAsync(navigationAwaiter, true) @@ -268,7 +271,7 @@ internal static class Activation { await Ioc.Default .GetRequiredService() - .RefreshDailyNotesAsync(true) + .RefreshDailyNotesAsync() .ConfigureAwait(false); // Check if it's redirected. diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs index 78059b0e..01610e6f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs @@ -18,7 +18,7 @@ internal class ConcurrentCancellationTokenSource /// 注册取消令牌 /// /// 取消令牌 - public CancellationToken Register() + public CancellationToken CancelPreviousOne() { source.Cancel(); source = new(); diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs deleted file mode 100644 index ae17eefc..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.Runtime.CompilerServices; - -namespace Snap.Hutao.Core.Threading; - -/// -/// 线程帮助类 -/// -[Obsolete("Use TaskContext instead")] -internal static class ThreadHelper -{ - /// - /// 主线程队列 - /// - private static volatile ITaskContext taskContext; - - /// - /// 初始化 - /// - public static void Initialize(ITaskContext taskContext) - { - ThreadHelper.taskContext = taskContext; - } - - /// - /// 使用此静态方法以 异步切换到 后台线程 - /// - /// 使用 异步切换到 主线程 - /// 等待体 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ThreadPoolSwitchOperation SwitchToBackgroundAsync() - { - return taskContext.SwitchToBackgroundAsync(); - } - - /// - /// 使用此静态方法以 异步切换到 主线程 - /// - /// 使用 异步切换到 后台线程 - /// 等待体 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DispatherQueueSwitchOperation SwitchToMainThreadAsync() - { - return taskContext.SwitchToMainThreadAsync(); - } - - /// - /// 在主线程上同步等待执行操作 - /// - /// 操作 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvokeOnMainThread(Action action) - { - taskContext.InvokeOnMainThread(action); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ValueResult.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ValueResult.cs index db5e455c..56b98aee 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ValueResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ValueResult.cs @@ -7,7 +7,6 @@ namespace Snap.Hutao.Core.Threading; /// /// 用于包装异步操作的结果 -/// 结构类型,在栈上分配 /// /// 结果类型 /// 值类型 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ValueResultExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ValueResultExtension.cs new file mode 100644 index 00000000..77815be9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ValueResultExtension.cs @@ -0,0 +1,23 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Threading; + +/// +/// 结果扩展 +/// +internal static class ValueResultExtension +{ + /// + /// 尝试获取结果的值 + /// + /// 值的类型 + /// 结果 + /// 值 + /// 是否获取成功 + public static bool TryGetValue(this in ValueResult valueResult,[NotNullWhen(true)] out TValue value) + { + value = valueResult.Value; + return valueResult.IsOk; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs index 64786886..28378d64 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs @@ -36,7 +36,7 @@ internal sealed class ExtendedWindow : IRecipient : IRecipient(); + IAppResourceProvider resourceProvider = serviceProvider.GetRequiredService(); - Color systemBaseLowColor = (Color)app.Resources["SystemBaseLowColor"]; + Color systemBaseLowColor = resourceProvider.GetResource("SystemBaseLowColor"); appTitleBar.ButtonHoverBackgroundColor = systemBaseLowColor; - Color systemBaseMediumLowColor = (Color)app.Resources["SystemBaseMediumLowColor"]; + Color systemBaseMediumLowColor = resourceProvider.GetResource("SystemBaseMediumLowColor"); appTitleBar.ButtonPressedBackgroundColor = systemBaseMediumLowColor; // The Foreground doesn't accept Alpha channel. So we translate it to gray. @@ -158,7 +158,7 @@ internal sealed class ExtendedWindow : IRecipient("SystemBaseHighColor"); appTitleBar.ButtonForegroundColor = systemBaseHighColor; appTitleBar.ButtonHoverForegroundColor = systemBaseHighColor; appTitleBar.ButtonPressedForegroundColor = systemBaseHighColor; diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs index 4dbd588f..4cbbad99 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs @@ -61,20 +61,4 @@ internal static partial class EnumerableExtension { return new ObservableCollection(source); } - - /// - /// 转换为枚举对象 - /// - /// 类型 - /// 源 - /// 为枚举对象 - [SuppressMessage("", "SH002")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable ToEnumerable(this Span span) - { - for (int i = 0; i < span.Length; i++) - { - yield return span[i]; - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IPickerFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IPickerFactory.cs index ba7d32b9..4cbad757 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IPickerFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IPickerFactory.cs @@ -24,8 +24,19 @@ internal interface IPickerFactory /// 获取 经过初始化的 /// /// 经过初始化的 + [Obsolete] FileSavePicker GetFileSavePicker(); + /// + /// 获取 经过初始化的 + /// + /// 初始位置 + /// 文件名 + /// 提交按钮文本 + /// 文件类型 + /// 经过初始化的 + FileSavePicker GetFileSavePicker(PickerLocationId location, string fileName, string commitButton, IDictionary> fileTypes); + /// /// 获取 经过初始化的 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs index 4e772f53..945a161b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs @@ -55,6 +55,23 @@ internal class PickerFactory : IPickerFactory return GetInitializedPicker(); } + /// + public FileSavePicker GetFileSavePicker(PickerLocationId location, string fileName, string commitButton, IDictionary> fileTypes) + { + FileSavePicker picker = GetInitializedPicker(); + + picker.SuggestedStartLocation = location; + picker.SuggestedFileName = fileName; + picker.CommitButtonText = commitButton; + + foreach (KeyValuePair> kvp in fileTypes) + { + picker.FileTypeChoices.Add(kvp); + } + + return picker; + } + /// public FolderPicker GetFolderPicker() { diff --git a/src/Snap.Hutao/Snap.Hutao/IAppResourceProvider.cs b/src/Snap.Hutao/Snap.Hutao/IAppResourceProvider.cs new file mode 100644 index 00000000..928b3e51 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/IAppResourceProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao; + +/// +/// 应用程序资源提供器 +/// +internal interface IAppResourceProvider +{ + /// + /// 获取资源 + /// + /// 资源的类型 + /// 资源的名称 + /// 资源 + T GetResource(string name); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Program.cs b/src/Snap.Hutao/Snap.Hutao/Program.cs index 3ca4dba9..22c4d7df 100644 --- a/src/Snap.Hutao/Snap.Hutao/Program.cs +++ b/src/Snap.Hutao/Snap.Hutao/Program.cs @@ -47,7 +47,6 @@ public static partial class Program IServiceProvider serviceProvider = Ioc.Default; ITaskContext taskContext = serviceProvider.GetRequiredService(); - ThreadHelper.Initialize(taskContext); _ = serviceProvider.GetRequiredService(); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Collection.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Collection.cs index f4c07b67..373fe5cb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Collection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.Collection.cs @@ -58,6 +58,7 @@ internal sealed partial class AchievementService // Sync cache await taskContext.SwitchToMainThreadAsync(); archiveCollection!.Add(newArchive); + CurrentArchive = newArchive; // Sync database await taskContext.SwitchToBackgroundAsync(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs index e1a91bcd..021748df 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs @@ -101,7 +101,7 @@ internal sealed partial class AchievementService : IAchievementService results.Add(new() { DisplayName = archive.Name, - FinishDescription = $"{finished}/{totalCount} - {(double)finished / totalCount:P2}", + FinishDescription = AchievementStatistics.Format(finished, totalCount, out _), Achievements = achievements.SelectList(entity => new AchievementView(entity, achievementMap[entity.Id])), }); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs index 25e79003..06c851c6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs @@ -64,8 +64,8 @@ internal sealed class SummaryReliquaryFactory if (subProperty.Count > 0) { Span span = CollectionsMarshal.AsSpan(subProperty); - result.PrimarySubProperties = new(span[..^affixCount].ToEnumerable()); - result.SecondarySubProperties = new(span[^affixCount..].ToEnumerable()); + result.PrimarySubProperties = new(span[..^affixCount].ToArray()); + result.SecondarySubProperties = new(span[^affixCount..].ToArray()); result.ComposedSubProperties = equip.Flat.ReliquarySubstats!.SelectList(CreateComposedSubProperty); ReliquaryLevel relicLevel = metadataContext.ReliqueryLevels.Single(r => r.Level == equip.Reliquary!.Level && r.Quality == reliquary.RankLevel); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs index e44d37fa..88035b9d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs @@ -38,7 +38,7 @@ internal sealed class GameRecordCharacterAvatarInfoTransformer : IAvatarInfoTran // update level avatarInfo.PropMap ??= new Dictionary(); - avatarInfo.PropMap[PlayerProperty.PROP_LEVEL] = new(PlayerProperty.PROP_LEVEL, $"{source.Level}"); + avatarInfo.PropMap[PlayerProperty.PROP_LEVEL] = new(PlayerProperty.PROP_LEVEL, source.Level); // update constellations avatarInfo.TalentIdList = source.Constellations.Where(t => t.IsActived).Select(t => t.Id).ToList(); 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 a38e8831..28ad85a4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -12,15 +12,17 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider; [Injection(InjectAs.Transient, typeof(IGachaLogQueryProvider))] internal sealed class GachaLogQueryManualInputProvider : IGachaLogQueryProvider { + private readonly IServiceProvider serviceProvider; private readonly ITaskContext taskContext; /// /// 构造一个新的手动输入方法提供器 /// /// 任务上下文 - public GachaLogQueryManualInputProvider(ITaskContext taskContext) + public GachaLogQueryManualInputProvider(IServiceProvider serviceProvider) { - this.taskContext = taskContext; + taskContext = serviceProvider.GetRequiredService(); + this.serviceProvider = serviceProvider; } /// @@ -31,7 +33,8 @@ internal sealed class GachaLogQueryManualInputProvider : IGachaLogQueryProvider { // ContentDialog must be created by main thread. await taskContext.SwitchToMainThreadAsync(); - (bool isOk, string query) = await new GachaLogUrlDialog().GetInputUrlAsync().ConfigureAwait(false); + GachaLogUrlDialog dialog = serviceProvider.CreateInstance(); + (bool isOk, string query) = await dialog.GetInputUrlAsync().ConfigureAwait(false); if (isOk) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs index fcf07de8..8ad8ba0a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs @@ -345,7 +345,8 @@ internal sealed class GameService : IGameService { // ContentDialog must be created by main thread. await taskContext.SwitchToMainThreadAsync(); - (bool isOk, string name) = await new LaunchGameAccountNameDialog().GetInputNameAsync().ConfigureAwait(false); + LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance(); + (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); if (isOk) { @@ -411,7 +412,9 @@ internal sealed class GameService : IGameService /// public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) { - (bool isOk, string name) = await new LaunchGameAccountNameDialog().GetInputNameAsync().ConfigureAwait(true); + await taskContext.SwitchToMainThreadAsync(); + LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance(); + (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true); if (isOk) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs index 0934f2d3..103aa775 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs @@ -217,7 +217,7 @@ internal sealed class HutaoCache : IHutaoCache AvatarConstellationInfos = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info => { - return new AvatarConstellationInfoView(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate)); + return new AvatarConstellationInfoView(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.SelectList(x => x.Rate)); }).ToList(); } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml.cs index 49596418..7c0a98ae 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml.cs @@ -11,14 +11,18 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class AchievementArchiveCreateDialog : ContentDialog { + private readonly ITaskContext taskContext; + /// /// 构造一个新的成就存档创建对话框 /// - /// 窗体 - public AchievementArchiveCreateDialog() + /// 服务提供器 + public AchievementArchiveCreateDialog(IServiceProvider serviceProvider) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); } /// @@ -27,7 +31,7 @@ internal sealed partial class AchievementArchiveCreateDialog : ContentDialog /// 输入的结果 public async Task> GetInputAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); string text = InputText.Text ?? string.Empty; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs index d2fa63c6..811a8963 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs @@ -17,14 +17,19 @@ internal sealed partial class AchievementImportDialog : ContentDialog { private static readonly DependencyProperty UIAFProperty = Property.Depend(nameof(UIAF), default(UIAF)); + private readonly ITaskContext taskContext; + /// /// 构造一个新的成就对话框 /// + /// 服务提供器 /// uiaf数据 - public AchievementImportDialog(UIAF uiaf) + public AchievementImportDialog(IServiceProvider serviceProvider, UIAF uiaf) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); UIAF = uiaf; } @@ -43,7 +48,7 @@ internal sealed partial class AchievementImportDialog : ContentDialog /// 导入选项 public async Task> GetImportStrategyAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); ImportStrategy strategy = (ImportStrategy)ImportModeSelector.SelectedIndex; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AdoptCalculatorDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AdoptCalculatorDialog.xaml.cs index fc74e5be..d93465b6 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AdoptCalculatorDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AdoptCalculatorDialog.xaml.cs @@ -16,18 +16,21 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class AdoptCalculatorDialog : ContentDialog { + private readonly ITaskContext taskContext; private readonly IServiceScope scope; private MiHoYoJSInterface? jsInterface; /// /// 构造一个新的养成计算器对话框 /// - /// 窗体 - public AdoptCalculatorDialog() + /// 服务提供器 + public AdoptCalculatorDialog(IServiceProvider serviceProvider) { InitializeComponent(); - scope = Ioc.Default.CreateScope(); - XamlRoot = scope.ServiceProvider.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); + scope = serviceProvider.CreateScope(); } private void OnGridLoaded(object sender, RoutedEventArgs e) @@ -57,7 +60,7 @@ internal sealed partial class AdoptCalculatorDialog : ContentDialog private void OnClosePageRequested() { - ThreadHelper.InvokeOnMainThread(Hide); + taskContext.InvokeOnMainThread(Hide); } private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CommunityGameRecordDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CommunityGameRecordDialog.xaml.cs index 4fe97297..de1ca65b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CommunityGameRecordDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CommunityGameRecordDialog.xaml.cs @@ -16,18 +16,21 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class CommunityGameRecordDialog : ContentDialog { + private readonly ITaskContext taskContext; private readonly IServiceScope scope; private MiHoYoJSInterface? jsInterface; /// /// 构造一个新的社区游戏记录对话框 /// - /// 窗体 - public CommunityGameRecordDialog() + /// 服务提供器 + public CommunityGameRecordDialog(IServiceProvider serviceProvider) { InitializeComponent(); - scope = Ioc.Default.CreateScope(); - XamlRoot = scope.ServiceProvider.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); + scope = serviceProvider.CreateScope(); } private void OnGridLoaded(object sender, RoutedEventArgs e) @@ -55,7 +58,7 @@ internal sealed partial class CommunityGameRecordDialog : ContentDialog private void OnClosePageRequested() { - ThreadHelper.InvokeOnMainThread(Hide); + taskContext.InvokeOnMainThread(Hide); } private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivateProjectDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivateProjectDialog.xaml.cs index 799e1c1c..f390b0fb 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivateProjectDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivateProjectDialog.xaml.cs @@ -13,14 +13,18 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class CultivateProjectDialog : ContentDialog { + private readonly ITaskContext taskContext; + /// /// һµɼƻԻ /// - /// - public CultivateProjectDialog() + /// ṩ + public CultivateProjectDialog(IServiceProvider serviceProvider) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); } /// @@ -29,7 +33,7 @@ internal sealed partial class CultivateProjectDialog : ContentDialog /// ƻ public async ValueTask> CreateProjectAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); if (result == ContentDialogResult.Primary) { @@ -38,8 +42,7 @@ internal sealed partial class CultivateProjectDialog : ContentDialog ? Ioc.Default.GetRequiredService().Current?.SelectedUserGameRole?.GameUid : null; - CultivateProject project = CultivateProject.Create(text, uid); - return new(true, project); + return new(true, CultivateProject.Create(text, uid)); } return new(false, null!); 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 0f22eb96..e86478ec 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs @@ -18,20 +18,53 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog private static readonly DependencyProperty AvatarProperty = Property.Depend(nameof(Avatar)); private static readonly DependencyProperty WeaponProperty = Property.Depend(nameof(Weapon)); + private readonly ITaskContext taskContext; + /// /// һµɼԻ /// + /// ṩ /// ɫ /// - public CultivatePromotionDeltaDialog(ICalculableAvatar? avatar, ICalculableWeapon? weapon) + public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, ICalculableAvatar? avatar, ICalculableWeapon? weapon) + : this(serviceProvider) { - InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; - DataContext = this; Avatar = avatar; Weapon = weapon; } + /// + /// һµɼԻ + /// + /// ṩ + /// ɫ + public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, ICalculableAvatar? avatar) + : this(serviceProvider) + { + Avatar = avatar; + } + + /// + /// һµɼԻ + /// + /// ṩ + /// + public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, ICalculableWeapon? weapon) + : this(serviceProvider) + { + Weapon = weapon; + } + + private CultivatePromotionDeltaDialog(IServiceProvider serviceProvider) + { + InitializeComponent(); + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); + + DataContext = this; + } + /// /// ɫ /// @@ -56,7 +89,7 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog /// public async Task> GetPromotionDeltaAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); if (result == ContentDialogResult.Primary) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteNotificationDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteNotificationDialog.xaml.cs index 6abd80b3..4eb68595 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteNotificationDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteNotificationDialog.xaml.cs @@ -15,11 +15,13 @@ internal sealed partial class DailyNoteNotificationDialog : ContentDialog /// /// һµʵʱ֪ͨöԻ /// + /// ṩ /// ʵʱ - public DailyNoteNotificationDialog(DailyNoteEntry entry) + public DailyNoteNotificationDialog(IServiceProvider serviceProvider, DailyNoteEntry entry) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + DataContext = entry; } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml.cs index 02e2e4c3..700c2fce 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml.cs @@ -6,6 +6,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.Web.WebView2.Core; using Snap.Hutao.ViewModel.User; using Snap.Hutao.Web.Bridge; +using Snap.Hutao.Web.Request.QueryString; namespace Snap.Hutao.View.Dialog; @@ -16,6 +17,7 @@ namespace Snap.Hutao.View.Dialog; [SuppressMessage("", "CA1001")] internal sealed partial class DailyNoteVerificationDialog : ContentDialog { + private readonly ITaskContext taskContext; private readonly IServiceScope scope; private readonly UserAndUid userAndUid; @@ -24,13 +26,16 @@ internal sealed partial class DailyNoteVerificationDialog : ContentDialog /// /// 构造一个新的实时便笺验证对话框 /// + /// 服务提供器 /// 用户与角色 - public DailyNoteVerificationDialog(UserAndUid userAndUid) + public DailyNoteVerificationDialog(IServiceProvider serviceProvider, UserAndUid userAndUid) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); + scope = serviceProvider.CreateScope(); this.userAndUid = userAndUid; - scope = Ioc.Default.CreateScope(); } private void OnGridLoaded(object sender, RoutedEventArgs e) @@ -48,13 +53,13 @@ internal sealed partial class DailyNoteVerificationDialog : ContentDialog jsInterface = new(coreWebView2, scope.ServiceProvider); jsInterface.ClosePageRequested += OnClosePageRequested; - string query = $"?role_id={userAndUid.Uid.Value}&server={userAndUid.Uid.Region}"; - coreWebView2.Navigate($"https://webstatic.mihoyo.com/app/community-game-records/index.html?bbs_presentation_style=fullscreen#/ys/daily/{query}"); + QueryString query = userAndUid.Uid.ToQueryString(); + coreWebView2.Navigate($"https://webstatic.mihoyo.com/app/community-game-records/index.html?bbs_presentation_style=fullscreen#/ys/daily/?{query}"); } private void OnClosePageRequested() { - ThreadHelper.InvokeOnMainThread(Hide); + taskContext.InvokeOnMainThread(Hide); } private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) 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 cef6e991..5892220a 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs @@ -16,14 +16,19 @@ internal sealed partial class GachaLogImportDialog : ContentDialog { private static readonly DependencyProperty UIGFProperty = Property.Depend(nameof(UIGF), default(UIGF)); + private readonly ITaskContext taskContext; + /// /// 构造一个新的祈愿记录导入对话框 /// + /// 服务提供器 /// uigf数据 - public GachaLogImportDialog(UIGF uigf) + public GachaLogImportDialog(IServiceProvider serviceProvider, UIGF uigf) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); UIGF = uigf; } @@ -42,7 +47,7 @@ internal sealed partial class GachaLogImportDialog : ContentDialog /// 是否导入 public async Task GetShouldImportAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); return await ShowAsync() == ContentDialogResult.Primary; } } \ No newline at end of file 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 4cfb9fef..748adf12 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs @@ -21,11 +21,11 @@ internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog /// /// 构造一个新的对话框 /// - /// 窗体 - public GachaLogRefreshProgressDialog() + /// 服务提供器 + public GachaLogRefreshProgressDialog(IServiceProvider serviceProvider) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; } /// 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 20495571..fa9ae45f 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs @@ -11,14 +11,18 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class GachaLogUrlDialog : ContentDialog { + private readonly ITaskContext taskContext; + /// /// 初始化一个新的祈愿记录Url对话框 /// - /// 窗体 - public GachaLogUrlDialog() + /// 服务提供器 + public GachaLogUrlDialog(IServiceProvider serviceProvider) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); } /// @@ -27,7 +31,7 @@ internal sealed partial class GachaLogUrlDialog : ContentDialog /// 输入的结果 public async Task> GetInputUrlAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); string url = InputText.Text; 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 13a1fb99..feb12843 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGameAccountNameDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGameAccountNameDialog.xaml.cs @@ -11,14 +11,18 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class LaunchGameAccountNameDialog : ContentDialog { + private readonly ITaskContext taskContext; + /// /// 构造一个新的游戏账号命名对话框 /// - /// 窗体 - public LaunchGameAccountNameDialog() + /// 服务提供器 + public LaunchGameAccountNameDialog(IServiceProvider serviceProvider) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); } /// @@ -27,7 +31,7 @@ internal sealed partial class LaunchGameAccountNameDialog : ContentDialog /// 输入的结果 public async Task> GetInputNameAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); string text = InputText.Text; return new(result == ContentDialogResult.Primary && (!string.IsNullOrEmpty(text)), text); diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs index 001cd512..d92ea5e4 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs @@ -19,10 +19,12 @@ internal sealed partial class LaunchGamePackageConvertDialog : ContentDialog /// /// 构造一个新的启动游戏客户端转换对话框 /// - public LaunchGamePackageConvertDialog() + /// 服务提供器 + public LaunchGamePackageConvertDialog(IServiceProvider serviceProvider) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + DataContext = this; } 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 11495a9e..f5c24cd6 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs @@ -22,11 +22,13 @@ internal sealed partial class SignInWebViewDialog : ContentDialog /// /// 构造一个新的签到网页视图对话框 /// - public SignInWebViewDialog() + /// 服务提供器 + public SignInWebViewDialog(IServiceProvider serviceProvider) { InitializeComponent(); - scope = Ioc.Default.CreateScope(); - XamlRoot = scope.ServiceProvider.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + scope = serviceProvider.CreateScope(); } private void OnGridLoaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) 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 cc194f33..093e6811 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml.cs @@ -11,14 +11,18 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class UserDialog : ContentDialog { + private readonly ITaskContext taskContext; + /// /// 构造一个新的添加用户对话框 /// - /// 呈现的父窗口 - public UserDialog() + /// 服务提供器 + public UserDialog(IServiceProvider serviceProvider) { InitializeComponent(); - XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); } /// @@ -27,7 +31,7 @@ internal sealed partial class UserDialog : ContentDialog /// 输入的结果 public async Task> GetInputCookieAsync() { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); string cookie = InputText.Text; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModelSlim.cs index e9a91cf3..b38164e1 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModelSlim.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Service.Navigation; namespace Snap.Hutao.ViewModel.Abstraction; @@ -12,7 +13,7 @@ namespace Snap.Hutao.ViewModel.Abstraction; /// /// 页面类型 internal abstract class ViewModelSlim : ObservableObject - where TPage : Microsoft.UI.Xaml.Controls.Page + where TPage : Page { private bool isInitialized; @@ -62,6 +63,7 @@ internal abstract class ViewModelSlim : ObservableObject /// protected virtual void Navigate() { - ServiceProvider.GetRequiredService().Navigate(INavigationAwaiter.Default, true); + INavigationService navigationService = ServiceProvider.GetRequiredService(); + navigationService.Navigate(INavigationAwaiter.Default, true); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs index 2256fd89..79333101 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs @@ -19,35 +19,34 @@ internal static class AchievementFinishPercent /// 视图模型 public static void Update(AchievementViewModel viewModel) { - int finished = 0; - int count = 0; + int totalFinished = 0; + int totalCount = 0; if (viewModel.Achievements is AdvancedCollectionView achievements) { if (viewModel.AchievementGoals is List achievementGoals) { - Dictionary counter = achievementGoals.ToDictionary(x => x.Id, x => new AchievementGoalStatistics(x)); + Dictionary counter = achievementGoals.ToDictionary(x => x.Id, AchievementGoalStatistics.Create); foreach (AchievementView achievement in achievements.SourceCollection.Cast()) { - // We want to make the state update as fast as possible, - // so we use CollectionsMarshal here to get the ref. + // Make the state update as fast as possible ref AchievementGoalStatistics stat = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal); stat.TotalCount += 1; - count += 1; + totalCount += 1; if (achievement.IsChecked) { stat.Finished += 1; - finished += 1; + totalFinished += 1; } } foreach (AchievementGoalStatistics statistics in counter.Values) { - statistics.AchievementGoal.UpdateFinishPercent(statistics.Finished, statistics.TotalCount); + statistics.AchievementGoal.UpdateFinishPercent(statistics); } - viewModel.FinishDescription = $"{finished}/{count} - {(double)finished / count:P2}"; + viewModel.FinishDescription = AchievementStatistics.Format(totalFinished, totalCount, out _); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalStatistics.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalStatistics.cs index 1d7ccf27..3e4716f5 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalStatistics.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalStatistics.cs @@ -30,8 +30,18 @@ internal struct AchievementGoalStatistics /// 构造一个新的成就分类统计 /// /// 分类 - public AchievementGoalStatistics(BindingAchievementGoal goal) + private AchievementGoalStatistics(BindingAchievementGoal goal) { AchievementGoal = goal; } + + /// + /// 构造一个新的成就分类统计 + /// + /// 分类 + /// 新的成就分类统计 + public static AchievementGoalStatistics Create(BindingAchievementGoal goal) + { + return new(goal); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs index 5c50af26..968cf7ea 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs @@ -56,18 +56,40 @@ internal sealed class AchievementGoalView : ObservableObject, INameIcon public double FinishPercent { get => finishPercent; set => SetProperty(ref finishPercent, value); } /// - /// 完成百分比 + /// 完成百分比描述 /// public string? FinishDescription { get => finishDescription; set => SetProperty(ref finishDescription, value); } + /// + /// 创建新的列表 + /// + /// 目标 + /// 列表 + public static List List(List goals) + { + return goals + .OrderBy(goal => goal.Order) + .Select(goal => new AchievementGoalView(goal)) + .ToList(); + } + + /// + /// 更新进度 + /// + /// 统计 + public void UpdateFinishPercent(AchievementGoalStatistics statistics) + { + UpdateFinishPercent(statistics.Finished, statistics.TotalCount); + } + /// /// 更新进度 /// /// 完成项 /// 总项 - public void UpdateFinishPercent(int finished, int count) + private void UpdateFinishPercent(int finished, int count) { - FinishPercent = finished / (double)count; - FinishDescription = $"{finished}/{count} - {FinishPercent:P2}"; + FinishDescription = AchievementStatistics.Format(finished, count, out double finishPercent); + FinishPercent = finishPercent; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs index 3e7e26cd..f0432dcb 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs @@ -23,6 +23,7 @@ namespace Snap.Hutao.ViewModel.Achievement; internal sealed class AchievementImporter { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly IAchievementService achievementService; private readonly IInfoBarService infoBarService; private readonly JsonSerializerOptions options; @@ -33,6 +34,7 @@ internal sealed class AchievementImporter /// 服务提供器 public AchievementImporter(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); achievementService = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); options = serviceProvider.GetRequiredService(); @@ -46,11 +48,11 @@ internal sealed class AchievementImporter /// 是否导入成功 public async Task FromClipboardAsync() { - if (achievementService.CurrentArchive != null) + if (achievementService.CurrentArchive is EntityAchievementArchive archive) { - if (await GetUIAFFromClipboardAsync().ConfigureAwait(false) is UIAF uiaf) + if (await TryCatchGetUIAFFromClipboardAsync().ConfigureAwait(false) is UIAF uiaf) { - return await TryImportAsync(achievementService.CurrentArchive!, uiaf).ConfigureAwait(false); + return await TryImportAsync(archive, uiaf).ConfigureAwait(false); } else { @@ -71,21 +73,21 @@ internal sealed class AchievementImporter /// 是否导入成功 public async Task FromFileAsync() { - if (achievementService.CurrentArchive != null) + if (achievementService.CurrentArchive is EntityAchievementArchive archive) { - (bool isPickerOk, ValueFile file) = await serviceProvider + ValueResult pickerResult = await serviceProvider .GetRequiredService() .GetFileOpenPicker(PickerLocationId.Desktop, SH.FilePickerImportCommit, ".json") .TryPickSingleFileAsync() .ConfigureAwait(false); - if (isPickerOk) + if (pickerResult.TryGetValue(out ValueFile file)) { - (bool isOk, UIAF? uiaf) = await file.DeserializeFromJsonAsync(options).ConfigureAwait(false); + ValueResult uiafResult = await file.DeserializeFromJsonAsync(options).ConfigureAwait(false); - if (isOk) + if (uiafResult.TryGetValue(out UIAF? uiaf)) { - return await TryImportAsync(achievementService.CurrentArchive, uiaf!).ConfigureAwait(false); + return await TryImportAsync(archive, uiaf).ConfigureAwait(false); } else { @@ -101,11 +103,14 @@ internal sealed class AchievementImporter return false; } - private async Task GetUIAFFromClipboardAsync() + private async Task TryCatchGetUIAFFromClipboardAsync() { try { - return await Clipboard.DeserializeTextAsync(serviceProvider).ConfigureAwait(false); + return await serviceProvider + .GetRequiredService() + .DeserializeTextAsync() + .ConfigureAwait(false); } catch (Exception ex) { @@ -119,8 +124,9 @@ internal sealed class AchievementImporter if (uiaf.IsCurrentVersionSupported()) { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - (bool isOk, ImportStrategy strategy) = await new AchievementImportDialog(uiaf).GetImportStrategyAsync().ConfigureAwait(true); + await taskContext.SwitchToMainThreadAsync(); + AchievementImportDialog importDialog = serviceProvider.CreateInstance(uiaf); + (bool isOk, ImportStrategy strategy) = await importDialog.GetImportStrategyAsync().ConfigureAwait(true); if (isOk) { @@ -129,7 +135,7 @@ internal sealed class AchievementImporter .CreateForIndeterminateProgressAsync(SH.ViewModelAchievementImportProgress) .ConfigureAwait(false); - using (await dialog.BlockAsync().ConfigureAwait(false)) + using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { result = await achievementService.ImportFromUIAFAsync(archive, uiaf.List, strategy).ConfigureAwait(false); } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementStatistics.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementStatistics.cs index bdb76384..17441fd4 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementStatistics.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementStatistics.cs @@ -22,4 +22,17 @@ internal sealed class AchievementStatistics /// 近期完成的成就 /// public List Achievements { get; set; } = default!; + + /// + /// 格式化完成进度 + /// + /// 完成的成就个数 + /// 总个数 + /// 完成进度 + /// 格式化的完成进度 + public static string Format(int finished, int totalCount, out double finishedPercent) + { + finishedPercent = (double)finished / totalCount; + return $"{finished}/{totalCount} - {finishedPercent:P2}"; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementView.cs index e7abef96..0e562540 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementView.cs @@ -69,6 +69,6 @@ internal sealed class AchievementView : ObservableObject, IEntityWithMetadata public string Time { - get => Entity.Time.ToString("yyyy.MM.dd HH:mm:ss"); + get => $"{Entity.Time:yyyy.MM.dd HH:mm:ss}"; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs index f8f757b8..faa593c3 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs @@ -33,6 +33,8 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR private static readonly SortDescription UncompletedItemsFirstSortDescription = new(nameof(AchievementView.IsChecked), SortDirection.Ascending); private static readonly SortDescription CompletionTimeSortDescription = new(nameof(AchievementView.Time), SortDirection.Descending); + private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly IAchievementService achievementService; private readonly IMetadataService metadataService; private readonly IInfoBarService infoBarService; @@ -41,7 +43,7 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR private readonly AchievementImporter achievementImporter; - private readonly TaskCompletionSource openUICompletionSource = new(); + private readonly TaskCompletionSource openUITaskCompletionSource = new(); private AdvancedCollectionView? achievements; private List? achievementGoals; @@ -58,12 +60,14 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR /// 服务提供器 public AchievementViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); metadataService = serviceProvider.GetRequiredService(); achievementService = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); contentDialogFactory = serviceProvider.GetRequiredService(); options = serviceProvider.GetRequiredService(); achievementImporter = serviceProvider.GetRequiredService(); + this.serviceProvider = serviceProvider; ImportUIAFFromClipboardCommand = new AsyncRelayCommand(ImportUIAFFromClipboardAsync); ImportUIAFFromFileCommand = new AsyncRelayCommand(ImportUIAFFromFileAsync); @@ -202,9 +206,9 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR /// public async Task ReceiveAsync(INavigationData data) { - if (await openUICompletionSource.Task.ConfigureAwait(false)) + if (await openUITaskCompletionSource.Task.ConfigureAwait(false)) { - if (data.Data is Activation.ImportUIAFFromClipBoard) + if (data.Data is Activation.ImportUIAFFromClipboard) { await ImportUIAFFromClipboardAsync().ConfigureAwait(false); return true; @@ -228,18 +232,18 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR using (await EnterCriticalExecutionAsync().ConfigureAwait(false)) { - List goals = await metadataService.GetAchievementGoalsAsync(CancellationToken).ConfigureAwait(false); - sortedGoals = goals - .OrderBy(goal => goal.Order) - .Select(goal => new AchievementGoalView(goal)) - .ToList(); + List goals = await metadataService + .GetAchievementGoalsAsync(CancellationToken) + .ConfigureAwait(false); + + sortedGoals = AchievementGoalView.List(goals); archives = achievementService.ArchiveCollection; } - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); AchievementGoals = sortedGoals; Archives = archives; - SelectedArchive = Archives.SelectedOrDefault(); + SelectedArchive = achievementService.CurrentArchive; IsInitialized = true; } @@ -247,12 +251,12 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR { // User canceled the loading operation, // Indicate initialization not succeed. - openUICompletionSource.TrySetResult(false); + openUITaskCompletionSource.TrySetResult(false); return; } } - openUICompletionSource.TrySetResult(metaInitialized); + openUITaskCompletionSource.TrySetResult(metaInitialized); } private async Task AddArchiveAsync() @@ -260,8 +264,9 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR if (Archives != null) { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - (bool isOk, string name) = await new AchievementArchiveCreateDialog().GetInputAsync().ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + AchievementArchiveCreateDialog dialog = serviceProvider.CreateInstance(); + (bool isOk, string name) = await dialog.GetInputAsync().ConfigureAwait(false); if (isOk) { @@ -270,8 +275,8 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR switch (result) { case ArchiveAddResult.Added: - await ThreadHelper.SwitchToMainThreadAsync(); - SelectedArchive = Archives.SingleOrDefault(a => a.Name == name); + await taskContext.SwitchToMainThreadAsync(); + SelectedArchive = achievementService.CurrentArchive; infoBarService.Success(string.Format(SH.ViewModelAchievementArchiveAdded, name)); break; case ArchiveAddResult.InvalidName: @@ -307,7 +312,7 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR } // Re-select first archive - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); SelectedArchive = Archives.FirstOrDefault(); } catch (OperationCanceledException) @@ -321,19 +326,24 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR { if (SelectedArchive != null && Achievements != null) { - FileSavePicker picker = Ioc.Default.GetRequiredService().GetFileSavePicker(); - picker.FileTypeChoices.Add(SH.ViewModelAchievementExportFileType, ".json".Enumerate().ToList()); - picker.SuggestedStartLocation = PickerLocationId.Desktop; - picker.CommitButtonText = SH.FilePickerExportCommit; - picker.SuggestedFileName = $"{achievementService.CurrentArchive?.Name}.json"; + Dictionary> fileTypes = new() + { + [SH.ViewModelAchievementExportFileType] = ".json".Enumerate().ToList(), + }; + + FileSavePicker picker = serviceProvider + .GetRequiredService() + .GetFileSavePicker( + PickerLocationId.Desktop, + $"{achievementService.CurrentArchive?.Name}.json", + SH.FilePickerExportCommit, + fileTypes); (bool isPickerOk, ValueFile file) = await picker.TryPickSaveFileAsync().ConfigureAwait(false); if (isPickerOk) { UIAF uiaf = await achievementService.ExportToUIAFAsync(SelectedArchive).ConfigureAwait(false); - bool isOk = await file.SerializeToJsonAsync(uiaf, options).ConfigureAwait(false); - - if (isOk) + if (await file.SerializeToJsonAsync(uiaf, options).ConfigureAwait(false)) { infoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage); } @@ -372,9 +382,8 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR if (TryGetAchievements(archive, achievements, out List? combined)) { - // Assemble achievements on the UI thread. - await ThreadHelper.SwitchToMainThreadAsync(); - Achievements = new(combined, true); + await taskContext.SwitchToMainThreadAsync(); + Achievements = new(combined, true); // Assemble achievements on the UI thread. UpdateAchievementsFinishPercent(); UpdateAchievementsFilterByGoal(SelectedAchievementGoal); @@ -438,7 +447,7 @@ internal sealed class AchievementViewModel : Abstraction.ViewModel, INavigationR { if (search.Length == 5 && int.TryParse(search, out int achievementId)) { - Achievements.Filter = obj => ((AchievementView)obj).Inner.Id == achievementId; + Achievements.Filter = obj => ((AchievementView)obj).Inner.Id.Value == achievementId; } else { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModelSlim.cs index bd17f5b3..da421a1e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModelSlim.cs @@ -34,6 +34,7 @@ internal sealed class AchievementViewModelSlim : Abstraction.ViewModelSlim(); IMetadataService metadataService = scope.ServiceProvider.GetRequiredService(); if (await metadataService.InitializeAsync().ConfigureAwait(false)) @@ -46,7 +47,7 @@ internal sealed class AchievementViewModelSlim : Abstraction.ViewModelSlim { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly IUserService userService; private readonly IInfoBarService infoBarService; private Summary? summary; @@ -51,6 +52,7 @@ internal sealed class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipien /// 服务提供器 public AvatarPropertyViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); userService = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; @@ -161,7 +163,7 @@ internal sealed class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipien .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyFetch) .ConfigureAwait(false); - using (await dialog.BlockAsync().ConfigureAwait(false)) + using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { summaryResult = await serviceProvider .GetRequiredService() @@ -173,7 +175,7 @@ internal sealed class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipien (RefreshResult result, Summary? summary) = summaryResult; if (result == RefreshResult.Ok) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); Summary = summary; SelectedAvatar = Summary?.Avatars.FirstOrDefault(); } @@ -213,10 +215,9 @@ internal sealed class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipien } // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - (bool isOk, CalcAvatarPromotionDelta delta) = await new CultivatePromotionDeltaDialog(avatar.ToCalculable(), avatar.Weapon.ToCalculable()) - .GetPromotionDeltaAsync() - .ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); + (bool isOk, CalcAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); if (isOk) { @@ -276,7 +277,7 @@ internal sealed class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipien bool clipboardOpened = false; using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight)) { - Color tintColor = (Color)serviceProvider.GetRequiredService().Resources["CompatBackgroundColor"]; + Color tintColor = serviceProvider.GetRequiredService().GetResource("CompatBackgroundColor"); Bgra8 tint = Bgra8.FromColor(tintColor); softwareBitmap.NormalBlend(tint); using (InMemoryRandomAccessStream memory = new()) @@ -285,15 +286,7 @@ internal sealed class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipien encoder.SetSoftwareBitmap(softwareBitmap); await encoder.FlushAsync(); - try - { - Clipboard.SetBitmap(memory); - clipboardOpened = true; - } - catch (COMException) - { - // CLIPBRD_E_CANT_OPEN - } + clipboardOpened = serviceProvider.GetRequiredService().SetBitmap(memory); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/AvatarConstellationInfoView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/AvatarConstellationInfoView.cs index 641a5c52..57eb836c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/AvatarConstellationInfoView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/AvatarConstellationInfoView.cs @@ -18,10 +18,10 @@ internal sealed class AvatarConstellationInfoView : AvatarView /// 角色 /// 持有率 /// 命座比率 - public AvatarConstellationInfoView(Avatar avatar, double rate, IEnumerable rates) + public AvatarConstellationInfoView(Avatar avatar, double rate, List rates) : base(avatar, rate) { - Rates = rates.Select(r => $"{r:P3}").ToList(); + Rates = rates.SelectList(r => $"{r:P3}"); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/ReliquarySetView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/ReliquarySetView.cs index eb3533ff..c5f09134 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/ReliquarySetView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/ReliquarySetView.cs @@ -4,6 +4,7 @@ using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Primitive; using Snap.Hutao.Web.Hutao.Model; +using System.Runtime.InteropServices; using System.Text; namespace Snap.Hutao.ViewModel.Complex; @@ -27,7 +28,7 @@ internal sealed class ReliquarySetView StringBuilder nameBuilder = new(); List icons = new(2); - foreach (ReliquarySet set in sets) + foreach (ReliquarySet set in CollectionsMarshal.AsSpan(sets)) { Model.Metadata.Reliquary.ReliquarySet metaSet = idReliquarySetMap[set.EquipAffixId / 10]; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/Team.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/Team.cs index 124b98a1..eb60033d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/Team.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/Team.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.Extensions.Primitives; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Primitive; using Snap.Hutao.Web.Hutao.Model; @@ -21,10 +22,9 @@ internal sealed class Team : List public Team(ItemRate team, Dictionary idAvatarMap) : base(4) { - IOrderedEnumerable ids = team.Item.Split(',').Select(int.Parse).OrderByDescending(x => x); - - foreach (int id in ids) + foreach (StringSegment item in new StringTokenizer(team.Item, new char[] { ',' })) { + int id = int.Parse(item.AsSpan()); Add(new(idAvatarMap[id], 0)); } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/TeamAppearanceView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/TeamAppearanceView.cs index 2a9e38c6..d2f1acd9 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/TeamAppearanceView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Complex/TeamAppearanceView.cs @@ -21,8 +21,8 @@ internal sealed class TeamAppearanceView public TeamAppearanceView(TeamAppearance teamRank, Dictionary idAvatarMap) { Floor = string.Format(SH.ModelBindingHutaoComplexRankFloor, teamRank.Floor); - Up = teamRank.Up.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList(); - Down = teamRank.Down.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList(); + 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 0aa4d936..984362b1 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs @@ -21,6 +21,7 @@ namespace Snap.Hutao.ViewModel.Cultivation; internal sealed class CultivationViewModel : Abstraction.ViewModel { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly ICultivationService cultivationService; private readonly IMetadataService metadataService; private readonly ILogger logger; @@ -39,6 +40,7 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel /// 服务提供器 public CultivationViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); cultivationService = serviceProvider.GetRequiredService(); metadataService = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); @@ -120,11 +122,15 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel /// protected override async Task OpenUIAsync() { - bool metaInitialized = await metadataService.InitializeAsync().ConfigureAwait(true); + bool metaInitialized = await metadataService.InitializeAsync().ConfigureAwait(false); if (metaInitialized) { - Projects = cultivationService.ProjectCollection(); - SelectedProject = cultivationService.Current; + ObservableCollection projects = cultivationService.ProjectCollection; + CultivateProject? selected = cultivationService.Current; + + await taskContext.SwitchToMainThreadAsync(); + Projects = projects; + SelectedProject = selected; } IsInitialized = metaInitialized; @@ -133,8 +139,9 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel private async Task AddProjectAsync() { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - (bool isOk, CultivateProject project) = await new CultivateProjectDialog().CreateProjectAsync().ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + CultivateProjectDialog dialog = serviceProvider.CreateInstance(); + (bool isOk, CultivateProject project) = await dialog.CreateProjectAsync().ConfigureAwait(false); if (isOk) { @@ -145,7 +152,7 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel { case ProjectAddResult.Added: infoBarService.Success(SH.ViewModelCultivationProjectAdded); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); SelectedProject = project; break; case ProjectAddResult.InvalidName: @@ -166,7 +173,7 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel { await cultivationService.RemoveProjectAsync(project).ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); SelectedProject = Projects!.FirstOrDefault(); } } @@ -181,7 +188,7 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel .GetCultivateEntriesAsync(project) .ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); CultivateEntries = entries; InventoryItems = cultivationService.GetInventoryItems(project, materials, SaveInventoryItemCommand); @@ -222,8 +229,8 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel { if (SelectedProject != null) { - await ThreadHelper.SwitchToBackgroundAsync(); - CancellationToken token = statisticsCancellationTokenSource.Register(); + await taskContext.SwitchToBackgroundAsync(); + CancellationToken token = statisticsCancellationTokenSource.CancelPreviousOne(); ObservableCollection statistics; try { @@ -234,7 +241,7 @@ internal sealed class CultivationViewModel : Abstraction.ViewModel return; } - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); StatisticsItems = statistics; } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs index 20806158..f254825e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs @@ -24,6 +24,7 @@ namespace Snap.Hutao.ViewModel.DailyNote; internal sealed class DailyNoteViewModel : Abstraction.ViewModel { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly IUserService userService; private readonly IDailyNoteService dailyNoteService; private readonly AppDbContext appDbContext; @@ -37,6 +38,7 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel /// 服务提供器 public DailyNoteViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); userService = serviceProvider.GetRequiredService(); dailyNoteService = serviceProvider.GetRequiredService(); appDbContext = serviceProvider.GetRequiredService(); @@ -95,11 +97,11 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel { try { - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); ObservableCollection roles = await userService.GetRoleCollectionAsync().ConfigureAwait(false); ObservableCollection entries = await dailyNoteService.GetDailyNoteEntriesAsync().ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); UserAndUids = roles; DailyNoteEntries = entries; } @@ -120,7 +122,7 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel private async Task RefreshAsync() { - await dailyNoteService.RefreshDailyNotesAsync(false).ConfigureAwait(false); + await dailyNoteService.RefreshDailyNotesAsync().ConfigureAwait(false); } private async Task RemoveDailyNoteAsync(DailyNoteEntry? entry) @@ -138,8 +140,9 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel using (await EnterCriticalExecutionAsync().ConfigureAwait(false)) { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - await new DailyNoteNotificationDialog(entry).ShowAsync(); + await taskContext.SwitchToMainThreadAsync(); + DailyNoteNotificationDialog dialog = serviceProvider.CreateInstance(entry); + await dialog.ShowAsync(); appDbContext.DailyNotes.UpdateAndSave(entry); } } @@ -147,23 +150,26 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel private async Task VerifyDailyNoteVerificationAsync() { + IInfoBarService infoBarService = serviceProvider.GetRequiredService(); + if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) { // TODO: Add verify support for oversea user if (userAndUid.User.IsOversea) { - serviceProvider.GetRequiredService().Warning(SH.ViewModelDailyNoteHoyolabVerificationUnsupported); + infoBarService.Warning(SH.ViewModelDailyNoteHoyolabVerificationUnsupported); } else { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - await new DailyNoteVerificationDialog(userAndUid).ShowAsync(); + await taskContext.SwitchToMainThreadAsync(); + DailyNoteVerificationDialog dialog = serviceProvider.CreateInstance(userAndUid); + await dialog.ShowAsync(); } } else { - serviceProvider.GetRequiredService().Warning(SH.MustSelectUserAndUid); + infoBarService.Warning(SH.MustSelectUserAndUid); } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModelSlim.cs index f0052c28..cc15fc8b 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModelSlim.cs @@ -37,7 +37,9 @@ internal sealed class DailyNoteViewModelSlim : Abstraction.ViewModelSlim(); + + await taskContext.SwitchToBackgroundAsync(); _ = await ServiceProvider .GetRequiredService() .GetRoleCollectionAsync() @@ -51,7 +53,7 @@ internal sealed class DailyNoteViewModelSlim : Abstraction.ViewModelSlim entryList = entries.ToList(); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); DailyNoteEntries = entryList; IsInitialized = true; } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs index 8cf9adca..6b4e9481 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs @@ -26,6 +26,7 @@ namespace Snap.Hutao.ViewModel.GachaLog; [Injection(InjectAs.Scoped)] internal sealed class GachaLogViewModel : Abstraction.ViewModel { + private readonly ITaskContext taskContext; private readonly IGachaLogService gachaLogService; private readonly IInfoBarService infoBarService; private readonly IPickerFactory pickerFactory; @@ -45,6 +46,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel /// 服务提供器 public GachaLogViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); gachaLogService = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); pickerFactory = serviceProvider.GetRequiredService(); @@ -150,8 +152,9 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel { if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(false)) { - await ThreadHelper.SwitchToMainThreadAsync(); - Archives = gachaLogService.ArchiveCollection(); + ObservableCollection archives = gachaLogService.ArchiveCollection; + await taskContext.SwitchToMainThreadAsync(); + Archives = archives; SetSelectedArchiveAndUpdateStatistics(Archives.SelectedOrDefault(), true); } } @@ -188,9 +191,10 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge; // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - GachaLogRefreshProgressDialog dialog = new(); - IDisposable dialogHider = await dialog.BlockAsync().ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + + GachaLogRefreshProgressDialog dialog = serviceProvider.CreateInstance(); + IDisposable dialogHider = await dialog.BlockAsync(taskContext).ConfigureAwait(false); Progress progress = new(dialog.OnReport); bool authkeyValid; @@ -216,7 +220,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel infoBarService.Warning(SH.ViewModelGachaLogRefreshOperationCancel); } - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); if (authkeyValid) { SetSelectedArchiveAndUpdateStatistics(gachaLogService.CurrentArchive, true); @@ -261,20 +265,23 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel { if (SelectedArchive != null) { - FileSavePicker picker = pickerFactory.GetFileSavePicker(); - picker.SuggestedStartLocation = PickerLocationId.Desktop; - picker.SuggestedFileName = SelectedArchive.Uid; - picker.CommitButtonText = SH.FilePickerExportCommit; - picker.FileTypeChoices.Add(SH.ViewModelGachaLogExportFileType, ".json".Enumerate().ToList()); + Dictionary> fileTypes = new() + { + [SH.ViewModelGachaLogExportFileType] = ".json".Enumerate().ToList(), + }; + + FileSavePicker picker = pickerFactory.GetFileSavePicker( + PickerLocationId.Desktop, + $"{SelectedArchive.Uid}.json", + SH.FilePickerExportCommit, + fileTypes); (bool isPickerOk, ValueFile file) = await picker.TryPickSaveFileAsync().ConfigureAwait(false); if (isPickerOk) { UIGF uigf = await gachaLogService.ExportToUIGFAsync(SelectedArchive).ConfigureAwait(false); - bool isOk = await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false); - - if (isOk) + if (await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false)) { infoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage); } @@ -301,7 +308,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel await gachaLogService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false); // reselect first archive - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); SelectedArchive = Archives.FirstOrDefault(); } } @@ -316,7 +323,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel if (isOk) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); Archives?.AddIfNotContains(archive!); SetSelectedArchiveAndUpdateStatistics(archive, true); } @@ -364,7 +371,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel { GachaStatistics? temp = await gachaLogService.GetStatisticsAsync(archive).ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); Statistics = temp; IsInitialized = true; } @@ -379,19 +386,20 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel if (uigf.IsCurrentVersionSupported()) { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - if (await new GachaLogImportDialog(uigf).GetShouldImportAsync().ConfigureAwait(true)) + await taskContext.SwitchToMainThreadAsync(); + GachaLogImportDialog importDialog = serviceProvider.CreateInstance(uigf); + if (await importDialog.GetShouldImportAsync().ConfigureAwait(true)) { if (uigf.IsValidList()) { ContentDialog dialog = await contentDialogFactory.CreateForIndeterminateProgressAsync(SH.ViewModelGachaLogImportProgress).ConfigureAwait(true); - using (await dialog.BlockAsync().ConfigureAwait(false)) + using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { await gachaLogService.ImportFromUIGFAsync(uigf.List, uigf.Info.Uid).ConfigureAwait(false); } infoBarService.Success(SH.ViewModelGachaLogImportComplete); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); SetSelectedArchiveAndUpdateStatistics(gachaLogService.CurrentArchive, true); return true; } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModelSlim.cs index ebc37e7f..8186f4b6 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModelSlim.cs @@ -33,11 +33,12 @@ internal sealed class GachaLogViewModelSlim : Abstraction.ViewModelSlim(); + ITaskContext taskContext = scope.ServiceProvider.GetRequiredService(); if (await gachaLogService.InitializeAsync(default).ConfigureAwait(false)) { List list = await gachaLogService.GetStatisticsSlimsAsync().ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); StatisticsList = list; IsInitialized = true; } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs index c1f3d4d8..3f7617bd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs @@ -21,6 +21,7 @@ namespace Snap.Hutao.ViewModel.GachaLog; [Injection(InjectAs.Scoped)] internal sealed class HutaoCloudViewModel : Abstraction.ViewModel { + private readonly ITaskContext taskContext; private readonly IHutaoCloudService hutaoCloudService; private readonly IContentDialogFactory contentDialogFactory; private readonly IInfoBarService infoBarService; @@ -34,6 +35,7 @@ internal sealed class HutaoCloudViewModel : Abstraction.ViewModel /// 服务提供器 public HutaoCloudViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); hutaoCloudService = serviceProvider.GetRequiredService(); contentDialogFactory = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); @@ -87,7 +89,7 @@ internal sealed class HutaoCloudViewModel : Abstraction.ViewModel .CreateForIndeterminateProgressAsync(SH.ViewModelGachaLogRetrieveFromHutaoCloudProgress) .ConfigureAwait(false); - using (await dialog.BlockAsync().ConfigureAwait(false)) + using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { return await hutaoCloudService.RetrieveGachaItemsAsync(uid).ConfigureAwait(false); } @@ -98,7 +100,7 @@ internal sealed class HutaoCloudViewModel : Abstraction.ViewModel { await serviceProvider.GetRequiredService().InitializeAsync().ConfigureAwait(false); await RefreshUidCollectionAsync().ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); IsInitialized = true; } @@ -113,7 +115,7 @@ internal sealed class HutaoCloudViewModel : Abstraction.ViewModel bool isOk; string message; - using (await dialog.BlockAsync().ConfigureAwait(false)) + using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { (isOk, message) = await hutaoCloudService.UploadGachaItemsAsync(gachaArchive).ConfigureAwait(false); } @@ -152,7 +154,7 @@ internal sealed class HutaoCloudViewModel : Abstraction.ViewModel { Response> resp = await hutaoCloudService.GetUidsAsync().ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); if (Options.IsCloudServiceAllowed = resp.IsOk()) { Uids = resp.Data!.ToObservableCollection(); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 19f0295e..59d5cc57 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -33,6 +33,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel public const string DesiredUid = nameof(DesiredUid); private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly IGameService gameService; private readonly IMemoryCache memoryCache; @@ -49,6 +50,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel /// 服务提供器 public LaunchGameViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); gameService = serviceProvider.GetRequiredService(); memoryCache = serviceProvider.GetRequiredService(); Options = serviceProvider.GetRequiredService(); @@ -175,9 +177,9 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel infoBarService.Warning(string.Format(SH.ViewModelLaunchGameMultiChannelReadFail, multi.ConfigFilePath)); } - ObservableCollection accounts = await gameService.GameAccountCollection().ConfigureAwait(false); + ObservableCollection accounts = gameService.GameAccountCollection; - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); GameAccounts = accounts; // Sync uid @@ -197,7 +199,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel else { infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); await serviceProvider.GetRequiredService() .NavigateAsync(INavigationAwaiter.Default, true) .ConfigureAwait(false); @@ -206,7 +208,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel private async Task UpdateGameResourceAsync(LaunchScheme scheme) { - await ThreadHelper.SwitchToBackgroundAsync(); + await taskContext.SwitchToBackgroundAsync(); Web.Response.Response response = await serviceProvider .GetRequiredService() .GetResourceAsync(scheme) @@ -214,7 +216,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel if (response.IsOk()) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); GameResource = response.Data; } } @@ -230,10 +232,10 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel if (gameService.SetMultiChannel(SelectedScheme)) { // Channel changed, we need to change local file. - await ThreadHelper.SwitchToMainThreadAsync(); - LaunchGamePackageConvertDialog dialog = new(); + await taskContext.SwitchToMainThreadAsync(); + LaunchGamePackageConvertDialog dialog = serviceProvider.CreateInstance(); Progress progress = new(state => dialog.State = state.Clone()); - using (await dialog.BlockAsync().ConfigureAwait(false)) + using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { if (!await gameService.EnsureGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false)) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs index fec86d29..264ac97e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs @@ -16,6 +16,7 @@ namespace Snap.Hutao.ViewModel.Game; internal sealed class LaunchGameViewModelSlim : Abstraction.ViewModelSlim { private readonly IGameService gameService; + private readonly ITaskContext taskContext; private ObservableCollection? gameAccounts; private GameAccount? selectedGameAccount; @@ -28,6 +29,7 @@ internal sealed class LaunchGameViewModelSlim : Abstraction.ViewModelSlim(); + taskContext = serviceProvider.GetRequiredService(); LaunchCommand = new AsyncRelayCommand(LaunchAsync, AsyncRelayCommandOptions.AllowConcurrentExecutions); } @@ -50,8 +52,8 @@ internal sealed class LaunchGameViewModelSlim : Abstraction.ViewModelSlim protected override async Task OpenUIAsync() { - ObservableCollection accounts = await gameService.GameAccountCollection().ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + ObservableCollection accounts = gameService.GameAccountCollection; + await taskContext.SwitchToMainThreadAsync(); GameAccounts = accounts; // Try set to the current account. diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoPassportViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoPassportViewModel.cs index 8c010eeb..c6e4103a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoPassportViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoPassportViewModel.cs @@ -18,6 +18,7 @@ namespace Snap.Hutao.ViewModel; internal sealed class HutaoPassportViewModel : Abstraction.ViewModel { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly HomaPassportClient homaPassportClient; private string? userName; @@ -30,6 +31,7 @@ internal sealed class HutaoPassportViewModel : Abstraction.ViewModel /// 服务提供器 public HutaoPassportViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); homaPassportClient = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; @@ -105,7 +107,7 @@ internal sealed class HutaoPassportViewModel : Abstraction.ViewModel SaveUserNameAndPassword(); serviceProvider.GetRequiredService().Information(response.Message); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); serviceProvider.GetRequiredService().LoginSucceed(UserName, response.Data); await serviceProvider @@ -134,7 +136,7 @@ internal sealed class HutaoPassportViewModel : Abstraction.ViewModel SaveUserNameAndPassword(); serviceProvider.GetRequiredService().Information(response.Message); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); serviceProvider.GetRequiredService().LoginSucceed(UserName, response.Data); await serviceProvider @@ -158,7 +160,7 @@ internal sealed class HutaoPassportViewModel : Abstraction.ViewModel SaveUserNameAndPassword(); serviceProvider.GetRequiredService().Information(response.Message); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); serviceProvider.GetRequiredService().LoginSucceed(UserName, response.Data); await serviceProvider diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs index 1c9db33c..bde1dc7f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs @@ -34,6 +34,7 @@ namespace Snap.Hutao.ViewModel; internal sealed class SettingViewModel : Abstraction.ViewModel { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly AppDbContext appDbContext; private readonly IGameService gameService; private readonly ILogger logger; @@ -62,6 +63,7 @@ internal sealed class SettingViewModel : Abstraction.ViewModel /// 服务提供器 public SettingViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); appDbContext = serviceProvider.GetRequiredService(); gameService = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); @@ -215,7 +217,7 @@ internal sealed class SettingViewModel : Abstraction.ViewModel (bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false); if (isOk) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); Options.GamePath = path; } } @@ -254,8 +256,9 @@ internal sealed class SettingViewModel : Abstraction.ViewModel private async Task ShowSignInWebViewDialogAsync() { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - await new SignInWebViewDialog().ShowAsync().AsTask().ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + SignInWebViewDialog dialog = serviceProvider.CreateInstance(); + await dialog.ShowAsync(); } private async Task CheckUpdateAsync() diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs index d32be9a2..05fbbb9e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs @@ -26,6 +26,7 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss; internal sealed class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly ISpiralAbyssRecordService spiralAbyssRecordService; private readonly IMetadataService metadataService; private readonly IUserService userService; @@ -45,6 +46,7 @@ internal sealed class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecip /// 消息器 public SpiralAbyssRecordViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); spiralAbyssRecordService = serviceProvider.GetRequiredService(); metadataService = serviceProvider.GetRequiredService(); userService = serviceProvider.GetRequiredService(); @@ -117,7 +119,7 @@ internal sealed class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecip if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) { await UpdateSpiralAbyssCollectionAsync(userAndUid).ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); IsInitialized = true; } else @@ -143,7 +145,7 @@ internal sealed class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecip { } - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); SpiralAbyssEntries = temp; SelectedEntry = SpiralAbyssEntries?.FirstOrDefault(); } @@ -167,7 +169,7 @@ internal sealed class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecip { } - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); SelectedEntry = SpiralAbyssEntries.FirstOrDefault(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs index 082306cc..74392b0c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs @@ -14,6 +14,7 @@ namespace Snap.Hutao.ViewModel; [Injection(InjectAs.Scoped)] internal sealed class TestViewModel : Abstraction.ViewModel { + private readonly ITaskContext taskContext; private readonly IServiceProvider serviceProvider; /// @@ -22,6 +23,7 @@ internal sealed class TestViewModel : Abstraction.ViewModel /// 服务提供器 public TestViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; ShowCommunityGameRecordDialogCommand = new AsyncRelayCommand(ShowCommunityGameRecordDialogAsync); @@ -53,15 +55,15 @@ internal sealed class TestViewModel : Abstraction.ViewModel private async Task ShowCommunityGameRecordDialogAsync() { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - await new CommunityGameRecordDialog().ShowAsync(); + await taskContext.SwitchToMainThreadAsync(); + await serviceProvider.CreateInstance().ShowAsync(); } private async Task ShowAdoptCalculatorDialogAsync() { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - await new AdoptCalculatorDialog().ShowAsync(); + await taskContext.SwitchToMainThreadAsync(); + await serviceProvider.CreateInstance().ShowAsync(); } private void RestartApp(bool elevated) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs index 48e6c629..86d43cd4 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs @@ -24,8 +24,10 @@ namespace Snap.Hutao.ViewModel.User; internal sealed class UserViewModel : ObservableObject { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly IUserService userService; private readonly IInfoBarService infoBarService; + private readonly Core.HutaoOptions hutaoOptions; private User? selectedUser; private ObservableCollection? users; @@ -38,6 +40,8 @@ internal sealed class UserViewModel : ObservableObject /// 信息条服务 public UserViewModel(IServiceProvider serviceProvider) { + hutaoOptions = serviceProvider.GetRequiredService(); + taskContext = serviceProvider.GetRequiredService(); userService = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; @@ -130,7 +134,7 @@ internal sealed class UserViewModel : ObservableObject case UserOptionResult.Added: if (Users!.Count == 1) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); SelectedUser = Users.Single(); } @@ -176,10 +180,11 @@ internal sealed class UserViewModel : ObservableObject private async Task AddUserCoreAsync(bool isOversea) { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); // Get cookie from user input - ValueResult result = await new UserDialog().GetInputCookieAsync().ConfigureAwait(false); + UserDialog dialog = serviceProvider.CreateInstance(); + ValueResult result = await dialog.GetInputCookieAsync().ConfigureAwait(false); // User confirms the input if (result.IsOk) @@ -194,9 +199,11 @@ internal sealed class UserViewModel : ObservableObject private void LoginMihoyoUser() { - if (Core.WebView2Helper.IsSupported) + if (hutaoOptions.IsWebView2Supported) { - serviceProvider.GetRequiredService().Navigate(INavigationAwaiter.Default); + serviceProvider + .GetRequiredService() + .Navigate(INavigationAwaiter.Default); } else { @@ -209,9 +216,11 @@ internal sealed class UserViewModel : ObservableObject /// private void LoginHoyoverseUser() { - if (Core.WebView2Helper.IsSupported) + if (hutaoOptions.IsWebView2Supported) { - serviceProvider.GetRequiredService().Navigate(INavigationAwaiter.Default); + serviceProvider + .GetRequiredService() + .Navigate(INavigationAwaiter.Default); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs index d226b73a..edc1ba27 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs @@ -27,6 +27,7 @@ namespace Snap.Hutao.ViewModel; internal sealed class WelcomeViewModel : ObservableObject { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private ObservableCollection? downloadSummaries; @@ -36,7 +37,9 @@ internal sealed class WelcomeViewModel : ObservableObject /// 服务提供器 public WelcomeViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; + OpenUICommand = new AsyncRelayCommand(OpenUIAsync); } @@ -60,7 +63,7 @@ internal sealed class WelcomeViewModel : ObservableObject { if (await summary.DownloadAndExtractAsync().ConfigureAwait(false)) { - ThreadHelper.InvokeOnMainThread(() => DownloadSummaries.Remove(summary)); + taskContext.InvokeOnMainThread(() => DownloadSummaries.Remove(summary)); } }).ConfigureAwait(true); @@ -131,6 +134,7 @@ internal sealed class WelcomeViewModel : ObservableObject internal sealed class DownloadSummary : ObservableObject, IEquatable { private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly HttpClient httpClient; private readonly string fileName; private readonly string fileUrl; @@ -146,6 +150,7 @@ internal sealed class WelcomeViewModel : ObservableObject /// 压缩文件名称 public DownloadSummary(IServiceProvider serviceProvider, string fileName) { + taskContext = serviceProvider.GetRequiredService(); httpClient = serviceProvider.GetRequiredService(); HutaoOptions hutaoOptions = serviceProvider.GetRequiredService(); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent); @@ -199,7 +204,7 @@ internal sealed class WelcomeViewModel : ObservableObject await new StreamCopyWorker(content, temp, contentLength).CopyAsync(progress).ConfigureAwait(false); ExtractFiles(temp); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); ProgressValue = 1; Description = SH.ViewModelWelcomeDownloadSummaryComplete; return true; @@ -209,7 +214,7 @@ internal sealed class WelcomeViewModel : ObservableObject catch (Exception ex) { logger.LogError(ex, "Download Static Zip failed"); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); Description = SH.ViewModelWelcomeDownloadSummaryException; return false; } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs index 2ba55ca7..320250fe 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs @@ -36,6 +36,7 @@ namespace Snap.Hutao.ViewModel.Wiki; [Injection(InjectAs.Scoped)] internal sealed class WikiAvatarViewModel : Abstraction.ViewModel { + private readonly ITaskContext taskContext; private readonly IServiceProvider serviceProvider; private readonly IMetadataService metadataService; private readonly IHutaoCache hutaoCache; @@ -53,6 +54,7 @@ internal sealed class WikiAvatarViewModel : Abstraction.ViewModel /// 服务提供器 public WikiAvatarViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); metadataService = serviceProvider.GetRequiredService(); hutaoCache = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; @@ -117,7 +119,7 @@ internal sealed class WikiAvatarViewModel : Abstraction.ViewModel await CombineComplexDataAsync(sorted, idMaterialMap).ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); Avatars = new AdvancedCollectionView(sorted, true); Selected = Avatars.Cast().FirstOrDefault(); } @@ -148,10 +150,9 @@ internal sealed class WikiAvatarViewModel : Abstraction.ViewModel if (userService.Current != null) { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - (bool isOk, CalcAvatarPromotionDelta delta) = await new CultivatePromotionDeltaDialog(avatar.ToCalculable(), null) - .GetPromotionDeltaAsync() - .ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance(avatar.ToCalculable()); + (bool isOk, CalcAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); if (isOk) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiMonsterViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiMonsterViewModel.cs index f35f39ee..8e8a5c8d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiMonsterViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiMonsterViewModel.cs @@ -18,6 +18,7 @@ namespace Snap.Hutao.ViewModel.Wiki; internal class WikiMonsterViewModel : Abstraction.ViewModel { private readonly IMetadataService metadataService; + private readonly ITaskContext taskContext; private AdvancedCollectionView? monsters; private Monster? selected; @@ -31,6 +32,7 @@ internal class WikiMonsterViewModel : Abstraction.ViewModel /// 服务提供器 public WikiMonsterViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); metadataService = serviceProvider.GetRequiredService(); } @@ -78,7 +80,7 @@ internal class WikiMonsterViewModel : Abstraction.ViewModel } List ordered = monsters.OrderBy(m => m.Id.Value).ToList(); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); Monsters = new AdvancedCollectionView(ordered, true); Selected = Monsters.Cast().FirstOrDefault(); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs index b3927daa..d29be90e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs @@ -38,6 +38,7 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel 11419, 11420, 11421, // 「一心传」名刀 }; + private readonly ITaskContext taskContext; private readonly IServiceProvider serviceProvider; private readonly IMetadataService metadataService; private readonly IHutaoCache hutaoCache; @@ -55,6 +56,7 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel /// 服务提供器 public WikiWeaponViewModel(IServiceProvider serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); metadataService = serviceProvider.GetRequiredService(); hutaoCache = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; @@ -120,7 +122,7 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel await CombineWithWeaponCollocationsAsync(sorted).ConfigureAwait(false); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); Weapons = new AdvancedCollectionView(sorted, true); Selected = Weapons.Cast().FirstOrDefault(); @@ -146,10 +148,9 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel if (userService.Current != null) { // ContentDialog must be created by main thread. - await ThreadHelper.SwitchToMainThreadAsync(); - (bool isOk, CalcAvatarPromotionDelta delta) = await new CultivatePromotionDeltaDialog(null, weapon.ToCalculable()) - .GetPromotionDeltaAsync() - .ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance(weapon.ToCalculable()); + (bool isOk, CalcAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); if (isOk) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs index 4bc55b5e..e0357f14 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs @@ -39,6 +39,7 @@ internal class MiHoYoJSInterface private readonly CoreWebView2 webView; private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; private readonly ILogger logger; private readonly SemaphoreSlim webMessageSemaphore = new(1); @@ -50,6 +51,7 @@ internal class MiHoYoJSInterface public MiHoYoJSInterface(CoreWebView2 webView, IServiceProvider serviceProvider) { this.webView = webView; + taskContext = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; logger = serviceProvider.GetRequiredService>(); @@ -204,7 +206,7 @@ internal class MiHoYoJSInterface await userService.RefreshCookieTokenAsync(user).ConfigureAwait(false); } - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); webView.SetCookie(user.CookieToken, user.LToken); return new() { Data = new() { [Cookie.COOKIE_TOKEN] = user.CookieToken![Cookie.COOKIE_TOKEN] } }; } @@ -216,7 +218,7 @@ internal class MiHoYoJSInterface /// 响应 public virtual async Task ClosePageAsync(JsParam param) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); if (webView.CanGoBack) { webView.GoBack(); @@ -251,7 +253,7 @@ internal class MiHoYoJSInterface public virtual async Task PushPageAsync(JsParam param) { - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); webView.Navigate(param.Payload.Page); return null; } @@ -340,7 +342,7 @@ internal class MiHoYoJSInterface logger?.LogInformation("[ExecuteScript: {callback}]\n{payload}", callback, payload); - await ThreadHelper.SwitchToMainThreadAsync(); + await taskContext.SwitchToMainThreadAsync(); try { return await webView.ExecuteScriptAsync(js); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs index 03713791..66caa4e6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Web.Request.QueryString; + namespace Snap.Hutao.Web.Hoyolab; /// @@ -58,6 +60,19 @@ internal readonly struct PlayerUid return Value; } + /// + /// 转换到查询字符串 + /// + /// 查询字符串 + public QueryString ToQueryString() + { + QueryString queryString = new(); + queryString.Set("role_id", Value); + queryString.Set("server", Region); + + return queryString; + } + private static string EvaluateRegion(char first) { return first switch