diff --git a/README.md b/README.md
index 95e0f88e..986abd1b 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,11 @@

-## 项目首页
+## 项目首页(文档)
[](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml)
-[https://hut.ao](https://hut.ao)
+[HUT.AO](https://hut.ao)
## 安装
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/ContentDialogBehavior.cs b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/ContentDialogBehavior.cs
deleted file mode 100644
index a37f31b5..00000000
--- a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/ContentDialogBehavior.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) DGP Studio. All rights reserved.
-// Licensed under the MIT license.
-
-using CommunityToolkit.WinUI.UI.Behaviors;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Media;
-using Microsoft.UI.Xaml.Shapes;
-
-namespace Snap.Hutao.Control.Behavior;
-
-///
-/// Make ContentDialog's SmokeLayerBackground dsiplay over custom titleBar
-///
-public class ContentDialogBehavior : BehaviorBase
-{
- ///
- protected override void OnAssociatedObjectLoaded()
- {
- DependencyObject parent = VisualTreeHelper.GetParent(AssociatedObject);
-
- for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
- {
- DependencyObject current = VisualTreeHelper.GetChild(parent, i);
- if (current is Rectangle { Name: "SmokeLayerBackground" } background)
- {
- background.ClearValue(FrameworkElement.MarginProperty);
- background.RegisterPropertyChangedCallback(FrameworkElement.MarginProperty, OnMarginChanged);
- break;
- }
- }
- }
-
- private static void OnMarginChanged(DependencyObject sender, DependencyProperty property)
- {
- if (property == FrameworkElement.MarginProperty)
- {
- sender.ClearValue(property);
- }
- }
-}
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtensions.cs
index 5dbe10a9..2c679a97 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtensions.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtensions.cs
@@ -3,8 +3,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
-using Microsoft.Xaml.Interactivity;
-using Snap.Hutao.Control.Behavior;
using Snap.Hutao.Core.Threading;
namespace Snap.Hutao.Control.Extension;
@@ -23,7 +21,6 @@ internal static class ContentDialogExtensions
public static ContentDialog InitializeWithWindow(this ContentDialog contentDialog, Window window)
{
contentDialog.XamlRoot = window.Content.XamlRoot;
- Interaction.SetBehaviors(contentDialog, new() { new ContentDialogBehavior() });
return contentDialog;
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs
index dc77c05c..a9ec0f49 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs
@@ -121,19 +121,27 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
await HideAsync(token);
+ LoadedImageSurface? imageSurface = null;
+ Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
+
if (uri != null)
{
- StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri);
- Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
-
- LoadedImageSurface? imageSurface = null;
- try
+ if (uri.Scheme == "ms-appx")
{
- imageSurface = await LoadImageSurfaceAsync(storageFile, token);
+ imageSurface = LoadedImageSurface.StartLoadFromUri(uri);
}
- catch (COMException)
+ else
{
- await imageCache.RemoveAsync(uri.Enumerate());
+ StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri);
+
+ try
+ {
+ imageSurface = await LoadImageSurfaceAsync(storageFile, token);
+ }
+ catch (COMException)
+ {
+ await imageCache.RemoveAsync(uri.Enumerate());
+ }
}
if (imageSurface != null)
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs
index 353d6902..4e982a4b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs
@@ -29,7 +29,6 @@ public class MonoChrome : CompositionImage
{
CompositionColorBrush blackLayerBursh = compositor.CreateColorBrush(Colors.Black);
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
-
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBursh, imageSurfaceBrush, BlendEffectMode.Overlay);
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs
index 0a8d0a09..615854a3 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs
@@ -24,8 +24,8 @@ internal class I18NExtension : MarkupExtension
static I18NExtension()
{
string currentName = CultureInfo.CurrentUICulture.Name;
- Type languageType = TranslationMap.GetValueOrDefault(currentName, typeof(LanguagezhCN));
- Translation = (ITranslation)Activator.CreateInstance(languageType)!;
+ Type? languageType = ((IDictionary)TranslationMap).GetValueOrDefault(currentName, typeof(LanguagezhCN));
+ Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
}
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Markup/UriExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Markup/UriExtension.cs
new file mode 100644
index 00000000..08e46afa
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Markup/UriExtension.cs
@@ -0,0 +1,32 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Markup;
+
+namespace Snap.Hutao.Control.Markup;
+
+///
+/// Uri扩展
+///
+[MarkupExtensionReturnType(ReturnType = typeof(Uri))]
+public sealed class UriExtension : MarkupExtension
+{
+ ///
+ /// 构造一个新的Uri扩展
+ ///
+ public UriExtension()
+ {
+ }
+
+ ///
+ /// 地址
+ ///
+ public string? Value { get; set; }
+
+ ///
+ protected override object ProvideValue()
+ {
+ return new Uri(Value ?? string.Empty);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs
index 91636fc1..6f660209 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs
@@ -102,7 +102,7 @@ public class DescriptionTextBlock : ContentControl
if (i == description.Length - 1)
{
- AppendText(text, description[last..i]);
+ AppendText(text, description[last..(i + 1)]);
}
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
index 50645d75..0b1fa3d6 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
@@ -22,7 +22,7 @@ internal static class CoreEnvironment
///
/// 动态密钥2的盐
///
- public const string DynamicSecret2Salt = "YVEIkzDFNHLeKXLxzqCA9TzxCpWwbIbk";
+ public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
///
/// 米游社请求UA
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs
index 737dac63..432e17ea 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs
@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using System.Reflection;
-
namespace Snap.Hutao.Core.Json.Converter;
///
@@ -33,7 +31,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
Type valueType = type.GetGenericArguments()[1];
Type innerConverterType = typeof(StringEnumDictionaryConverterInner<,>).MakeGenericType(keyType, valueType);
- JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType, BindingFlags.Instance | BindingFlags.Public, null, new object[] { options }, null)!;
+ JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType)!;
return converter;
}
@@ -41,13 +39,11 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
where TKey : struct, Enum
{
private readonly Type keyType;
- private readonly Type valueType;
- public StringEnumDictionaryConverterInner(JsonSerializerOptions options)
+ public StringEnumDictionaryConverterInner()
{
// Cache the key and value types.
keyType = typeof(TKey);
- valueType = typeof(TValue);
}
public override IDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs
index 86741bb3..5f36760f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs
@@ -88,6 +88,11 @@ internal static class EventIds
/// 成就
///
public static readonly EventId Achievement = 100130;
+
+ ///
+ /// 祈愿统计生成
+ ///
+ public static readonly EventId GachaStatisticGeneration = 100140;
#endregion
#region 杂项
diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs
index e4795d9b..b4367e5b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs
@@ -99,6 +99,26 @@ public static partial class EnumerableExtensions
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
}
+ ///
+ /// 获取值或默认值
+ ///
+ /// 键类型
+ /// 值类型
+ /// 字典
+ /// 键
+ /// 默认值
+ /// 结果值
+ public static TValue? GetValueOrDefault(this IDictionary dictionary, TKey key, TValue? defaultValue = default)
+ where TKey : notnull
+ {
+ if (dictionary.TryGetValue(key, out TValue? value))
+ {
+ return value;
+ }
+
+ return defaultValue;
+ }
+
///
/// 增加计数
///
@@ -111,6 +131,21 @@ public static partial class EnumerableExtensions
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
+ ///
+ /// 增加计数
+ ///
+ /// 键类型
+ /// 字典
+ /// 键
+ /// 增加的值
+ public static void Increase(this Dictionary dict, TKey key, int value)
+ where TKey : notnull
+ {
+ // ref the value, so that we can manipulate it outside the dict.
+ ref int current = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
+ current += value;
+ }
+
///
/// 增加计数
///
@@ -166,6 +201,20 @@ public static partial class EnumerableExtensions
return dictionary;
}
+ ///
+ public static Dictionary ToDictionaryOverride(this IEnumerable source, Func keySelector, Func valueSelector)
+ where TKey : notnull
+ {
+ Dictionary dictionary = new();
+
+ foreach (TSource value in source)
+ {
+ dictionary[keySelector(value)] = valueSelector(value);
+ }
+
+ return dictionary;
+ }
+
///
/// 表示一个对 类型的计数器
///
diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs
index 6cc4a0d2..c748caea 100644
--- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs
@@ -10,7 +10,6 @@ namespace Snap.Hutao;
/// 主窗体
///
[Injection(InjectAs.Singleton)]
-[Injection(InjectAs.Singleton, typeof(Window))] // This is for IEnumerable
[SuppressMessage("", "CA1001")]
public sealed partial class MainWindow : Window
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs
new file mode 100644
index 00000000..d723a0dd
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs
@@ -0,0 +1,67 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Intrinsic;
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 角色信息
+///
+public class Avatar
+{
+ ///
+ /// 名称
+ ///
+ public string Name { get; set; } = default!;
+
+ ///
+ /// 图标
+ ///
+ public Uri Icon { get; set; } = default!;
+
+ ///
+ /// 侧面图标
+ ///
+ public Uri SideIcon { get; set; } = default!;
+
+ ///
+ /// 星级
+ ///
+ public ItemQuality Quality { get; set; }
+
+ ///
+ /// 等级
+ ///
+ public string Level { get; set; } = default!;
+
+ ///
+ /// 好感度等级
+ ///
+ public int FetterLevel { get; set; }
+
+ ///
+ /// 武器
+ ///
+ public Weapon Weapon { get; set; } = default!;
+
+ ///
+ /// 圣遗物列表
+ ///
+ public List Reliquaries { get; set; } = default!;
+
+ ///
+ /// 命之座列表
+ ///
+ public List Constellations { get; set; } = default!;
+
+ ///
+ /// 技能列表
+ ///
+ public List Skills { get; set; } = default!;
+
+ ///
+ /// 属性
+ ///
+ public List> Properties { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Constellation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Constellation.cs
new file mode 100644
index 00000000..3425ea83
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Constellation.cs
@@ -0,0 +1,15 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 命座信息
+///
+public class Constellation : NameIconDescription
+{
+ ///
+ /// 是否激活
+ ///
+ public bool IsActiviated { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/EquipBase.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/EquipBase.cs
new file mode 100644
index 00000000..ae93b6be
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/EquipBase.cs
@@ -0,0 +1,27 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Intrinsic;
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 装备基类
+///
+public class EquipBase : NameIconDescription
+{
+ ///
+ /// 等级
+ ///
+ public string Level { get; set; } = default!;
+
+ ///
+ /// 品质
+ ///
+ public ItemQuality Quality { get; set; }
+
+ ///
+ /// 主属性
+ ///
+ public Pair MainProperty { get; set; } = default!;
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/NameIconDescription.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/NameIconDescription.cs
new file mode 100644
index 00000000..38cf8603
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/NameIconDescription.cs
@@ -0,0 +1,25 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 名称与描述抽象
+///
+public abstract class NameIconDescription
+{
+ ///
+ /// 名称
+ ///
+ public string Name { get; set; } = default!;
+
+ ///
+ /// 图标
+ ///
+ public Uri Icon { get; set; } = default!;
+
+ ///
+ /// 描述
+ ///
+ public string Description { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs
new file mode 100644
index 00000000..18fa5772
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs
@@ -0,0 +1,40 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 玩家信息
+///
+public class Player
+{
+ ///
+ /// 昵称
+ ///
+ public string Nickname { get; set; } = default!;
+
+ ///
+ /// 等级
+ ///
+ public int Level { get; set; }
+
+ ///
+ /// 签名
+ ///
+ public string Signature { get; set; } = default!;
+
+ ///
+ /// 完成成就数
+ ///
+ public int FinishAchievementNumber { get; set; }
+
+ ///
+ /// 深渊层间
+ ///
+ public string SipralAbyssFloorLevel { get; set; } = default!;
+
+ ///
+ /// 头像
+ ///
+ public Uri ProfilePicture { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs
new file mode 100644
index 00000000..a4767c12
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs
@@ -0,0 +1,15 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 圣遗物
+///
+public class Reliquary : EquipBase
+{
+ ///
+ /// 副属性列表
+ ///
+ public List> SubProperties { get; set; } = default!;
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Skill.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Skill.cs
new file mode 100644
index 00000000..0f52b165
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Skill.cs
@@ -0,0 +1,17 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Metadata;
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 天赋
+///
+public class Skill : NameIconDescription
+{
+ ///
+ /// 技能属性
+ ///
+ public LevelParam Info { get; set; } = default!;
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Summary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Summary.cs
new file mode 100644
index 00000000..22f53ea2
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Summary.cs
@@ -0,0 +1,20 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 玩家与角色列表的包装器
+///
+public class Summary
+{
+ ///
+ /// 玩家信息
+ ///
+ public Player Player { 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/AvatarProperty/Weapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Weapon.cs
new file mode 100644
index 00000000..8079dc66
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Weapon.cs
@@ -0,0 +1,30 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Binding.AvatarProperty;
+
+///
+/// 武器
+///
+public class Weapon : EquipBase
+{
+ ///
+ /// 副属性
+ ///
+ public Pair SubProperty { get; set; } = default!;
+
+ ///
+ /// 精炼属性
+ ///
+ public string AffixLevel { get; set; } = default!;
+
+ ///
+ /// 精炼名称
+ ///
+ public string AffixName { get; set; } = default!;
+
+ ///
+ /// 精炼被动
+ ///
+ public string AffixDescription { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs
index d33aa6b1..47af3cc4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs
@@ -28,4 +28,15 @@ public class AvatarInfo
/// 角色的信息
///
public Web.Enka.Model.AvatarInfo Info { get; set; } = default!;
+
+ ///
+ /// 创建一个新的实体角色信息
+ ///
+ /// uid
+ /// 角色信息
+ /// 实体角色信息
+ public static AvatarInfo Create(string uid, Web.Enka.Model.AvatarInfo info)
+ {
+ return new() { Uid = uid, Info = info };
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs
index 6b46e62d..fc1b49f8 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs
@@ -24,8 +24,10 @@ public enum FightProperty
FIGHT_PROP_BASE_HP = 1,
///
- /// 生命值加成
+ /// 小生命值加成
///
+ [Description("生命值")]
+ [Format(FormatMethod.Integer)]
FIGHT_PROP_HP = 2,
///
@@ -45,6 +47,8 @@ public enum FightProperty
///
/// 攻击力加成
///
+ [Description("攻击力")]
+ [Format(FormatMethod.Integer)]
FIGHT_PROP_ATTACK = 5,
///
@@ -64,6 +68,8 @@ public enum FightProperty
///
/// 防御力加成
///
+ [Description("防御力")]
+ [Format(FormatMethod.Integer)]
FIGHT_PROP_DEFENSE = 8,
///
@@ -387,6 +393,8 @@ public enum FightProperty
///
/// 最大生命值
///
+ [Description("生命值")]
+ [Format(FormatMethod.Integer)]
FIGHT_PROP_MAX_HP = 2000,
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemType.cs
index a415446c..61fe1241 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemType.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemType.cs
@@ -15,7 +15,7 @@ public enum ItemType
ITEM_NONE = 0,
///
- /// 贵重道具
+ /// 虚拟道具
///
ITEM_VIRTUAL = 1,
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs
index a434f13b..49721a6e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs
@@ -27,4 +27,14 @@ public class Costume
/// 是否为默认
///
public bool IsDefault { get; set; }
+
+ ///
+ /// 图标
+ ///
+ public string? Icon { get; set; }
+
+ ///
+ /// 侧面图标
+ ///
+ public string? SideIcon { get; set; }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs
index 8d5a151c..03d6f539 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs
@@ -24,7 +24,7 @@ public class SkillDepot
public IList Inherents { get; set; } = default!;
///
- /// 全部天赋
+ /// 全部天赋,包括固有天赋
///
public IList CompositeSkills
{
@@ -36,6 +36,20 @@ public class SkillDepot
///
public IList Talents { get; set; } = default!;
+ ///
+ /// 获取无固有天赋的技能列表
+ ///
+ /// 天赋列表
+ public IEnumerable GetCompositeSkillsNoInherents()
+ {
+ foreach (ProudableSkill skill in Skills)
+ {
+ yield return skill;
+ }
+
+ yield return EnergySkill;
+ }
+
private IEnumerable GetCompositeSkills()
{
foreach (ProudableSkill skill in Skills)
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs
index 7b2f41be..753800d4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs
@@ -12,22 +12,43 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
internal sealed class DescParamDescriptor : ValueConverterBase>>
{
+ ///
+ /// 获取特定等级的解释
+ ///
+ /// 源
+ /// 等级
+ /// 特定等级的解释
+ public static LevelParam Convert(DescParam from, int level)
+ {
+ // DO NOT INLINE!
+ // Cache the formats
+ IList formats = from.Descriptions
+ .Select(desc => new DescFormat(desc))
+ .ToList();
+
+ LevelParam param = from.Parameters.Single(param => param.Level == level);
+
+ return new LevelParam($"Lv.{param.Level}", GetParameterInfos(formats, param.Parameters));
+ }
+
///
public override IList> Convert(DescParam from)
{
+ // DO NOT INLINE!
+ // Cache the formats
+ IList formats = from.Descriptions
+ .Select(desc => new DescFormat(desc))
+ .ToList();
+
IList> parameters = from.Parameters
- .Select(param => new LevelParam(param.Level.ToString(), GetParameterInfos(from, param.Parameters)))
+ .Select(param => new LevelParam(param.Level.ToString(), GetParameterInfos(formats, param.Parameters)))
.ToList();
return parameters;
}
- private static IList GetParameterInfos(DescParam rawDescParam, IList param)
+ private static IList GetParameterInfos(IList formats, IList param)
{
- IList formats = rawDescParam.Descriptions
- .Select(desc => new DescFormat(desc))
- .ToList();
-
List results = new();
for (int index = 0; index < formats.Count; index++)
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs
index e5f7caf7..295ee201 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs
@@ -27,4 +27,4 @@ internal class EquipIconConverter : ValueConverterBase
{
return IconNameToUri(from);
}
-}
\ No newline at end of file
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs
index 7aad8d22..7969ab6f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs
@@ -13,6 +13,25 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
internal class PropertyInfoDescriptor : ValueConverterBase>?>
{
+ ///
+ /// 格式化战斗属性
+ ///
+ /// 战斗属性
+ /// 值
+ /// 格式化的值
+ public static string FormatProperty(FightProperty property, double value)
+ {
+ FormatMethod method = property.GetFormatMethod();
+
+ string valueFormatted = method switch
+ {
+ FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(),
+ FormatMethod.Percent => string.Format("{0:P1}", value),
+ _ => value.ToString(),
+ };
+ return valueFormatted;
+ }
+
///
public override IList> Convert(PropertyInfo from)
{
@@ -34,14 +53,7 @@ internal class PropertyInfoDescriptor : ValueConverterBase Math.Round((double)param, MidpointRounding.AwayFromZero).ToString(),
- FormatMethod.Percent => string.Format("{0:P1}", param),
- _ => param.ToString(),
- };
+ string valueFormatted = FormatProperty(properties[index], param);
results.Add(new ParameterInfo { Description = properties[index].GetDescription(), Parameter = valueFormatted });
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs
new file mode 100644
index 00000000..ff014fbe
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs
@@ -0,0 +1,28 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Control;
+using Snap.Hutao.Model.Intrinsic;
+using Windows.UI;
+
+namespace Snap.Hutao.Model.Metadata.Converter;
+
+///
+/// 品质颜色转换器
+///
+internal class QualityColorConverter : ValueConverterBase
+{
+ ///
+ public override Color Convert(ItemQuality from)
+ {
+ return from switch
+ {
+ ItemQuality.QUALITY_WHITE => Color.FromArgb(0xFF, 0x72, 0x77, 0x8B),
+ ItemQuality.QUALITY_GREEN => Color.FromArgb(0xFF, 0x2A, 0x8F, 0x72),
+ ItemQuality.QUALITY_BLUE => Color.FromArgb(0xFF, 0x51, 0x80, 0xCB),
+ ItemQuality.QUALITY_PURPLE => Color.FromArgb(0xFF, 0xA1, 0x56, 0xE0),
+ ItemQuality.QUALITY_ORANGE or ItemQuality.QUALITY_ORANGE_SP => Color.FromArgb(0xFF, 0xBC, 0x69, 0x32),
+ _ => Color.FromArgb(0x00, 0x00, 0x00, 0x00),
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs
new file mode 100644
index 00000000..4b33193e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs
@@ -0,0 +1,30 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Control;
+
+namespace Snap.Hutao.Model.Metadata.Converter;
+
+///
+/// 武器图片转换器
+///
+internal class RelicIconConverter : ValueConverterBase
+{
+ private const string BaseUrl = "https://static.snapgenshin.com/RelicIcon/{0}.png";
+
+ ///
+ /// 名称转Uri
+ ///
+ /// 名称
+ /// 链接
+ public static Uri IconNameToUri(string name)
+ {
+ return new Uri(string.Format(BaseUrl, name));
+ }
+
+ ///
+ public override Uri Convert(string from)
+ {
+ return IconNameToUri(from);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs
index 047e6513..7dc477bd 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs
@@ -13,16 +13,26 @@ internal class SkillIconConverter : ValueConverterBase
private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png";
private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png";
- ///
- public override Uri Convert(string from)
+ ///
+ /// 名称转Uri
+ ///
+ /// 名称
+ /// 链接
+ public static Uri IconNameToUri(string name)
{
- if (from.StartsWith("UI_Talent_"))
+ if (name.StartsWith("UI_Talent_"))
{
- return new Uri(string.Format(TalentUrl, from));
+ return new Uri(string.Format(TalentUrl, name));
}
else
{
- return new Uri(string.Format(SkillUrl, from));
+ return new Uri(string.Format(SkillUrl, name));
}
}
+
+ ///
+ public override Uri Convert(string from)
+ {
+ return IconNameToUri(from);
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs
index b6c65184..36f914f3 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs
@@ -18,7 +18,7 @@ public class Reliquary
///
/// 允许出现的等级
///
- public IEnumerable RankLevels { get; set; } = default!;
+ public ItemQuality RankLevel { get; set; } = default!;
///
/// 套装Id
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffixBase.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffixBase.cs
index d4dd9c22..d37210ce 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffixBase.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffixBase.cs
@@ -19,4 +19,4 @@ public class ReliquaryAffixBase
/// 战斗属性
///
public FightProperty Type { get; set; }
-}
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs
new file mode 100644
index 00000000..8e9109ee
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs
@@ -0,0 +1,29 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Core.Json.Converter;
+using Snap.Hutao.Model.Intrinsic;
+
+namespace Snap.Hutao.Model.Metadata.Reliquary;
+
+///
+/// 圣遗物等级
+///
+public class ReliquaryLevel
+{
+ ///
+ /// 品质
+ ///
+ public ItemQuality Quality { get; set; }
+
+ ///
+ /// 等级 1-21
+ ///
+ public int Level { get; set; }
+
+ ///
+ /// 属性
+ ///
+ [JsonConverter(typeof(StringEnumKeyDictionaryConverter))]
+ public IDictionary Properties { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/AffixInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/AffixInfo.cs
new file mode 100644
index 00000000..df52d98e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/AffixInfo.cs
@@ -0,0 +1,21 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Metadata.Weapon;
+
+///
+/// 武器被动信息
+///
+public class AffixInfo
+{
+ ///
+ /// 被动的名称
+ ///
+ public string Name { get; set; } = default!;
+
+ ///
+ /// 各个等级的描述
+ /// 0-4
+ ///
+ public List> Descriptions { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs
new file mode 100644
index 00000000..5924230d
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs
@@ -0,0 +1,21 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model.Metadata.Weapon;
+
+///
+/// 等级与描述
+///
+/// 等级的类型
+public class LevelDescription
+{
+ ///
+ /// 等级
+ ///
+ public TLevel Level { get; set; } = default!;
+
+ ///
+ /// 描述
+ ///
+ public string Description { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs
index 46f613d0..de5fc9bd 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs
@@ -54,6 +54,11 @@ public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality
///
public PropertyInfo Property { get; set; } = default!;
+ ///
+ /// 被动信息, 无被动的武器为
+ ///
+ public AffixInfo? Affix { get; set; } = default!;
+
///
[JsonIgnore]
public ItemQuality Quality => RankLevel;
@@ -110,4 +115,4 @@ public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality
IsUp = isUp,
};
}
-}
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Pair2.cs b/src/Snap.Hutao/Snap.Hutao/Model/Pair2.cs
new file mode 100644
index 00000000..c4715a70
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Pair2.cs
@@ -0,0 +1,41 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Model;
+
+///
+/// 对
+///
+/// 键的类型
+/// 值1的类型
+/// 值2的类型
+public class Pair2
+{
+ ///
+ /// 构造一个新的对
+ ///
+ /// 键
+ /// 值1
+ /// 值2
+ public Pair2(TKey key, TValue1 value1, TValue2 value2)
+ {
+ Key = key;
+ Value1 = value1;
+ Value2 = value2;
+ }
+
+ ///
+ /// 键
+ ///
+ public TKey Key { get; set; }
+
+ ///
+ /// 值
+ ///
+ public TValue1 Value1 { get; set; }
+
+ ///
+ /// 值
+ ///
+ public TValue2 Value2 { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_BoostUp.png b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_BoostUp.png
new file mode 100644
index 00000000..d91d016f
Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_BoostUp.png differ
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Locked.png b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Locked.png
new file mode 100644
index 00000000..db6ba038
Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Locked.png differ
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
new file mode 100644
index 00000000..98d9edee
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
@@ -0,0 +1,118 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Context.Database;
+using Snap.Hutao.Core.Threading;
+using Snap.Hutao.Model.Binding.AvatarProperty;
+using Snap.Hutao.Service.AvatarInfo.Factory;
+using Snap.Hutao.Web.Enka;
+using Snap.Hutao.Web.Enka.Model;
+using Snap.Hutao.Web.Hoyolab;
+
+namespace Snap.Hutao.Service.AvatarInfo;
+
+///
+/// 角色信息服务
+///
+[Injection(InjectAs.Transient, typeof(IAvatarInfoService))]
+internal class AvatarInfoService : IAvatarInfoService
+{
+ private readonly AppDbContext appDbContext;
+ private readonly ISummaryFactory summaryFactory;
+ private readonly EnkaClient enkaClient;
+
+ ///
+ /// 构造一个新的角色信息服务
+ ///
+ /// 数据库上下文
+ /// 简述工厂
+ /// Enka客户端
+ public AvatarInfoService(AppDbContext appDbContext, ISummaryFactory summaryFactory, EnkaClient enkaClient)
+ {
+ this.appDbContext = appDbContext;
+ this.summaryFactory = summaryFactory;
+ this.enkaClient = enkaClient;
+ }
+
+ ///
+ public async Task> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default)
+ {
+ if (HasOption(refreshOption, RefreshOption.RequestFromAPI))
+ {
+ EnkaResponse? resp = await GetEnkaResponseAsync(uid, token).ConfigureAwait(false);
+ if (resp == null)
+ {
+ return new(RefreshResult.APIUnavailable, null);
+ }
+
+ if (resp.IsValid)
+ {
+ IList list = HasOption(refreshOption, RefreshOption.StoreInDatabase)
+ ? UpdateDbAvatarInfo(uid.Value, resp.AvatarInfoList)
+ : resp.AvatarInfoList;
+
+ Summary summary = await summaryFactory.CreateAsync(resp.PlayerInfo, list).ConfigureAwait(false);
+ return new(RefreshResult.Ok, summary);
+ }
+ else
+ {
+ return new(RefreshResult.ShowcaseNotOpen, null);
+ }
+ }
+ else
+ {
+ PlayerInfo info = PlayerInfo.CreateEmpty(uid.Value);
+
+ Summary summary = await summaryFactory.CreateAsync(info, GetDbAvatarInfos(uid.Value)).ConfigureAwait(false);
+ return new(RefreshResult.Ok, summary);
+ }
+ }
+
+ private static bool HasOption(RefreshOption source, RefreshOption define)
+ {
+ return (source & define) == define;
+ }
+
+ private async Task GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default)
+ {
+ return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
+ ?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
+ }
+
+ private List UpdateDbAvatarInfo(string uid, IEnumerable webInfos)
+ {
+ List dbInfos = appDbContext.AvatarInfos
+ .Where(i => i.Uid == uid)
+ .ToList();
+
+ foreach (Web.Enka.Model.AvatarInfo webInfo in webInfos)
+ {
+ Model.Entity.AvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
+
+ if (entity == null)
+ {
+ entity = Model.Entity.AvatarInfo.Create(uid, webInfo);
+ appDbContext.Add(entity);
+ }
+ else
+ {
+ entity.Info = webInfo;
+ appDbContext.Update(entity);
+ }
+ }
+
+ appDbContext.SaveChanges();
+
+ return GetDbAvatarInfos(uid);
+ }
+
+ private List GetDbAvatarInfos(string uid)
+ {
+ return appDbContext.AvatarInfos
+ .Where(i => i.Uid == uid)
+ .Select(i => i.Info)
+ .AsEnumerable()
+ .OrderByDescending(i => i.AvatarId)
+ .ToList();
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs
new file mode 100644
index 00000000..d7c8d20b
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs
@@ -0,0 +1,21 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Binding.AvatarProperty;
+using Snap.Hutao.Web.Enka.Model;
+
+namespace Snap.Hutao.Service.AvatarInfo.Factory;
+
+///
+/// 简述工厂
+///
+internal interface ISummaryFactory
+{
+ ///
+ /// 异步创建简述对象
+ ///
+ /// 玩家信息
+ /// 角色列表
+ /// 简述对象
+ Task CreateAsync(PlayerInfo playerInfo, IEnumerable avatarInfos);
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs
new file mode 100644
index 00000000..240b6e5c
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs
@@ -0,0 +1,196 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Extension;
+using Snap.Hutao.Model;
+using Snap.Hutao.Model.Binding.AvatarProperty;
+using Snap.Hutao.Model.Intrinsic;
+using Snap.Hutao.Model.Metadata.Converter;
+using Snap.Hutao.Model.Metadata.Reliquary;
+using Snap.Hutao.Service.Metadata;
+using Snap.Hutao.Web.Enka.Model;
+using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
+using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
+using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
+using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
+using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo;
+using PropertyAvatar = Snap.Hutao.Model.Binding.AvatarProperty.Avatar;
+using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.Reliquary;
+using PropertyWeapon = Snap.Hutao.Model.Binding.AvatarProperty.Weapon;
+
+namespace Snap.Hutao.Service.AvatarInfo.Factory;
+
+///
+/// 简述工厂
+///
+[Injection(InjectAs.Transient, typeof(ISummaryFactory))]
+internal class SummaryFactory : ISummaryFactory
+{
+ private readonly IMetadataService metadataService;
+
+ ///
+ /// 构造一个新的简述工厂
+ ///
+ /// 元数据服务
+ public SummaryFactory(IMetadataService metadataService)
+ {
+ this.metadataService = metadataService;
+ }
+
+ ///
+ public async Task CreateAsync(ModelPlayerInfo playerInfo, IEnumerable avatarInfos)
+ {
+ Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
+ Dictionary idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false);
+ Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
+ Dictionary idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync().ConfigureAwait(false);
+
+ List reliqueryLevels = await metadataService.GetReliquaryLevelsAsync().ConfigureAwait(false);
+ List reliquaries = await metadataService.GetReliquariesAsync().ConfigureAwait(false);
+
+ SummaryFactoryInner inner = new(idAvatarMap, idRelicMainPropMap, idWeaponMap, idReliquaryAffixMap, reliqueryLevels, reliquaries);
+ return inner.Create(playerInfo, avatarInfos);
+ }
+
+ private class SummaryFactoryInner
+ {
+ private readonly Dictionary idAvatarMap;
+ private readonly Dictionary idRelicMainPropMap;
+ private readonly Dictionary idWeaponMap;
+ private readonly Dictionary idReliquaryAffixMap;
+ private readonly List reliqueryLevels;
+ private readonly List reliquaries;
+
+ public SummaryFactoryInner(
+ Dictionary idAvatarMap,
+ Dictionary idRelicMainPropMap,
+ Dictionary idWeaponMap,
+ Dictionary idReliquaryAffixMap,
+ List reliqueryLevels,
+ List reliquaries)
+ {
+ this.idAvatarMap = idAvatarMap;
+ this.idRelicMainPropMap = idRelicMainPropMap;
+ this.idWeaponMap = idWeaponMap;
+ this.reliqueryLevels = reliqueryLevels;
+ this.reliquaries = reliquaries;
+ this.idReliquaryAffixMap = idReliquaryAffixMap;
+ }
+
+ public Summary Create(ModelPlayerInfo playerInfo, IEnumerable avatarInfos)
+ {
+ // 用作头像
+ MetadataAvatar avatar = idAvatarMap[playerInfo.ProfilePicture.AvatarId];
+
+ return new()
+ {
+ Player = SummaryHelper.CreatePlayer(playerInfo, avatar),
+ Avatars = avatarInfos.Select(a => CreateAvatar(a)).ToList(),
+ };
+ }
+
+ private PropertyAvatar CreateAvatar(ModelAvatarInfo avatarInfo)
+ {
+ (List reliquaries, PropertyWeapon weapon) = ProcessEquip(avatarInfo.EquipList);
+ MetadataAvatar avatar = idAvatarMap[avatarInfo.AvatarId];
+
+ return new()
+ {
+ Name = avatar.Name,
+ Icon = AvatarIconConverter.IconNameToUri(avatar.Icon),
+ SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon),
+ Quality = avatar.Quality,
+ Level = avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value ?? string.Empty,
+ FetterLevel = avatarInfo.FetterInfo.ExpLevel,
+ Weapon = weapon,
+ Reliquaries = reliquaries,
+ Constellations = SummaryHelper.CreateConstellations(avatarInfo.TalentIdList, avatar.SkillDepot.Talents),
+ Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.GetCompositeSkillsNoInherents()),
+ Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
+ };
+ }
+
+ private (List Reliquaries, PropertyWeapon Weapon) ProcessEquip(IList equipments)
+ {
+ List reliquaries = new();
+ PropertyWeapon? weapon = null;
+
+ foreach (Equip equip in equipments)
+ {
+ switch (equip.Flat.ItemType)
+ {
+ case ItemType.ITEM_RELIQUARY:
+ reliquaries.Add(CreateReliquary(equip));
+ break;
+ case ItemType.ITEM_WEAPON:
+ weapon = CreateWeapon(equip);
+ break;
+ }
+ }
+
+ return (reliquaries, weapon!);
+ }
+
+ private PropertyReliquary CreateReliquary(Equip equip)
+ {
+ MetadataReliquary reliquary = reliquaries.Single(r => r.Ids.Contains(equip.ItemId));
+
+ return new()
+ {
+ // NameIconDescription
+ Name = reliquary.Name,
+ Icon = RelicIconConverter.IconNameToUri(reliquary.Icon),
+ Description = reliquary.Description,
+
+ // EquipBase
+ Level = $"+{equip.Reliquary!.Level - 1}",
+ Quality = reliquary.RankLevel,
+ MainProperty = CreateReliquaryMainProperty(equip.Reliquary.MainPropId, reliquary.RankLevel, equip.Reliquary.Level),
+
+ // Reliquary
+ SubProperties = equip.Reliquary.AppendPropIdList.Select(id => CreateReliquarySubProperty(id)).ToList(),
+ };
+ }
+
+ private Pair CreateReliquaryMainProperty(int propId, ItemQuality quality, int level)
+ {
+ ReliquaryLevel reliquaryLevel = reliqueryLevels.Single(r => r.Level == level && r.Quality == quality);
+ FightProperty property = idRelicMainPropMap[propId];
+
+ return new(property.GetDescription(), PropertyInfoDescriptor.FormatProperty(property, reliquaryLevel.Properties[property]));
+ }
+
+ private Pair CreateReliquarySubProperty(int appendPropId)
+ {
+ ReliquaryAffix affix = idReliquaryAffixMap[appendPropId];
+ FightProperty property = affix.Type;
+
+ return new(property.GetDescription(), PropertyInfoDescriptor.FormatProperty(property, affix.Value));
+ }
+
+ private PropertyWeapon CreateWeapon(Equip equip)
+ {
+ MetadataWeapon weapon = idWeaponMap[equip.ItemId];
+ (string id, int level) = equip.Weapon!.AffixMap.Single();
+
+ return new()
+ {
+ // NameIconDescription
+ Name = weapon.Name,
+ Icon = EquipIconConverter.IconNameToUri(weapon.Icon),
+ Description = weapon.Description,
+
+ // EquipBase
+ Level = $"Lv.{equip.Weapon!.Level}",
+ Quality = weapon.Quality,
+ MainProperty = new(string.Empty, string.Empty), // TODO
+
+ // Weapon
+ SubProperty = new(string.Empty, string.Empty), // TODO
+ AffixLevel = $"精炼{level + 1}",
+ AffixName = weapon.Affix?.Name ?? string.Empty,
+ AffixDescription = weapon.Affix?.Descriptions.Single(a => a.Level == level).Description ?? string.Empty,
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs
new file mode 100644
index 00000000..4bf21ff3
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs
@@ -0,0 +1,227 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Extension;
+using Snap.Hutao.Model;
+using Snap.Hutao.Model.Binding.AvatarProperty;
+using Snap.Hutao.Model.Intrinsic;
+using Snap.Hutao.Model.Metadata.Annotation;
+using Snap.Hutao.Model.Metadata.Avatar;
+using Snap.Hutao.Model.Metadata.Converter;
+using Snap.Hutao.Web.Enka.Model;
+using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
+using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo;
+
+namespace Snap.Hutao.Service.AvatarInfo.Factory;
+
+///
+/// 简述帮助类
+///
+internal static class SummaryHelper
+{
+ ///
+ /// 创建玩家对象
+ ///
+ /// 玩家信息
+ /// 角色
+ /// 玩家对象
+ public static Player CreatePlayer(ModelPlayerInfo playerInfo, MetadataAvatar avatar)
+ {
+ return new()
+ {
+ Nickname = playerInfo.Nickname,
+ Level = playerInfo.Level,
+ Signature = playerInfo.Signature,
+ FinishAchievementNumber = playerInfo.FinishAchievementNum,
+ SipralAbyssFloorLevel = $"{playerInfo.TowerFloorIndex} - {playerInfo.TowerLevelIndex}",
+ ProfilePicture = AvatarIconConverter.IconNameToUri(GetIconName(playerInfo.ProfilePicture, avatar)),
+ };
+ }
+
+ ///
+ /// 创建命之座
+ ///
+ /// 激活的命座列表
+ /// 全部命座
+ /// 命之座
+ public static List CreateConstellations(IList? talentIds, IList talents)
+ {
+ List constellations = new();
+
+ foreach (SkillBase talent in talents)
+ {
+ Constellation constellation = new()
+ {
+ Name = talent.Name,
+ Icon = SkillIconConverter.IconNameToUri(talent.Icon),
+ Description = talent.Description,
+ IsActiviated = talentIds?.Contains(talent.Id) ?? false,
+ };
+
+ constellations.Add(constellation);
+ }
+
+ return constellations;
+ }
+
+ ///
+ /// 创建技能组
+ ///
+ /// 技能等级映射
+ /// 额外提升等级映射
+ /// 技能列表
+ /// 技能
+ public static List CreateSkills(IDictionary skillLevelMap, IDictionary? proudSkillExtraLevelMap, IEnumerable proudSkills)
+ {
+ Dictionary skillLevelMapCopy = new(skillLevelMap);
+
+ if (proudSkillExtraLevelMap != null)
+ {
+ foreach ((string skillGroupId, int extraLevel) in proudSkillExtraLevelMap)
+ {
+ int skillGroupIdInt32 = int.Parse(skillGroupId);
+ int skillId = proudSkills.Single(p => p.GroupId == skillGroupIdInt32).Id;
+
+ skillLevelMapCopy.Increase($"{skillId}", extraLevel);
+ }
+ }
+
+ List skills = new();
+
+ foreach (ProudableSkill proudableSkill in proudSkills)
+ {
+ Skill skill = new()
+ {
+ Name = proudableSkill.Name,
+ Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon),
+ Description = proudableSkill.Description,
+ Info = DescParamDescriptor.Convert(proudableSkill.Proud, skillLevelMapCopy[$"{proudableSkill.Id}"]),
+ };
+
+ skills.Add(skill);
+ }
+
+ return skills;
+ }
+
+ ///
+ /// 创建角色属性
+ ///
+ /// 属性映射
+ /// 列表
+ public static List> CreateAvatarProperties(IDictionary fightPropMap)
+ {
+ List> properties;
+
+ double baseHp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_HP); // 1
+ double hp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP); // 2
+ double hpPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP_PERCENT); // 3
+ double hpAdd = hp + (baseHp * hpPercent);
+ double maxHp = baseHp + hpAdd;
+ Pair2 hpPair2 = new("生命值", FormatValue(FormatMethod.Integer, maxHp), $"[+{FormatValue(FormatMethod.Integer, hpAdd)}]");
+
+ double baseAtk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_ATTACK); // 4
+ double atk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK); // 5
+ double atkPrecent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK_PERCENT); // 6
+ double atkAdd = atk + (baseAtk * atkPrecent);
+ double maxAtk = baseAtk + atkAdd;
+ Pair2 atkPair2 = new("攻击力", FormatValue(FormatMethod.Integer, maxAtk), $"[+{FormatValue(FormatMethod.Integer, atkAdd)}]");
+
+ double baseDef = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_DEFENSE); // 7
+ double def = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE); // 8
+ double defPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE_PERCENT); // 9
+ double defAdd = def + (baseDef * defPercent);
+ double maxDef = baseDef + defPercent;
+ Pair2 defPair2 = new("防御力", FormatValue(FormatMethod.Integer, maxDef), $"[+{FormatValue(FormatMethod.Integer, defAdd)}]");
+
+ double em = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28
+ Pair2 emPair2 = new("元素精通", FormatValue(FormatMethod.Integer, em), null);
+
+ double critRate = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL); // 20
+ Pair2 critRatePair2 = new("暴击率", FormatValue(FormatMethod.Percent, critRate), null);
+
+ double critDMG = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22
+ Pair2 critDMGPair2 = new("暴击伤害", FormatValue(FormatMethod.Percent, critDMG), null);
+
+ double chargeEff = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23
+ Pair2 chargeEffPair2 = new("元素充能效率", FormatValue(FormatMethod.Percent, chargeEff), null);
+
+ properties = new() { hpPair2, atkPair2, defPair2, emPair2, critRatePair2, critDMGPair2, chargeEffPair2 };
+
+ FightProperty bonusProperty = GetBonusFightProperty(fightPropMap);
+ if (bonusProperty != FightProperty.FIGHT_PROP_NONE)
+ {
+ double value = fightPropMap[bonusProperty];
+ Pair2 bonusPair2 = new(bonusProperty.GetDescription(), FormatValue(FormatMethod.Percent, value), null);
+ properties.Add(bonusPair2);
+ }
+
+ return properties;
+ }
+
+ private static string FormatValue(FormatMethod method, double value)
+ {
+ return method switch
+ {
+ FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(),
+ FormatMethod.Percent => string.Format("{0:P1}", value),
+ _ => value.ToString(),
+ };
+ }
+
+ private static FightProperty GetBonusFightProperty(IDictionary fightPropMap)
+ {
+ if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY))
+ {
+ return FightProperty.FIGHT_PROP_FIRE_ADD_HURT;
+ }
+
+ if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY))
+ {
+ return FightProperty.FIGHT_PROP_ELEC_ADD_HURT;
+ }
+
+ if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_WATER_ENERGY))
+ {
+ return FightProperty.FIGHT_PROP_WATER_ADD_HURT;
+ }
+
+ if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY))
+ {
+ return FightProperty.FIGHT_PROP_GRASS_ADD_HURT;
+ }
+
+ if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_WIND_ENERGY))
+ {
+ return FightProperty.FIGHT_PROP_WIND_ADD_HURT;
+ }
+
+ if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ICE_ENERGY))
+ {
+ return FightProperty.FIGHT_PROP_ICE_ADD_HURT;
+ }
+
+ if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY))
+ {
+ return FightProperty.FIGHT_PROP_ROCK_ADD_HURT;
+ }
+
+ // 物伤
+ if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT))
+ {
+ return FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT;
+ }
+
+ return FightProperty.FIGHT_PROP_NONE;
+ }
+
+ private static string GetIconName(ProfilePicture profilePicture, MetadataAvatar avatar)
+ {
+ if (profilePicture.CostumeId != null)
+ {
+ return avatar.Costumes.Single(c => c.Id == profilePicture.CostumeId).Icon ?? string.Empty;
+ }
+
+ return avatar.Icon;
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs
new file mode 100644
index 00000000..8252f91d
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs
@@ -0,0 +1,23 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Core.Threading;
+using Snap.Hutao.Model.Binding.AvatarProperty;
+using Snap.Hutao.Web.Hoyolab;
+
+namespace Snap.Hutao.Service.AvatarInfo;
+
+///
+/// 角色信息服务
+///
+internal interface IAvatarInfoService
+{
+ ///
+ /// 异步获取总览数据
+ ///
+ /// uid
+ /// 刷新选项
+ /// 取消令牌
+ /// 总览数据
+ Task> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default);
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshOption.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshOption.cs
new file mode 100644
index 00000000..65bd424f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshOption.cs
@@ -0,0 +1,31 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AvatarInfo;
+
+///
+/// 刷新选项
+///
+[Flags]
+public enum RefreshOption
+{
+ ///
+ /// 是否存入本地数据库
+ ///
+ StoreInDatabase = 0b00000001,
+
+ ///
+ /// 从API获取
+ ///
+ RequestFromAPI = 0b00000010,
+
+ ///
+ /// 仅数据库
+ ///
+ DatabaseOnly = 0b00000000,
+
+ ///
+ /// 标准操作
+ ///
+ Standard = StoreInDatabase | RequestFromAPI,
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs
new file mode 100644
index 00000000..d498383b
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs
@@ -0,0 +1,25 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.AvatarInfo;
+
+///
+/// 刷新结果
+///
+public enum RefreshResult
+{
+ ///
+ /// 正常
+ ///
+ Ok,
+
+ ///
+ /// API 不可用
+ ///
+ APIUnavailable,
+
+ ///
+ /// 角色橱窗未对外开放
+ ///
+ ShowcaseNotOpen,
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
index af31d25e..40c4453e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
@@ -5,6 +5,8 @@ using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database;
+using Snap.Hutao.Core.Diagnostics;
+using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
@@ -45,6 +47,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
private readonly IMetadataService metadataService;
private readonly IInfoBarService infoBarService;
private readonly IGachaStatisticsFactory gachaStatisticsFactory;
+ private readonly ILogger logger;
private readonly DbCurrent dbCurrent;
private readonly Dictionary itemBaseCache = new();
@@ -65,6 +68,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
/// 元数据服务
/// 信息条服务
/// 祈愿统计工厂
+ /// 日志器
/// 消息器
public GachaLogService(
AppDbContext appDbContext,
@@ -73,6 +77,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
IMetadataService metadataService,
IInfoBarService infoBarService,
IGachaStatisticsFactory gachaStatisticsFactory,
+ ILogger logger,
IMessenger messenger)
{
this.appDbContext = appDbContext;
@@ -80,6 +85,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
this.gachaInfoClient = gachaInfoClient;
this.metadataService = metadataService;
this.infoBarService = infoBarService;
+ this.logger = logger;
this.gachaStatisticsFactory = gachaStatisticsFactory;
dbCurrent = new(appDbContext, appDbContext.GachaArchives, messenger);
@@ -143,21 +149,24 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
}
///
- public Task GetStatisticsAsync(GachaArchive? archive = null)
+ public async Task GetStatisticsAsync(GachaArchive? archive = null)
{
archive ??= CurrentArchive;
// Return statistics
if (archive != null)
{
+ ValueStopwatch stopwatch = ValueStopwatch.StartNew();
IQueryable items = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId);
- return gachaStatisticsFactory.CreateAsync(items);
+ GachaStatistics statistics = await gachaStatisticsFactory.CreateAsync(items).ConfigureAwait(false);
+ logger.LogInformation(EventIds.GachaStatisticGeneration, "GachaStatistic Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds);
+ return statistics;
}
else
{
- return Must.Fault("没有选中的存档");
+ throw Must.NeverHappen();
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs
index 8ed56937..9109577b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs
@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
+using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Avatar;
@@ -47,21 +48,35 @@ internal interface IMetadataService
///
/// 取消令牌
/// 卡池配置列表
- ValueTask> GetGachaEventsAsync(CancellationToken token = default(CancellationToken));
+ ValueTask> GetGachaEventsAsync(CancellationToken token = default);
///
/// 异步获取Id到角色的字典
///
/// 取消令牌
/// Id到角色的字典
- ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default(CancellationToken));
+ ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default);
+
+ ///
+ /// 异步获取ID到圣遗物副词条的字典
+ ///
+ /// 取消令牌
+ /// 字典
+ ValueTask> GetIdReliquaryAffixMapAsync(CancellationToken token = default);
+
+ ///
+ /// 异步获取圣遗物主词条Id与属性的字典
+ ///
+ /// 取消令牌
+ /// 字典
+ ValueTask> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default);
///
/// 异步获取ID到武器的字典
///
/// 取消令牌
/// Id到武器的字典
- ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default(CancellationToken));
+ ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default);
///
/// 异步获取名称到角色的字典
@@ -91,6 +106,13 @@ internal interface IMetadataService
/// 圣遗物强化属性列表
ValueTask> GetReliquaryAffixesAsync(CancellationToken token = default);
+ ///
+ /// 异步获取圣遗物等级数据
+ ///
+ /// 取消令牌
+ /// 圣遗物等级数据
+ ValueTask> GetReliquaryLevelsAsync(CancellationToken token = default);
+
///
/// 异步获取圣遗物主属性强化属性列表
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs
index d6ebac6b..bf47506f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs
@@ -8,6 +8,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
+using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Avatar;
@@ -122,6 +123,18 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
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)
{
@@ -152,6 +165,12 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
return FromCacheOrFileAsync>("ReliquaryAffix", token);
}
+ ///
+ public ValueTask> GetReliquaryLevelsAsync(CancellationToken token = default)
+ {
+ return FromCacheOrFileAsync>("ReliquaryMainAffixLevel", token);
+ }
+
///
public ValueTask> GetReliquaryMainAffixesAsync(CancellationToken token = default)
{
@@ -295,4 +314,20 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
Dictionary dict = list.ToDictionaryOverride(keySelector);
return memoryCache.Set(cacheKey, dict);
}
+
+ private async ValueTask> FromCacheAsDictionaryAsync(string fileName, Func keySelector, Func valueSelector, CancellationToken token)
+ where TKey : notnull
+ {
+ Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败");
+ string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}.Map.{typeof(TKey).Name}.{typeof(TValue).Name}";
+
+ if (memoryCache.TryGetValue(cacheKey, out object? value))
+ {
+ return Must.NotNull((Dictionary)value!);
+ }
+
+ List list = await FromCacheOrFileAsync>(fileName, token).ConfigureAwait(false);
+ Dictionary dict = list.ToDictionaryOverride(keySelector, valueSelector);
+ return memoryCache.Set(cacheKey, dict);
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
index 17851f76..98e200cc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
+++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
@@ -38,6 +38,8 @@
+
+
@@ -48,6 +50,7 @@
+
@@ -56,6 +59,7 @@
+
@@ -83,6 +87,8 @@
+
+
@@ -128,6 +134,16 @@
+
+
+ MSBuild:Compile
+
+
+
+
+ MSBuild:Compile
+
+
MSBuild:Compile
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml
index 86378265..206e9a65 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml
@@ -1,9 +1,8 @@
-
-
-
-
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml
new file mode 100644
index 00000000..5b66372b
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml.cs
new file mode 100644
index 00000000..b6df6a3f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml.cs
@@ -0,0 +1,47 @@
+// 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;
+using Snap.Hutao.Web.Hoyolab;
+
+namespace Snap.Hutao.View.Dialog;
+
+///
+/// 角色信息查询UID对话框
+///
+public sealed partial class AvatarInfoQueryDialog : ContentDialog
+{
+ ///
+ /// 构造一个新的角色信息查询UID对话框
+ ///
+ /// 窗口
+ public AvatarInfoQueryDialog(Window window)
+ {
+ InitializeComponent();
+ XamlRoot = window.Content.XamlRoot;
+ }
+
+ ///
+ /// 获取玩家UID
+ ///
+ /// 玩家UID
+ public async Task> GetPlayerUidAsync()
+ {
+ ContentDialogResult result = await ShowAsync();
+
+ return new(result == ContentDialogResult.Primary, result == ContentDialogResult.Primary ? new(InputText.Text) : default);
+ }
+
+ private void InputTextChanged(object sender, TextChangedEventArgs e)
+ {
+ bool inputValid = string.IsNullOrEmpty(InputText.Text) && InputText.Text.Length == 9;
+
+ (PrimaryButtonText, IsPrimaryButtonEnabled) = inputValid switch
+ {
+ true => ("请输入正确的UID", false),
+ false => ("确认", true),
+ };
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml
index 23f30928..5f34f208 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml
@@ -5,18 +5,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
- xmlns:mxi="using:Microsoft.Xaml.Interactivity"
- xmlns:shcb="using:Snap.Hutao.Control.Behavior"
mc:Ignorable="d"
Title="导入祈愿记录"
DefaultButton="Primary"
PrimaryButtonText="确认"
CloseButtonText="取消"
Style="{StaticResource DefaultContentDialogStyle}">
-
-
-
-
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml
index 839e3c57..35c85b3a 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml
@@ -7,12 +7,10 @@
xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:cwuconv="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:shvc="using:Snap.Hutao.View.Control"
- xmlns:mxi="using:Microsoft.Xaml.Interactivity"
- xmlns:shcb="using:Snap.Hutao.Control.Behavior"
mc:Ignorable="d"
Style="{StaticResource DefaultContentDialogStyle}"
Title="获取祈愿物品中">
-
+
@@ -27,10 +25,6 @@
-
-
-
-
-
-
-
-
-
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
index 5a393837..42f837ca 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
@@ -22,7 +22,7 @@
IsBackEnabled="{Binding ElementName=ContentFrame,Path=CanGoBack}">
@@ -34,14 +34,19 @@
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BtnIcon_Gacha.png}"/>
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml
index e2b64d9a..b6cfd7ee 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml
@@ -4,14 +4,15 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
+ xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
- xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shc="using:Snap.Hutao.Control"
+ xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shci="using:Snap.Hutao.Control.Image"
+ xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
xmlns:shv="using:Snap.Hutao.ViewModel"
- xmlns:shcm="using:Snap.Hutao.Control.Markup"
- xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
d:DataContext="{d:DesignInstance shv:AchievementViewModel}">
@@ -42,12 +43,23 @@
DefaultLabelPosition="Right">
+ VerticalContentAlignment="Center"
+ Style="{StaticResource DefaultAutoSuggestBoxStyle}"
+ QueryIcon="{shcm:FontIcon Glyph=}">
+
+
+
+
+
+
@@ -134,7 +146,7 @@
-
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml
new file mode 100644
index 00000000..215da3b8
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml
@@ -0,0 +1,492 @@
+
+
+
+
+
+
+
+
+
+ 1
+
+
+ 0.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml.cs
new file mode 100644
index 00000000..b7c5b05c
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.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 AvatarPropertyPage : ScopedPage
+{
+ ///
+ /// 初始化一个新的角色属性页
+ ///
+ public AvatarPropertyPage()
+ {
+ InitializeWith();
+ InitializeComponent();
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml
index da331f14..1fcf46c2 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml
@@ -23,7 +23,7 @@
-
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml
index 6d4f0db1..927b5df5 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml
@@ -73,10 +73,8 @@
-
-
-
+ OpenPaneLength="200"
+ PaneBackground="{StaticResource CardBackgroundFillColorSecondary}">
diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
index 3f6f2da3..0bcfe77c 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
@@ -197,15 +197,15 @@
+ Text="Cookie操作"/>
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs
index b40dd36f..62598222 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs
@@ -57,6 +57,7 @@ internal class AchievementViewModel
private ObservableCollection? archives;
private Model.Entity.AchievementArchive? selectedArchive;
private bool isIncompletedItemsFirst = true;
+ private string searchText = string.Empty;
///
/// 构造一个新的成就视图模型
@@ -88,6 +89,7 @@ internal class AchievementViewModel
ImportUIAFFromFileCommand = asyncRelayCommandFactory.Create(ImportUIAFFromFileAsync);
AddArchiveCommand = asyncRelayCommandFactory.Create(AddArchiveAsync);
RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync);
+ SearchAchievementCommand = new RelayCommand(SearchAchievement);
SortIncompletedSwitchCommand = new RelayCommand(UpdateAchievementsSort);
messenger.Register(this);
@@ -147,10 +149,20 @@ internal class AchievementViewModel
set
{
SetProperty(ref selectedAchievementGoal, value);
+ SearchText = string.Empty;
UpdateAchievementFilter(value);
}
}
+ ///
+ /// 搜索文本
+ ///
+ public string SearchText
+ {
+ get => searchText;
+ set => SetProperty(ref searchText, value);
+ }
+
///
/// 未完成优先
///
@@ -175,6 +187,11 @@ internal class AchievementViewModel
///
public ICommand RemoveArchiveCommand { get; }
+ ///
+ /// 搜索成就命令
+ ///
+ public ICommand SearchAchievementCommand { get; }
+
///
/// 从剪贴板导入UIAF命令
///
@@ -345,6 +362,30 @@ internal class AchievementViewModel
}
}
+ private void SearchAchievement(string? search)
+ {
+ if (Achievements != null)
+ {
+ SetProperty(ref selectedAchievementGoal, null);
+
+ if (!string.IsNullOrEmpty(search))
+ {
+ if (search.Length == 5 && int.TryParse(search, out int achiId))
+ {
+ Achievements.Filter = (object o) => ((Model.Binding.Achievement)o).Inner.Id == achiId;
+ }
+ else
+ {
+ Achievements.Filter = (object o) =>
+ {
+ Model.Binding.Achievement achi = (Model.Binding.Achievement)o;
+ return achi.Inner.Title.Contains(search) || achi.Inner.Description.Contains(search);
+ };
+ }
+ }
+ }
+ }
+
[ThreadAccess(ThreadAccessState.AnyThread)]
private async Task ImportUIAFFromClipboardAsync()
{
@@ -469,7 +510,7 @@ internal class AchievementViewModel
{
Achievements.Filter = goal != null
? ((object o) => o is Model.Binding.Achievement achi && achi.Inner.Goal == goal.Id)
- : ((object o) => true);
+ : null;
}
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs
new file mode 100644
index 00000000..aef9c5c6
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs
@@ -0,0 +1,148 @@
+// 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.AvatarProperty;
+using Snap.Hutao.Service.Abstraction;
+using Snap.Hutao.Service.AvatarInfo;
+using Snap.Hutao.Service.User;
+using Snap.Hutao.View.Dialog;
+using Snap.Hutao.Web.Hoyolab;
+using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
+
+namespace Snap.Hutao.ViewModel;
+
+///
+/// 角色属性视图模型
+///
+[Injection(InjectAs.Transient)]
+internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
+{
+ private readonly IUserService userService;
+ private readonly IAvatarInfoService avatarInfoService;
+ private readonly IInfoBarService infoBarService;
+ private Summary? summary;
+ private Avatar? selectedAvatar;
+
+ ///
+ /// 构造一个新的角色属性视图模型
+ ///
+ /// 用户服务
+ /// 角色信息服务
+ /// 异步命令工厂
+ /// 信息条服务
+ public AvatarPropertyViewModel(
+ IUserService userService,
+ IAvatarInfoService avatarInfoService,
+ IAsyncRelayCommandFactory asyncRelayCommandFactory,
+ IInfoBarService infoBarService)
+ {
+ this.userService = userService;
+ this.avatarInfoService = avatarInfoService;
+ this.infoBarService = infoBarService;
+
+ OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
+ RefreshByUserGameRoleCommand = asyncRelayCommandFactory.Create(RefreshByUserGameRoleAsync);
+ RefreshByInputUidCommand = asyncRelayCommandFactory.Create(RefreshByInputUidAsync);
+ }
+
+ ///
+ public CancellationToken CancellationToken { get; set; }
+
+ ///
+ /// 简述对象
+ ///
+ public Summary? Summary { get => summary; set => SetProperty(ref summary, value); }
+
+ ///
+ /// 选中的角色
+ ///
+ public Avatar? SelectedAvatar { get => selectedAvatar; set => SetProperty(ref selectedAvatar, value); }
+
+ ///
+ /// 加载界面命令
+ ///
+ public ICommand OpenUICommand { get; set; }
+
+ ///
+ /// 按当前角色刷新命令
+ ///
+ public ICommand RefreshByUserGameRoleCommand { get; set; }
+
+ ///
+ /// 按UID刷新命令
+ ///
+ public ICommand RefreshByInputUidCommand { get; set; }
+
+ private Task OpenUIAsync()
+ {
+ return RefreshCoreAsync(RefreshOption.DatabaseOnly);
+ }
+
+ private Task RefreshByUserGameRoleAsync()
+ {
+ return RefreshCoreAsync(RefreshOption.Standard);
+ }
+
+ private async Task RefreshByInputUidAsync()
+ {
+ MainWindow mainWindow = Ioc.Default.GetRequiredService();
+ (bool isOk, PlayerUid uid) = await new AvatarInfoQueryDialog(mainWindow).GetPlayerUidAsync().ConfigureAwait(false);
+
+ if (isOk)
+ {
+ (RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(uid, RefreshOption.RequestFromAPI).ConfigureAwait(false);
+
+ if (result == RefreshResult.Ok)
+ {
+ await ThreadHelper.SwitchToMainThreadAsync();
+ Summary = summary;
+ }
+ else
+ {
+ switch (result)
+ {
+ case RefreshResult.APIUnavailable:
+ infoBarService.Warning("面板服务当前不可用");
+ break;
+ case RefreshResult.ShowcaseNotOpen:
+ infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
+ break;
+ }
+ }
+ }
+ }
+
+ private async Task RefreshCoreAsync(RefreshOption option)
+ {
+ if (userService.Current is Model.Binding.User user)
+ {
+ if (user.SelectedUserGameRole is UserGameRole role)
+ {
+ (RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync((PlayerUid)role, option).ConfigureAwait(false);
+
+ if (result == RefreshResult.Ok)
+ {
+ await ThreadHelper.SwitchToMainThreadAsync();
+ Summary = summary;
+ SelectedAvatar = Summary?.Avatars.FirstOrDefault();
+ }
+ else
+ {
+ switch (result)
+ {
+ case RefreshResult.APIUnavailable:
+ infoBarService.Warning("面板服务当前不可用");
+ break;
+ case RefreshResult.ShowcaseNotOpen:
+ infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
+ break;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs
index bd664ff8..4e029e91 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs
@@ -5,7 +5,6 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Enka.Model;
using Snap.Hutao.Web.Hoyolab;
using System.Net.Http;
-using System.Net.Http.Json;
namespace Snap.Hutao.Web.Enka;
@@ -19,14 +18,20 @@ internal class EnkaClient
private const string EnkaAPIHutaoForward = "https://enka-api.hut.ao/{0}";
private readonly HttpClient httpClient;
+ private readonly JsonSerializerOptions options;
+ private readonly ILogger logger;
///
/// 构造一个新的 Enka API 客户端
///
/// http客户端
- public EnkaClient(HttpClient httpClient)
+ /// 序列化选项
+ /// 日志器
+ public EnkaClient(HttpClient httpClient, JsonSerializerOptions options, ILogger logger)
{
this.httpClient = httpClient;
+ this.options = options;
+ this.logger = logger;
}
///
@@ -37,7 +42,7 @@ internal class EnkaClient
/// Enka API 响应
public Task GetForwardDataAsync(PlayerUid playerUid, CancellationToken token)
{
- return httpClient.GetFromJsonAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), token);
+ return httpClient.TryCatchGetFromJsonAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), options, logger, token);
}
///
@@ -48,6 +53,6 @@ internal class EnkaClient
/// Enka API 响应
public Task GetDataAsync(PlayerUid playerUid, CancellationToken token)
{
- return httpClient.GetFromJsonAsync(string.Format(EnkaAPI, playerUid.Value), token);
+ return httpClient.TryCatchGetFromJsonAsync(string.Format(EnkaAPI, playerUid.Value), options, logger, token);
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs
index 28cd20b2..bb7c9457 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs
@@ -31,7 +31,7 @@ public class AvatarInfo
/// 命座 Id
///
[JsonPropertyName("talentIdList")]
- public IList TalentIdList { get; set; } = default!;
+ public IList? TalentIdList { get; set; }
///
/// 属性Map
@@ -86,5 +86,5 @@ public class AvatarInfo
/// 命座额外技能等级
///
[JsonPropertyName("proudSkillExtraLevelMap")]
- public IDictionary ProudSkillExtraLevelMap { get; set; } = default!;
+ public IDictionary? ProudSkillExtraLevelMap { get; set; } = default!;
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs
index b90ff6d6..b44aba40 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs
@@ -32,14 +32,7 @@ public class EnkaResponse
///
public bool IsValid
{
- get => Ttl.HasValue;
- }
-
- ///
- /// 是否包含角色详细数据
- ///
- public bool HasDetail
- {
- get => AvatarInfoList != null;
+ [MemberNotNullWhen(true, nameof(PlayerInfo), nameof(AvatarInfoList))]
+ get => PlayerInfo != null && AvatarInfoList != null;
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/PlayerInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/PlayerInfo.cs
index 7f6f8903..f70a6463 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/PlayerInfo.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/PlayerInfo.cs
@@ -83,4 +83,19 @@ public class PlayerInfo
///
[JsonPropertyName("profilePicture")]
public ProfilePicture ProfilePicture { get; set; } = default!;
+
+ ///
+ /// 创建空对象
+ ///
+ /// uid
+ /// 空的玩家信息
+ public static PlayerInfo CreateEmpty(string uid)
+ {
+ return new()
+ {
+ Nickname = uid,
+ Signature = string.Empty,
+ ProfilePicture = new() { AvatarId = 10000046 }, // use Hutao as default.
+ };
+ }
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/ProfilePicture.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/ProfilePicture.cs
index dd64788b..a32f6873 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/ProfilePicture.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/ProfilePicture.cs
@@ -13,4 +13,10 @@ public class ProfilePicture
///
[JsonPropertyName("avatarId")]
public int AvatarId { get; set; }
+
+ ///
+ /// 衣装Id
+ ///
+ [JsonPropertyName("costumeId")]
+ public int? CostumeId { get; set; }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
index d80bc00b..0df5901e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Extension;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs
index 18f71349..ccc5458d 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs
@@ -29,7 +29,7 @@ internal abstract class DynamicSecretProvider2 : Md5Convert
string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options);
// query
- string q = string.Join("&", new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x));
+ string q = string.Join('&', new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x));
// check
string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret2Salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs
deleted file mode 100644
index c4bd216f..00000000
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) DGP Studio. All rights reserved.
-// Licensed under the MIT license.
-
-namespace Snap.Hutao.Web.Hoyolab;
-
-///
-/// Uid Token 对
-///
-public struct UidToken
-{
- ///
- /// Uid
- ///
- public string Uid;
-
- ///
- /// Token
- ///
- public string Token;
-
- ///
- /// 构造一个新的 Uid Token 对
- ///
- /// uid
- /// token
- public UidToken(string uid, string token)
- {
- Uid = uid;
- Token = token;
- }
-}
\ No newline at end of file