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 0000000000..bb1ec9f122
Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ChapterIcon_Hutao.png differ
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IHutaoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IHutaoService.cs
index ace86f5d97..e55c86c999 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IHutaoService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IHutaoService.cs
@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
+using Snap.Hutao.Web.Hutao.Model;
+
namespace Snap.Hutao.Service.Abstraction;
///
@@ -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; }
+
///
/// 上半
///