From fda642b72fb6a8b157e539a7a84b9a91af641f16 Mon Sep 17 00:00:00 2001
From: DismissedLight <1686188646@qq.com>
Date: Fri, 21 Oct 2022 16:40:10 +0800
Subject: [PATCH] hutao api v2 page
---
src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 8 +-
.../Snap.Hutao/Control/ScopedPage.cs | 3 +
.../Snap.Hutao/Core/CoreEnvironment.cs | 21 ++
.../Core/Exception/ExceptionRecorder.cs | 10 +-
.../Factory/AsyncRelayCommandFactory.cs | 9 +-
.../Model/Binding/Hutao/ComplexAvatar.cs | 47 +++
.../Binding/Hutao/ComplexAvatarCollocation.cs | 39 +++
.../Hutao/ComplexAvatarConstellationInfo.cs | 29 ++
.../Model/Binding/Hutao/ComplexAvatarRank.cs | 20 ++
.../Binding/Hutao/ComplexReliquarySet.cs | 66 ++++
.../Model/Binding/Hutao/ComplexTeamRank.cs | 40 +++
.../Model/Binding/Hutao/ComplexWeapon.cs | 47 +++
.../Snap.Hutao/Model/Binding/Hutao/Team.cs | 36 ++
.../Model/InterChange/GachaLog/UIGFInfo.cs | 1 +
.../Model/Metadata/Avatar/AvatarIds.cs | 2 +
.../Model/Metadata/Reliquary/ReliquarySet.cs | 21 +-
.../Snap.Hutao/Package.appxmanifest | 2 +-
src/Snap.Hutao/Snap.Hutao/Program.cs | 1 +
.../Resource/Icon/UI_ChapterIcon_Hutao.png | Bin 0 -> 13015 bytes
.../Service/Abstraction/IHutaoService.cs | 36 ++
.../Snap.Hutao/Service/AppCenter/AppCenter.cs | 95 ++++++
.../Service/AppCenter/DeviceHelper.cs | 64 ++++
.../AppCenter/Model/AppCenterException.cs | 20 ++
.../Service/AppCenter/Model/Device.cs | 53 +++
.../Service/AppCenter/Model/Log/EventLog.cs | 22 ++
.../AppCenter/Model/Log/HandledErrorLog.cs | 23 ++
.../Service/AppCenter/Model/Log/Log.cs | 23 ++
.../AppCenter/Model/Log/LogContainer.cs | 16 +
.../AppCenter/Model/Log/LogConverter.cs | 22 ++
.../Service/AppCenter/Model/Log/LogHelper.cs | 46 +++
.../Service/AppCenter/Model/Log/LogStatus.cs | 13 +
.../AppCenter/Model/Log/ManagedErrorLog.cs | 51 +++
.../Service/AppCenter/Model/Log/PageLog.cs | 19 ++
.../AppCenter/Model/Log/PropertiesLog.cs | 11 +
.../AppCenter/Model/Log/StartServiceLog.cs | 19 ++
.../AppCenter/Model/Log/StartSessionLog.cs | 11 +
.../AppCenter/Model/LogUploadResult.cs | 20 ++
.../GachaLogUrlManualInputProvider.cs | 23 +-
.../UrlProvider/GachaLogUrlStokenProvider.cs | 18 +-
.../GachaLogUrlWebCacheProvider.cs | 14 +-
.../Game/Locator/RegistryLauncherLocator.cs | 2 +-
.../Snap.Hutao/Service/HutaoService.cs | 53 ++-
.../Service/Metadata/IMetadataService.cs | 14 +
.../MetadataService.Implementation.cs | 43 +--
.../Metadata/MetadataService.Indexing.cs | 57 ++++
.../Snap.Hutao/Service/User/UserService.cs | 2 +-
src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 25 +-
.../View/Dialog/GachaLogUrlDialog.xaml | 19 ++
.../View/Dialog/GachaLogUrlDialog.xaml.cs | 36 ++
src/Snap.Hutao/Snap.Hutao/View/MainView.xaml | 7 +-
.../View/Page/HutaoDatabasePage.xaml | 309 ++++++++++++++++++
.../View/Page/HutaoDatabasePage.xaml.cs | 22 ++
.../Snap.Hutao/ViewModel/GachaLogViewModel.cs | 7 +
.../ViewModel/HutaoDatabaseViewModel.cs | 151 +++++++++
.../Snap.Hutao/Web/Hoyolab/Cookie.cs | 2 +-
.../Web/Hoyolab/HttpClientExtensions.cs | 15 +
.../Snap.Hutao/Web/Hutao/HomaClient.cs | 30 +-
.../Model/Converter/ReliquarySetsConverter.cs | 2 +-
.../Web/Hutao/Model/ReliquarySet.cs | 6 +-
.../Web/Hutao/Model/TeamAppearance.cs | 6 +
60 files changed, 1745 insertions(+), 84 deletions(-)
create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatar.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarCollocation.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarConstellationInfo.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarRank.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexReliquarySet.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexTeamRank.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexWeapon.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/Team.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ChapterIcon_Hutao.png
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/AppCenter.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/DeviceHelper.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/AppCenterException.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Device.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/EventLog.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/HandledErrorLog.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/Log.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogContainer.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogConverter.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogHelper.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogStatus.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/ManagedErrorLog.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/PageLog.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/PropertiesLog.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/StartServiceLog.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/StartSessionLog.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/LogUploadResult.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Indexing.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePage.xaml
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePage.xaml.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
index 6974a34c57..fa75ecd0e3 100644
--- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
@@ -9,6 +9,7 @@
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
+using Snap.Hutao.Service.AppCenter;
using Snap.Hutao.Service.Metadata;
using System.Diagnostics;
using Windows.Storage;
@@ -27,13 +28,14 @@ public partial class App : Application
/// Initializes the singleton application object.
///
/// 日志器
- public App(ILogger logger)
+ /// App Center
+ public App(ILogger logger, AppCenter appCenter)
{
// load app resource
InitializeComponent();
this.logger = logger;
- _ = new ExceptionRecorder(this, logger);
+ _ = new ExceptionRecorder(this, logger, appCenter);
}
///
@@ -57,6 +59,8 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args)
.ImplictAs()?
.InitializeInternalAsync()
.SafeForget(logger);
+
+ Ioc.Default.GetRequiredService().Initialize();
}
else
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs b/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs
index f1d37b92ec..88b6d70b93 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs
@@ -11,6 +11,9 @@ namespace Snap.Hutao.Control;
///
/// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知
+///
+/// InitializeWith{T}();
+/// InitializeComponent();
///
public class ScopedPage : Page
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
index 54e38e48d4..5378799a40 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
@@ -1,7 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
+using Microsoft.Win32;
using Snap.Hutao.Extension;
+using System.Security.Cryptography;
+using System.Text;
using System.Text.Encodings.Web;
using Windows.ApplicationModel;
@@ -12,6 +15,9 @@ namespace Snap.Hutao.Core;
///
internal static class CoreEnvironment
{
+ private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
+ private const string MachineGuidValue = "MachineGuid";
+
// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
///
@@ -49,6 +55,11 @@ internal static class CoreEnvironment
///
public static readonly string HoyolabDeviceId;
+ ///
+ /// AppCenter 设备Id
+ ///
+ public static readonly string AppCenterDeviceId;
+
///
/// 默认的Json序列化选项
///
@@ -67,5 +78,15 @@ static CoreEnvironment()
// simply assign a random guid
HoyolabDeviceId = Guid.NewGuid().ToString();
+ AppCenterDeviceId = GetUniqueUserID();
+ }
+
+ private static string GetUniqueUserID()
+ {
+ string userName = Environment.UserName;
+ object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
+ byte[] bytes = Encoding.UTF8.GetBytes($"{userName}{machineGuid}");
+ byte[] hash = MD5.Create().ComputeHash(bytes);
+ return System.Convert.ToHexString(hash);
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Exception/ExceptionRecorder.cs b/src/Snap.Hutao/Snap.Hutao/Core/Exception/ExceptionRecorder.cs
index 6bd7d8ecbb..f9fe94b7c0 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Exception/ExceptionRecorder.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Exception/ExceptionRecorder.cs
@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
+using Snap.Hutao.Service.AppCenter;
namespace Snap.Hutao.Core.Exception;
@@ -12,15 +13,18 @@ namespace Snap.Hutao.Core.Exception;
internal class ExceptionRecorder
{
private readonly ILogger logger;
+ private readonly AppCenter appCenter;
///
/// 构造一个新的异常记录器
///
/// 应用程序
/// 日志器
- public ExceptionRecorder(Application application, ILogger logger)
+ /// App Center
+ public ExceptionRecorder(Application application, ILogger logger, AppCenter appCenter)
{
this.logger = logger;
+ this.appCenter = appCenter;
application.UnhandledException += OnAppUnhandledException;
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
@@ -28,9 +32,7 @@ public ExceptionRecorder(Application application, ILogger logger)
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
- // string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
- // string fileName = $"ex-{DateTimeOffset.Now:yyyyMMddHHmmssffff}.txt";
- // File.WriteAllText(Path.Combine(path, fileName), $"{e.Exception}\r\n{e.Exception.StackTrace}");
+ appCenter.TrackCrash(e.Exception);
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService>())
diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs
index 9c1c159d4d..2ffc73b63b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs
@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Factory.Abstraction;
+using Snap.Hutao.Service.AppCenter;
namespace Snap.Hutao.Factory;
@@ -11,15 +12,18 @@ namespace Snap.Hutao.Factory;
[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
- private readonly ILogger logger;
+ private readonly ILogger logger;
+ private readonly AppCenter appCenter;
///
/// 构造一个新的异步命令工厂
///
/// 日志器
- public AsyncRelayCommandFactory(ILogger logger)
+ /// App Center
+ public AsyncRelayCommandFactory(ILogger logger, AppCenter appCenter)
{
this.logger = logger;
+ this.appCenter = appCenter;
}
///
@@ -94,6 +98,7 @@ private void ReportException(IAsyncRelayCommand command)
{
Exception baseException = exception.GetBaseException();
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
+ appCenter.TrackError(exception);
}
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatar.cs
new file mode 100644
index 0000000000..8b2867297d
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatar.cs
@@ -0,0 +1,47 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Intrinsic;
+using Snap.Hutao.Model.Metadata.Avatar;
+using Snap.Hutao.Model.Metadata.Converter;
+
+namespace Snap.Hutao.Model.Binding.Hutao;
+
+///
+/// 角色
+///
+internal class ComplexAvatar
+{
+ ///
+ /// 构造一个胡桃数据库角色
+ ///
+ /// 元数据角色
+ /// 率
+ public ComplexAvatar(Avatar avatar, double rate)
+ {
+ Name = avatar.Name;
+ Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
+ Quality = avatar.Quality;
+ Rate = $"{rate:P3}";
+ }
+
+ ///
+ /// 名称
+ ///
+ public string Name { get; set; } = default!;
+
+ ///
+ /// 图标
+ ///
+ public Uri Icon { get; set; } = default!;
+
+ ///
+ /// 星级
+ ///
+ public ItemQuality Quality { get; set; }
+
+ ///
+ /// 比率
+ ///
+ public string Rate { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarCollocation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarCollocation.cs
new file mode 100644
index 0000000000..f0557f7abe
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarCollocation.cs
@@ -0,0 +1,39 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Intrinsic;
+using Snap.Hutao.Model.Metadata.Avatar;
+using Snap.Hutao.Model.Metadata.Converter;
+
+namespace Snap.Hutao.Model.Binding.Hutao;
+
+///
+/// 角色搭配
+///
+internal class ComplexAvatarCollocation : ComplexAvatar
+{
+ ///
+ /// 构造一个新的角色搭配
+ ///
+ /// 角色
+ /// 比率
+ public ComplexAvatarCollocation(Avatar avatar)
+ : base(avatar, 0)
+ {
+ }
+
+ ///
+ /// 角色
+ ///
+ public List Avatars { get; set; } = default!;
+
+ ///
+ /// 武器
+ ///
+ public List Weapons { get; set; } = default!;
+
+ ///
+ /// 圣遗物套装
+ ///
+ public List ReliquarySets { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarConstellationInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarConstellationInfo.cs
new file mode 100644
index 0000000000..758d4b9e35
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarConstellationInfo.cs
@@ -0,0 +1,29 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Metadata.Avatar;
+
+namespace Snap.Hutao.Model.Binding.Hutao;
+
+///
+/// 角色命座信息
+///
+internal class ComplexAvatarConstellationInfo : ComplexAvatar
+{
+ ///
+ /// 构造一个新的角色命座信息
+ ///
+ /// 角色
+ /// 持有率
+ /// 命座比率
+ public ComplexAvatarConstellationInfo(Avatar avatar, double rate, IEnumerable rates)
+ : base(avatar, rate)
+ {
+ Rates = rates.Select(r => $"{r:P3}").ToList();
+ }
+
+ ///
+ /// 命座比率
+ ///
+ public List Rates { get; set; }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarRank.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarRank.cs
new file mode 100644
index 0000000000..0973c0bc2b
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexAvatarRank.cs
@@ -0,0 +1,20 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Binding.Hutao;
+
+///
+/// 角色榜
+///
+internal class ComplexAvatarRank
+{
+ ///
+ /// 层数
+ ///
+ public string Floor { get; set; } = default!;
+
+ ///
+ /// 排行信息
+ ///
+ public List Avatars { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexReliquarySet.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexReliquarySet.cs
new file mode 100644
index 0000000000..b6c7d08925
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexReliquarySet.cs
@@ -0,0 +1,66 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Metadata.Converter;
+using Snap.Hutao.Web.Hutao.Model;
+using System.Text;
+
+namespace Snap.Hutao.Model.Binding.Hutao;
+
+///
+/// 圣遗物套装
+///
+internal class ComplexReliquarySet
+{
+ ///
+ /// 构造一个新的胡桃数据库圣遗物套装
+ ///
+ /// 圣遗物套装率
+ /// 圣遗物套装映射
+ public ComplexReliquarySet(ItemRate reliquarySetRate, Dictionary idReliquarySetMap)
+ {
+ ReliquarySets sets = reliquarySetRate.Item;
+
+ if (sets.Count >= 1)
+ {
+ StringBuilder setStringBuilder = new();
+ List icons = new();
+ foreach (ReliquarySet set in sets)
+ {
+ Metadata.Reliquary.ReliquarySet metaSet = idReliquarySetMap[set.EquipAffixId / 10];
+
+ if (setStringBuilder.Length != 0)
+ {
+ setStringBuilder.Append(Environment.NewLine);
+ }
+
+ setStringBuilder.Append(set.Count).Append('×').Append(metaSet.Name);
+ icons.Add(RelicIconConverter.IconNameToUri(metaSet.Icon));
+ }
+
+ Name = setStringBuilder.ToString();
+ Icons = icons;
+ }
+ else
+ {
+ Name = "无圣遗物";
+ }
+
+ Rate = $"{reliquarySetRate.Rate:P3}";
+ }
+
+ ///
+ /// 名称
+ ///
+ public string Name { get; set; } = default!;
+
+ ///
+ /// 图标
+ ///
+ public List Icons { get; set; } = default!;
+
+ ///
+ /// 比率
+ ///
+ public string Rate { get; set; } = default!;
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexTeamRank.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexTeamRank.cs
new file mode 100644
index 0000000000..8d2ce83c0a
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexTeamRank.cs
@@ -0,0 +1,40 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Metadata.Avatar;
+using Snap.Hutao.Web.Hutao.Model;
+
+namespace Snap.Hutao.Model.Binding.Hutao;
+
+///
+/// 队伍排行
+///
+internal class ComplexTeamRank
+{
+ ///
+ /// 构造一个新的队伍排行
+ ///
+ /// 队伍排行
+ /// 映射
+ public ComplexTeamRank(TeamAppearance teamRank, Dictionary idAvatarMap)
+ {
+ Floor = $"第 {teamRank.Floor} 层";
+ Up = teamRank.Up.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList();
+ Down = teamRank.Down.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList();
+ }
+
+ ///
+ /// 层数
+ ///
+ public string Floor { get; set; } = default!;
+
+ ///
+ /// 上半阵容
+ ///
+ public List Up { get; set; } = default!;
+
+ ///
+ /// 下半阵容
+ ///
+ public List Down { get; set; } = default!;
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexWeapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexWeapon.cs
new file mode 100644
index 0000000000..1a8b6ae0f9
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/ComplexWeapon.cs
@@ -0,0 +1,47 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Intrinsic;
+using Snap.Hutao.Model.Metadata.Converter;
+using Snap.Hutao.Model.Metadata.Weapon;
+
+namespace Snap.Hutao.Model.Binding.Hutao;
+
+///
+/// 胡桃数据库武器
+///
+internal class ComplexWeapon
+{
+ ///
+ /// 构造一个胡桃数据库武器
+ ///
+ /// 元数据武器
+ /// 率
+ public ComplexWeapon(Weapon weapon, double rate)
+ {
+ Name = weapon.Name;
+ Icon = EquipIconConverter.IconNameToUri(weapon.Icon);
+ Quality = weapon.Quality;
+ Rate = $"{rate:P3}";
+ }
+
+ ///
+ /// 名称
+ ///
+ public string Name { get; set; } = default!;
+
+ ///
+ /// 图标
+ ///
+ public Uri Icon { get; set; } = default!;
+
+ ///
+ /// 星级
+ ///
+ public ItemQuality Quality { get; set; }
+
+ ///
+ /// 比率
+ ///
+ public string Rate { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/Team.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/Team.cs
new file mode 100644
index 0000000000..66f8ef5287
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/Team.cs
@@ -0,0 +1,36 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Metadata.Avatar;
+using Snap.Hutao.Web.Hutao.Model;
+
+namespace Snap.Hutao.Model.Binding.Hutao;
+
+///
+/// 队伍
+///
+internal class Team : List
+{
+ ///
+ /// 构造一个新的队伍
+ ///
+ /// 队伍
+ /// 映射
+ public Team(ItemRate team, Dictionary idAvatarMap)
+ : base(4)
+ {
+ IEnumerable ids = team.Item.Split(',').Select(i => int.Parse(i));
+
+ foreach (int id in ids)
+ {
+ Add(new(idAvatarMap[id], 0));
+ }
+
+ Rate = $"上场 {team.Rate} 次";
+ }
+
+ ///
+ /// 上场次数
+ ///
+ public string Rate { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs
index ec7f6f278c..144c78c6d0 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs
@@ -27,6 +27,7 @@ public class UIGFInfo
/// 导出的时间戳
///
[JsonPropertyName("export_timestamp")]
+ [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long? ExportTimestamp { get; set; }
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs
index 42b2899e4c..d77013532d 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs
@@ -71,4 +71,6 @@ public static class AvatarIds
public const int Nilou = 10000070;
public const int Cyno = 10000071;
public const int Candace = 10000072;
+ public const int Nahida = 10000073;
+ public const int Layla = 10000074;
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquarySet.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquarySet.cs
index 3f711fe5b8..4cc90bffd4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquarySet.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquarySet.cs
@@ -11,15 +11,30 @@ public class ReliquarySet
///
/// 套装Id
///
- public int SetId { get; set; } = default!;
+ public int SetId { get; set; }
+
+ ///
+ /// 装备被动Id
+ ///
+ public int EquipAffixId { get; set; }
+
+ ///
+ /// 套装名称
+ ///
+ public string Name { get; set; } = default!;
+
+ ///
+ /// 套装图标
+ ///
+ public string Icon { get; set; } = default!;
///
/// 需要的数量
///
- public IEnumerable NeedNumber { get; set; } = default!;
+ public List NeedNumber { get; set; } = default!;
///
/// 描述
///
- public IEnumerable Descriptions { get; set; } = default!;
+ public List Descriptions { get; set; } = default!;
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
index 5f25f93ca7..1a04cd7100 100644
--- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
+++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
@@ -9,7 +9,7 @@
+ Version="1.1.13.0" />
胡桃
diff --git a/src/Snap.Hutao/Snap.Hutao/Program.cs b/src/Snap.Hutao/Snap.Hutao/Program.cs
index 486050da25..aa64a0a54c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Program.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Program.cs
@@ -20,6 +20,7 @@ public static partial class Program
///
/// 主线程队列
///
+ [Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[SuppressMessage("", "SA1401")]
internal static volatile DispatcherQueue? DispatcherQueue;
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ChapterIcon_Hutao.png b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ChapterIcon_Hutao.png
new file mode 100644
index 0000000000000000000000000000000000000000..bb1ec9f1222c087d28566997c07912169dbe61bb
GIT binary patch
literal 13015
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
@@ -8,5 +10,39 @@ namespace Snap.Hutao.Service.Abstraction;
///
internal interface IHutaoService
{
+ ///
+ /// 异步获取角色上场率
+ ///
+ /// 角色上场率
+ ValueTask> GetAvatarAppearanceRanksAsync();
+
+ ///
+ /// 异步获取角色搭配
+ ///
+ /// 角色搭配
+ ValueTask> GetAvatarCollocationsAsync();
+
+ ///
+ /// 异步获取角色持有率信息
+ ///
+ /// 角色持有率信息
+ ValueTask> GetAvatarConstellationInfosAsync();
+
+ ///
+ /// 异步获取角色使用率
+ ///
+ /// 角色使用率
+ ValueTask> GetAvatarUsageRanksAsync();
+
+ ///
+ /// 异步获取统计数据
+ ///
+ /// 统计数据
+ ValueTask GetOverviewAsync();
+ ///
+ /// 异步获取队伍上场
+ ///
+ /// 队伍上场
+ ValueTask> GetTeamAppearancesAsync();
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/AppCenter.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/AppCenter.cs
new file mode 100644
index 0000000000..042cebd6ce
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/AppCenter.cs
@@ -0,0 +1,95 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Core;
+using Snap.Hutao.Core.Threading;
+using Snap.Hutao.Service.AppCenter.Model;
+using Snap.Hutao.Service.AppCenter.Model.Log;
+using Snap.Hutao.Web.Hoyolab;
+using System.Net.Http;
+
+namespace Snap.Hutao.Service.AppCenter;
+
+[SuppressMessage("", "SA1600")]
+[Injection(InjectAs.Singleton)]
+public sealed class AppCenter : IDisposable
+{
+ private const string AppSecret = "de5bfc48-17fc-47ee-8e7e-dee7dc59d554";
+ private const string API = "https://in.appcenter.ms/logs?api-version=1.0.0";
+
+ private readonly TaskCompletionSource uploadTaskCompletionSource = new();
+ private readonly CancellationTokenSource uploadTaskCancllationTokenSource = new();
+ private readonly HttpClient httpClient;
+ private readonly List queue;
+ private readonly Device deviceInfo;
+ private readonly JsonSerializerOptions options;
+
+ private Guid sessionID;
+
+ public AppCenter()
+ {
+ options = new(CoreEnvironment.JsonOptions);
+ options.Converters.Add(new LogConverter());
+
+ httpClient = new() { DefaultRequestHeaders = { { "Install-ID", CoreEnvironment.AppCenterDeviceId }, { "App-Secret", AppSecret } } };
+ queue = new List();
+ deviceInfo = new Device();
+ Task.Run(async () =>
+ {
+ while (!uploadTaskCancllationTokenSource.Token.IsCancellationRequested)
+ {
+ await UploadAsync().ConfigureAwait(false);
+ await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
+ }
+
+ uploadTaskCompletionSource.TrySetResult();
+ }).SafeForget();
+ }
+
+ public async Task UploadAsync()
+ {
+ if (queue.Count == 0)
+ {
+ return;
+ }
+
+ string? uploadStatus = null;
+ do
+ {
+ queue.ForEach(log => log.Status = LogStatus.Uploading);
+ LogContainer container = new(queue);
+
+ LogUploadResult? response = await httpClient
+ .TryCatchPostAsJsonAsync(API, container, options)
+ .ConfigureAwait(false);
+ uploadStatus = response?.Status;
+ }
+ while (uploadStatus != "Success");
+
+ queue.RemoveAll(log => log.Status == LogStatus.Uploading);
+ }
+
+ public void Initialize()
+ {
+ sessionID = Guid.NewGuid();
+ queue.Add(new StartServiceLog("Analytics", "Crashes").Initialize(sessionID, deviceInfo));
+ queue.Add(new StartSessionLog().Initialize(sessionID, deviceInfo).Initialize(sessionID, deviceInfo));
+ }
+
+ public void TrackCrash(Exception exception, bool isFatal = true)
+ {
+ queue.Add(new ManagedErrorLog(exception, isFatal).Initialize(sessionID, deviceInfo));
+ }
+
+ public void TrackError(Exception exception)
+ {
+ queue.Add(new HandledErrorLog(exception).Initialize(sessionID, deviceInfo));
+ }
+
+ [SuppressMessage("", "VSTHRD002")]
+ public void Dispose()
+ {
+ uploadTaskCancllationTokenSource.Cancel();
+ uploadTaskCompletionSource.Task.GetAwaiter().GetResult();
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/DeviceHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/DeviceHelper.cs
new file mode 100644
index 0000000000..a45b48c08b
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/DeviceHelper.cs
@@ -0,0 +1,64 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Windowing;
+using Microsoft.Win32;
+using Windows.Graphics;
+
+namespace Snap.Hutao.Service.AppCenter;
+
+///
+/// 设备帮助类
+///
+[SuppressMessage("", "SA1600")]
+public static class DeviceHelper
+{
+ private static readonly RegistryKey? BiosKey = Registry.LocalMachine.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\BIOS");
+ private static readonly RegistryKey? GeoKey = Registry.CurrentUser.OpenSubKey("Control Panel\\International\\Geo");
+ private static readonly RegistryKey? CurrentVersionKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
+
+ public static string? GetOem()
+ {
+ string? oem = BiosKey?.GetValue("SystemManufacturer") as string;
+ return oem == "System manufacturer" ? null : oem;
+ }
+
+ public static string? GetModel()
+ {
+ string? model = BiosKey?.GetValue("SystemProductName") as string;
+ return model == "System Product Name" ? null : model;
+ }
+
+ public static string GetScreenSize()
+ {
+ RectInt32 screen = DisplayArea.Primary.OuterBounds;
+ return $"{screen.Width}x{screen.Height}";
+ }
+
+ public static string? GetCountry()
+ {
+ return GeoKey?.GetValue("Name") as string;
+ }
+
+ public static string GetSystemVersion()
+ {
+ object? majorVersion = CurrentVersionKey?.GetValue("CurrentMajorVersionNumber");
+ if (majorVersion != null)
+ {
+ object? minorVersion = CurrentVersionKey?.GetValue("CurrentMinorVersionNumber", "0");
+ object? buildNumber = CurrentVersionKey?.GetValue("CurrentBuildNumber", "0");
+ return $"{majorVersion}.{minorVersion}.{buildNumber}";
+ }
+ else
+ {
+ object? version = CurrentVersionKey?.GetValue("CurrentVersion", "0.0");
+ object? buildNumber = CurrentVersionKey?.GetValue("CurrentBuild", "0");
+ return $"{version}.{buildNumber}";
+ }
+ }
+
+ public static int GetSystemBuild()
+ {
+ return (int)(CurrentVersionKey?.GetValue("UBR") ?? 0);
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/AppCenterException.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/AppCenterException.cs
new file mode 100644
index 0000000000..97f30077a6
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/AppCenterException.cs
@@ -0,0 +1,20 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model;
+
+[SuppressMessage("", "SA1600")]
+public class AppCenterException
+{
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = "UnknownType";
+
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+
+ [JsonPropertyName("stackTrace")]
+ public string? StackTrace { get; set; }
+
+ [JsonPropertyName("innerExceptions")]
+ public List? InnerExceptions { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Device.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Device.cs
new file mode 100644
index 0000000000..e92c28808e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Device.cs
@@ -0,0 +1,53 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Core;
+using System.Globalization;
+
+namespace Snap.Hutao.Service.AppCenter.Model;
+
+[SuppressMessage("", "SA1600")]
+public class Device
+{
+ [JsonPropertyName("sdkName")]
+ public string SdkName { get; set; } = "appcenter.winui";
+
+ [JsonPropertyName("sdkVersion")]
+ public string SdkVersion { get; set; } = "4.5.0";
+
+ [JsonPropertyName("osName")]
+ public string OsName { get; set; } = "WINDOWS";
+
+ [JsonPropertyName("osVersion")]
+ public string OsVersion { get; set; } = DeviceHelper.GetSystemVersion();
+
+ [JsonPropertyName("osBuild")]
+ public string OsBuild { get; set; } = $"{DeviceHelper.GetSystemVersion()}.{DeviceHelper.GetSystemBuild()}";
+
+ [JsonPropertyName("model")]
+ public string? Model { get; set; } = DeviceHelper.GetModel();
+
+ [JsonPropertyName("oemName")]
+ public string? OemName { get; set; } = DeviceHelper.GetOem();
+
+ [JsonPropertyName("screenSize")]
+ public string ScreenSize { get; set; } = DeviceHelper.GetScreenSize();
+
+ [JsonPropertyName("carrierCountry")]
+ public string Country { get; set; } = DeviceHelper.GetCountry() ?? "CN";
+
+ [JsonPropertyName("locale")]
+ public string Locale { get; set; } = CultureInfo.CurrentCulture.Name;
+
+ [JsonPropertyName("timeZoneOffset")]
+ public int TimeZoneOffset { get; set; } = (int)TimeZoneInfo.Local.BaseUtcOffset.TotalMinutes;
+
+ [JsonPropertyName("appVersion")]
+ public string AppVersion { get; set; } = CoreEnvironment.Version.ToString();
+
+ [JsonPropertyName("appBuild")]
+ public string AppBuild { get; set; } = CoreEnvironment.Version.ToString();
+
+ [JsonPropertyName("appNamespace")]
+ public string AppNamespace { get; set; } = typeof(App).Namespace ?? string.Empty;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/EventLog.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/EventLog.cs
new file mode 100644
index 0000000000..dd5354edce
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/EventLog.cs
@@ -0,0 +1,22 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public class EventLog : PropertiesLog
+{
+ public EventLog(string name)
+ {
+ Name = name;
+ }
+
+ [JsonPropertyName("type")]
+ public override string Type { get => "event"; }
+
+ [JsonPropertyName("id")]
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/HandledErrorLog.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/HandledErrorLog.cs
new file mode 100644
index 0000000000..9ab5a763e6
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/HandledErrorLog.cs
@@ -0,0 +1,23 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public class HandledErrorLog : PropertiesLog
+{
+ public HandledErrorLog(Exception exception)
+ {
+ Id = Guid.NewGuid();
+ Exception = LogHelper.Create(exception);
+ }
+
+ [JsonPropertyName("id")]
+ public Guid? Id { get; set; }
+
+ [JsonPropertyName("exception")]
+ public AppCenterException Exception { get; set; }
+
+ [JsonPropertyName("type")]
+ public override string Type { get => "handledError"; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/Log.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/Log.cs
new file mode 100644
index 0000000000..5375dc75e7
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/Log.cs
@@ -0,0 +1,23 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public abstract class Log
+{
+ [JsonIgnore]
+ public LogStatus Status { get; set; } = LogStatus.Pending;
+
+ [JsonPropertyName("type")]
+ public abstract string Type { get; }
+
+ [JsonPropertyName("sid")]
+ public Guid Session { get; set; }
+
+ [JsonPropertyName("timestamp")]
+ public string Timestamp { get; set; } = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ");
+
+ [JsonPropertyName("device")]
+ public Device Device { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogContainer.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogContainer.cs
new file mode 100644
index 0000000000..dd4d8c020f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogContainer.cs
@@ -0,0 +1,16 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public class LogContainer
+{
+ public LogContainer(IEnumerable logs)
+ {
+ Logs = logs;
+ }
+
+ [JsonPropertyName("logs")]
+ public IEnumerable Logs { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogConverter.cs
new file mode 100644
index 0000000000..ada58c5991
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogConverter.cs
@@ -0,0 +1,22 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+///
+/// 日志转换器
+///
+public class LogConverter : JsonConverter
+{
+ ///
+ public override Log? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ throw Must.NeverHappen();
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, Log value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value, value.GetType(), options);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogHelper.cs
new file mode 100644
index 0000000000..c91412a43e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogHelper.cs
@@ -0,0 +1,46 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public static class LogHelper
+{
+ public static T Initialize(this T log, Guid sid, Device device)
+ where T : Log
+ {
+ log.Session = sid;
+ log.Device = device;
+
+ return log;
+ }
+
+ public static AppCenterException Create(Exception exception)
+ {
+ AppCenterException current = new()
+ {
+ Type = exception.GetType().ToString(),
+ Message = exception.Message,
+ StackTrace = exception.ToString(),
+ };
+
+ if (exception is AggregateException aggregateException)
+ {
+ if (aggregateException.InnerExceptions.Count != 0)
+ {
+ current.InnerExceptions = new();
+ foreach (var innerException in aggregateException.InnerExceptions)
+ {
+ current.InnerExceptions.Add(Create(innerException));
+ }
+ }
+ }
+ else if (exception.InnerException != null)
+ {
+ current.InnerExceptions ??= new();
+ current.InnerExceptions.Add(Create(exception.InnerException));
+ }
+
+ return current;
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogStatus.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogStatus.cs
new file mode 100644
index 0000000000..5f570a90fd
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogStatus.cs
@@ -0,0 +1,13 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+[SuppressMessage("", "SA1602")]
+public enum LogStatus
+{
+ Pending,
+ Uploading,
+ Uploaded,
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/ManagedErrorLog.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/ManagedErrorLog.cs
new file mode 100644
index 0000000000..f071b88dff
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/ManagedErrorLog.cs
@@ -0,0 +1,51 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Core;
+using System.Diagnostics;
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public class ManagedErrorLog : Log
+{
+ public ManagedErrorLog(Exception exception, bool fatal = true)
+ {
+ var p = Process.GetCurrentProcess();
+ Id = Guid.NewGuid();
+ Fatal = fatal;
+ UserId = CoreEnvironment.AppCenterDeviceId;
+ ProcessId = p.Id;
+ Exception = LogHelper.Create(exception);
+ ProcessName = p.ProcessName;
+ Architecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
+ AppLaunchTimestamp = p.StartTime.ToUniversalTime();
+ }
+
+ [JsonPropertyName("id")]
+ public Guid Id { get; set; }
+
+ [JsonPropertyName("userId")]
+ public string? UserId { get; set; }
+
+ [JsonPropertyName("processId")]
+ public int ProcessId { get; set; }
+
+ [JsonPropertyName("processName")]
+ public string ProcessName { get; set; }
+
+ [JsonPropertyName("fatal")]
+ public bool Fatal { get; set; }
+
+ [JsonPropertyName("appLaunchTimestamp")]
+ public DateTime? AppLaunchTimestamp { get; set; }
+
+ [JsonPropertyName("architecture")]
+ public string? Architecture { get; set; }
+
+ [JsonPropertyName("exception")]
+ public AppCenterException Exception { get; set; }
+
+ [JsonPropertyName("type")]
+ public override string Type { get => "managedError"; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/PageLog.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/PageLog.cs
new file mode 100644
index 0000000000..a340965243
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/PageLog.cs
@@ -0,0 +1,19 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public class PageLog : PropertiesLog
+{
+ public PageLog(string name)
+ {
+ Name = name;
+ }
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ [JsonPropertyName("type")]
+ public override string Type { get => "page"; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/PropertiesLog.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/PropertiesLog.cs
new file mode 100644
index 0000000000..cc4eb32b3c
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/PropertiesLog.cs
@@ -0,0 +1,11 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public abstract class PropertiesLog : Log
+{
+ [JsonPropertyName("properties")]
+ public IDictionary Properties { get; set; } = new Dictionary();
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/StartServiceLog.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/StartServiceLog.cs
new file mode 100644
index 0000000000..06370d890d
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/StartServiceLog.cs
@@ -0,0 +1,19 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public class StartServiceLog : Log
+{
+ public StartServiceLog(params string[] services)
+ {
+ Services = services;
+ }
+
+ [JsonPropertyName("services")]
+ public string[] Services { get; set; }
+
+ [JsonPropertyName("type")]
+ public override string Type { get => "startService"; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/StartSessionLog.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/StartSessionLog.cs
new file mode 100644
index 0000000000..c690f38a8e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/StartSessionLog.cs
@@ -0,0 +1,11 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model.Log;
+
+[SuppressMessage("", "SA1600")]
+public class StartSessionLog : Log
+{
+ [JsonPropertyName("type")]
+ public override string Type { get => "startSession"; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/LogUploadResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/LogUploadResult.cs
new file mode 100644
index 0000000000..95d6aaac23
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/LogUploadResult.cs
@@ -0,0 +1,20 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AppCenter.Model;
+
+[SuppressMessage("", "SA1600")]
+public class LogUploadResult
+{
+ [JsonPropertyName("status")]
+ public string Status { get; set; } = null!;
+
+ [JsonPropertyName("validDiagnosticsIds")]
+ public List ValidDiagnosticsIds { get; set; } = null!;
+
+ [JsonPropertyName("throttledDiagnosticsIds")]
+ public List ThrottledDiagnosticsIds { get; set; } = null!;
+
+ [JsonPropertyName("correlationId")]
+ public Guid CorrelationId { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlManualInputProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlManualInputProvider.cs
index e5ee75f67b..62b55c7675 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlManualInputProvider.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlManualInputProvider.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
+using Snap.Hutao.View.Dialog;
namespace Snap.Hutao.Service.GachaLog;
@@ -15,8 +16,26 @@ internal class GachaLogUrlManualInputProvider : IGachaLogUrlProvider
public string Name { get => nameof(GachaLogUrlManualInputProvider); }
///
- public Task> GetQueryAsync()
+ public async Task> GetQueryAsync()
{
- throw new NotImplementedException();
+ MainWindow mainWindow = Ioc.Default.GetRequiredService();
+ await ThreadHelper.SwitchToMainThreadAsync();
+ ValueResult result = await new GachaLogUrlDialog(mainWindow).GetInputUrlAsync().ConfigureAwait(false);
+
+ if (result.IsOk)
+ {
+ if (result.Value.Contains("&auth_appid=webview_gacha"))
+ {
+ return result;
+ }
+ else
+ {
+ return new(false, "提供的Url无效");
+ }
+ }
+ else
+ {
+ return new(false, null!);
+ }
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlStokenProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlStokenProvider.cs
index 9994545f30..eeb689769c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlStokenProvider.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlStokenProvider.cs
@@ -36,9 +36,9 @@ public GachaLogUrlStokenProvider(IUserService userService, BindingClient2 bindin
public async Task> GetQueryAsync()
{
Model.Binding.User? user = userService.Current;
- if (user != null)
+ if (user != null && user.SelectedUserGameRole != null)
{
- if (user.Cookie!.ContainsSToken() && user.SelectedUserGameRole != null)
+ if (user.Cookie!.ContainsSToken())
{
PlayerUid uid = (PlayerUid)user.SelectedUserGameRole;
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(uid);
@@ -48,9 +48,19 @@ public async Task> GetQueryAsync()
{
return new(true, GachaLogConfigration.AsQuery(data, authkey));
}
+ else
+ {
+ return new(false, "请求验证密钥失败");
+ }
+ }
+ else
+ {
+ return new(false, "当前用户的Cookie不包含 Stoken");
}
}
-
- return new(false, "当前用户的Cookie不包含 Stoken");
+ else
+ {
+ return new(false, "尚未选择要刷新的用户以及角色");
+ }
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlWebCacheProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlWebCacheProvider.cs
index a621c9803f..58c5dafcf5 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlWebCacheProvider.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlWebCacheProvider.cs
@@ -40,7 +40,17 @@ public async Task> GetQueryAsync()
string folder = Path.GetDirectoryName(path) ?? string.Empty;
string cacheFile = Path.Combine(folder, @"YuanShen_Data\webCaches\Cache\Cache_Data\data_2");
- using (TemporaryFile tempFile = TemporaryFile.CreateFromFileCopy(cacheFile))
+ TemporaryFile tempFile;
+ try
+ {
+ tempFile = TemporaryFile.CreateFromFileCopy(cacheFile);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ return new(false, $"找不到原神内置浏览器缓存路径:\n{cacheFile}");
+ }
+
+ using (tempFile)
{
using (FileStream fileStream = new(tempFile.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
@@ -74,7 +84,7 @@ public async Task> GetQueryAsync()
}
else
{
- return new(false, null!);
+ return new(false, $"未正确提供原神路径,或当前设置的路径不正确");
}
}
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 4035256d48..616b462fc4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs
@@ -21,7 +21,7 @@ internal class RegistryLauncherLocator : IGameLocator
///
public Task> LocateGamePathAsync()
{
- ValueResult result = LocateInternal("InstallPath");
+ ValueResult result = LocateInternal("DisplayIcon");
if (result.IsOk == false)
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/HutaoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/HutaoService.cs
index 2a5b83d201..120c265419 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/HutaoService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/HutaoService.cs
@@ -4,16 +4,18 @@
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hutao;
+using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Service;
///
/// 胡桃 API 服务
///
-[Injection(InjectAs.Transient)]
+[Injection(InjectAs.Transient, typeof(IHutaoService))]
internal class HutaoService : IHutaoService
{
private readonly HomaClient homaClient;
+ private readonly IMemoryCache memoryCache;
///
/// 构造一个新的胡桃 API 服务
@@ -23,5 +25,54 @@ internal class HutaoService : IHutaoService
public HutaoService(HomaClient homaClient, IMemoryCache memoryCache)
{
this.homaClient = homaClient;
+ this.memoryCache = memoryCache;
+ }
+
+ ///
+ public ValueTask GetOverviewAsync()
+ {
+ return FromCacheOrWebAsync(nameof(Overview), homaClient.GetOverviewAsync);
+ }
+
+ ///
+ public ValueTask> GetAvatarAppearanceRanksAsync()
+ {
+ return FromCacheOrWebAsync(nameof(AvatarAppearanceRank), homaClient.GetAvatarAttendanceRatesAsync);
+ }
+
+ ///
+ public ValueTask> GetAvatarUsageRanksAsync()
+ {
+ return FromCacheOrWebAsync(nameof(AvatarUsageRank), homaClient.GetAvatarUtilizationRatesAsync);
+ }
+
+ ///
+ public ValueTask> GetAvatarConstellationInfosAsync()
+ {
+ return FromCacheOrWebAsync(nameof(AvatarConstellationInfo), homaClient.GetAvatarHoldingRatesAsync);
+ }
+
+ ///
+ public ValueTask> GetAvatarCollocationsAsync()
+ {
+ return FromCacheOrWebAsync(nameof(AvatarCollocation), homaClient.GetAvatarCollocationsAsync);
+ }
+
+ ///
+ public ValueTask> GetTeamAppearancesAsync()
+ {
+ return FromCacheOrWebAsync(nameof(TeamAppearance), homaClient.GetTeamCombinationsAsync);
+ }
+
+ private async ValueTask FromCacheOrWebAsync(string typeName, Func> taskFunc)
+ {
+ string key = $"{nameof(HutaoService)}.Cache.{typeName}";
+ if (memoryCache.TryGetValue(key, out object? cache))
+ {
+ return (T)cache;
+ }
+
+ T web = await taskFunc(default).ConfigureAwait(false);
+ return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs
index 9109577b2c..6e5fb6191c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs
@@ -43,6 +43,13 @@ internal interface IMetadataService
/// 角色列表
ValueTask> GetAvatarsAsync(CancellationToken token = default);
+ ///
+ /// 异步获取装备被动Id到圣遗物套装的映射
+ ///
+ /// 取消令牌
+ /// 装备被动Id到圣遗物套装的映射
+ ValueTask> GetEquipAffixIdToReliquarySetMapAsync(CancellationToken token = default);
+
///
/// 异步获取卡池配置列表
///
@@ -126,4 +133,11 @@ internal interface IMetadataService
/// 取消令牌
/// 武器列表
ValueTask> GetWeaponsAsync(CancellationToken token = default);
+
+ ///
+ /// 异步获取圣遗物套装
+ ///
+ /// 取消令牌
+ /// 圣遗物套装列表
+ ValueTask> GetReliquarySetsAsync(CancellationToken token = default);
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Implementation.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Implementation.cs
index 2e55651fae..c6c6c20307 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Implementation.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Implementation.cs
@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Avatar;
@@ -39,42 +38,6 @@ public ValueTask> GetGachaEventsAsync(CancellationToken token =
return FromCacheOrFileAsync>("GachaEvent", token);
}
- ///
- public ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default)
- {
- return FromCacheAsDictionaryAsync("Avatar", a => a.Id, token);
- }
-
- ///
- public ValueTask> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
- {
- return FromCacheAsDictionaryAsync("ReliquaryAffix", a => a.Id, token);
- }
-
- ///
- public ValueTask> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
- {
- return FromCacheAsDictionaryAsync("ReliquaryMainAffix", r => r.Id, r => r.Type, 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)
{
@@ -99,6 +62,12 @@ public ValueTask> GetReliquaryMainAffixesAsync(Cancella
return FromCacheOrFileAsync>("ReliquaryMainAffix", token);
}
+ ///
+ public ValueTask> GetReliquarySetsAsync(CancellationToken token = default)
+ {
+ return FromCacheOrFileAsync>("ReliquarySet", token);
+ }
+
///
public ValueTask> GetWeaponsAsync(CancellationToken token = default)
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Indexing.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Indexing.cs
new file mode 100644
index 0000000000..e7496a772d
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Indexing.cs
@@ -0,0 +1,57 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Intrinsic;
+using Snap.Hutao.Model.Metadata.Avatar;
+using Snap.Hutao.Model.Metadata.Reliquary;
+using Snap.Hutao.Model.Metadata.Weapon;
+
+namespace Snap.Hutao.Service.Metadata;
+
+///
+/// 索引部分
+///
+internal partial class MetadataService
+{
+ ///
+ public ValueTask> GetEquipAffixIdToReliquarySetMapAsync(CancellationToken token = default)
+ {
+ return FromCacheAsDictionaryAsync("ReliquarySet", r => r.EquipAffixId, token);
+ }
+
+ ///
+ public ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default)
+ {
+ return FromCacheAsDictionaryAsync("Avatar", a => a.Id, token);
+ }
+
+ ///
+ public ValueTask> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
+ {
+ return FromCacheAsDictionaryAsync("ReliquaryAffix", a => a.Id, token);
+ }
+
+ ///
+ public ValueTask> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
+ {
+ return FromCacheAsDictionaryAsync("ReliquaryMainAffix", r => r.Id, r => r.Type, 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);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
index 314a0a8448..eed4d38eae 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
@@ -137,6 +137,7 @@ public async Task> GetUserCollectionAsync()
///
public async Task> ProcessInputCookieAsync(Cookie cookie)
{
+ cookie.Trim();
Must.NotNull(userCollection!);
// 检查 uid 是否存在
@@ -197,7 +198,6 @@ private async Task TryAddMultiTokenAsync(Cookie cookie, string uid)
private async Task> TryCreateUserAndAddAsync(ObservableCollection users, Cookie cookie)
{
- cookie.Trim();
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
if (newUser != null)
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
index 96dbb563bb..575d5367ae 100644
--- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
+++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
@@ -29,6 +29,12 @@
$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT
True
+
+ embedded
+
+
+ embedded
+
@@ -38,6 +44,7 @@
+
@@ -54,6 +61,7 @@
+
@@ -62,6 +70,7 @@
+
@@ -87,6 +96,7 @@
+
@@ -99,10 +109,9 @@
-
-
+
-
+
@@ -139,6 +148,16 @@
+
+
+ MSBuild:Compile
+
+
+
+
+ MSBuild:Compile
+
+
MSBuild:Compile
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml
new file mode 100644
index 0000000000..6654d1e391
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs
new file mode 100644
index 0000000000..febbb96e85
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogUrlDialog.xaml.cs
@@ -0,0 +1,36 @@
+// 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.Threading;
+
+namespace Snap.Hutao.View.Dialog;
+
+///
+/// 祈愿记录Url对话框
+///
+public sealed partial class GachaLogUrlDialog : ContentDialog
+{
+ ///
+ /// 初始化一个新的祈愿记录Url对话框
+ ///
+ /// 窗体
+ public GachaLogUrlDialog(Window window)
+ {
+ InitializeComponent();
+ XamlRoot = window.Content.XamlRoot;
+ }
+
+ ///
+ /// 获取输入的Url
+ ///
+ /// 输入的结果
+ public async Task> GetInputUrlAsync()
+ {
+ ContentDialogResult result = await ShowAsync();
+ string url = InputText.Text;
+
+ return new(result == ContentDialogResult.Primary, url);
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
index 42f837ca42..302cefe57d 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
@@ -42,7 +42,12 @@
Content="属性统计"
shvh:NavHelper.NavigateTo="shvp:AvatarPropertyPage"
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_Icon_BoostUp.png}"/>
-
+
+
+
+
+
+
+
+
+
+ 8,0,8,0
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePage.xaml.cs
new file mode 100644
index 0000000000..ef29fdb602
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePage.xaml.cs
@@ -0,0 +1,22 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Control;
+using Snap.Hutao.ViewModel;
+
+namespace Snap.Hutao.View.Page;
+
+///
+/// 胡桃数据库页面
+///
+public sealed partial class HutaoDatabasePage : ScopedPage
+{
+ ///
+ /// 构造一个新的胡桃数据库页面
+ ///
+ public HutaoDatabasePage()
+ {
+ InitializeWith();
+ InitializeComponent();
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
index de9c0b2f89..52b66a5217 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
@@ -233,6 +233,13 @@ private async Task RefreshInternalAsync(RefreshOption option)
dialog.DefaultButton = ContentDialogButton.Primary;
}
}
+ else
+ {
+ if (query is string message)
+ {
+ infoBarService.Warning(message);
+ }
+ }
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
new file mode 100644
index 0000000000..b2ed0cbe23
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
@@ -0,0 +1,151 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using CommunityToolkit.Mvvm.ComponentModel;
+using Snap.Hutao.Control;
+using Snap.Hutao.Core.Threading;
+using Snap.Hutao.Factory.Abstraction;
+using Snap.Hutao.Model.Binding.Hutao;
+using Snap.Hutao.Model.Metadata.Avatar;
+using Snap.Hutao.Model.Metadata.Weapon;
+using Snap.Hutao.Service.Abstraction;
+using Snap.Hutao.Service.Metadata;
+using Snap.Hutao.Web.Hutao.Model;
+
+namespace Snap.Hutao.ViewModel;
+
+///
+/// 胡桃数据库视图模型
+///
+[Injection(InjectAs.Transient)]
+internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
+{
+ private readonly IHutaoService hutaoService;
+ private readonly IMetadataService metadataService;
+
+ private List? avatarUsageRanks;
+ private List? avatarAppearanceRanks;
+ private List? avatarConstellationInfos;
+ private List? teamAppearances;
+
+ ///
+ /// 构造一个新的胡桃数据库视图模型
+ ///
+ /// 胡桃服务
+ /// 元数据服务
+ /// 异步命令工厂
+ public HutaoDatabaseViewModel(IHutaoService hutaoService, IMetadataService metadataService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
+ {
+ this.hutaoService = hutaoService;
+ this.metadataService = metadataService;
+
+ OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
+ }
+
+ ///
+ public CancellationToken CancellationToken { get; set; }
+
+ ///
+ /// 角色使用率
+ ///
+ public List? AvatarUsageRanks { get => avatarUsageRanks; set => SetProperty(ref avatarUsageRanks, value); }
+
+ ///
+ /// 角色上场率
+ ///
+ public List? AvatarAppearanceRanks { get => avatarAppearanceRanks; set => SetProperty(ref avatarAppearanceRanks, value); }
+
+ ///
+ /// 角色命座信息
+ ///
+ public List? AvatarConstellationInfos { get => avatarConstellationInfos; set => avatarConstellationInfos = value; }
+
+ ///
+ /// 队伍出场
+ ///
+ public List? TeamAppearances { get => teamAppearances; set => SetProperty(ref teamAppearances, value); }
+
+ ///
+ /// 打开界面命令
+ ///
+ public ICommand OpenUICommand { get; }
+
+ private async Task OpenUIAsync()
+ {
+ if (await metadataService.InitializeAsync().ConfigureAwait(false))
+ {
+ Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
+ idAvatarMap = new(idAvatarMap)
+ {
+ [10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
+ [10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
+ };
+
+ Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
+ Dictionary idReliquarySetMap = await metadataService.GetEquipAffixIdToReliquarySetMapAsync().ConfigureAwait(false);
+
+ List avatarAppearanceRanksLocal = default!;
+ List avatarUsageRanksLocal = default!;
+ List avatarConstellationInfosLocal = default!;
+ List teamAppearancesLocal = default!;
+
+ Task avatarAppearanceRankTask = Task.Run(async () =>
+ {
+ // AvatarAppearanceRank
+ List avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
+ avatarAppearanceRanksLocal = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
+ {
+ Floor = $"第 {rank.Floor} 层",
+ Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
+ }).ToList();
+ });
+
+ Task avatarUsageRank = Task.Run(async () =>
+ {
+ // AvatarUsageRank
+ List avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
+ avatarUsageRanksLocal = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
+ {
+ Floor = $"第 {rank.Floor} 层",
+ Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
+ }).ToList();
+ });
+
+ Task avatarConstellationInfoTask = Task.Run(async () =>
+ {
+ // AvatarConstellationInfo
+ List avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
+ avatarConstellationInfosLocal = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
+ {
+ return new ComplexAvatarConstellationInfo(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate));
+ }).ToList();
+ });
+
+ Task teamAppearanceTask = Task.Run(async () =>
+ {
+ List teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
+ teamAppearancesLocal = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();
+ });
+
+ await Task.WhenAll(avatarAppearanceRankTask, avatarUsageRank, avatarConstellationInfoTask, teamAppearanceTask).ConfigureAwait(false);
+
+ await ThreadHelper.SwitchToMainThreadAsync();
+ AvatarAppearanceRanks = avatarAppearanceRanksLocal;
+ AvatarUsageRanks = avatarUsageRanksLocal;
+ AvatarConstellationInfos = avatarConstellationInfosLocal;
+ TeamAppearances = teamAppearancesLocal;
+
+ //// AvatarCollocation
+ //List avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
+ //List avatarCollocationsLocal = avatarCollocationsRaw.Select(co =>
+ //{
+ // return new ComplexAvatarCollocation(idAvatarMap[co.AvatarId])
+ // {
+ // Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
+ // Weapons = co.Weapons.Select(w => new ComplexWeapon(idWeaponMap[w.Item], w.Rate)).ToList(),
+ // ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
+ // };
+ //}).ToList();
+ }
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
index f1549fc220..b98f3f92e5 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
@@ -38,7 +38,7 @@ private Cookie(SortedDictionary dict)
public static Cookie Parse(string cookieString)
{
SortedDictionary cookieMap = new();
-
+ cookieString = cookieString.Replace(" ", string.Empty);
string[] values = cookieString.TrimEnd(';').Split(';');
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs
index a412c05556..7e558fe3f0 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs
@@ -45,6 +45,21 @@ internal static class HttpClientExtensions
}
}
+ ///
+ internal static async Task TryCatchPostAsJsonAsync(this HttpClient httpClient, string requestUri, TValue value, JsonSerializerOptions options, CancellationToken token = default)
+ where TResult : class
+ {
+ try
+ {
+ HttpResponseMessage message = await httpClient.PostAsJsonAsync(requestUri, value, options, token).ConfigureAwait(false);
+ return await message.Content.ReadFromJsonAsync(options, token).ConfigureAwait(false);
+ }
+ catch (HttpRequestException)
+ {
+ return null;
+ }
+ }
+
///
/// 设置用户的Cookie
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs
index 16459662a5..bd3b5c1e0d 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs
@@ -97,10 +97,10 @@ public async Task CheckRecordUploadedAsync(PlayerUid uid, CancellationToke
///
/// 取消令牌
/// 角色出场率
- public async Task> GetAvatarAttendanceRatesAsync(CancellationToken token = default)
+ public async Task> GetAvatarAttendanceRatesAsync(CancellationToken token = default)
{
- Response>? resp = await httpClient
- .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Avatar/AttendanceRate", token)
+ Response>? resp = await httpClient
+ .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Avatar/AttendanceRate", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -112,10 +112,10 @@ public async Task> GetAvatarAttendanceRatesAsy
///
/// 取消令牌
/// 角色出场率
- public async Task> GetAvatarUtilizationRatesAsync(CancellationToken token = default)
+ public async Task> GetAvatarUtilizationRatesAsync(CancellationToken token = default)
{
- Response>? resp = await httpClient
- .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Avatar/UtilizationRate", token)
+ Response>? resp = await httpClient
+ .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Avatar/UtilizationRate", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -127,10 +127,10 @@ public async Task> GetAvatarUtilizationRatesAsync(C
///
/// 取消令牌
/// 角色/武器/圣遗物搭配
- public async Task> GetAvatarCollocationsAsync(CancellationToken token = default)
+ public async Task> GetAvatarCollocationsAsync(CancellationToken token = default)
{
- Response>? resp = await httpClient
- .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Avatar/AvatarCollocation", token)
+ Response>? resp = await httpClient
+ .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Avatar/AvatarCollocation", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -142,10 +142,10 @@ public async Task> GetAvatarCollocationsAsync(Can
///
/// 取消令牌
/// 角色图片列表
- public async Task> GetAvatarHoldingRatesAsync(CancellationToken token = default)
+ public async Task> GetAvatarHoldingRatesAsync(CancellationToken token = default)
{
- Response>? resp = await httpClient
- .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Avatar/HoldingRate", token)
+ Response>? resp = await httpClient
+ .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Avatar/HoldingRate", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -157,10 +157,10 @@ public async Task> GetAvatarHoldingRatesAsy
///
/// 取消令牌
/// 队伍出场列表
- public async Task> GetTeamCombinationsAsync(CancellationToken token = default)
+ public async Task> GetTeamCombinationsAsync(CancellationToken token = default)
{
- Response>? resp = await httpClient
- .GetFromJsonAsync>>($"{HutaoAPI}/Team/Combination", token)
+ Response>? resp = await httpClient
+ .GetFromJsonAsync>>($"{HutaoAPI}/Statistics/Team/Combination", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Converter/ReliquarySetsConverter.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Converter/ReliquarySetsConverter.cs
index b2e9fd9746..9fee87592a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Converter/ReliquarySetsConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Converter/ReliquarySetsConverter.cs
@@ -15,7 +15,7 @@ internal class ReliquarySetsConverter : JsonConverter
{
if (reader.GetString() is string source)
{
- string[] sets = source.Split(Separator);
+ string[] sets = source.Split(Separator, StringSplitOptions.RemoveEmptyEntries);
return new(sets.Select(set => new ReliquarySet(set)));
}
else
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ReliquarySet.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ReliquarySet.cs
index 7bc85c5363..05ad81345d 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ReliquarySet.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ReliquarySet.cs
@@ -16,14 +16,14 @@ public ReliquarySet(string set)
{
string[]? deconstructed = set.Split('-');
- Id = int.Parse(deconstructed[0]);
+ EquipAffixId = int.Parse(deconstructed[0]);
Count = int.Parse(deconstructed[1]);
}
///
/// Id
///
- public int Id { get; }
+ public int EquipAffixId { get; }
///
/// 个数
@@ -33,6 +33,6 @@ public ReliquarySet(string set)
///
public override string ToString()
{
- return $"{Id}-{Count}";
+ return $"{EquipAffixId}-{Count}";
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/TeamAppearance.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/TeamAppearance.cs
index 2526f284c9..576fe87032 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/TeamAppearance.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/TeamAppearance.cs
@@ -2,11 +2,17 @@
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
+
///
/// 队伍出场次数
///
public class TeamAppearance
{
+ ///
+ /// 层
+ ///
+ public int Floor { get; set; }
+
///
/// 上半
///