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; } + /// /// 上半 ///