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
///