diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml b/src/Snap.Hutao/Snap.Hutao/App.xaml index 12746e37..070d47c5 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml @@ -21,6 +21,8 @@ 6,16,16,16 16,0,0,0 + 16 + 6 6,6,0,0 0,6,6,0 diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs index 5febbd59..106a9b11 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs @@ -5,7 +5,6 @@ using CommunityToolkit.WinUI.UI.Controls; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Snap.Hutao.Core.Caching; -using Snap.Hutao.Core.Exception; using Snap.Hutao.Extension; using System.Runtime.InteropServices; using Windows.Storage; @@ -34,21 +33,21 @@ public class CachedImage : ImageEx try { Verify.Operation(imageUri.Host != string.Empty, "无效的Uri"); - StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri); + StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // check token state to determine whether the operation should be canceled. - Must.ThrowOnCanceled(token, "Image source has changed."); + token.ThrowIfCancellationRequested(); // BitmapImage initialize with a uri will increase image quality and loading speed. return new BitmapImage(new(file.Path)); } - catch (COMException ex) when (ex.Is(COMError.WINCODEC_ERR_COMPONENTNOTFOUND)) + catch (COMException) { // The image is corrupted, remove it. await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false); return null; } - catch (TaskCanceledException) + catch (OperationCanceledException) { // task was explicitly canceled return null; diff --git a/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs b/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs index df2575db..f1d37b92 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; +using Snap.Hutao.Service.Navigation; namespace Snap.Hutao.Control; @@ -39,6 +40,21 @@ public class ScopedPage : Page DataContext = viewModel; } + /// + /// 异步通知接收器 + /// + /// 额外内容 + /// 任务 + public async Task NotifyRecipentAsync(INavigationData extra) + { + if (extra.Data != null && DataContext is INavigationRecipient recipient) + { + await recipient.ReceiveAsync(extra).ConfigureAwait(false); + } + + extra.NotifyNavigationCompleted(); + } + /// protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { @@ -48,4 +64,16 @@ public class ScopedPage : Page // Try dispose scope when page is not presented serviceScope.Dispose(); } + + /// + [SuppressMessage("", "VSTHRD100")] + protected override async void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + if (e.Parameter is INavigationData extra) + { + await NotifyRecipentAsync(extra).ConfigureAwait(false); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/INamed.cs b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/INamed.cs index b3071587..dbc094d8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/INamed.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/INamed.cs @@ -5,6 +5,7 @@ namespace Snap.Hutao.Core.Abstraction; /// /// 有名称的对象 +/// 指示该对象可通过名称区分 /// internal interface INamed { diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Convert/Md5Convert.cs b/src/Snap.Hutao/Snap.Hutao/Core/Convert/Md5Convert.cs index de4a22d7..f52ee927 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Convert/Md5Convert.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Convert/Md5Convert.cs @@ -18,8 +18,7 @@ internal abstract class Md5Convert /// 计算的结果 public static string ToHexString(string source) { - byte[] bytes = Encoding.UTF8.GetBytes(source); - byte[] hash = MD5.HashData(bytes); + byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source)); return System.Convert.ToHexString(hash); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs index 9863f0fa..b7aeba22 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs @@ -12,15 +12,21 @@ namespace Snap.Hutao.Core; internal static class CoreEnvironment { /// - /// 动态密钥的盐 + /// 动态密钥1的盐 /// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd /// - public const string DynamicSecretSalt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"; + public const string DynamicSecret1Salt = "n0KjuIrKgLHh08LWSCYP0WXlVXaYvV64"; + + /// + /// 动态密钥2的盐 + /// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd + /// + public const string DynamicSecret2Salt = "YVEIkzDFNHLeKXLxzqCA9TzxCpWwbIbk"; /// /// 米游社请求UA /// - public const string HoyolabUA = $"miHoYoBBS/{HoyolabXrpcVersion}"; + public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}"; /// /// 米游社 Rpc 版本 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs new file mode 100644 index 00000000..fa9ba271 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs @@ -0,0 +1,28 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Threading; +using Windows.ApplicationModel.DataTransfer; + +namespace Snap.Hutao.Core.IO.DataTransfer; + +/// +/// 剪贴板 +/// +internal static class Clipboard +{ + /// + /// 从剪贴板文本中反序列化 + /// + /// 目标类型 + /// Json序列化选项 + /// 实例 + public static async Task DeserializeTextAsync(JsonSerializerOptions options) + where T : class + { + await ThreadHelper.SwitchToMainThreadAsync(); + DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent(); + string json = await view.GetTextAsync(); + return JsonSerializer.Deserialize(json, options); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs index b21e9bc5..86741bb3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs @@ -45,6 +45,11 @@ internal static class EventIds /// Xaml绑定错误 /// public static readonly EventId UnobservedTaskException = 100006; + + /// + /// Xaml绑定错误 + /// + public static readonly EventId HttpException = 100007; #endregion #region 服务 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Validation/Must.cs b/src/Snap.Hutao/Snap.Hutao/Core/Validation/Must.cs index 057f6089..09a31df1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Validation/Must.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Validation/Must.cs @@ -103,19 +103,4 @@ public static class Must return value; } - - /// - /// 尝试抛出任务取消异常 - /// - /// 取消令牌 - /// 取消消息 - /// 任务被取消 - [SuppressMessage("", "CA1068")] - public static void ThrowOnCanceled(CancellationToken token, string message) - { - if (token.IsCancellationRequested) - { - throw new TaskCanceledException("Image source has changed."); - } - } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs index a4563e53..e44049ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs @@ -55,10 +55,11 @@ public class SystemBackdrop configuration.IsInputActive = true; SetConfigurationSourceTheme(configuration); - backdropController = new(); - - // Mica Alt - backdropController.Kind = MicaKind.BaseAlt; + backdropController = new() + { + // Mica Alt + Kind = MicaKind.BaseAlt + }; backdropController.AddSystemBackdropTarget(window.As()); backdropController.SetSystemBackdropConfiguration(configuration); diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs index 462d997e..900346d5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using System.Runtime.InteropServices; + namespace Snap.Hutao.Extension; /// @@ -8,6 +10,26 @@ namespace Snap.Hutao.Extension; /// public static partial class EnumerableExtensions { + /// + public static double AverageNoThrow(this List source) + { + Span span = CollectionsMarshal.AsSpan(source); + + if (span.IsEmpty) + { + return 0; + } + + long sum = 0; + + for (int i = 0; i < span.Length; i++) + { + sum += span[i]; + } + + return (double)sum / span.Length; + } + /// /// 计数 /// @@ -76,6 +98,26 @@ public static partial class EnumerableExtensions return source.FirstOrDefault(predicate) ?? source.FirstOrDefault(); } + /// + /// 获取值或默认值 + /// + /// 键类型 + /// 值类型 + /// 字典 + /// 键 + /// 默认值 + /// 结果值 + public static TValue GetValueOrDefault(this IDictionary dictionary, TKey key, TValue defaultValue = default!) + where TKey : notnull + { + if (dictionary.TryGetValue(key, out TValue? value)) + { + return value; + } + + return defaultValue; + } + /// /// 移除表中首个满足条件的项 /// @@ -97,6 +139,20 @@ public static partial class EnumerableExtensions return false; } + /// + public static Dictionary ToDictionaryOverride(this IEnumerable source, Func keySelector) + where TKey : notnull + { + Dictionary dictionary = new(); + + foreach (TSource value in source) + { + dictionary[keySelector(value)] = value; + } + + return dictionary; + } + /// /// 表示一个对 类型的计数器 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs new file mode 100644 index 00000000..c9762dee --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.IO; + +namespace Snap.Hutao.Extension; + +/// +/// 数高性能扩展 +/// +public static class NumberExtensions +{ + /// + /// 计算给定整数的位数 + /// + /// 给定的整数 + /// 位数 + public static int Place(this int x) + { + // Benchmarked and compared as a most optimized solution + return (int)(MathF.Log10(x) + 1); + } + + /// + /// 计算给定整数的位数 + /// + /// 给定的整数 + /// 位数 + public static int Place(this long x) + { + // Benchmarked and compared as a most optimized solution + return (int)(MathF.Log10(x) + 1); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20220914131149_AddGachaQueryType.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20220914131149_AddGachaQueryType.Designer.cs new file mode 100644 index 00000000..cab88401 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20220914131149_AddGachaQueryType.Designer.cs @@ -0,0 +1,171 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Context.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220914131149_AddGachaQueryType")] + partial class AddGachaQueryType + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.8"); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("GachaArchives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("GachaType") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("QueryType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("GachaItems"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Cookie") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20220914131149_AddGachaQueryType.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20220914131149_AddGachaQueryType.cs new file mode 100644 index 00000000..d58aa6f9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20220914131149_AddGachaQueryType.cs @@ -0,0 +1,27 @@ +// +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + public partial class AddGachaQueryType : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "QueryType", + table: "GachaItems", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "QueryType", + table: "GachaItems"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20220918062300_RenameGachaTable.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20220918062300_RenameGachaTable.Designer.cs new file mode 100644 index 00000000..ac413e07 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20220918062300_RenameGachaTable.Designer.cs @@ -0,0 +1,171 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Context.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220918062300_RenameGachaTable")] + partial class RenameGachaTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("gacha_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("GachaType") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("QueryType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("gacha_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Cookie") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20220918062300_RenameGachaTable.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20220918062300_RenameGachaTable.cs new file mode 100644 index 00000000..b7a0d736 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20220918062300_RenameGachaTable.cs @@ -0,0 +1,102 @@ +// +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + public partial class RenameGachaTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_GachaItems_GachaArchives_ArchiveId", + table: "GachaItems"); + + migrationBuilder.DropPrimaryKey( + name: "PK_GachaItems", + table: "GachaItems"); + + migrationBuilder.DropPrimaryKey( + name: "PK_GachaArchives", + table: "GachaArchives"); + + migrationBuilder.RenameTable( + name: "GachaItems", + newName: "gacha_items"); + + migrationBuilder.RenameTable( + name: "GachaArchives", + newName: "gacha_archives"); + + migrationBuilder.RenameIndex( + name: "IX_GachaItems_ArchiveId", + table: "gacha_items", + newName: "IX_gacha_items_ArchiveId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_gacha_items", + table: "gacha_items", + column: "InnerId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_gacha_archives", + table: "gacha_archives", + column: "InnerId"); + + migrationBuilder.AddForeignKey( + name: "FK_gacha_items_gacha_archives_ArchiveId", + table: "gacha_items", + column: "ArchiveId", + principalTable: "gacha_archives", + principalColumn: "InnerId", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_gacha_items_gacha_archives_ArchiveId", + table: "gacha_items"); + + migrationBuilder.DropPrimaryKey( + name: "PK_gacha_items", + table: "gacha_items"); + + migrationBuilder.DropPrimaryKey( + name: "PK_gacha_archives", + table: "gacha_archives"); + + migrationBuilder.RenameTable( + name: "gacha_items", + newName: "GachaItems"); + + migrationBuilder.RenameTable( + name: "gacha_archives", + newName: "GachaArchives"); + + migrationBuilder.RenameIndex( + name: "IX_gacha_items_ArchiveId", + table: "GachaItems", + newName: "IX_GachaItems_ArchiveId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_GachaItems", + table: "GachaItems", + column: "InnerId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_GachaArchives", + table: "GachaArchives", + column: "InnerId"); + + migrationBuilder.AddForeignKey( + name: "FK_GachaItems_GachaArchives_ArchiveId", + table: "GachaItems", + column: "ArchiveId", + principalTable: "GachaArchives", + principalColumn: "InnerId", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index b5b7f27d..6ece9b88 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.8"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => { @@ -78,7 +78,7 @@ namespace Snap.Hutao.Migrations b.HasKey("InnerId"); - b.ToTable("GachaArchives"); + b.ToTable("gacha_archives"); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => @@ -99,6 +99,9 @@ namespace Snap.Hutao.Migrations b.Property("ItemId") .HasColumnType("INTEGER"); + b.Property("QueryType") + .HasColumnType("INTEGER"); + b.Property("Time") .HasColumnType("TEXT"); @@ -106,7 +109,7 @@ namespace Snap.Hutao.Migrations b.HasIndex("ArchiveId"); - b.ToTable("GachaItems"); + b.ToTable("gacha_items"); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Annotation/DescriptionAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Model/Annotation/DescriptionAttribute.cs deleted file mode 100644 index 9b73569c..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Model/Annotation/DescriptionAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Model.Annotation; - -/// -/// 枚举的文本描述特性 -/// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] -internal class DescriptionAttribute : Attribute -{ - /// - /// 构造一个新的枚举的文本描述特性 - /// - /// 描述 - public DescriptionAttribute(string description) - { - Description = description; - } - - /// - /// 获取文本描述 - /// - public string Description { get; init; } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/Abstraction/ItemBase.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/Abstraction/ItemBase.cs new file mode 100644 index 00000000..37220ba0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/Abstraction/ItemBase.cs @@ -0,0 +1,32 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.Model.Binding.Gacha.Abstraction; + +/// +/// 物品基类 +/// +public class ItemBase +{ + /// + /// 物品名称 + /// + public string Name { get; set; } = default!; + + /// + /// 主图标 + /// + public Uri Icon { get; set; } = default!; + + /// + /// 小图标 + /// + public Uri Badge { get; set; } = default!; + + /// + /// 星级 + /// + public ItemQuality Quality { get; set; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/Abstraction/WishBase.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/Abstraction/WishBase.cs new file mode 100644 index 00000000..9aae3c1c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/Abstraction/WishBase.cs @@ -0,0 +1,46 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.Gacha.Abstraction; + +/// +/// 祈愿基类 +/// +public abstract class WishBase +{ + /// + /// 卡池名称 + /// + public string Name { get; set; } = default!; + + /// + /// 统计开始时间 + /// + public DateTimeOffset From { get; set; } + + /// + /// 统计结束时间 + /// + public DateTimeOffset To { get; set; } + + /// + /// 统计开始时间 + /// + public string FromFormatted + { + get => $"{From:yyyy.MM.dd}"; + } + + /// + /// 统计开始时间 + /// + public string ToFormatted + { + get => $"{To:yyyy.MM.dd}"; + } + + /// + /// 总数 + /// + public int TotalCount { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/GachaStatistics.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/GachaStatistics.cs new file mode 100644 index 00000000..9ae4b1d0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/GachaStatistics.cs @@ -0,0 +1,60 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.Gacha; + +/// +/// 祈愿统计 +/// +public class GachaStatistics +{ + /// + /// 默认的空祈愿统计 + /// + public static readonly GachaStatistics Default = new(); + + /// + /// 角色活动 + /// + public TypedWishSummary AvatarWish { get; set; } = default!; + + /// + /// 神铸赋形 + /// + public TypedWishSummary WeaponWish { get; set; } = default!; + + /// + /// 奔行世间 + /// + public TypedWishSummary PermanentWish { get; set; } = default!; + + /// + /// 历史 + /// + public List HistoryWishes { get; set; } = default!; + + /// + /// 五星角色 + /// + public List OrangeAvatars { get; set; } = default!; + + /// + /// 四星角色 + /// + public List PurpleAvatars { get; set; } = default!; + + /// + /// 五星武器 + /// + public List OrangeWeapons { get; set; } = default!; + + /// + /// 四星武器 + /// + public List PurpleWeapons { get; set; } = default!; + + /// + /// 三星武器 + /// + public List BlueWeapons { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/HistoryWish.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/HistoryWish.cs new file mode 100644 index 00000000..c6a33168 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/HistoryWish.cs @@ -0,0 +1,38 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha.Abstraction; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +namespace Snap.Hutao.Model.Binding.Gacha; + +/// +/// 历史卡池概览 +/// +public class HistoryWish : WishBase +{ + /// + /// 五星Up + /// + public List OrangeUpList { get; set; } = default!; + + /// + /// 四星Up + /// + public List PurpleUpList { get; set; } = default!; + + /// + /// 五星Up + /// + public List OrangeList { get; set; } = default!; + + /// + /// 四星Up + /// + public List PurpleList { get; set; } = default!; + + /// + /// 三星Up + /// + public List BlueList { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/StatisticsItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/StatisticsItem.cs new file mode 100644 index 00000000..52e6acd9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/StatisticsItem.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha.Abstraction; + +namespace Snap.Hutao.Model.Binding.Gacha; + +/// +/// 历史物品 +/// +public class StatisticsItem : ItemBase +{ + /// + /// 获取物品的个数 + /// + public int Count { get; set; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/SummaryItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/SummaryItem.cs new file mode 100644 index 00000000..a3442e58 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/SummaryItem.cs @@ -0,0 +1,45 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha.Abstraction; + +namespace Snap.Hutao.Model.Binding.Gacha; + +/// +/// 祈愿卡池列表物品 +/// +public class SummaryItem : ItemBase +{ + /// + /// 据上次 + /// + public int LastPull { get; set; } + + /// + /// 是否为Up物品 + /// + public bool IsUp { get; set; } + + /// + /// 是否为大保底 + /// + public bool IsGuarentee { get; set; } + + /// + /// 获取时间 + /// + public DateTimeOffset Time { get; set; } + + /// + /// 获取时间 + /// + public string TimeFormatted + { + get => $"{Time:yyy.MM.dd HH:mm:ss}"; + } + + /// + /// 颜色 + /// + public Windows.UI.Color Color { get; set; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/TypedWishSummary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/TypedWishSummary.cs new file mode 100644 index 00000000..3e89c7e1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/TypedWishSummary.cs @@ -0,0 +1,143 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha.Abstraction; + +namespace Snap.Hutao.Model.Binding.Gacha; + +/// +/// 类型化的祈愿概览 +/// +public class TypedWishSummary : WishBase +{ + /// + /// 最大五星抽数 + /// + public int MaxOrangePull { get; set; } + + /// + /// 最大五星抽数 + /// + public string MaxOrangePullFormatted + { + get => $"最非 {MaxOrangePull} 抽"; + } + + /// + /// 最小五星抽数 + /// + public int MinOrangePull { get; set; } + + /// + /// 最大五星抽数 + /// + public string MinOrangePullFormatted + { + get => $"最欧 {MinOrangePull} 抽"; + } + + /// + /// 据上个五星抽数 + /// + public int LastOrangePull { get; set; } + + /// + /// 五星保底阈值 + /// + public int GuarenteeOrangeThreshold { get; set; } + + /// + /// 据上个四星抽数 + /// + public int LastPurplePull { get; set; } + + /// + /// 四星保底阈值 + /// + public int GuarenteePurpleThreshold { get; set; } + + /// + /// 五星总数 + /// + public int TotalOrangePull { get; set; } + + /// + /// 五星总百分比 + /// + public double TotalOrangePercent { get; set; } + + /// + /// 五星格式化字符串 + /// + public string TotalOrangeFormatted + { + get => $"{TotalOrangePull} [{TotalOrangePercent,6:p2}]"; + } + + /// + /// 四星总数 + /// + public int TotalPurplePull { get; set; } + + /// + /// 四星总百分比 + /// + public double TotalPurplePercent { get; set; } + + /// + /// 四星格式化字符串 + /// + public string TotalPurpleFormatted + { + get => $"{TotalPurplePull} [{TotalPurplePercent,6:p2}]"; + } + + /// + /// 三星总数 + /// + public int TotalBluePull { get; set; } + + /// + /// 三星总百分比 + /// + public double TotalBluePercent { get; set; } + + /// + /// 三星格式化字符串 + /// + public string TotalBlueFormatted + { + get => $"{TotalBluePull} [{TotalBluePercent,6:p2}]"; + } + + /// + /// 平均五星抽数 + /// + public double AverageOrangePull { get; set; } + + /// + /// 平均五星抽数 + /// + public string AverageOrangePullFormatted + { + get => $"{AverageOrangePull:f2} 抽"; + } + + /// + /// 平均Up五星抽数 + /// + public double AverageUpOrangePull { get; set; } + + /// + /// 平均Up五星抽数 + /// + public string AverageUpOrangePullFormatted + { + get => $"{AverageUpOrangePull:f2} 抽"; + } + + /// + /// 五星列表 + /// + public List OrangeList { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/GachaStatistics.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/GachaStatistics.cs deleted file mode 100644 index 0145b50d..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/GachaStatistics.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Model.Binding; - -/// -/// 祈愿统计 -/// -public class GachaStatistics -{ -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs index 020d2135..f1436284 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs @@ -10,6 +10,7 @@ namespace Snap.Hutao.Model.Entity; /// /// 祈愿记录存档 /// +[Table("gacha_archives")] public class GachaArchive : ISelectable { /// @@ -26,4 +27,14 @@ public class GachaArchive : ISelectable /// public bool IsSelected { get; set; } + + /// + /// 构造一个新的卡池存档 + /// + /// uid + /// 新的卡池存档 + public static GachaArchive Create(string uid) + { + return new() { Uid = uid }; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaItem.cs index be9d1853..76f42e65 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaItem.cs @@ -10,6 +10,7 @@ namespace Snap.Hutao.Model.Entity; /// /// 抽卡记录物品 /// +[Table("gacha_items")] public class GachaItem { /// @@ -34,6 +35,13 @@ public class GachaItem /// public GachaConfigType GachaType { get; set; } + /// + /// 祈愿记录查询分类 + /// 合并保底的卡池使用此属性 + /// 仅4种(不含400) + /// + public GachaConfigType QueryType { get; set; } + /// /// 物品Id /// @@ -48,4 +56,38 @@ public class GachaItem /// 物品 /// public long Id { get; set; } + + /// + /// 构造一个新的数据库祈愿物品 + /// + /// 存档Id + /// 祈愿物品 + /// 物品Id + /// 新的祈愿物品 + public static GachaItem Create(Guid archiveId, GachaLogItem item, int itemId) + { + return new() + { + ArchiveId = archiveId, + GachaType = item.GachaType, + QueryType = ToQueryType(item.GachaType), + ItemId = itemId, + Time = item.Time, + Id = item.Id, + }; + } + + /// + /// 将祈愿配置类型转换到祈愿查询类型 + /// + /// 配置类型 + /// 祈愿查询类型 + public static GachaConfigType ToQueryType(GachaConfigType configType) + { + return configType switch + { + GachaConfigType.AvatarEventWish2 => GachaConfigType.AvatarEventWish, + _ => configType, + }; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs index 724bef79..ebe6fd8c 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; /// 武器类型 /// https://github.com/Grasscutters/Grasscutter/blob/development/src/main/java/emu/grasscutter/game/props/WeaponType.java /// +[SuppressMessage("", "SA1124")] public enum WeaponType { /// @@ -19,6 +20,7 @@ public enum WeaponType /// WEAPON_SWORD_ONE_HAND = 1, + #region Not Used /// /// ? /// @@ -66,6 +68,7 @@ public enum WeaponType /// [Obsolete("尚未发现使用")] WEAPON_SHIELD_SMALL = 9, + #endregion /// /// 法器 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IStatisticsItemSource.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IStatisticsItemSource.cs new file mode 100644 index 00000000..272b8c0c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IStatisticsItemSource.cs @@ -0,0 +1,19 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha; + +namespace Snap.Hutao.Model.Metadata.Abstraction; + +/// +/// 指示该类为统计物品的源 +/// +public interface IStatisticsItemSource +{ + /// + /// 转换到统计物品 + /// + /// 个数 + /// 统计物品 + StatisticsItem ToStatisticsItem(int count); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs index 82488c97..440e6a70 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs @@ -1,14 +1,18 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Binding.Gacha; +using Snap.Hutao.Model.Binding.Gacha.Abstraction; using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Abstraction; +using Snap.Hutao.Model.Metadata.Converter; namespace Snap.Hutao.Model.Metadata.Avatar; /// /// 角色 /// -public class Avatar +public class Avatar : IStatisticsItemSource { /// /// Id @@ -71,7 +75,7 @@ public class Avatar public SkillDepot SkillDepot { get; set; } = default!; /// - /// 好感信息 + /// 好感信息/基本信息 /// public FetterInfo FetterInfo { get; set; } = default!; @@ -79,4 +83,57 @@ public class Avatar /// 皮肤 /// public IEnumerable Costumes { get; set; } = default!; + + /// + /// 转换为基础物品 + /// + /// 基础物品 + public ItemBase ToItemBase() + { + return new() + { + Name = Name, + Icon = AvatarIconConverter.NameToUri(Icon), + Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore), + Quality = Quality, + }; + } + + /// + /// 转换到统计物品 + /// + /// 个数 + /// 统计物品 + public StatisticsItem ToStatisticsItem(int count) + { + return new() + { + Name = Name, + Icon = AvatarIconConverter.NameToUri(Icon), + Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore), + Quality = Quality, + Count = count, + }; + } + + /// + /// 转换到简述统计物品 + /// + /// 距上个五星 + /// 时间 + /// 是否为Up物品 + /// 简述统计物品 + public SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp) + { + return new() + { + Name = Name, + Icon = AvatarIconConverter.NameToUri(Icon), + Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore), + Quality = Quality, + Time = time, + LastPull = lastPull, + IsUp = isUp, + }; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarIconConverter.cs index 07178fbe..b83951b1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarIconConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarIconConverter.cs @@ -12,10 +12,20 @@ internal class AvatarIconConverter : IValueConverter { private const string BaseUrl = "https://static.snapgenshin.com/AvatarIcon/{0}.png"; + /// + /// 名称转Uri + /// + /// 名称 + /// 链接 + public static Uri NameToUri(string name) + { + return new Uri(string.Format(BaseUrl, name)); + } + /// public object Convert(object value, Type targetType, object parameter, string language) { - return new Uri(string.Format(BaseUrl, value)); + return NameToUri((string)value); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ElementNameIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ElementNameIconConverter.cs index 95c34619..31ec2db3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ElementNameIconConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ElementNameIconConverter.cs @@ -12,10 +12,14 @@ internal class ElementNameIconConverter : IValueConverter { private const string BaseUrl = "https://static.snapgenshin.com/IconElement/UI_Icon_Element_{0}.png"; - /// - public object Convert(object value, Type targetType, object parameter, string language) + /// + /// 将中文元素名称转换为图标链接 + /// + /// 元素名称 + /// 图标链接 + public static Uri ElementNameToIconUri(string elementName) { - string element = (string)value switch + string element = elementName switch { "雷" => "Electric", "火" => "Fire", @@ -30,6 +34,12 @@ internal class ElementNameIconConverter : IValueConverter return new Uri(string.Format(BaseUrl, element)); } + /// + public object Convert(object value, Type targetType, object parameter, string language) + { + return ElementNameToIconUri((string)value); + } + /// public object ConvertBack(object value, Type targetType, object parameter, string language) { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs new file mode 100644 index 00000000..2f801bf3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs @@ -0,0 +1,36 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Data; + +namespace Snap.Hutao.Model.Metadata.Converter; + +/// +/// 武器图片转换器 +/// +internal class EquipIconConverter : IValueConverter +{ + private const string BaseUrl = "https://static.snapgenshin.com/EquipIcon/{0}.png"; + + /// + /// 名称转Uri + /// + /// 名称 + /// 链接 + public static Uri NameToUri(string name) + { + return new Uri(string.Format(BaseUrl, name)); + } + + /// + public object Convert(object value, Type targetType, object parameter, string language) + { + return NameToUri((string)value); + } + + /// + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw Must.NeverHappen(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs index 8f25be85..2d6afcba 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs @@ -13,10 +13,14 @@ internal class WeaponTypeIconConverter : IValueConverter { private const string BaseUrl = "https://static.snapgenshin.com/Skill/Skill_A_{0}.png"; - /// - public object Convert(object value, Type targetType, object parameter, string language) + /// + /// 将武器类型转换为图标链接 + /// + /// 武器类型 + /// 图标链接 + public static Uri WeaponTypeToIconUri(WeaponType type) { - string element = (WeaponType)value switch + string element = type switch { WeaponType.WEAPON_SWORD_ONE_HAND => "01", WeaponType.WEAPON_BOW => "02", @@ -29,6 +33,12 @@ internal class WeaponTypeIconConverter : IValueConverter return new Uri(string.Format(BaseUrl, element)); } + /// + public object Convert(object value, Type targetType, object parameter, string language) + { + return WeaponTypeToIconUri((WeaponType)value); + } + /// public object ConvertBack(object value, Type targetType, object parameter, string language) { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs new file mode 100644 index 00000000..9ca299b2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs @@ -0,0 +1,42 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +namespace Snap.Hutao.Model.Metadata; + +/// +/// 祈愿卡池配置 +/// +public class GachaEvent +{ + /// + /// 卡池名称 + /// + public string Name { get; set; } = default!; + + /// + /// 开始时间 + /// + public DateTimeOffset From { get; set; } + + /// + /// 结束时间 + /// + public DateTimeOffset To { get; set; } + + /// + /// 卡池类型 + /// + public GachaConfigType Type { get; set; } + + /// + /// 五星列表 + /// + public List UpOrangeList { get; set; } = default!; + + /// + /// 四星列表 + /// + public List UpPurpleList { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs index 90371b71..50316e1f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs @@ -1,14 +1,18 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Binding.Gacha; +using Snap.Hutao.Model.Binding.Gacha.Abstraction; using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Abstraction; +using Snap.Hutao.Model.Metadata.Converter; namespace Snap.Hutao.Model.Metadata.Weapon; /// /// 武器 /// -public class Weapon +public class Weapon : IStatisticsItemSource { /// /// Id @@ -49,4 +53,57 @@ public class Weapon /// 属性 /// public PropertyInfo Property { get; set; } = default!; + + /// + /// 转换为基础物品 + /// + /// 基础物品 + public ItemBase ToItemBase() + { + return new() + { + Name = Name, + Icon = EquipIconConverter.NameToUri(Icon), + Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType), + Quality = RankLevel, + }; + } + + /// + /// 转换到统计物品 + /// + /// 个数 + /// 统计物品 + public StatisticsItem ToStatisticsItem(int count) + { + return new() + { + Name = Name, + Icon = EquipIconConverter.NameToUri(Icon), + Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType), + Quality = RankLevel, + Count = count, + }; + } + + /// + /// 转换到简述统计物品 + /// + /// 距上个五星 + /// 时间 + /// 是否为Up物品 + /// 简述统计物品 + public SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp) + { + return new() + { + Name = Name, + Icon = EquipIconConverter.NameToUri(Icon), + Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType), + Time = time, + Quality = RankLevel, + LastPull = lastPull, + IsUp = isUp, + }; + } } diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index a886c2c8..9f62a58a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -9,7 +9,7 @@ + Version="1.0.34.0" /> 胡桃 diff --git a/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json b/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json index b8b51163..dc55a580 100644 --- a/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json +++ b/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Snap.Hutao (Package)": { "commandName": "MsixPackage", - "nativeDebugging": true + "nativeDebugging": false }, "Snap.Hutao (Unpackaged)": { "commandName": "Project" diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs index ff728d5f..518e4230 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs @@ -122,25 +122,25 @@ internal class AchievementService : IAchievementService } /// - public ImportResult ImportFromUIAF(EntityArchive archive, List list, ImportOption option) + public ImportResult ImportFromUIAF(EntityArchive archive, List list, ImportStrategy strategy) { Guid archiveId = archive.InnerId; - switch (option) + switch (strategy) { - case ImportOption.AggressiveMerge: + case ImportStrategy.AggressiveMerge: { IOrderedEnumerable orederedUIAF = list.OrderBy(a => a.Id); return achievementDbOperation.Merge(archiveId, orederedUIAF, true); } - case ImportOption.LazyMerge: + case ImportStrategy.LazyMerge: { IOrderedEnumerable orederedUIAF = list.OrderBy(a => a.Id); return achievementDbOperation.Merge(archiveId, orederedUIAF, false); } - case ImportOption.Overwrite: + case ImportStrategy.Overwrite: { IEnumerable orederedUIAF = list .Select(uiaf => EntityAchievement.Create(archiveId, uiaf)) @@ -154,9 +154,9 @@ internal class AchievementService : IAchievementService } /// - public Task ImportFromUIAFAsync(EntityArchive archive, List list, ImportOption option) + public Task ImportFromUIAFAsync(EntityArchive archive, List list, ImportStrategy strategy) { - return Task.Run(() => ImportFromUIAF(archive, list, option)); + return Task.Run(() => ImportFromUIAF(archive, list, strategy)); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs index dd166633..72ee1892 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs @@ -39,18 +39,18 @@ internal interface IAchievementService /// /// 用户 /// UIAF数据 - /// 选项 + /// 选项 /// 导入结果 - ImportResult ImportFromUIAF(EntityArchive archive, List list, ImportOption option); + ImportResult ImportFromUIAF(EntityArchive archive, List list, ImportStrategy strategy); /// /// 异步导入UIAF数据 /// /// 用户 /// UIAF数据 - /// 选项 + /// 选项 /// 导入结果 - Task ImportFromUIAFAsync(EntityArchive archive, List list, ImportOption option); + Task ImportFromUIAFAsync(EntityArchive archive, List list, ImportStrategy strategy); /// /// 异步移除存档 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportOption.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportStrategy.cs similarity index 90% rename from src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportOption.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportStrategy.cs index 8fe50d39..d507f110 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportOption.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportStrategy.cs @@ -4,9 +4,9 @@ namespace Snap.Hutao.Service.Achievement; /// -/// 导入选项 +/// 导入策略 /// -public enum ImportOption +public enum ImportStrategy { /// /// 贪婪合并 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs new file mode 100644 index 00000000..31c84ae9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs @@ -0,0 +1,108 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha; +using Snap.Hutao.Model.Metadata.Abstraction; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Weapon; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.Service.GachaLog.Factory; + +/// +/// 统计拓展 +/// +public static class GachaStatisticsExtensions +{ + /// + /// 值域压缩 + /// + /// 源 + /// 压缩值 + public static byte HalfRange(this byte b) + { + // [0,256] -> [0,128]-> [64,172] + return (byte)((b / 2) + 64); + } + + /// + /// 求平均值 + /// + /// 跨度 + /// 平均值 + public static byte Average(this Span span) + { + int sum = 0; + int count = 0; + foreach (byte b in span) + { + sum += b; + count++; + } + + return unchecked((byte)(sum / count)); + } + + /// + /// 增加计数 + /// + /// 键类型 + /// 字典 + /// 键 + public static void Increase(this Dictionary dict, TKey key) + where TKey : notnull + { + ++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _); + } + + /// + /// 增加计数 + /// + /// 键类型 + /// 字典 + /// 键 + /// 是否存在键值 + public static bool TryIncrease(this Dictionary dict, TKey key) + where TKey : notnull + { + ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key); + if (!Unsafe.IsNullRef(ref value)) + { + ++value; + return true; + } + + return false; + } + + /// + /// 将计数器转换为统计物品列表 + /// + /// 计数器 + /// 统计物品列表 + public static List ToStatisticsList(this Dictionary dict) + { + return dict.Select(kvp => kvp.Key.ToStatisticsItem(kvp.Value)).ToList(); + } + + /// + /// 将计数器转换为统计物品列表 + /// + /// 计数器 + /// 统计物品列表 + public static List ToStatisticsList(this Dictionary dict) + { + return dict.Select(kvp => kvp.Key.ToStatisticsItem(kvp.Value)).ToList(); + } + + /// + /// 将计数器转换为统计物品列表 + /// + /// 计数器 + /// 统计物品列表 + public static List ToStatisticsList(this Dictionary dict) + { + return dict.Select(kvp => kvp.Key.ToStatisticsItem(kvp.Value)).ToList(); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs new file mode 100644 index 00000000..61749498 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs @@ -0,0 +1,158 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Convert; +using Snap.Hutao.Extension; +using Snap.Hutao.Model.Binding.Gacha; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata; +using Snap.Hutao.Model.Metadata.Abstraction; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Weapon; +using Snap.Hutao.Service.Metadata; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; +using Windows.UI; + +namespace Snap.Hutao.Service.GachaLog.Factory; + +/// +/// 祈愿统计工厂 +/// +[Injection(InjectAs.Transient, typeof(IGachaStatisticsFactory))] +internal class GachaStatisticsFactory : IGachaStatisticsFactory +{ + private readonly IMetadataService metadataService; + + /// + /// 构造一个新的祈愿统计工厂 + /// + /// 元数据服务 + public GachaStatisticsFactory(IMetadataService metadataService) + { + this.metadataService = metadataService; + } + + /// + public async Task CreateAsync(IEnumerable items) + { + Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); + Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false); + + Dictionary nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false); + Dictionary nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false); + + List gachaevents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false); + List historyWishBuilders = gachaevents.Select(g => new HistoryWishBuilder(g, nameAvatarMap, nameWeaponMap)).ToList(); + + IOrderedEnumerable orderedItems = items.OrderBy(i => i.Id); + return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap)).ConfigureAwait(false); + } + + private static GachaStatistics CreateCore( + IOrderedEnumerable items, + List historyWishBuilders, + Dictionary avatarMap, + Dictionary weaponMap) + { + TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.PermanentWish, 90, 10); + TypedWishSummaryBuilder avatarWishBuilder = new("角色活动", TypedWishSummaryBuilder.AvatarEventWish, 90, 10); + TypedWishSummaryBuilder weaponWishBuilder = new("神铸赋形", TypedWishSummaryBuilder.WeaponEventWish, 80, 10); + + Dictionary orangeAvatarCounter = new(); + Dictionary purpleAvatarCounter = new(); + Dictionary orangeWeaponCounter = new(); + Dictionary purpleWeaponCounter = new(); + Dictionary blueWeaponCounter = new(); + + // Items are ordered by precise time + // first is oldest + foreach (GachaItem item in items) + { + // Find target history wish to operate. + // TODO: improve performance. + HistoryWishBuilder? targetHistoryWishBuilder = historyWishBuilders + .Where(w => w.ConfigType == item.GachaType) + .SingleOrDefault(w => w.From <= item.Time && w.To >= item.Time); + + // It's an avatar + if (item.ItemId.Place() == 8) + { + Avatar avatar = avatarMap[item.ItemId]; + + bool isUp = false; + switch (avatar.Quality) + { + case ItemQuality.QUALITY_ORANGE: + orangeAvatarCounter.Increase(avatar); + isUp = targetHistoryWishBuilder?.IncreaseOrangeAvatar(avatar) ?? false; + break; + case ItemQuality.QUALITY_PURPLE: + purpleAvatarCounter.Increase(avatar); + targetHistoryWishBuilder?.IncreasePurpleAvatar(avatar); + break; + } + + permanentWishBuilder.TrackAvatar(item, avatar, isUp); + avatarWishBuilder.TrackAvatar(item, avatar, isUp); + weaponWishBuilder.TrackAvatar(item, avatar, isUp); + } + + // It's a weapon + else if (item.ItemId.Place() == 5) + { + Weapon weapon = weaponMap[item.ItemId]; + + bool isUp = false; + switch (weapon.RankLevel) + { + case ItemQuality.QUALITY_ORANGE: + isUp = targetHistoryWishBuilder?.IncreaseOrangeWeapon(weapon) ?? false; + orangeWeaponCounter.Increase(weapon); + break; + case ItemQuality.QUALITY_PURPLE: + targetHistoryWishBuilder?.IncreasePurpleWeapon(weapon); + purpleWeaponCounter.Increase(weapon); + break; + case ItemQuality.QUALITY_BLUE: + targetHistoryWishBuilder?.IncreaseBlueWeapon(weapon); + blueWeaponCounter.Increase(weapon); + break; + } + + permanentWishBuilder.TrackWeapon(item, weapon, isUp); + avatarWishBuilder.TrackWeapon(item, weapon, isUp); + weaponWishBuilder.TrackWeapon(item, weapon, isUp); + } + else + { + // ItemId place not correct. + Must.NeverHappen(); + } + } + + return new() + { + // history + HistoryWishes = historyWishBuilders.Select(builder => builder.ToHistoryWish()).ToList(), + + // avatars + OrangeAvatars = orangeAvatarCounter.ToStatisticsList(), + PurpleAvatars = purpleAvatarCounter.ToStatisticsList(), + + // weapons + OrangeWeapons = orangeWeaponCounter.ToStatisticsList(), + PurpleWeapons = purpleWeaponCounter.ToStatisticsList(), + BlueWeapons = blueWeaponCounter.ToStatisticsList(), + + PermanentWish = permanentWishBuilder.ToTypedWishSummary(), + AvatarWish = avatarWishBuilder.ToTypedWishSummary(), + WeaponWish = weaponWishBuilder.ToTypedWishSummary(), + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HistoryWishBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HistoryWishBuilder.cs new file mode 100644 index 00000000..f9d10d5d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HistoryWishBuilder.cs @@ -0,0 +1,144 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha; +using Snap.Hutao.Model.Metadata; +using Snap.Hutao.Model.Metadata.Abstraction; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Weapon; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +namespace Snap.Hutao.Service.GachaLog.Factory; + +/// +/// 卡池历史记录建造器 +/// +internal class HistoryWishBuilder +{ + private readonly GachaEvent gachaEvent; + private readonly GachaConfigType configType; + + private readonly Dictionary orangeUpCounter = new(); + private readonly Dictionary purpleUpCounter = new(); + private readonly Dictionary orangeCounter = new(); + private readonly Dictionary purpleCounter = new(); + private readonly Dictionary blueCounter = new(); + + private int totalCountTracker; + + /// + /// 构造一个新的卡池历史记录建造器 + /// + /// 卡池配置 + /// 命名角色映射 + /// 命名武器映射 + public HistoryWishBuilder(GachaEvent gachaEvent, Dictionary nameAvatarMap, Dictionary nameWeaponMap) + { + this.gachaEvent = gachaEvent; + configType = gachaEvent.Type; + + if (configType == GachaConfigType.AvatarEventWish || configType == GachaConfigType.AvatarEventWish2) + { + orangeUpCounter = gachaEvent.UpOrangeList.Select(name => nameAvatarMap[name]).ToDictionary(a => (IStatisticsItemSource)a, a => 0); + purpleUpCounter = gachaEvent.UpPurpleList.Select(name => nameAvatarMap[name]).ToDictionary(a => (IStatisticsItemSource)a, a => 0); + } + else if (configType == GachaConfigType.WeaponEventWish) + { + orangeUpCounter = gachaEvent.UpOrangeList.Select(name => nameWeaponMap[name]).ToDictionary(w => (IStatisticsItemSource)w, w => 0); + purpleUpCounter = gachaEvent.UpPurpleList.Select(name => nameWeaponMap[name]).ToDictionary(w => (IStatisticsItemSource)w, w => 0); + } + } + + /// + /// 祈愿配置类型 + /// + public GachaConfigType ConfigType { get => configType; } + + /// + public DateTimeOffset From => gachaEvent.From; + + /// + public DateTimeOffset To => gachaEvent.To; + + /// + /// 计数五星角色 + /// + /// 角色 + /// 是否为Up角色 + public bool IncreaseOrangeAvatar(Avatar avatar) + { + orangeCounter.Increase(avatar); + ++totalCountTracker; + + return orangeUpCounter.TryIncrease(avatar); + } + + /// + /// 计数四星角色 + /// + /// 角色 + public void IncreasePurpleAvatar(Avatar avatar) + { + purpleUpCounter.TryIncrease(avatar); + purpleCounter.Increase(avatar); + ++totalCountTracker; + } + + /// + /// 计数五星武器 + /// + /// 武器 + /// 是否为Up武器 + public bool IncreaseOrangeWeapon(Weapon weapon) + { + orangeCounter.Increase(weapon); + ++totalCountTracker; + return orangeUpCounter.TryIncrease(weapon); + } + + /// + /// 计数四星武器 + /// + /// 武器 + public void IncreasePurpleWeapon(Weapon weapon) + { + purpleUpCounter.TryIncrease(weapon); + purpleCounter.Increase(weapon); + ++totalCountTracker; + } + + /// + /// 计数三星武器 + /// + /// 武器 + public void IncreaseBlueWeapon(Weapon weapon) + { + blueCounter.Increase(weapon); + ++totalCountTracker; + } + + /// + /// 转换到卡池历史记录 + /// + /// 卡池历史记录 + public HistoryWish ToHistoryWish() + { + HistoryWish historyWish = new() + { + // base + Name = gachaEvent.Name, + From = gachaEvent.From, + To = gachaEvent.To, + TotalCount = totalCountTracker, + + // fill + OrangeUpList = orangeUpCounter.ToStatisticsList(), + PurpleUpList = purpleUpCounter.ToStatisticsList(), + OrangeList = orangeCounter.ToStatisticsList(), + PurpleList = purpleCounter.ToStatisticsList(), + BlueList = blueCounter.ToStatisticsList(), + }; + + return historyWish; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/IGachaStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/IGachaStatisticsFactory.cs new file mode 100644 index 00000000..b5caa39f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/IGachaStatisticsFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha; +using Snap.Hutao.Model.Entity; + +namespace Snap.Hutao.Service.GachaLog.Factory; + +/// +/// 祈愿统计工厂 +/// +public interface IGachaStatisticsFactory +{ + /// + /// 异步创建祈愿统计对象 + /// + /// 物品列表 + /// 祈愿统计对象 + Task CreateAsync(IEnumerable items); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs new file mode 100644 index 00000000..c699b212 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs @@ -0,0 +1,268 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Extension; +using Snap.Hutao.Model.Binding.Gacha; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Weapon; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using System.Security.Cryptography; +using System.Text; +using Windows.UI; + +namespace Snap.Hutao.Service.GachaLog.Factory; + +/// +/// 类型化祈愿统计信息构建器 +/// +internal class TypedWishSummaryBuilder +{ + /// + /// 常驻祈愿 + /// + public static readonly Func PermanentWish = type => type is GachaConfigType.PermanentWish; + + /// + /// 角色活动 + /// + public static readonly Func AvatarEventWish = type => type is GachaConfigType.AvatarEventWish or GachaConfigType.AvatarEventWish2; + + /// + /// 武器活动 + /// + public static readonly Func WeaponEventWish = type => type is GachaConfigType.WeaponEventWish; + + private readonly string name; + private readonly int guarenteeOrangeThreshold; + private readonly int guarenteePurpleThreshold; + private readonly Func typeEvaluator; + + private readonly List averageOrangePullTracker = new(); + private readonly List averageUpOrangePullTracker = new(); + private readonly List summaryItemCache = new(); + + private int maxOrangePullTracker; + private int minOrangePullTracker; + private int lastOrangePullTracker; + private int lastUpOrangePullTracker; + private int lastPurplePullTracker; + private int totalCountTracker; + private int totalOrangePullTracker; + private int totalPurplePullTracker; + private int totalBluePullTracker; + + private DateTimeOffset fromTimeTracker = DateTimeOffset.MaxValue; + private DateTimeOffset toTimeTracker = DateTimeOffset.MinValue; + + /// + /// 构造一个新的类型化祈愿统计信息构建器 + /// + /// 祈愿配置 + /// 祈愿类型判断器 + /// 五星保底 + /// 四星保底 + public TypedWishSummaryBuilder(string name, Func typeEvaluator, int guarenteeOrangeThreshold, int guarenteePurpleThreshold) + { + this.name = name; + this.typeEvaluator = typeEvaluator; + this.guarenteeOrangeThreshold = guarenteeOrangeThreshold; + this.guarenteePurpleThreshold = guarenteePurpleThreshold; + } + + /// + /// 追踪物品 + /// + /// 祈愿物品 + /// 对应角色 + /// 是否为Up物品 + public void TrackAvatar(GachaItem item, Avatar avatar, bool isUp) + { + if (typeEvaluator(item.GachaType)) + { + ++lastOrangePullTracker; + ++lastPurplePullTracker; + ++lastUpOrangePullTracker; + + // track total pulls + ++totalCountTracker; + + switch (avatar.Quality) + { + case ItemQuality.QUALITY_ORANGE: + { + TrackMinMaxOrangePull(lastOrangePullTracker); + TrackFromToTime(item.Time); + averageOrangePullTracker.Add(lastOrangePullTracker); + + if (isUp) + { + averageUpOrangePullTracker.Add(lastUpOrangePullTracker); + lastUpOrangePullTracker = 0; + } + + summaryItemCache.Add(avatar.ToSummaryItem(lastOrangePullTracker, item.Time, isUp)); + + lastOrangePullTracker = 0; + ++totalOrangePullTracker; + break; + } + + case ItemQuality.QUALITY_PURPLE: + { + lastPurplePullTracker = 0; + ++totalPurplePullTracker; + break; + } + } + } + } + + /// + /// 追踪物品 + /// + /// 祈愿物品 + /// 对应武器 + /// 是否为Up物品 + public void TrackWeapon(GachaItem item, Weapon weapon, bool isUp) + { + if (typeEvaluator(item.GachaType)) + { + ++lastOrangePullTracker; + ++lastPurplePullTracker; + ++lastUpOrangePullTracker; + + // track total pulls + ++totalCountTracker; + + switch (weapon.RankLevel) + { + case ItemQuality.QUALITY_ORANGE: + { + TrackMinMaxOrangePull(lastOrangePullTracker); + TrackFromToTime(item.Time); + averageOrangePullTracker.Add(lastOrangePullTracker); + + if (isUp) + { + averageUpOrangePullTracker.Add(lastUpOrangePullTracker); + lastUpOrangePullTracker = 0; + } + + summaryItemCache.Add(weapon.ToSummaryItem(lastOrangePullTracker, item.Time, isUp)); + + lastOrangePullTracker = 0; + ++totalOrangePullTracker; + break; + } + + case ItemQuality.QUALITY_PURPLE: + { + lastPurplePullTracker = 0; + ++totalPurplePullTracker; + break; + } + + case ItemQuality.QUALITY_BLUE: + { + ++totalBluePullTracker; + break; + } + } + } + } + + /// + /// 转换到类型化祈愿统计信息 + /// + /// 类型化祈愿统计信息 + public TypedWishSummary ToTypedWishSummary() + { + CompleteSummaryItems(summaryItemCache); + double totalCountDouble = totalCountTracker; + + return new() + { + // base + Name = name, + From = fromTimeTracker, + To = toTimeTracker, + TotalCount = totalCountTracker, + + // TypedWishSummary + MaxOrangePull = maxOrangePullTracker, + MinOrangePull = minOrangePullTracker, + LastOrangePull = lastOrangePullTracker, + GuarenteeOrangeThreshold = guarenteeOrangeThreshold, + LastPurplePull = lastPurplePullTracker, + GuarenteePurpleThreshold = guarenteePurpleThreshold, + TotalOrangePull = totalOrangePullTracker, + TotalPurplePull = totalPurplePullTracker, + TotalBluePull = totalBluePullTracker, + TotalOrangePercent = totalOrangePullTracker / totalCountDouble, + TotalPurplePercent = totalPurplePullTracker / totalCountDouble, + TotalBluePercent = totalBluePullTracker / totalCountDouble, + AverageOrangePull = averageOrangePullTracker.AverageNoThrow(), + AverageUpOrangePull = averageUpOrangePullTracker.AverageNoThrow(), + OrangeList = summaryItemCache, + }; + } + + private static void CompleteSummaryItems(List summaryItems) + { + // we can't trust first item's prev state. + bool isPreviousUp = true; + + // mark the IsGuarentee + foreach (SummaryItem item in summaryItems) + { + if (item.IsUp && (!isPreviousUp)) + { + item.IsGuarentee = true; + } + + isPreviousUp = item.IsUp; + item.Color = GetColorByName(item.Name); + } + + // reverse items + summaryItems.Reverse(); + } + + private static Color GetColorByName(string name) + { + byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name)); + Span first = new(codes, 0, 5); + Span second = new(codes, 5, 5); + Span third = new(codes, 10, 5); + Color color = Color.FromArgb(255, first.Average()/*.HalfRange()*/, second.Average()/*.HalfRange()*/, third.Average()/*.HalfRange()*/); + return color; + } + + private void TrackMinMaxOrangePull(int lastOrangePull) + { + if (lastOrangePull < minOrangePullTracker || minOrangePullTracker == 0) + { + minOrangePullTracker = lastOrangePull; + } + + if (lastOrangePull > maxOrangePullTracker || maxOrangePullTracker == 0) + { + maxOrangePullTracker = lastOrangePull; + } + } + + private void TrackFromToTime(DateTimeOffset time) + { + if (time < fromTimeTracker) + { + fromTimeTracker = time; + } + + if (time > toTimeTracker) + { + toTimeTracker = time; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/FetchState.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/FetchState.cs new file mode 100644 index 00000000..916f569b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/FetchState.cs @@ -0,0 +1,36 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.Gacha.Abstraction; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 获取状态 +/// +public class FetchState +{ + /// + /// 初始化一个新的获取状态 + /// + public FetchState() + { + Items = new(20); + } + + /// + /// 验证密钥是否过期 + /// + public bool AuthKeyTimeout { get; set; } + + /// + /// 卡池类型 + /// + public GachaConfigType ConfigType { get; set; } + + /// + /// 当前获取的物品 + /// + public List Items { get; set; } +} \ 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 1bd763c7..0141b58f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs @@ -3,8 +3,17 @@ using CommunityToolkit.Mvvm.Messaging; using Snap.Hutao.Context.Database; +using Snap.Hutao.Core.Abstraction; using Snap.Hutao.Core.Database; +using Snap.Hutao.Model.Binding.Gacha; +using Snap.Hutao.Model.Binding.Gacha.Abstraction; using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Service.GachaLog.Factory; +using Snap.Hutao.Service.Metadata; +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; @@ -13,21 +22,59 @@ namespace Snap.Hutao.Service.GachaLog; /// 祈愿记录服务 /// [Injection(InjectAs.Transient, typeof(IGachaLogService))] -internal class GachaLogService : IGachaLogService +internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization { + /// + /// 祈愿记录查询的类型 + /// + private static readonly ImmutableList QueryTypes = ImmutableList.Create(new GachaConfigType[] + { + GachaConfigType.NoviceWish, + GachaConfigType.PermanentWish, + GachaConfigType.AvatarEventWish, + GachaConfigType.WeaponEventWish, + }); + private readonly AppDbContext appDbContext; + private readonly IEnumerable urlProviders; + private readonly GachaInfoClient gachaInfoClient; + private readonly IMetadataService metadataService; + private readonly IInfoBarService infoBarService; + private readonly IGachaStatisticsFactory gachaStatisticsFactory; private readonly DbCurrent dbCurrent; + private readonly Dictionary itemBaseCache = new(); + + private Dictionary? avatarMap; + private Dictionary? weaponMap; private ObservableCollection? archiveCollection; /// /// 构造一个新的祈愿记录服务 /// /// 数据库上下文 + /// Url提供器集合 + /// 祈愿记录客户端 + /// 元数据服务 + /// 信息条服务 + /// 祈愿统计工厂 /// 消息器 - public GachaLogService(AppDbContext appDbContext, IMessenger messenger) + public GachaLogService( + AppDbContext appDbContext, + IEnumerable urlProviders, + GachaInfoClient gachaInfoClient, + IMetadataService metadataService, + IInfoBarService infoBarService, + IGachaStatisticsFactory gachaStatisticsFactory, + IMessenger messenger) { this.appDbContext = appDbContext; + this.urlProviders = urlProviders; + this.gachaInfoClient = gachaInfoClient; + this.metadataService = metadataService; + this.infoBarService = infoBarService; + this.gachaStatisticsFactory = gachaStatisticsFactory; + dbCurrent = new(appDbContext, appDbContext.GachaArchives, messenger); } @@ -38,9 +85,235 @@ internal class GachaLogService : IGachaLogService set => dbCurrent.Current = value; } + /// + public bool IsInitialized { get; set; } + /// public ObservableCollection GetArchiveCollection() { return archiveCollection ??= new(appDbContext.GachaArchives.ToList()); } + + /// + public async ValueTask InitializeAsync(CancellationToken token = default) + { + if (await metadataService.InitializeAsync(token).ConfigureAwait(false)) + { + avatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false); + weaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false); + + IsInitialized = true; + } + else + { + IsInitialized = false; + } + + return IsInitialized; + } + + /// + public Task GetStatisticsAsync(GachaArchive? archive = null) + { + archive ??= CurrentArchive; + + // Return statistics + if (archive != null) + { + IQueryable items = appDbContext.GachaItems + .Where(i => i.ArchiveId == archive.InnerId); + + return gachaStatisticsFactory.CreateAsync(items); + } + else + { + return Task.FromResult(GachaStatistics.Default); + } + } + + /// + public IGachaLogUrlProvider? GetGachaLogUrlProvider(RefreshOption option) + { + return option switch + { + RefreshOption.WebCache => urlProviders.Single(p => p.Name == nameof(GachaLogUrlWebCacheProvider)), + RefreshOption.ManualInput => urlProviders.Single(p => p.Name == nameof(GachaLogUrlManualInputProvider)), + _ => null, + }; + } + + /// + public async Task RefreshGachaLogAsync(string query, RefreshStrategy strategy, IProgress progress, CancellationToken token) + { + Verify.Operation(IsInitialized, "祈愿记录服务未能正常初始化"); + + bool isLazy = strategy switch + { + RefreshStrategy.AggressiveMerge => false, + RefreshStrategy.LazyMerge => true, + _ => throw Must.NeverHappen(), + }; + + GachaArchive? result = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false); + CurrentArchive = result ?? CurrentArchive; + } + + private static Task RandomDelayAsync(CancellationToken token) + { + return Task.Delay(TimeSpan.FromSeconds(Random.Shared.NextDouble() + 1), token); + } + + private async Task FetchGachaLogsAsync(string query, bool isLazy, IProgress progress, CancellationToken token) + { + GachaArchive? archive = null; + FetchState state = new(); + + foreach (GachaConfigType configType in QueryTypes) + { + state.ConfigType = configType; + long? dbEndId = null; + GachaLogConfigration configration = new(query, configType); + List itemsToAdd = new(); + + do + { + Response? response = await gachaInfoClient.GetGachaLogPageAsync(configration, token).ConfigureAwait(false); + + if (response?.Data is GachaLogPage page) + { + state.Items.Clear(); + List items = page.List; + bool completedCurrentTypeAdding = false; + + foreach (GachaLogItem item in items) + { + SkipOrInitArchive(ref archive, item.Uid); + dbEndId ??= GetEndId(archive, configType); + + if ((!isLazy) || item.Id > dbEndId) + { + itemsToAdd.Add(GachaItem.Create(archive.InnerId, item, GetItemId(item))); + state.Items.Add(GetItemBaseByName(item.Name, item.ItemType)); + configration.EndId = item.Id; + } + else + { + completedCurrentTypeAdding = true; + break; + } + } + + progress.Report(state); + + if (completedCurrentTypeAdding || items.Count < GachaLogConfigration.Size) + { + // exit current type fetch loop + break; + } + } + else + { + state.AuthKeyTimeout = true; + progress.Report(state); + break; + } + + await RandomDelayAsync(token).ConfigureAwait(false); + } + while (true); + + if (state.AuthKeyTimeout) + { + break; + } + + SaveGachaItems(itemsToAdd, isLazy, archive, configration.EndId); + await RandomDelayAsync(token).ConfigureAwait(false); + } + + return archive; + } + + private void SkipOrInitArchive([NotNull] ref GachaArchive? archive, string uid) + { + if (archive == null) + { + archive = appDbContext.GachaArchives.SingleOrDefault(a => a.Uid == uid); + + if (archive == null) + { + GachaArchive created = GachaArchive.Create(uid); + appDbContext.GachaArchives.Add(created); + appDbContext.SaveChanges(); + + archive = appDbContext.GachaArchives.Single(a => a.Uid == uid); + GachaArchive temp = archive; + Program.DispatcherQueue!.TryEnqueue(() => archiveCollection!.Add(temp)); + } + } + } + + private long GetEndId(GachaArchive? archive, GachaConfigType configType) + { + GachaItem? item = null; + + if (archive != null) + { + item = appDbContext.GachaItems + .Where(i => i.ArchiveId == archive.InnerId) + .Where(i => i.QueryType == configType) + + // MaxBy should be supported by .NET 7 + .AsEnumerable() + .MaxBy(i => i.Id); + } + + return item?.Id ?? 0L; + } + + private int GetItemId(GachaLogItem item) + { + return item.ItemType switch + { + "角色" => avatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0, + "武器" => weaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0, + _ => 0, + }; + } + + private ItemBase GetItemBaseByName(string name, string type) + { + if (!itemBaseCache.TryGetValue(name, out ItemBase? result)) + { + result = type switch + { + "角色" => avatarMap![name].ToItemBase(), + "武器" => weaponMap![name].ToItemBase(), + _ => throw Must.NeverHappen(), + }; + + itemBaseCache[name] = result; + } + + return result; + } + + private void SaveGachaItems(List itemsToAdd, bool isLazy, GachaArchive? archive, long endId) + { + if (itemsToAdd.Count > 0) + { + if ((!isLazy) && archive != null) + { + IQueryable toRemove = appDbContext.GachaItems + .Where(i => i.ArchiveId == archive.InnerId) + .Where(i => i.Id >= endId); + + appDbContext.GachaItems.RemoveRange(toRemove); + appDbContext.SaveChanges(); + } + + appDbContext.GachaItems.AddRange(itemsToAdd); + appDbContext.SaveChanges(); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs index afa7016f..2402c3ab 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Binding.Gacha; using Snap.Hutao.Model.Entity; using System.Collections.ObjectModel; @@ -21,4 +22,36 @@ internal interface IGachaLogService /// /// 存档集合 ObservableCollection GetArchiveCollection(); -} + + /// + /// 获取祈愿日志Url提供器 + /// + /// 刷新模式 + /// 祈愿日志Url提供器 + IGachaLogUrlProvider? GetGachaLogUrlProvider(RefreshOption option); + + /// + /// 获得对应的祈愿统计 + /// + /// 存档 + /// 祈愿统计 + Task GetStatisticsAsync(GachaArchive? archive = null); + + /// + /// 异步初始化 + /// + /// 取消令牌 + /// 是否初始化成功 + ValueTask InitializeAsync(CancellationToken token = default); + + /// + /// 刷新祈愿记录 + /// 切换选中的存档 + /// + /// 查询语句 + /// 刷新策略 + /// 进度 + /// 取消令牌 + /// 任务 + Task RefreshGachaLogAsync(string query, RefreshStrategy strategy, IProgress progress, CancellationToken token); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs new file mode 100644 index 00000000..20240709 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs @@ -0,0 +1,27 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 刷新选项 +/// +public enum RefreshOption +{ + /// + /// 无模式刷新 + /// 用于返回新的统计数据 + /// 或者切换存档后的刷新 + /// + None, + + /// + /// 通过浏览器缓存刷新 + /// + WebCache, + + /// + /// 手动输入Url刷新 + /// + ManualInput, +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategy.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategy.cs new file mode 100644 index 00000000..87cf7f1d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategy.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 刷新策略 +/// +public enum RefreshStrategy +{ + /// + /// 无策略 用于切换存档时使用 + /// + None = 0, + + /// + /// 贪婪合并 + /// + AggressiveMerge = 1, + + /// + /// 懒惰合并 + /// + LazyMerge = 2, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs index 35ea2ad4..74eb44e1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs @@ -3,7 +3,6 @@ using Microsoft.Win32; using Snap.Hutao.Core.Threading; -using System.IO; namespace Snap.Hutao.Service.Game.Locator; @@ -19,7 +18,7 @@ internal class RegistryLauncherLocator : IGameLocator /// public Task> LocateGamePathAsync() { - return Task.FromResult(LocateInternal("InstallPath", "YuanShen.exe")); + return Task.FromResult(LocateInternal("InstallPath", "\\Genshin Impact Game\\YuanShen.exe")); } /// @@ -28,16 +27,16 @@ internal class RegistryLauncherLocator : IGameLocator return Task.FromResult(LocateInternal("DisplayIcon")); } - private static ValueResult LocateInternal(string key, string? combine = null) + private static ValueResult LocateInternal(string key, string? append = null) { RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神"); if (uninstallKey != null) { if (uninstallKey.GetValue(key) is string path) { - if (!string.IsNullOrEmpty(combine)) + if (!string.IsNullOrEmpty(append)) { - path = Path.Combine(combine); + path += append; } return new(true, path); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs index 3afd641d..8ed56937 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.Model.Metadata; using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Reliquary; @@ -41,6 +42,41 @@ internal interface IMetadataService /// 角色列表 ValueTask> GetAvatarsAsync(CancellationToken token = default); + /// + /// 异步获取卡池配置列表 + /// + /// 取消令牌 + /// 卡池配置列表 + ValueTask> GetGachaEventsAsync(CancellationToken token = default(CancellationToken)); + + /// + /// 异步获取Id到角色的字典 + /// + /// 取消令牌 + /// Id到角色的字典 + ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default(CancellationToken)); + + /// + /// 异步获取ID到武器的字典 + /// + /// 取消令牌 + /// Id到武器的字典 + ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default(CancellationToken)); + + /// + /// 异步获取名称到角色的字典 + /// + /// 取消令牌 + /// 名称到角色的字典 + ValueTask> GetNameToAvatarMapAsync(CancellationToken token = default); + + /// + /// 异步获取名称到武器的字典 + /// + /// 取消令牌 + /// 名称到武器的字典 + ValueTask> GetNameToWeaponMapAsync(CancellationToken token = default); + /// /// 异步获取圣遗物列表 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index 56221c28..d6ebac6b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -7,6 +7,8 @@ using Snap.Hutao.Core.Abstraction; using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.Logging; +using Snap.Hutao.Extension; +using Snap.Hutao.Model.Metadata; using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Reliquary; @@ -108,6 +110,36 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor return FromCacheOrFileAsync>("Avatar", token); } + /// + public ValueTask> GetGachaEventsAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("GachaEvent", token); + } + + /// + public ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("Avatar", a => a.Id, token); + } + + /// + public ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("Weapon", w => w.Id, token); + } + + /// + public ValueTask> GetNameToAvatarMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("Avatar", a => a.Name, token); + } + + /// + public ValueTask> GetNameToWeaponMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("Weapon", w => w.Name, token); + } + /// public ValueTask> GetReliquariesAsync(CancellationToken token = default) { @@ -132,7 +164,7 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor return FromCacheOrFileAsync>("Weapon", token); } - private async Task TryUpdateMetadataAsync(CancellationToken token = default) + private async Task TryUpdateMetadataAsync(CancellationToken token) { IDictionary? metaMd5Map = null; try @@ -238,7 +270,7 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor if (memoryCache.TryGetValue(cacheKey, out object? value)) { - return Must.NotNull((value as T)!); + return Must.NotNull((T)value!); } using (Stream fileStream = metadataContext.OpenRead($"{fileName}.json")) @@ -247,4 +279,20 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor return memoryCache.Set(cacheKey, Must.NotNull(result!)); } } + + private async ValueTask> FromCacheAsDictionaryAsync(string fileName, Func keySelector, CancellationToken token) + where TKey : notnull + { + Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败"); + string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}.Map.{typeof(TKey).Name}"; + + if (memoryCache.TryGetValue(cacheKey, out object? value)) + { + return Must.NotNull((Dictionary)value!); + } + + List list = await FromCacheOrFileAsync>(fileName, token).ConfigureAwait(false); + Dictionary dict = list.ToDictionaryOverride(keySelector); + return memoryCache.Set(cacheKey, dict); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs index 7febced6..906507ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Control; using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Setting; using Snap.Hutao.Service.Abstraction; @@ -137,18 +138,32 @@ internal class NavigationService : INavigationService { NavigationResult result = Navigate(data, syncNavigationViewItem); - if (result is NavigationResult.Succeed) + switch (result) { - try - { - await data - .WaitForCompletionAsync() - .ConfigureAwait(false); - } - catch (AggregateException) - { - return NavigationResult.Failed; - } + case NavigationResult.Succeed: + { + try + { + await data.WaitForCompletionAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogError(EventIds.NavigationFailed, ex, "异步导航时发生异常"); + return NavigationResult.Failed; + } + } + + break; + + case NavigationResult.AlreadyNavigatedTo: + { + if (Frame!.Content is ScopedPage scopedPage) + { + await scopedPage.NotifyRecipentAsync((INavigationData)data).ConfigureAwait(false); + } + } + + break; } return result; diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 8f7e0aa0..1741da7e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -44,8 +44,10 @@ + + @@ -86,9 +88,9 @@ - + - + @@ -99,7 +101,7 @@ - + all @@ -203,8 +205,19 @@ + + + + MSBuild:Compile + + + + + MSBuild:Compile + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/ItemIcon.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/ItemIcon.xaml index 116256bb..f20444f5 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/ItemIcon.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/ItemIcon.xaml @@ -1,9 +1,8 @@  - @@ -22,6 +20,13 @@ Source="https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png"/> + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/ItemIcon.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Control/ItemIcon.xaml.cs index ba2ae312..abead18b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/ItemIcon.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/ItemIcon.xaml.cs @@ -15,6 +15,7 @@ public sealed partial class ItemIcon : UserControl { private static readonly DependencyProperty QualityProperty = Property.Depend(nameof(Quality), ItemQuality.QUALITY_NONE); private static readonly DependencyProperty IconProperty = Property.Depend(nameof(Icon)); + private static readonly DependencyProperty BadgeProperty = Property.Depend(nameof(Badge)); /// /// 构造一个新的物品图标 @@ -41,4 +42,13 @@ public sealed partial class ItemIcon : UserControl get => (Uri)GetValue(IconProperty); set => SetValue(IconProperty, value); } + + /// + /// 角标 + /// + public Uri Badge + { + get => (Uri)GetValue(BadgeProperty); + set => SetValue(BadgeProperty, value); + } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml index 9502f10a..1ce2145b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml @@ -4,9 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls" xmlns:shci="using:Snap.Hutao.Control.Image" - xmlns:shct="using:Snap.Hutao.Control.Text" xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter" mc:Ignorable="d"> @@ -16,7 +14,7 @@ 0,0,16,0 0 - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml new file mode 100644 index 00000000..70e11248 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml.cs new file mode 100644 index 00000000..1a325d7d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; + +namespace Snap.Hutao.View.Control; + +/// +/// 统计卡片 +/// +public sealed partial class StatisticsCard : UserControl +{ + /// + /// 构造一个新的统计卡片 + /// + public StatisticsCard() + { + InitializeComponent(); + } +} 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 d7cfe003..70f26222 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs @@ -42,13 +42,13 @@ public sealed partial class AchievementImportDialog : ContentDialog /// 异步获取导入选项 /// /// 导入选项 - public async Task> GetImportOptionAsync() + public async Task> GetImportStrategyAsync() { await ThreadHelper.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); - ImportOption option = (ImportOption)ImportModeSelector.SelectedIndex; + ImportStrategy strategy = (ImportStrategy)ImportModeSelector.SelectedIndex; - return new(result == ContentDialogResult.Primary, option); + return new(result == ContentDialogResult.Primary, strategy); } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml new file mode 100644 index 00000000..d9676bb6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs new file mode 100644 index 00000000..3c8e4010 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs @@ -0,0 +1,63 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Core; +using Snap.Hutao.Extension; +using Snap.Hutao.Model.Binding.Gacha.Abstraction; +using Snap.Hutao.Service.GachaLog; +using Snap.Hutao.View.Control; + +namespace Snap.Hutao.View.Dialog; + +/// +/// 祈愿记录刷新进度对话框 +/// +public sealed partial class GachaLogRefreshProgressDialog : ContentDialog +{ + private static readonly DependencyProperty StateProperty = Property.Depend(nameof(State)); + + /// + /// 构造一个新的对话框 + /// + /// 窗体 + public GachaLogRefreshProgressDialog(Window window) + { + InitializeComponent(); + XamlRoot = window.Content.XamlRoot; + } + + /// + /// 刷新状态 + /// + public FetchState State + { + get { return (FetchState)GetValue(StateProperty); } + set { SetValue(StateProperty, value); } + } + + /// + /// 接收进度更新 + /// + /// 状态 + public void OnReport(FetchState state) + { + State = state; + GachaItemsPresenter.Header = state.ConfigType.GetDescription(); + + // Binding not working here. + GachaItemsPresenter.Items.Clear(); + foreach (ItemBase item in state.Items) + { + GachaItemsPresenter.Items.Add(new ItemIcon + { + Width = 60, + Height = 60, + Quality = item.Quality, + Icon = item.Icon, + Badge = item.Badge, + }); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml index 5b6b4cc9..74a2195a 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml @@ -60,26 +60,54 @@ MaxWidth="640" VerticalAlignment="Bottom"> - - - - + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml.cs index b1183150..b75027c6 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml.cs @@ -1,9 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.UI.Xaml.Navigation; using Snap.Hutao.Control; -using Snap.Hutao.Service.Navigation; using Snap.Hutao.ViewModel; namespace Snap.Hutao.View.Page; @@ -21,21 +19,4 @@ public sealed partial class AchievementPage : ScopedPage InitializeWith(); InitializeComponent(); } - - /// - [SuppressMessage("", "VSTHRD100")] - protected override async void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - - if (e.Parameter is INavigationData extra) - { - if (extra.Data != null) - { - await ((INavigationRecipient)DataContext).ReceiveAsync(extra).ConfigureAwait(false); - } - - extra.NotifyNavigationCompleted(); - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml index edd7175b..cbcf9f46 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml @@ -54,7 +54,7 @@ diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml index c2fb1a28..e85c21d3 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml @@ -3,68 +3,113 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:mxi="using:Microsoft.Xaml.Interactivity" xmlns:shc="using:Snap.Hutao.Control" + xmlns:shcb="using:Snap.Hutao.Control.Behavior" + xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shv="using:Snap.Hutao.ViewModel" + xmlns:shvc="using:Snap.Hutao.View.Control" mc:Ignorable="d" d:DataContext="{d:DesignInstance shv:GachaLogViewModel}" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> + + + + + + 8,0,8,0 + 0 + + - - - - - - - - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs index eba53191..d3bb102c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs @@ -8,6 +8,7 @@ using CommunityToolkit.WinUI.UI; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Control; using Snap.Hutao.Control.Extension; +using Snap.Hutao.Core.IO.DataTransfer; using Snap.Hutao.Core.Threading; using Snap.Hutao.Core.Threading.CodeAnalysis; using Snap.Hutao.Factory.Abstraction; @@ -21,8 +22,6 @@ using Snap.Hutao.Service.Navigation; using Snap.Hutao.View.Dialog; using System.Collections.ObjectModel; using System.IO; -using System.Runtime.InteropServices; -using Windows.ApplicationModel.DataTransfer; using Windows.Storage; using Windows.Storage.Pickers; using Windows.Storage.Streams; @@ -398,29 +397,15 @@ internal class AchievementViewModel [ThreadAccess(ThreadAccessState.AnyThread)] private async Task GetUIAFFromClipboardAsync() { - UIAF? uiaf = null; - string json; try { - await ThreadHelper.SwitchToMainThreadAsync(); - json = await Clipboard.GetContent().GetTextAsync(); - } - catch (COMException ex) - { - infoBarService?.Error(ex); - return null; - } - - try - { - uiaf = JsonSerializer.Deserialize(json, options); + return await Clipboard.DeserializeTextAsync(options).ConfigureAwait(false); } catch (Exception ex) { infoBarService?.Error(ex); + return null; } - - return uiaf; } [ThreadAccess(ThreadAccessState.AnyThread)] @@ -452,7 +437,7 @@ internal class AchievementViewModel { MainWindow mainWindow = Ioc.Default.GetRequiredService(); await ThreadHelper.SwitchToMainThreadAsync(); - (bool isOk, ImportOption option) = await new AchievementImportDialog(mainWindow, uiaf).GetImportOptionAsync().ConfigureAwait(true); + (bool isOk, ImportStrategy strategy) = await new AchievementImportDialog(mainWindow, uiaf).GetImportStrategyAsync().ConfigureAwait(true); if (isOk) { @@ -466,7 +451,7 @@ internal class AchievementViewModel ImportResult result; await using (await importingDialog.InitializeWithWindow(mainWindow).BlockAsync().ConfigureAwait(false)) { - result = await achievementService.ImportFromUIAFAsync(archive, uiaf.List, option).ConfigureAwait(false); + result = await achievementService.ImportFromUIAFAsync(archive, uiaf.List, strategy).ConfigureAwait(false); } infoBarService.Success(result.ToString()); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs index 5f844d14..dfe37f13 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs @@ -3,13 +3,15 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Control; +using Snap.Hutao.Control.Extension; +using Snap.Hutao.Core.Threading; +using Snap.Hutao.Core.Threading.CodeAnalysis; using Snap.Hutao.Factory.Abstraction; -using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model.Binding.Gacha; using Snap.Hutao.Model.Entity; -using Snap.Hutao.Service; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.GachaLog; -using Snap.Hutao.Service.Metadata; +using Snap.Hutao.View.Dialog; using System.Collections.ObjectModel; namespace Snap.Hutao.ViewModel; @@ -20,28 +22,25 @@ namespace Snap.Hutao.ViewModel; [Injection(InjectAs.Transient)] internal class GachaLogViewModel : ObservableObject, ISupportCancellation { - private readonly IMetadataService metadataService; private readonly IGachaLogService gachaLogService; private readonly IInfoBarService infoBarService; private ObservableCollection? archives; private GachaArchive? selectedArchive; private GachaStatistics? statistics; + private bool isAggressiveRefresh; /// /// 构造一个新的祈愿记录视图模型 /// - /// 元数据服务 /// 祈愿记录服务 /// 信息 /// 异步命令工厂 public GachaLogViewModel( - IMetadataService metadataService, IGachaLogService gachaLogService, IInfoBarService infoBarService, IAsyncRelayCommandFactory asyncRelayCommandFactory) { - this.metadataService = metadataService; this.gachaLogService = gachaLogService; this.infoBarService = infoBarService; @@ -64,14 +63,30 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation /// /// 选中的存档 + /// 切换存档时异步获取对应的统计 /// - public GachaArchive? SelectedArchive { get => selectedArchive; set => SetProperty(ref selectedArchive, value); } + public GachaArchive? SelectedArchive + { + get => selectedArchive; + set + { + if (SetProperty(ref selectedArchive, value)) + { + UpdateStatisticsAsync(selectedArchive).SafeForget(); + } + } + } /// /// 当前统计信息 /// public GachaStatistics? Statistics { get => statistics; set => SetProperty(ref statistics, value); } + /// + /// 是否为贪婪刷新 + /// + public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); } + /// /// 页面加载命令 /// @@ -109,7 +124,7 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation private async Task OpenUIAsync() { - if (await metadataService.InitializeAsync().ConfigureAwait(false)) + if (await gachaLogService.InitializeAsync().ConfigureAwait(true)) { Archives = gachaLogService.GetArchiveCollection(); SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true); @@ -121,14 +136,40 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation } } - private async Task RefreshByWebCacheAsync() + private Task RefreshByWebCacheAsync() { - //Statistics = await gachaLogService.RefreshAsync(); + return RefreshInternalAsync(RefreshOption.WebCache); } - private async Task RefreshByManualInputAsync() + private Task RefreshByManualInputAsync() { + return RefreshInternalAsync(RefreshOption.ManualInput); + } + private async Task RefreshInternalAsync(RefreshOption option) + { + IGachaLogUrlProvider? provider = gachaLogService.GetGachaLogUrlProvider(option); + + if (provider != null) + { + (bool isOk, string query) = await provider.GetQueryAsync().ConfigureAwait(false); + + if (isOk) + { + RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge; + + MainWindow mainWindow = Ioc.Default.GetRequiredService(); + GachaLogRefreshProgressDialog dialog = new(mainWindow); + await using (await dialog.BlockAsync().ConfigureAwait(false)) + { + Progress progress = new(dialog.OnReport); + await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, default).ConfigureAwait(false); + + await ThreadHelper.SwitchToMainThreadAsync(); + SelectedArchive = gachaLogService.CurrentArchive; + } + } + } } private async Task ImportFromUIGFExcelAsync() @@ -150,4 +191,12 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation { } + + [ThreadAccess(ThreadAccessState.MainThread)] + private async Task UpdateStatisticsAsync(GachaArchive? archive) + { + GachaStatistics temp = await gachaLogService.GetStatisticsAsync(archive).ConfigureAwait(false); + await ThreadHelper.SwitchToMainThreadAsync(); + Statistics = temp; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs index 86280e20..f5920471 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs @@ -15,7 +15,7 @@ namespace Snap.Hutao.Web.Enka; [HttpClient(HttpClientConfigration.Default)] internal class EnkaClient { - private const string EnkaAPI = "https://enka.shinshin.moe/u/{0}/__data.json"; + private const string EnkaAPIHutaoForward = "https://enka-api.hut.ao/{0}"; private readonly HttpClient httpClient; @@ -36,6 +36,6 @@ internal class EnkaClient /// Enka API 响应 public Task GetDataAsync(PlayerUid playerUid, CancellationToken token) { - return httpClient.GetFromJsonAsync(string.Format(EnkaAPI, playerUid.Value), token); + return httpClient.GetFromJsonAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), token); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs index e5a8e846..ac2e5b59 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs @@ -68,6 +68,11 @@ internal static class ApiEndpoints #region UserFullInfo + /// + /// BBS 指向引用 + /// + public const string BbsReferer = "https://bbs.mihoyo.com/"; + /// /// 用户详细信息 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs index 4f8fb11a..237b893f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs @@ -4,7 +4,6 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Web.Response; using System.Net.Http; -using System.Net.Http.Json; namespace Snap.Hutao.Web.Hoyolab.Bbs.User; @@ -15,17 +14,20 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User; internal class UserClient { private readonly HttpClient httpClient; - private readonly JsonSerializerOptions jsonSerializerOptions; + private readonly JsonSerializerOptions options; + private readonly ILogger logger; /// /// 构造一个新的用户信息客户端 /// /// http客户端 - /// Json序列化选项 - public UserClient(HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions) + /// Json序列化选项 + /// 日志器 + public UserClient(HttpClient httpClient, JsonSerializerOptions options, ILogger logger) { this.httpClient = httpClient; - this.jsonSerializerOptions = jsonSerializerOptions; + this.options = options; + this.logger = logger; } /// @@ -37,26 +39,10 @@ internal class UserClient public async Task GetUserFullInfoAsync(Model.Binding.User user, CancellationToken token = default) { Response? resp = await httpClient - .SetUser(user) - .GetFromJsonAsync>(ApiEndpoints.UserFullInfo, jsonSerializerOptions, token) - .ConfigureAwait(false); - - return resp?.Data?.UserInfo; - } - - /// - /// 获取其他用户详细信息 - /// - /// 当前用户 - /// 米游社Uid - /// 取消令牌 - /// 详细信息 - public async Task GetUserFullInfoAsync(Model.Binding.User user, string uid, CancellationToken token = default) - { - Response? resp = await httpClient - .SetUser(user) - .GetFromJsonAsync>(ApiEndpoints.UserFullInfoQuery(uid), jsonSerializerOptions, token) - .ConfigureAwait(false); + .SetUser(user) + .SetReferer(ApiEndpoints.BbsReferer) + .TryCatchGetFromJsonAsync>(ApiEndpoints.UserFullInfo, options, logger, token) + .ConfigureAwait(false); return resp?.Data?.UserInfo; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs index b308c9a4..aad9294f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs @@ -2,52 +2,43 @@ // Licensed under the MIT license. using Snap.Hutao.Core.Convert; +using System.Text; namespace Snap.Hutao.Web.Hoyolab.DynamicSecret; /// -/// 为MiHoYo接口请求器 提供2代动态密钥 +/// 为MiHoYo接口请求器 提供动态密钥 /// internal abstract class DynamicSecretProvider : Md5Convert { + private const string RandomRange = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + /// /// 创建动态密钥 /// - /// json格式化器 - /// 查询url - /// 请求体 /// 密钥 - public static string Create(JsonSerializerOptions options, string queryUrl, object? postBody = null) + public static string Create() { // unix timestamp long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - // random - int r = GetRandom(); + string r = GetRandomString(); - // body - string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options); - - // query - string q = string.Join("&", new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x)); - - // check - string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecretSalt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant(); + string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret1Salt}&t={t}&r={r}").ToLowerInvariant(); return $"{t},{r},{check}"; } - private static int GetRandom() + private static string GetRandomString() { - // 原汁原味 - // v16 = time(0LL); - // srand(v16); - // v17 = (int)((double)rand() / 2147483650.0 * 100000.0 + 100000.0) % 1000000; - // if (v17 >= 100001) - // v18 = v17; - // else - // v18 = v17 + 542367; - int rand = Random.Shared.Next(100000, 200000); - return rand == 100000 ? 642367 : rand; + StringBuilder sb = new(6); + + for (int i = 0; i < 6; i++) + { + int pos = Random.Shared.Next(0, RandomRange.Length); + sb.Append(RandomRange[pos]); + } + + return sb.ToString(); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs new file mode 100644 index 00000000..18f71349 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs @@ -0,0 +1,53 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Convert; + +namespace Snap.Hutao.Web.Hoyolab.DynamicSecret; + +/// +/// 为MiHoYo接口请求器 提供2代动态密钥 +/// +internal abstract class DynamicSecretProvider2 : Md5Convert +{ + /// + /// 创建动态密钥 + /// + /// json格式化器 + /// 查询url + /// 请求体 + /// 密钥 + public static string Create(JsonSerializerOptions options, string queryUrl, object? postBody = null) + { + // unix timestamp + long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + // random + int r = GetRandom(); + + // body + string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options); + + // query + string q = string.Join("&", new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x)); + + // check + string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret2Salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant(); + + return $"{t},{r},{check}"; + } + + private static int GetRandom() + { + // 原汁原味 + // v16 = time(0LL); + // srand(v16); + // v17 = (int)((double)rand() / 2147483650.0 * 100000.0 + 100000.0) % 1000000; + // if (v17 >= 100001) + // v18 = v17; + // else + // v18 = v17 + 542367; + int rand = Random.Shared.Next(100000, 200000); + return rand == 100000 ? 642367 : rand; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/DynamicSecretHttpClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/DynamicSecretHttpClient.cs index 7dd31c13..90f82b59 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/DynamicSecretHttpClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/Http/DynamicSecretHttpClient.cs @@ -1,9 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Web.Request; using System.Net.Http; using System.Net.Http.Json; -using Snap.Hutao.Web.Request; namespace Snap.Hutao.Web.Hoyolab.DynamicSecret.Http; @@ -30,7 +30,7 @@ internal class DynamicSecretHttpClient : IDynamicSecretHttpClient this.options = options; this.url = url; - httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider.Create(options, url, null)); + httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider2.Create(options, url, null)); } /// @@ -67,7 +67,7 @@ internal class DynamicSecretHttpClient : IDynamicSecretHttpClient diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs index 40e03aa3..a255b28b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Web.Hoyolab.DynamicSecret.Http; +using Snap.Hutao.Web.Request; using System.Net.Http; namespace Snap.Hutao.Web.Hoyolab.DynamicSecret; @@ -11,6 +12,17 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret; /// internal static class HttpClientDynamicSecretExtensions { + /// + /// 使用一代动态密钥执行 GET 操作 + /// + /// 请求器 + /// 响应 + public static HttpClient UsingDynamicSecret(this HttpClient httpClient) + { + httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider.Create()); + return httpClient; + } + /// /// 使用二代动态密钥执行 GET 操作 /// 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 e9d6ea01..f2f05a8d 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 @@ -11,25 +11,30 @@ public enum GachaConfigType /// /// 新手池 /// + [Description("新手祈愿")] NoviceWish = 100, /// /// 常驻池 /// + [Description("常驻祈愿")] PermanentWish = 200, /// /// 角色1池 /// + [Description("角色活动祈愿")] AvatarEventWish = 301, /// /// 武器池 /// + [Description("武器活动祈愿")] WeaponEventWish = 302, /// /// 角色2池 /// + [Description("角色活动祈愿-2")] AvatarEventWish2 = 400, } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogConfigration.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogConfigration.cs index 5c56d8e6..8d52732a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogConfigration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogConfigration.cs @@ -10,6 +10,11 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; /// public struct GachaLogConfigration { + /// + /// 尺寸 + /// + public const int Size = 20; + private readonly QueryString innerQuery; /// @@ -18,37 +23,24 @@ public struct GachaLogConfigration /// 原始查询字符串 /// 祈愿类型 /// 终止Id - public GachaLogConfigration(string query, GachaConfigType type, ulong endId = 0UL) + public GachaLogConfigration(string query, GachaConfigType type, long endId = 0L) { innerQuery = QueryString.Parse(query); - innerQuery.Set("lang", "zh-cn"); - Size = 20; - Type = type; + innerQuery.Set("lang", "zh-cn"); + innerQuery.Set("gacha_type", (int)type); + innerQuery.Set("size", Size); + EndId = endId; } - /// - /// 尺寸 - /// - public int Size - { - set => innerQuery.Set("size", value); - } - - /// - /// 类型 - /// - public GachaConfigType Type - { - set => innerQuery.Set("gacha_type", (int)value); - } - /// /// 结束Id + /// 控制API返回的分页 /// - public ulong EndId + public long EndId { + get => long.Parse(innerQuery["end_id"]); set => innerQuery.Set("end_id", value); } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientCookieExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientCookieExtensions.cs deleted file mode 100644 index 8137c8d9..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientCookieExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Model.Binding; -using Snap.Hutao.Web.Request; -using System.Net.Http; - -namespace Snap.Hutao.Web.Hoyolab; - -/// -/// 扩展 -/// -internal static class HttpClientCookieExtensions -{ - /// - /// 设置用户的Cookie - /// - /// http客户端 - /// 用户 - /// 客户端 - internal static HttpClient SetUser(this HttpClient httpClient, User user) - { - httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie); - return httpClient; - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs new file mode 100644 index 00000000..2788b96d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs @@ -0,0 +1,55 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Logging; +using Snap.Hutao.Model.Binding; +using Snap.Hutao.Web.Request; +using System.Net.Http; +using System.Net.Http.Json; + +namespace Snap.Hutao.Web.Hoyolab; + +/// +/// 扩展 +/// +internal static class HttpClientExtensions +{ + /// + internal static async Task TryCatchGetFromJsonAsync(this HttpClient httpClient, string requestUri, JsonSerializerOptions options, ILogger logger, CancellationToken token = default) + where T : class + { + try + { + return await httpClient.GetFromJsonAsync(requestUri, options, token).ConfigureAwait(false); + } + catch (HttpRequestException ex) + { + logger.LogWarning(EventIds.HttpException, ex, "请求异常已忽略"); + return null; + } + } + + /// + /// 设置用户的Cookie + /// + /// http客户端 + /// 用户 + /// 客户端 + internal static HttpClient SetUser(this HttpClient httpClient, User user) + { + httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie); + return httpClient; + } + + /// + /// 设置Referer + /// + /// http客户端 + /// 用户 + /// 客户端 + internal static HttpClient SetReferer(this HttpClient httpClient, string referer) + { + httpClient.DefaultRequestHeaders.Set("Referer", referer); + return httpClient; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRoleClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRoleClient.cs index 40120632..b6a66004 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRoleClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/UserGameRoleClient.cs @@ -6,7 +6,6 @@ using Snap.Hutao.Extension; using Snap.Hutao.Model.Binding; using Snap.Hutao.Web.Response; using System.Net.Http; -using System.Net.Http.Json; namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding; @@ -17,15 +16,21 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding; internal class UserGameRoleClient { private readonly HttpClient httpClient; + private readonly JsonSerializerOptions options; + private readonly ILogger logger; /// /// 构造一个新的用户游戏角色提供器 /// /// 用户服务 /// 请求器 - public UserGameRoleClient(HttpClient httpClient) + /// Json序列化选项 + /// 日志器 + public UserGameRoleClient(HttpClient httpClient, JsonSerializerOptions options, ILogger logger) { this.httpClient = httpClient; + this.options = options; + this.logger = logger; } /// @@ -38,9 +43,9 @@ internal class UserGameRoleClient { Response>? resp = await httpClient .SetUser(user) - .GetFromJsonAsync>>(ApiEndpoints.UserGameRoles, token) + .TryCatchGetFromJsonAsync>>(ApiEndpoints.UserGameRoles, options, logger, token) .ConfigureAwait(false); return EnumerableExtensions.EmptyIfNull(resp?.Data?.List); } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs index a62bba78..fb756ade 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs @@ -44,7 +44,6 @@ public struct QueryString /// /// /// The query string to deserialize. - /// This should NOT have a leading ? character. /// Valid input would be something like "a=1&b=5". /// URL decoding of keys/values is automatically performed. /// Also supports query strings that are serialized using ; instead of &, like "a=1;b=5" @@ -56,6 +55,9 @@ public struct QueryString return new QueryString(); } + int questionMarkIndex = queryString.IndexOf('?'); + queryString = queryString[(questionMarkIndex + 1)..]; + string[] pairs = queryString.Split('&', ';'); QueryString answer = new(); foreach (string pair in pairs) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index dcfde2fe..47f8e15f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -39,15 +39,20 @@ public enum KnownReturnCode : int RET_NEED_AIGIS = -3101, /// - /// 尚未登录 + /// 访问过于频繁 /// - RET_TOKEN_INVALID = -100, + VIsitTooFrequently = -110, /// /// 验证密钥过期 /// AuthKeyTimeOut = -101, + /// + /// 尚未登录 + /// + RET_TOKEN_INVALID = -100, + /// /// Ok ///