Compare commits

..

13 Commits

Author SHA1 Message Date
DismissedLight
d28624dea1 Merge pull request #84 from DGP-Studio/feat/avatar_info
Feat/avatar info
2022-10-04 16:48:22 +08:00
DismissedLight
d802d1af15 avatar info complete 1 2022-10-04 16:44:53 +08:00
DismissedLight
43e3df9cba avatar info phase 1 2022-10-04 00:09:25 +08:00
DismissedLight
8e5e59ad0d remove null check to cookie 2022-09-29 16:33:01 +08:00
DismissedLight
ed7d55ddd5 Merge pull request #64 from DGP-Studio/refactor/userserive
UserService v2 implementation
2022-09-29 16:14:27 +08:00
DismissedLight
94ef94a621 impl 2022-09-29 16:13:47 +08:00
Masterain
fd35213741 Merge pull request #52 from Masterain98/main
Update README.md
2022-09-27 19:23:15 -07:00
Masterain
0f752129b7 Update README.md 2022-09-27 19:20:49 -07:00
DismissedLight
331cc14532 Update Persistence.cs 2022-09-27 14:38:06 +08:00
DismissedLight
c0ddb24825 fix window issue 2022-09-27 14:36:13 +08:00
DismissedLight
e925c5909c update readme 2022-09-27 12:35:37 +08:00
DismissedLight
f29bfda4d9 Stoken upgradable 2022-09-26 16:55:27 +08:00
DismissedLight
cb6a9badc0 File IO API 2022-09-24 22:10:29 +08:00
171 changed files with 4504 additions and 904 deletions

View File

@@ -3,10 +3,18 @@
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
## 项目首页(文档)
[![Deploy Docs](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml/badge.svg)](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml)
[HUT.AO](https://hut.ao)
## 安装
* 前往 [下载页面](https://go.hut.ao/archive) 下载最新版本的 `胡桃` 安装包
* 完全解压后,使用 powershell 运行 `install.ps1` 文件
* 前往 [下载页面](https://go.hut.ao/down) 下载最新版本的 `胡桃` 安装包
* (曾启用的可以跳过此步骤)在系统设置中打开 **开发者选项** 界面,勾选 `开发人员模式``允许 PowerShell 脚本`
* 完全解压后,右键使用 powershell 运行 `install.ps1` 文件
* 安装完成后可以关闭 `允许 PowerShell 脚本`
## 特别感谢

View File

@@ -74,39 +74,43 @@ internal static partial class ServiceCollectionExtensions
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
lineBuilder
.Clear()
.Append("\r\n");
AttributeData injectionInfo = classSymbol
IEnumerable<AttributeData> datas = classSymbol
.GetAttributes()
.Single(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
.Where(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
foreach (AttributeData injectionInfo in datas)
{
case InjectAsSingletonName:
lineBuilder.Append(@" services.AddSingleton(");
break;
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient(");
break;
default:
throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]");
lineBuilder
.Clear()
.Append("\r\n");
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
{
case InjectAsSingletonName:
lineBuilder.Append(@" services.AddSingleton(");
break;
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient(");
break;
default:
throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]");
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.ToCSharpString()}, ");
}
lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}));");
lines.Add(lineBuilder.ToString());
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.ToCSharpString()}, ");
}
lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}));");
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Exception;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;

View File

@@ -3,6 +3,7 @@
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Configuration;
namespace Snap.Hutao.Context.Database;
@@ -50,6 +51,11 @@ public class AppDbContext : DbContext
/// </summary>
public DbSet<GachaArchive> GachaArchives { get; set; } = default!;
/// <summary>
/// 角色信息
/// </summary>
public DbSet<AvatarInfo> AvatarInfos { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
@@ -59,4 +65,12 @@ public class AppDbContext : DbContext
{
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
}
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.ApplyConfiguration(new AvatarInfoConfiguration())
.ApplyConfiguration(new UserConfiguration());
}
}

View File

@@ -1,40 +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.Media;
using Microsoft.UI.Xaml.Shapes;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// Make ContentDialog's SmokeLayerBackground dsiplay over custom titleBar
/// </summary>
public class ContentDialogBehavior : BehaviorBase<FrameworkElement>
{
/// <inheritdoc/>
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);
}
}
}

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -24,8 +24,8 @@ internal class I18NExtension : MarkupExtension
static I18NExtension()
{
string currentName = CultureInfo.CurrentUICulture.Name;
Type languageType = EnumerableExtensions.GetValueOrDefault(TranslationMap, currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType)!;
Type? languageType = ((IDictionary<string, Type>)TranslationMap).GetValueOrDefault(currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
}
/// <summary>

View File

@@ -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;
/// <summary>
/// Uri扩展
/// </summary>
[MarkupExtensionReturnType(ReturnType = typeof(Uri))]
public sealed class UriExtension : MarkupExtension
{
/// <summary>
/// 构造一个新的Uri扩展
/// </summary>
public UriExtension()
{
}
/// <summary>
/// 地址
/// </summary>
public string? Value { get; set; }
/// <inheritdoc/>
protected override object ProvideValue()
{
return new Uri(Value ?? string.Empty);
}
}

View File

@@ -102,7 +102,7 @@ public class DescriptionTextBlock : ContentControl
if (i == description.Length - 1)
{
AppendText(text, description[last..i]);
AppendText(text, description[last..(i + 1)]);
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.Control;
/// <summary>
/// 值转换器
/// </summary>
/// <typeparam name="TFrom">源类型</typeparam>
/// <typeparam name="TTo">目标类型</typeparam>
public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
{
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language)
{
#if DEBUG
try
{
return Convert((TFrom)value);
}
catch (Exception ex)
{
Ioc.Default
.GetRequiredService<ILogger<ValueConverterBase<TFrom, TTo>>>()
.LogError(ex, "值转换器异常");
}
return null;
#else
return Convert((TFrom)value);
#endif
}
/// <inheritdoc/>
public object? ConvertBack(object value, Type targetType, object parameter, string language)
{
return ConvertBack((TTo)value);
}
/// <summary>
/// 从源类型转换到目标类型
/// </summary>
/// <param name="from">源</param>
/// <returns>目标</returns>
public abstract TTo Convert(TFrom from);
/// <summary>
/// 从目标类型转换到源类型
/// 重写时请勿调用基类方法
/// </summary>
/// <param name="to">目标</param>
/// <returns>源</returns>
public virtual TFrom ConvertBack(TTo to)
{
throw Must.NeverHappen();
}
}

View File

@@ -6,6 +6,7 @@ using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Windows.Storage;
using Windows.Storage.FileProperties;
@@ -169,34 +170,21 @@ public abstract class CacheBase<T>
private static string GetCacheFileName(Uri uri)
{
return CreateHash64(uri.ToString()).ToString();
}
private static ulong CreateHash64(string str)
{
byte[] utf8 = Encoding.UTF8.GetBytes(str);
ulong value = (ulong)utf8.Length;
for (int n = 0; n < utf8.Length; n++)
{
value += (ulong)utf8[n] << ((n * 5) % 56);
}
return value;
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
}
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
{
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri);
using (Stream httpStream = await httpClient.GetStreamAsync(uri))
using (Stream httpStream = await httpClient.GetStreamAsync(uri).ConfigureAwait(false))
{
using (Stream fileStream = await baseFile.OpenStreamForWriteAsync())
using (FileStream fileStream = File.Create(baseFile.Path))
{
await httpStream.CopyToAsync(fileStream);
// Call this before dispose fileStream.
await fileStream.FlushAsync();
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Core.Caching;
/// </summary>
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 20)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 16)]
public class ImageCache : CacheBase<BitmapImage>, IImageCache
{
private const string DateAccessedProperty = "System.DateAccessed";

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using System.Text.Encodings.Web;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core;
@@ -11,17 +12,17 @@ namespace Snap.Hutao.Core;
/// </summary>
internal static class CoreEnvironment
{
// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// <summary>
/// 动态密钥1的盐
/// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// </summary>
public const string DynamicSecret1Salt = "n0KjuIrKgLHh08LWSCYP0WXlVXaYvV64";
public const string DynamicSecret1Salt = "Qqx8cyv7kuyD8fTw11SmvXSFHp7iZD29";
/// <summary>
/// 动态密钥2的盐
/// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// </summary>
public const string DynamicSecret2Salt = "YVEIkzDFNHLeKXLxzqCA9TzxCpWwbIbk";
public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
/// <summary>
/// 米游社请求UA
@@ -31,7 +32,7 @@ internal static class CoreEnvironment
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.36.1";
public const string HoyolabXrpcVersion = "2.37.1";
/// <summary>
/// 标准UA
@@ -48,6 +49,17 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string HoyolabDeviceId;
/// <summary>
/// 默认的Json序列化选项
/// </summary>
public static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true,
WriteIndented = true,
};
static CoreEnvironment()
{
Version = Package.Current.Id.Version.ToVersion();

View File

@@ -70,6 +70,83 @@ internal class DbCurrent<TEntity, TMessage>
dbContext.SaveChanges();
}
messenger.Send(message);
}
}
}
/// <summary>
/// 数据库当前项
/// 简化对数据库中选中项的管理
/// </summary>
/// <typeparam name="TObservable">绑定类型</typeparam>
/// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam>
[SuppressMessage("", "SA1402")]
internal class DbCurrent<TObservable, TEntity, TMessage>
where TObservable : class
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TObservable>
{
private readonly DbContext dbContext;
private readonly DbSet<TEntity> dbSet;
private readonly IMessenger messenger;
private readonly Func<TObservable, TEntity> selector;
private TObservable? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="dbSet">数据集</param>
/// <param name="selector">选择器</param>
/// <param name="messenger">消息器</param>
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, Func<TObservable, TEntity> selector, IMessenger messenger)
{
this.dbContext = dbContext;
this.dbSet = dbSet;
this.selector = selector;
this.messenger = messenger;
}
/// <summary>
/// 当前选中的项
/// </summary>
public TObservable? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
// only update when not processing a deletion
if (value != null)
{
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = false;
dbSet.Update(entity);
dbContext.SaveChanges();
}
}
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
current = value;
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = true;
dbSet.Update(entity);
dbContext.SaveChanges();
}
messenger.Send(message);
}
}

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Annotation;
/// 指示被标注的类型可注入
/// 由源生成器生成注入代码
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class InjectionAttribute : Attribute
{
/// <summary>

View File

@@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Context.FileSystem;
using System.Diagnostics;
using System.Text.Encodings.Web;
namespace Snap.Hutao.Core.DependencyInjection;
@@ -22,14 +21,7 @@ internal static class IocConfiguration
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddJsonSerializerOptions(this IServiceCollection services)
{
return services
.AddSingleton(new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true,
WriteIndented = true,
});
return services.AddSingleton(CoreEnvironment.JsonOptions);
}
/// <summary>

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Core.LifeCycle;
namespace Snap.Hutao.Core.Exception;
/// <summary>
/// 异常记录器

View File

@@ -25,4 +25,16 @@ internal static class Clipboard
string json = await view.GetTextAsync();
return JsonSerializer.Deserialize<T>(json, options);
}
/// <summary>
/// 设置文本
/// </summary>
/// <param name="text">文本</param>
public static void SetText(string text)
{
DataPackage content = new();
content.SetText(text);
Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(content);
Windows.ApplicationModel.DataTransfer.Clipboard.Flush();
}
}

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using System.IO;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Core.IO;
@@ -18,27 +18,47 @@ public static class StorageFileExtensions
/// <typeparam name="T">内容的类型</typeparam>
/// <param name="file">文件</param>
/// <param name="options">序列化选项</param>
/// <param name="onException">错误时调用</param>
/// <returns>反序列化后的内容</returns>
public static async Task<T?> DeserializeJsonAsync<T>(this StorageFile file, JsonSerializerOptions options, Action<System.Exception>? onException = null)
/// <returns>操作是否成功,反序列化后的内容</returns>
public static async Task<ValueResult<bool, T?>> DeserializeFromJsonAsync<T>(this StorageFile file, JsonSerializerOptions options)
where T : class
{
T? t = null;
try
{
using (IRandomAccessStreamWithContentType fileSream = await file.OpenReadAsync())
using (FileStream stream = File.OpenRead(file.Path))
{
using (Stream stream = fileSream.AsStream())
{
t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
}
T? t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
return new(true, t);
}
}
catch (System.Exception ex)
{
onException?.Invoke(ex);
_ = ex;
return new(false, null);
}
}
return t;
/// <summary>
/// 将对象异步序列化入文件
/// </summary>
/// <typeparam name="T">对象的类型</typeparam>
/// <param name="file">文件</param>
/// <param name="obj">对象</param>
/// <param name="options">序列化选项</param>
/// <returns>操作是否成功</returns>
public static async Task<bool> SerializeToJsonAsync<T>(this StorageFile file, T obj, JsonSerializerOptions options)
{
try
{
using (FileStream stream = File.Create(file.Path))
{
await JsonSerializer.SerializeAsync(stream, obj, options).ConfigureAwait(false);
}
return true;
}
catch (System.Exception)
{
return false;
}
}
}

View File

@@ -24,4 +24,4 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 枚举 - 字符串数字 转换器
/// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam>
internal class EnumStringValueConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum
{
/// <inheritdoc/>
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is string str)
{
return Enum.Parse<TEnum>(str);
}
throw Must.NeverHappen();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("D"));
}
}

View File

@@ -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;
/// <summary>
@@ -18,7 +16,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
return false;
}
if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
if (typeToConvert.GetGenericTypeDefinition() != typeof(IDictionary<,>))
{
return false;
}
@@ -33,24 +31,19 @@ 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;
}
private class StringEnumDictionaryConverterInner<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>>
where TKey : struct, Enum
{
private readonly JsonConverter<TValue>? valueConverter;
private readonly Type keyType;
private readonly Type valueType;
public StringEnumDictionaryConverterInner(JsonSerializerOptions options)
public StringEnumDictionaryConverterInner()
{
valueConverter = (JsonConverter<TValue>)options.GetConverter(typeof(TValue));
// Cache the key and value types.
keyType = typeof(TKey);
valueType = typeof(TValue);
}
public override IDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
@@ -77,22 +70,13 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
string? propertyName = reader.GetString();
if (!Enum.TryParse(propertyName, out TKey key))
if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) && !Enum.TryParse(propertyName, ignoreCase: true, out key))
{
throw new JsonException($"Unable to convert \"{propertyName}\" to Enum \"{keyType}\".");
}
// Get the value.
TValue value;
if (valueConverter != null)
{
reader.Read();
value = valueConverter.Read(ref reader, valueType, options)!;
}
else
{
value = JsonSerializer.Deserialize<TValue>(ref reader, options)!;
}
TValue value = JsonSerializer.Deserialize<TValue>(ref reader, options)!;
// Add to dictionary.
dictionary.Add(key, value);
@@ -111,15 +95,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
string? convertedName = options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName;
writer.WritePropertyName(convertedName);
if (valueConverter != null)
{
valueConverter.Write(writer, value, options);
}
else
{
JsonSerializer.Serialize(writer, value, options);
}
JsonSerializer.Serialize(writer, value, options);
}
writer.WriteEndObject();

View File

@@ -88,6 +88,11 @@ internal static class EventIds
/// 成就
/// </summary>
public static readonly EventId Achievement = 100130;
/// <summary>
/// 祈愿统计生成
/// </summary>
public static readonly EventId GachaStatisticGeneration = 100140;
#endregion
#region

View File

@@ -12,8 +12,6 @@ namespace Snap.Hutao.Core.Threading;
/// <typeparam name="TResult">结果类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
public readonly struct ValueResult<TResult, TValue> : IDeconstructable<TResult, TValue>
where TResult : notnull
where TValue : notnull
{
/// <summary>
/// 是否成功

View File

@@ -16,7 +16,7 @@ namespace Snap.Hutao.Core.Windowing;
/// 窗口管理器
/// 主要包含了针对窗体的 P/Inoke 逻辑
/// </summary>
internal sealed class WindowManager : IDisposable
internal sealed class ExtendedWindow
{
private readonly HWND handle;
private readonly AppWindow appWindow;
@@ -24,7 +24,7 @@ internal sealed class WindowManager : IDisposable
private readonly Window window;
private readonly FrameworkElement titleBar;
private readonly ILogger<WindowManager> logger;
private readonly ILogger<ExtendedWindow> logger;
private readonly WindowSubclassManager subclassManager;
private readonly bool useLegacyDragBar;
@@ -34,15 +34,15 @@ internal sealed class WindowManager : IDisposable
/// </summary>
/// <param name="window">窗口</param>
/// <param name="titleBar">充当标题栏的元素</param>
public WindowManager(Window window, FrameworkElement titleBar)
private ExtendedWindow(Window window, FrameworkElement titleBar)
{
this.window = window;
this.titleBar = titleBar;
logger = Ioc.Default.GetRequiredService<ILogger<WindowManager>>();
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow>>();
handle = (HWND)WindowNative.GetWindowHandle(window);
Microsoft.UI.WindowId windowId = Win32Interop.GetWindowIdFromWindow(handle);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(handle);
appWindow = AppWindow.GetFromWindowId(windowId);
useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported();
@@ -51,11 +51,15 @@ internal sealed class WindowManager : IDisposable
InitializeWindow();
}
/// <inheritdoc/>
public void Dispose()
/// <summary>
/// 初始化
/// </summary>
/// <param name="window">窗口</param>
/// <param name="titleBar">标题栏</param>
/// <returns>实例</returns>
public static ExtendedWindow Initialize(Window window, FrameworkElement titleBar)
{
Persistence.Save(appWindow);
subclassManager?.Dispose();
return new(window, titleBar);
}
private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar)
@@ -105,11 +109,19 @@ internal sealed class WindowManager : IDisposable
appWindow.Show(true);
bool micaApplied = new SystemBackdrop(window).TrySetBackdrop();
bool micaApplied = new SystemBackdrop(window).TryApply();
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
bool subClassApplied = subclassManager.TrySetWindowSubclass();
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager), subClassApplied ? "succeed" : "failed");
window.Closed += OnWindowClosed;
}
private void OnWindowClosed(object sender, WindowEventArgs args)
{
Persistence.Save(appWindow);
subclassManager?.Dispose();
}
private void ExtendsContentIntoTitleBar()

View File

@@ -4,6 +4,7 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Win32;
using System.Runtime.InteropServices;
using Windows.Graphics;
using Windows.Win32.Foundation;
@@ -25,11 +26,15 @@ internal static class Persistence
// Set first launch size.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd);
RectInt32 rect = new(0, 0, size.Width, size.Height);
RectInt32 rect = StructMarshal.RectInt32(size);
// Make it centralized
TransformToCenterScreen(ref rect);
RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (target.Width * target.Height < 848 * 524)
{
target = rect;
}
TransformToCenterScreen(ref target);
appWindow.MoveAndResize(target);
}

View File

@@ -34,7 +34,7 @@ public class SystemBackdrop
/// 尝试设置背景
/// </summary>
/// <returns>是否设置成功</returns>
public bool TrySetBackdrop()
public bool TryApply()
{
if (!MicaController.IsSupported())
{
@@ -58,7 +58,7 @@ public class SystemBackdrop
backdropController = new()
{
// Mica Alt
Kind = MicaKind.BaseAlt
Kind = MicaKind.BaseAlt,
};
backdropController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>());
backdropController.SetSystemBackdropConfiguration(configuration);

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
@@ -107,7 +108,7 @@ public static partial class EnumerableExtensions
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>结果值</returns>
public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue = default!)
public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
where TKey : notnull
{
if (dictionary.TryGetValue(key, out TValue? value))
@@ -118,6 +119,53 @@ public static partial class EnumerableExtensions
return defaultValue;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <param name="value">增加的值</param>
public static void Increase<TKey>(this Dictionary<TKey, int> 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;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <returns>是否存在键值</returns>
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref value))
{
++value;
return true;
}
return false;
}
/// <summary>
/// 移除表中首个满足条件的项
/// </summary>
@@ -153,6 +201,20 @@ public static partial class EnumerableExtensions
return dictionary;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
where TKey : notnull
{
Dictionary<TKey, TValue> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = valueSelector(value);
}
return dictionary;
}
/// <summary>
/// 表示一个对 <see cref="TItem"/> 类型的计数器
/// </summary>

View File

@@ -1,18 +1,18 @@
<Window
x:Class="Snap.Hutao.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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:view="using:Snap.Hutao.View"
xmlns:shv="using:Snap.Hutao.View"
mc:Ignorable="d">
<Grid>
<view:TitleView
<shv:TitleView
Margin="48,0,0,0"
Height="44"
x:Name="TitleBarView"/>
<view:MainView/>
<shv:MainView/>
</Grid>
</Window>

View File

@@ -13,21 +13,12 @@ namespace Snap.Hutao;
[SuppressMessage("", "CA1001")]
public sealed partial class MainWindow : Window
{
private readonly WindowManager windowManager;
/// <summary>
/// 构造一个新的主窗体
/// </summary>
public MainWindow()
{
InitializeComponent();
Closed += MainWindowClosed;
windowManager = new WindowManager(this, TitleBarView.DragArea);
}
private void MainWindowClosed(object sender, WindowEventArgs args)
{
// Must dispose it before window is completely closed
windowManager?.Dispose();
ExtendedWindow.Initialize(this, TitleBarView.DragArea);
}
}

View File

@@ -0,0 +1,190 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20220924135810_AddAvatarInfo")]
partial class AddAvatarInfo
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,32 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
public partial class AddAvatarInfo : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "avatar_infos",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
Uid = table.Column<string>(type: "TEXT", nullable: false),
Info = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_avatar_infos", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "avatar_infos");
}
}
}

View File

@@ -63,6 +63,25 @@ namespace Snap.Hutao.Migrations
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")

View File

@@ -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;
/// <summary>
/// 角色信息
/// </summary>
public class Avatar
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 侧面图标
/// </summary>
public Uri SideIcon { get; set; } = default!;
/// <summary>
/// 星级
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 等级
/// </summary>
public string Level { get; set; } = default!;
/// <summary>
/// 好感度等级
/// </summary>
public int FetterLevel { get; set; }
/// <summary>
/// 武器
/// </summary>
public Weapon Weapon { get; set; } = default!;
/// <summary>
/// 圣遗物列表
/// </summary>
public List<Reliquary> Reliquaries { get; set; } = default!;
/// <summary>
/// 命之座列表
/// </summary>
public List<Constellation> Constellations { get; set; } = default!;
/// <summary>
/// 技能列表
/// </summary>
public List<Skill> Skills { get; set; } = default!;
/// <summary>
/// 属性
/// </summary>
public List<Pair2<string, string, string?>> Properties { get; set; } = default!;
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 命座信息
/// </summary>
public class Constellation : NameIconDescription
{
/// <summary>
/// 是否激活
/// </summary>
public bool IsActiviated { get; set; }
}

View File

@@ -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;
/// <summary>
/// 装备基类
/// </summary>
public class EquipBase : NameIconDescription
{
/// <summary>
/// 等级
/// </summary>
public string Level { get; set; } = default!;
/// <summary>
/// 品质
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 主属性
/// </summary>
public Pair<string, string> MainProperty { get; set; } = default!;
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 名称与描述抽象
/// </summary>
public abstract class NameIconDescription
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; } = default!;
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 玩家信息
/// </summary>
public class Player
{
/// <summary>
/// 昵称
/// </summary>
public string Nickname { get; set; } = default!;
/// <summary>
/// 等级
/// </summary>
public int Level { get; set; }
/// <summary>
/// 签名
/// </summary>
public string Signature { get; set; } = default!;
/// <summary>
/// 完成成就数
/// </summary>
public int FinishAchievementNumber { get; set; }
/// <summary>
/// 深渊层间
/// </summary>
public string SipralAbyssFloorLevel { get; set; } = default!;
/// <summary>
/// 头像
/// </summary>
public Uri ProfilePicture { get; set; } = default!;
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 圣遗物
/// </summary>
public class Reliquary : EquipBase
{
/// <summary>
/// 副属性列表
/// </summary>
public List<Pair<string, string>> SubProperties { get; set; } = default!;
}

View File

@@ -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;
/// <summary>
/// 天赋
/// </summary>
public class Skill : NameIconDescription
{
/// <summary>
/// 技能属性
/// </summary>
public LevelParam<string, ParameterInfo> Info { get; set; } = default!;
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 玩家与角色列表的包装器
/// </summary>
public class Summary
{
/// <summary>
/// 玩家信息
/// </summary>
public Player Player { get; set; } = default!;
/// <summary>
/// 角色列表
/// </summary>
public List<Avatar> Avatars { get; set; } = default!;
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 武器
/// </summary>
public class Weapon : EquipBase
{
/// <summary>
/// 副属性
/// </summary>
public Pair<string, string> SubProperty { get; set; } = default!;
/// <summary>
/// 精炼属性
/// </summary>
public string AffixLevel { get; set; } = default!;
/// <summary>
/// 精炼名称
/// </summary>
public string AffixName { get; set; } = default!;
/// <summary>
/// 精炼被动
/// </summary>
public string AffixDescription { get; set; } = default!;
}

View File

@@ -8,11 +8,6 @@ namespace Snap.Hutao.Model.Binding.Gacha;
/// </summary>
public class GachaStatistics
{
/// <summary>
/// 默认的空祈愿统计
/// </summary>
public static readonly GachaStatistics Default = new();
/// <summary>
/// 角色活动
/// </summary>

View File

@@ -2,8 +2,10 @@
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Model.Binding;
@@ -12,7 +14,7 @@ namespace Snap.Hutao.Model.Binding;
/// </summary>
public class User : Observable
{
private readonly Entity.User inner;
private readonly EntityUser inner;
private UserGameRole? selectedUserGameRole;
private bool isInitialized;
@@ -21,7 +23,7 @@ public class User : Observable
/// 构造一个新的绑定视图用户
/// </summary>
/// <param name="user">用户实体</param>
private User(Entity.User user)
private User(EntityUser user)
{
inner = user;
}
@@ -45,15 +47,15 @@ public class User : Observable
private set => Set(ref selectedUserGameRole, value);
}
/// <inheritdoc cref="Entity.User.IsSelected"/>
/// <inheritdoc cref="EntityUser.IsSelected"/>
public bool IsSelected
{
get => inner.IsSelected;
set => inner.IsSelected = value;
}
/// <inheritdoc cref="Entity.User.Cookie"/>
public string? Cookie
/// <inheritdoc cref="EntityUser.Cookie"/>
public Cookie Cookie
{
get => inner.Cookie;
set => inner.Cookie = value;
@@ -62,7 +64,7 @@ public class User : Observable
/// <summary>
/// 内部的用户实体
/// </summary>
public Entity.User Entity { get => inner; }
public EntityUser Entity { get => inner; }
/// <summary>
/// 是否初始化完成
@@ -70,27 +72,62 @@ public class User : Observable
public bool IsInitialized { get => isInitialized; }
/// <summary>
/// 初始化用户
/// 从数据库恢复用户
/// </summary>
/// <param name="inner">用户实体</param>
/// <param name="inner">数据库实体</param>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>用户是否初始化完成若Cookie失效会返回 <see langword="false"/> </returns>
internal static async Task<User?> CreateAsync(Entity.User inner, UserClient userClient, UserGameRoleClient userGameRoleClient, CancellationToken token = default)
internal static async Task<User?> ResumeAsync(
EntityUser inner,
UserClient userClient,
BindingClient userGameRoleClient,
CancellationToken token = default)
{
User user = new(inner);
bool successful = await user.InitializeAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
return successful ? user : null;
}
private async Task<bool> InitializeAsync(UserClient userClient, UserGameRoleClient userGameRoleClient, CancellationToken token = default)
/// <summary>
/// 创建并初始化用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>用户是否初始化完成若Cookie失效会返回 <see langword="null"/> </returns>
internal static async Task<User?> CreateAsync(
Cookie cookie,
UserClient userClient,
BindingClient userGameRoleClient,
CancellationToken token = default)
{
User user = new(EntityUser.Create(cookie));
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
return successful ? user : null;
}
private async Task<bool> InitializeCoreAsync(
UserClient userClient,
BindingClient userGameRoleClient,
CancellationToken token = default)
{
if (isInitialized)
{
return true;
}
await InitializeUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
isInitialized = true;
return UserInfo != null && UserGameRoles.Any();
}
private async Task InitializeUserInfoAndUserGameRolesAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token)
{
UserInfo = await userClient
.GetUserFullInfoAsync(this, token)
.ConfigureAwait(false);
@@ -100,9 +137,5 @@ public class User : Observable
.ConfigureAwait(false);
SelectedUserGameRole = UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
isInitialized = true;
return UserInfo != null && UserGameRoles.Any();
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Model.Entity;
/// <summary>
/// 角色信息表
/// </summary>
[Table("avatar_infos")]
public class AvatarInfo
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// Uid
/// </summary>
public string Uid { get; set; } = default!;
/// <summary>
/// 角色的信息
/// </summary>
public Web.Enka.Model.AvatarInfo Info { get; set; } = default!;
/// <summary>
/// 创建一个新的实体角色信息
/// </summary>
/// <param name="uid">uid</param>
/// <param name="info">角色信息</param>
/// <returns>实体角色信息</returns>
public static AvatarInfo Create(string uid, Web.Enka.Model.AvatarInfo info)
{
return new() { Uid = uid, Info = info };
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Snap.Hutao.Model.Entity.Configuration;
/// <summary>
/// 角色信息配置
/// </summary>
internal class AvatarInfoConfiguration : IEntityTypeConfiguration<AvatarInfo>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<AvatarInfo> builder)
{
builder.Property(e => e.Info)
.HasColumnType("TEXT")
.HasConversion<JsonTextValueConverter<Web.Enka.Model.AvatarInfo>>();
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Core;
namespace Snap.Hutao.Model.Entity.Configuration;
/// <summary>
/// Json文本转换器
/// </summary>
/// <typeparam name="TProperty">实体类型</typeparam>
internal class JsonTextValueConverter<TProperty> : ValueConverter<TProperty, string>
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonTextValueConverter{TProperty}"/> class.
/// </summary>
public JsonTextValueConverter()
: base(
obj => JsonSerializer.Serialize(obj, CoreEnvironment.JsonOptions),
str => JsonSerializer.Deserialize<TProperty>(str, CoreEnvironment.JsonOptions)!)
{
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Model.Entity.Configuration;
/// <summary>
/// 用户配置
/// </summary>
internal class UserConfiguration : IEntityTypeConfiguration<User>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<User> builder)
{
builder.Property(e => e.Cookie)
.HasColumnType("TEXT")
.HasConversion(
e => e == null ? string.Empty : e.ToString(),
e => Cookie.Parse(e));
}
}

View File

@@ -1,7 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -58,6 +60,17 @@ public class GachaItem
/// </summary>
public long Id { get; set; }
/// <summary>
/// 获取物品类型字符串
/// </summary>
/// <param name="itemId">物品Id</param>
/// <returns>物品类型字符串</returns>
public static string GetItemTypeStringByItemId(int itemId)
{
int idLength = itemId.Place();
return idLength == 8 ? "角色" : idLength == 5 ? "武器" : "未知";
}
/// <summary>
/// 构造一个新的数据库祈愿物品
/// </summary>
@@ -111,4 +124,24 @@ public class GachaItem
_ => configType,
};
}
/// <summary>
/// 转换到UIGF物品
/// </summary>
/// <param name="nameQuality">物品</param>
/// <returns>UIGF 物品</returns>
public UIGFItem ToUIGFItem(INameQuality nameQuality)
{
return new()
{
GachaType = GachaType,
Count = 1,
Time = Time,
Name = nameQuality.Name,
ItemType = GetItemTypeStringByItemId(ItemId),
Rank = nameQuality.Quality,
Id = Id,
UIGFGachaType = QueryType,
};
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Web.Hoyolab;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -28,14 +29,14 @@ public class User : ISelectable
/// <summary>
/// 用户的Cookie
/// </summary>
public string? Cookie { get; set; }
public Cookie Cookie { get; set; } = default!;
/// <summary>
/// 创建一个新的用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <returns>新创建的用户</returns>
public static User Create(string cookie)
public static User Create(Cookie cookie)
{
return new() { Cookie = cookie };
}

View File

@@ -9,9 +9,14 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// </summary>
public class UIGF
{
/// <summary>
/// 当前发行的版本
/// </summary>
public const string CurrentVersion = "v2.2";
private static readonly List<string> SupportedVersion = new()
{
"v2.2",
"v2.1", CurrentVersion,
};
/// <summary>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Extension;
namespace Snap.Hutao.Model.InterChange.GachaLog;
@@ -53,4 +54,22 @@ public class UIGFInfo
/// </summary>
[JsonPropertyName("uigf_version")]
public string UIGFVersion { get; set; } = default!;
/// <summary>
/// 构造一个新的专用 UIGF 信息
/// </summary>
/// <param name="uid">uid</param>
/// <returns>专用 UIGF 信息</returns>
public static UIGFInfo Create(string uid)
{
return new()
{
Uid = uid,
Language = "zh-cn",
ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),
ExportApp = "胡桃",
ExportAppVersion = CoreEnvironment.Version.ToString(),
UIGFVersion = UIGF.CurrentVersion,
};
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using MiniExcelLibs.Attributes;
using Snap.Hutao.Core.Json.Converter;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
namespace Snap.Hutao.Model.InterChange.GachaLog;
@@ -16,6 +17,6 @@ public class UIGFItem : GachaLogItem
/// </summary>
[ExcelColumn(Name = "uigf_gacha_type")]
[JsonPropertyName("uigf_gacha_type")]
[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumStringValueConverter<GachaConfigType>))]
public GachaConfigType UIGFGachaType { get; set; } = default!;
}

View File

@@ -24,8 +24,10 @@ public enum FightProperty
FIGHT_PROP_BASE_HP = 1,
/// <summary>
/// 生命值加成
/// 生命值加成
/// </summary>
[Description("生命值")]
[Format(FormatMethod.Integer)]
FIGHT_PROP_HP = 2,
/// <summary>
@@ -45,6 +47,8 @@ public enum FightProperty
/// <summary>
/// 攻击力加成
/// </summary>
[Description("攻击力")]
[Format(FormatMethod.Integer)]
FIGHT_PROP_ATTACK = 5,
/// <summary>
@@ -64,6 +68,8 @@ public enum FightProperty
/// <summary>
/// 防御力加成
/// </summary>
[Description("防御力")]
[Format(FormatMethod.Integer)]
FIGHT_PROP_DEFENSE = 8,
/// <summary>
@@ -387,6 +393,8 @@ public enum FightProperty
/// <summary>
/// 最大生命值
/// </summary>
[Description("生命值")]
[Format(FormatMethod.Integer)]
FIGHT_PROP_MAX_HP = 2000,
/// <summary>

View File

@@ -15,7 +15,7 @@ public enum ItemType
ITEM_NONE = 0,
/// <summary>
/// 贵重道具
/// 虚拟道具
/// </summary>
ITEM_VIRTUAL = 1,

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Enka.Model;
namespace Snap.Hutao.Model.Intrinsic;
/// <summary>
/// 玩家属性
@@ -225,4 +225,4 @@ public enum PlayerProperty
///
/// </summary>
PROP_PLAYER_WAIT_SUB_HOME_COIN = 10043,
}
}

View File

@@ -21,6 +21,7 @@ public enum WeaponType
WEAPON_SWORD_ONE_HAND = 1,
#region Not Used
/// <summary>
/// ?
/// </summary>

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Metadata.Abstraction;
/// <summary>
/// 物品与星级
/// </summary>
public interface INameQuality
{
/// <summary>
/// 名称
/// </summary>
string Name { get; }
/// <summary>
/// 星级
/// </summary>
ItemQuality Quality { get; }
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Metadata.Abstraction;
/// <summary>
/// 指示该类为简述统计物品的源
/// </summary>
public interface ISummaryItemSource
{
/// <summary>
/// 星级
/// </summary>
ItemQuality Quality { get; }
/// <summary>
/// 转换到简述统计物品
/// </summary>
/// <param name="lastPull">距上个五星</param>
/// <param name="time">时间</param>
/// <param name="isUp">是否为Up物品</param>
/// <returns>简述统计物品</returns>
SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp);
}

View File

@@ -16,7 +16,7 @@ internal static class EnumExtensions
/// <typeparam name="TEnum">枚举的类型</typeparam>
/// <param name="enum">枚举值</param>
/// <returns>描述</returns>
internal static FormatMethod GetFormat<TEnum>(this TEnum @enum)
internal static FormatMethod GetFormatMethod<TEnum>(this TEnum @enum)
where TEnum : struct, Enum
{
string enumName = Must.NotNull(Enum.GetName(@enum)!);

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Avatar;
/// <summary>
/// 角色
/// </summary>
public class Avatar : IStatisticsItemSource
public class Avatar : IStatisticsItemSource, ISummaryItemSource, INameQuality
{
/// <summary>
/// Id
@@ -93,7 +93,7 @@ public class Avatar : IStatisticsItemSource
return new()
{
Name = Name,
Icon = AvatarIconConverter.NameToUri(Icon),
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
};
@@ -109,7 +109,7 @@ public class Avatar : IStatisticsItemSource
return new()
{
Name = Name,
Icon = AvatarIconConverter.NameToUri(Icon),
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
Count = count,
@@ -128,7 +128,7 @@ public class Avatar : IStatisticsItemSource
return new()
{
Name = Name,
Icon = AvatarIconConverter.NameToUri(Icon),
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
Time = time,

View File

@@ -27,4 +27,14 @@ public class Costume
/// 是否为默认
/// </summary>
public bool IsDefault { get; set; }
/// <summary>
/// 图标
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// 侧面图标
/// </summary>
public string? SideIcon { get; set; }
}

View File

@@ -24,7 +24,7 @@ public class SkillDepot
public IList<ProudableSkill> Inherents { get; set; } = default!;
/// <summary>
/// 全部天赋
/// 全部天赋,包括固有天赋
/// </summary>
public IList<ProudableSkill> CompositeSkills
{
@@ -36,6 +36,20 @@ public class SkillDepot
/// </summary>
public IList<SkillBase> Talents { get; set; } = default!;
/// <summary>
/// 获取无固有天赋的技能列表
/// </summary>
/// <returns>天赋列表</returns>
public IEnumerable<ProudableSkill> GetCompositeSkillsNoInherents()
{
foreach (ProudableSkill skill in Skills)
{
yield return skill;
}
yield return EnergySkill;
}
private IEnumerable<ProudableSkill> GetCompositeSkills()
{
foreach (ProudableSkill skill in Skills)

View File

@@ -1,26 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色头像转换器
/// </summary>
internal class AchievementIconConverter : IValueConverter
internal class AchievementIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/AchievementIcon/{0}.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
public override Uri Convert(string from)
{
return new Uri(string.Format(BaseUrl, value));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
return new Uri(string.Format(BaseUrl, from));
}
}

View File

@@ -1,14 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色头像转换器
/// </summary>
internal class AvatarIconConverter : IValueConverter
internal class AvatarIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/AvatarIcon/{0}.png";
@@ -17,20 +17,14 @@ internal class AvatarIconConverter : IValueConverter
/// </summary>
/// <param name="name">名称</param>
/// <returns>链接</returns>
public static Uri NameToUri(string name)
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
}
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
public override Uri Convert(string from)
{
return NameToUri((string)value);
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
return IconNameToUri(from);
}
}

View File

@@ -1,36 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色名片转换器
/// </summary>
internal class AvatarNameCardPicConverter : IValueConverter
internal class AvatarNameCardPicConverter : ValueConverterBase<Avatar.Avatar?, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/NameCardPic/UI_NameCardPic_{0}_P.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
public override Uri Convert(Avatar.Avatar? avatar)
{
if (value == null)
if (avatar == null)
{
return null!;
}
Avatar.Avatar avatar = (Avatar.Avatar)value;
string avatarName = ReplaceSpecialCaseNaming(avatar.Icon[14..]);
string avatarName = ReplaceSpecialCaseNaming(avatar.Icon["UI_AvatarIcon_".Length..]);
return new Uri(string.Format(BaseUrl, avatarName));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
private static string ReplaceSpecialCaseNaming(string avatarName)
{
return avatarName switch

View File

@@ -1,26 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色侧面头像转换器
/// </summary>
internal class AvatarSideIconConverter : IValueConverter
internal class AvatarSideIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/AvatarIcon/{0}.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
public override Uri Convert(string from)
{
return new Uri(string.Format(BaseUrl, value));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
return new Uri(string.Format(BaseUrl, from));
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
using Snap.Hutao.Model.Metadata.Avatar;
using System.Text.RegularExpressions;
@@ -10,40 +10,44 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 描述参数解析器
/// </summary>
internal class DescParamDescriptor : IValueConverter
internal sealed class DescParamDescriptor : ValueConverterBase<DescParam, IList<LevelParam<string, ParameterInfo>>>
{
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
/// <summary>
/// 获取特定等级的解释
/// </summary>
/// <param name="from">源</param>
/// <param name="level">等级</param>
/// <returns>特定等级的解释</returns>
public static LevelParam<string, ParameterInfo> Convert(DescParam from, int level)
{
DescParam rawDescParam = (DescParam)value;
// Spilt rawDesc into two parts: desc and format
IList<DescFormat> parsedDescriptions = rawDescParam.Descriptions
.Select(desc =>
{
string[] parts = desc.Split('|', 2);
return new DescFormat(parts[0], parts[1]);
})
// DO NOT INLINE!
// Cache the formats
IList<DescFormat> formats = from.Descriptions
.Select(desc => new DescFormat(desc))
.ToList();
IList<LevelParam<string, ParameterInfo>> parameters = rawDescParam.Parameters
.Select(param =>
{
IList<ParameterInfo> parameters = GetFormattedParameters(parsedDescriptions, param.Parameters);
return new LevelParam<string, ParameterInfo>() { Level = param.Level.ToString(), Parameters = parameters };
})
LevelParam<int, double> param = from.Parameters.Single(param => param.Level == level);
return new LevelParam<string, ParameterInfo>($"Lv.{param.Level}", GetParameterInfos(formats, param.Parameters));
}
/// <inheritdoc/>
public override IList<LevelParam<string, ParameterInfo>> Convert(DescParam from)
{
// DO NOT INLINE!
// Cache the formats
IList<DescFormat> formats = from.Descriptions
.Select(desc => new DescFormat(desc))
.ToList();
IList<LevelParam<string, ParameterInfo>> parameters = from.Parameters
.Select(param => new LevelParam<string, ParameterInfo>(param.Level.ToString(), GetParameterInfos(formats, param.Parameters)))
.ToList();
return parameters;
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
private static IList<ParameterInfo> GetFormattedParameters(IList<DescFormat> formats, IList<double> param)
private static IList<ParameterInfo> GetParameterInfos(IList<DescFormat> formats, IList<double> param)
{
List<ParameterInfo> results = new();
@@ -63,25 +67,12 @@ internal class DescParamDescriptor : IValueConverter
{
if (match.Success)
{
// remove parentheses and split by {value:format}
string[] parts = match.Value[1..^1].Split(':', 2);
int index = int.Parse(parts[0][5..]) - 1;
if (parts[1] == "I")
{
return ((int)param[index]).ToString();
}
int index = int.Parse(parts[0]["param".Length..]) - 1;
if (parts[1] == "F1P")
{
return string.Format("{0:P1}", param[index]);
}
if (parts[1] == "F2P")
{
return string.Format("{0:P2}", param[index]);
}
return string.Format($"{{0:{parts[1]}}}", param[index]);
return string.Format(new ParameterFormat(), $"{{0:{parts[1]}}}", param[index]);
}
else
{
@@ -89,16 +80,19 @@ internal class DescParamDescriptor : IValueConverter
}
}
private class DescFormat
private sealed class DescFormat
{
public DescFormat(string description, string format)
public DescFormat(string desc)
{
Description = description;
Format = format;
// Spilt rawDesc into two parts: desc and format
string[] parts = desc.Split('|', 2);
Description = parts[0];
Format = parts[1];
}
public string Description { get; set; }
public string Format { get; set; }
}
}
}

View File

@@ -1,14 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 元素名称图标转换器
/// </summary>
internal class ElementNameIconConverter : IValueConverter
internal class ElementNameIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/IconElement/UI_Icon_Element_{0}.png";
@@ -35,14 +35,8 @@ internal class ElementNameIconConverter : IValueConverter
}
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
public override Uri Convert(string from)
{
return ElementNameToIconUri((string)value);
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
return ElementNameToIconUri(from);
}
}

View File

@@ -1,14 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 武器图片转换器
/// </summary>
internal class EquipIconConverter : IValueConverter
internal class EquipIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/EquipIcon/{0}.png";
@@ -17,20 +17,14 @@ internal class EquipIconConverter : IValueConverter
/// </summary>
/// <param name="name">名称</param>
/// <returns>链接</returns>
public static Uri NameToUri(string name)
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
}
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
public override Uri Convert(string from)
{
return NameToUri((string)value);
return IconNameToUri(from);
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Annotation;
@@ -11,48 +11,49 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 基础属性翻译器
/// </summary>
internal class PropertyInfoDescriptor : IValueConverter
internal class PropertyInfoDescriptor : ValueConverterBase<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
{
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language)
/// <summary>
/// 格式化战斗属性
/// </summary>
/// <param name="property">战斗属性</param>
/// <param name="value">值</param>
/// <returns>格式化的值</returns>
public static string FormatProperty(FightProperty property, double value)
{
if (value is PropertyInfo rawDescParam)
FormatMethod method = property.GetFormatMethod();
string valueFormatted = method switch
{
IList<LevelParam<string, ParameterInfo>> parameters = rawDescParam.Parameters
FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(),
FormatMethod.Percent => string.Format("{0:P1}", value),
_ => value.ToString(),
};
return valueFormatted;
}
/// <inheritdoc/>
public override IList<LevelParam<string, ParameterInfo>> Convert(PropertyInfo from)
{
IList<LevelParam<string, ParameterInfo>> parameters = from.Parameters
.Select(param =>
{
IList<ParameterInfo> parameters = GetFormattedParameters(param.Parameters, rawDescParam.Properties);
IList<ParameterInfo> parameters = GetParameterInfos(param.Parameters, from.Properties);
return new LevelParam<string, ParameterInfo>() { Level = param.Level, Parameters = parameters };
})
.ToList();
return parameters;
}
return null;
return parameters;
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
private static IList<ParameterInfo> GetFormattedParameters(IList<double> parameters, IList<FightProperty> properties)
private static IList<ParameterInfo> GetParameterInfos(IList<double> parameters, IList<FightProperty> properties)
{
List<ParameterInfo> results = new();
for (int index = 0; index < parameters.Count; index++)
{
double param = parameters[index];
FormatMethod method = properties[index].GetFormat();
string valueFormatted = method switch
{
FormatMethod.Integer => 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 });
}

View File

@@ -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;
/// <summary>
/// 品质颜色转换器
/// </summary>
internal class QualityColorConverter : ValueConverterBase<ItemQuality, Color>
{
/// <inheritdoc/>
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),
};
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Metadata.Converter;
@@ -9,14 +9,14 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 物品等级转换器
/// </summary>
internal class QualityConverter : IValueConverter
internal class QualityConverter : ValueConverterBase<ItemQuality, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/Bg/UI_{0}.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
public override Uri Convert(ItemQuality from)
{
string? name = value.ToString();
string? name = from.ToString();
if (name == nameof(ItemQuality.QUALITY_ORANGE_SP))
{
name = "QUALITY_RED";
@@ -24,10 +24,4 @@ internal class QualityConverter : IValueConverter
return new Uri(string.Format(BaseUrl, name));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
}

View File

@@ -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;
/// <summary>
/// 武器图片转换器
/// </summary>
internal class RelicIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/RelicIcon/{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
/// <param name="name">名称</param>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
}
/// <inheritdoc/>
public override Uri Convert(string from)
{
return IconNameToUri(from);
}
}

View File

@@ -1,36 +1,38 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 技能图标转换器
/// </summary>
internal class SkillIconConverter : IValueConverter
internal class SkillIconConverter : ValueConverterBase<string, Uri>
{
private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png";
private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
/// <summary>
/// 名称转Uri
/// </summary>
/// <param name="name">名称</param>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
string target = (string)value;
if (target.StartsWith("UI_Talent_"))
if (name.StartsWith("UI_Talent_"))
{
return new Uri(string.Format(TalentUrl, target));
return new Uri(string.Format(TalentUrl, name));
}
else
{
return new Uri(string.Format(SkillUrl, target));
return new Uri(string.Format(SkillUrl, name));
}
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
public override Uri Convert(string from)
{
throw Must.NeverHappen();
return IconNameToUri(from);
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Metadata.Converter;
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 元素名称图标转换器
/// </summary>
internal class WeaponTypeIconConverter : IValueConverter
internal class WeaponTypeIconConverter : ValueConverterBase<WeaponType, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/Skill/Skill_A_{0}.png";
@@ -34,14 +34,8 @@ internal class WeaponTypeIconConverter : IValueConverter
}
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
public override Uri Convert(WeaponType from)
{
return WeaponTypeToIconUri((WeaponType)value);
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
return WeaponTypeToIconUri(from);
}
}

View File

@@ -10,6 +10,25 @@ namespace Snap.Hutao.Model.Metadata;
/// <typeparam name="TParam">参数的类型</typeparam>
public class LevelParam<TLevel, TParam>
{
/// <summary>
/// 默认的构造器
/// </summary>
[JsonConstructor]
public LevelParam()
{
}
/// <summary>
/// 构造一个新的等级与参数
/// </summary>
/// <param name="level">等级</param>
/// <param name="parameters">参数</param>
public LevelParam(TLevel level, IList<TParam> parameters)
{
Level = level;
Parameters = parameters;
}
/// <summary>
/// 等级
/// </summary>

View File

@@ -0,0 +1,45 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Metadata;
/// <summary>
/// 参数格式化器
/// </summary>
internal class ParameterFormat : IFormatProvider, ICustomFormatter
{
/// <inheritdoc/>
public string Format(string? fmt, object? arg, IFormatProvider? formatProvider)
{
if (fmt != null)
{
switch (fmt.Length)
{
case 3: // FnP
return string.Format($"{{0:P{fmt[1]}}}", arg);
case 2: // Fn
return string.Format($"{{0:{fmt}}}", arg);
case 1: // P I
switch (fmt[0])
{
case 'P':
return string.Format($"{{0:P0}}", arg);
case 'I':
return arg == null ? "0" : ((IConvertible)arg).ToInt32(null).ToString();
}
break;
}
}
return arg?.ToString() ?? string.Empty;
}
/// <inheritdoc/>
public object? GetFormat(Type? formatType)
{
return formatType == typeof(ICustomFormatter)
? this
: null;
}
}

View File

@@ -18,7 +18,7 @@ public class Reliquary
/// <summary>
/// 允许出现的等级
/// </summary>
public IEnumerable<ItemQuality> RankLevels { get; set; } = default!;
public ItemQuality RankLevel { get; set; } = default!;
/// <summary>
/// 套装Id

View File

@@ -19,4 +19,4 @@ public class ReliquaryAffixBase
/// 战斗属性
/// </summary>
public FightProperty Type { get; set; }
}
}

View File

@@ -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;
/// <summary>
/// 圣遗物等级
/// </summary>
public class ReliquaryLevel
{
/// <summary>
/// 品质
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 等级 1-21
/// </summary>
public int Level { get; set; }
/// <summary>
/// 属性
/// </summary>
[JsonConverter(typeof(StringEnumKeyDictionaryConverter))]
public IDictionary<FightProperty, double> Properties { get; set; } = default!;
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Metadata.Weapon;
/// <summary>
/// 武器被动信息
/// </summary>
public class AffixInfo
{
/// <summary>
/// 被动的名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 各个等级的描述
/// 0-4
/// </summary>
public List<LevelDescription<int>> Descriptions { get; set; } = default!;
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Metadata.Weapon;
/// <summary>
/// 等级与描述
/// </summary>
/// <typeparam name="TLevel">等级的类型</typeparam>
public class LevelDescription<TLevel>
{
/// <summary>
/// 等级
/// </summary>
public TLevel Level { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; } = default!;
}

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Weapon;
/// <summary>
/// 武器
/// </summary>
public class Weapon : IStatisticsItemSource
public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality
{
/// <summary>
/// Id
@@ -54,6 +54,15 @@ public class Weapon : IStatisticsItemSource
/// </summary>
public PropertyInfo Property { get; set; } = default!;
/// <summary>
/// 被动信息, 无被动的武器为 <see langword="null"/>
/// </summary>
public AffixInfo? Affix { get; set; } = default!;
/// <inheritdoc/>
[JsonIgnore]
public ItemQuality Quality => RankLevel;
/// <summary>
/// 转换为基础物品
/// </summary>
@@ -63,7 +72,7 @@ public class Weapon : IStatisticsItemSource
return new()
{
Name = Name,
Icon = EquipIconConverter.NameToUri(Icon),
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Quality = RankLevel,
};
@@ -79,7 +88,7 @@ public class Weapon : IStatisticsItemSource
return new()
{
Name = Name,
Icon = EquipIconConverter.NameToUri(Icon),
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Quality = RankLevel,
Count = count,
@@ -98,7 +107,7 @@ public class Weapon : IStatisticsItemSource
return new()
{
Name = Name,
Icon = EquipIconConverter.NameToUri(Icon),
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Time = time,
Quality = RankLevel,
@@ -106,4 +115,4 @@ public class Weapon : IStatisticsItemSource
IsUp = isUp,
};
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model;
/// <summary>
/// 对
/// </summary>
/// <typeparam name="TKey">键的类型</typeparam>
/// <typeparam name="TValue1">值1的类型</typeparam>
/// <typeparam name="TValue2">值2的类型</typeparam>
public class Pair2<TKey, TValue1, TValue2>
{
/// <summary>
/// 构造一个新的对
/// </summary>
/// <param name="key">键</param>
/// <param name="value1">值1</param>
/// <param name="value2">值2</param>
public Pair2(TKey key, TValue1 value1, TValue2 value2)
{
Key = key;
Value1 = value1;
Value2 = value2;
}
/// <summary>
/// 键
/// </summary>
public TKey Key { get; set; }
/// <summary>
/// 值
/// </summary>
public TValue1 Value1 { get; set; }
/// <summary>
/// 值
/// </summary>
public TValue2 Value2 { get; set; }
}

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.1.0.0" />
Version="1.1.5.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -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;
/// <summary>
/// 角色信息服务
/// </summary>
[Injection(InjectAs.Transient, typeof(IAvatarInfoService))]
internal class AvatarInfoService : IAvatarInfoService
{
private readonly AppDbContext appDbContext;
private readonly ISummaryFactory summaryFactory;
private readonly EnkaClient enkaClient;
/// <summary>
/// 构造一个新的角色信息服务
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="summaryFactory">简述工厂</param>
/// <param name="enkaClient">Enka客户端</param>
public AvatarInfoService(AppDbContext appDbContext, ISummaryFactory summaryFactory, EnkaClient enkaClient)
{
this.appDbContext = appDbContext;
this.summaryFactory = summaryFactory;
this.enkaClient = enkaClient;
}
/// <inheritdoc/>
public async Task<ValueResult<RefreshResult, Summary?>> 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<Web.Enka.Model.AvatarInfo> 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<EnkaResponse?> GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default)
{
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
}
private List<Web.Enka.Model.AvatarInfo> UpdateDbAvatarInfo(string uid, IEnumerable<Web.Enka.Model.AvatarInfo> webInfos)
{
List<Model.Entity.AvatarInfo> 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<Web.Enka.Model.AvatarInfo> GetDbAvatarInfos(string uid)
{
return appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.Select(i => i.Info)
.AsEnumerable()
.OrderByDescending(i => i.AvatarId)
.ToList();
}
}

View File

@@ -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;
/// <summary>
/// 简述工厂
/// </summary>
internal interface ISummaryFactory
{
/// <summary>
/// 异步创建简述对象
/// </summary>
/// <param name="playerInfo">玩家信息</param>
/// <param name="avatarInfos">角色列表</param>
/// <returns>简述对象</returns>
Task<Summary> CreateAsync(PlayerInfo playerInfo, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos);
}

View File

@@ -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;
/// <summary>
/// 简述工厂
/// </summary>
[Injection(InjectAs.Transient, typeof(ISummaryFactory))]
internal class SummaryFactory : ISummaryFactory
{
private readonly IMetadataService metadataService;
/// <summary>
/// 构造一个新的简述工厂
/// </summary>
/// <param name="metadataService">元数据服务</param>
public SummaryFactory(IMetadataService metadataService)
{
this.metadataService = metadataService;
}
/// <inheritdoc/>
public async Task<Summary> CreateAsync(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos)
{
Dictionary<int, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false);
Dictionary<int, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync().ConfigureAwait(false);
List<ReliquaryLevel> reliqueryLevels = await metadataService.GetReliquaryLevelsAsync().ConfigureAwait(false);
List<MetadataReliquary> 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<int, MetadataAvatar> idAvatarMap;
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
private readonly Dictionary<int, MetadataWeapon> idWeaponMap;
private readonly Dictionary<int, ReliquaryAffix> idReliquaryAffixMap;
private readonly List<ReliquaryLevel> reliqueryLevels;
private readonly List<MetadataReliquary> reliquaries;
public SummaryFactoryInner(
Dictionary<int, MetadataAvatar> idAvatarMap,
Dictionary<int, FightProperty> idRelicMainPropMap,
Dictionary<int, MetadataWeapon> idWeaponMap,
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap,
List<ReliquaryLevel> reliqueryLevels,
List<MetadataReliquary> 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<ModelAvatarInfo> 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<PropertyReliquary> 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<PropertyReliquary> Reliquaries, PropertyWeapon Weapon) ProcessEquip(IList<Equip> equipments)
{
List<PropertyReliquary> 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<string, string> 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<string, string> 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,
};
}
}
}

View File

@@ -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;
/// <summary>
/// 简述帮助类
/// </summary>
internal static class SummaryHelper
{
/// <summary>
/// 创建玩家对象
/// </summary>
/// <param name="playerInfo">玩家信息</param>
/// <param name="avatar">角色</param>
/// <returns>玩家对象</returns>
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)),
};
}
/// <summary>
/// 创建命之座
/// </summary>
/// <param name="talentIds">激活的命座列表</param>
/// <param name="talents">全部命座</param>
/// <returns>命之座</returns>
public static List<Constellation> CreateConstellations(IList<int>? talentIds, IList<SkillBase> talents)
{
List<Constellation> 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;
}
/// <summary>
/// 创建技能组
/// </summary>
/// <param name="skillLevelMap">技能等级映射</param>
/// <param name="proudSkillExtraLevelMap">额外提升等级映射</param>
/// <param name="proudSkills">技能列表</param>
/// <returns>技能</returns>
public static List<Skill> CreateSkills(IDictionary<string, int> skillLevelMap, IDictionary<string, int>? proudSkillExtraLevelMap, IEnumerable<ProudableSkill> proudSkills)
{
Dictionary<string, int> 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<Skill> 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;
}
/// <summary>
/// 创建角色属性
/// </summary>
/// <param name="fightPropMap">属性映射</param>
/// <returns>列表</returns>
public static List<Pair2<string, string, string?>> CreateAvatarProperties(IDictionary<FightProperty, double> fightPropMap)
{
List<Pair2<string, string, string?>> 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<string, string, string?> 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<string, string, string?> 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<string, string, string?> defPair2 = new("防御力", FormatValue(FormatMethod.Integer, maxDef), $"[+{FormatValue(FormatMethod.Integer, defAdd)}]");
double em = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28
Pair2<string, string, string?> emPair2 = new("元素精通", FormatValue(FormatMethod.Integer, em), null);
double critRate = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL); // 20
Pair2<string, string, string?> critRatePair2 = new("暴击率", FormatValue(FormatMethod.Percent, critRate), null);
double critDMG = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22
Pair2<string, string, string?> critDMGPair2 = new("暴击伤害", FormatValue(FormatMethod.Percent, critDMG), null);
double chargeEff = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23
Pair2<string, string, string?> 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<string, string, string?> 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<FightProperty, double> 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;
}
}

View File

@@ -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;
/// <summary>
/// 角色信息服务
/// </summary>
internal interface IAvatarInfoService
{
/// <summary>
/// 异步获取总览数据
/// </summary>
/// <param name="uid">uid</param>
/// <param name="refreshOption">刷新选项</param>
/// <param name="token">取消令牌</param>
/// <returns>总览数据</returns>
Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default);
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 刷新选项
/// </summary>
[Flags]
public enum RefreshOption
{
/// <summary>
/// 是否存入本地数据库
/// </summary>
StoreInDatabase = 0b00000001,
/// <summary>
/// 从API获取
/// </summary>
RequestFromAPI = 0b00000010,
/// <summary>
/// 仅数据库
/// </summary>
DatabaseOnly = 0b00000000,
/// <summary>
/// 标准操作
/// </summary>
Standard = StoreInDatabase | RequestFromAPI,
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 刷新结果
/// </summary>
public enum RefreshResult
{
/// <summary>
/// 正常
/// </summary>
Ok,
/// <summary>
/// API 不可用
/// </summary>
APIUnavailable,
/// <summary>
/// 角色橱窗未对外开放
/// </summary>
ShowcaseNotOpen,
}

View File

@@ -5,8 +5,9 @@ using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Windows.UI;
namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -34,35 +35,28 @@ public static class GachaStatisticsExtensions
}
/// <summary>
/// 增加计数
/// 完成添加
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
/// <param name="summaryItems">简述物品列表</param>
public static void CompleteAdding(this List<SummaryItem> summaryItems)
{
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
// we can't trust first item's prev state.
bool isPreviousUp = true;
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <returns>是否存在键值</returns>
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref value))
// mark the IsGuarentee
foreach (SummaryItem item in summaryItems)
{
++value;
return true;
if (item.IsUp && (!isPreviousUp))
{
item.IsGuarentee = true;
}
isPreviousUp = item.IsUp;
item.Color = GetColorByName(item.Name);
}
return false;
// reverse items
summaryItems.Reverse();
}
/// <summary>
@@ -103,4 +97,14 @@ public static class GachaStatisticsExtensions
.OrderByDescending(item => item.Count)
.ToList();
}
private static Color GetColorByName(string name)
{
byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
Span<byte> first = new(codes, 0, 5);
Span<byte> second = new(codes, 5, 5);
Span<byte> third = new(codes, 10, 5);
Color color = Color.FromArgb(255, first.Average(), second.Average(), third.Average());
return color;
}
}

View File

@@ -89,9 +89,9 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
break;
}
permanentWishBuilder.TrackAvatar(item, avatar, isUp);
avatarWishBuilder.TrackAvatar(item, avatar, isUp);
weaponWishBuilder.TrackAvatar(item, avatar, isUp);
permanentWishBuilder.Track(item, avatar, isUp);
avatarWishBuilder.Track(item, avatar, isUp);
weaponWishBuilder.Track(item, avatar, isUp);
}
// It's a weapon
@@ -116,9 +116,9 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
break;
}
permanentWishBuilder.TrackWeapon(item, weapon, isUp);
avatarWishBuilder.TrackWeapon(item, weapon, isUp);
weaponWishBuilder.TrackWeapon(item, weapon, isUp);
permanentWishBuilder.Track(item, weapon, isUp);
avatarWishBuilder.Track(item, weapon, isUp);
weaponWishBuilder.Track(item, weapon, isUp);
}
else
{

Some files were not shown because too many files have changed in this diff Show More