mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f29bfda4d9 | ||
|
|
cb6a9badc0 |
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,10 @@ public class AppDbContext : DbContext
|
||||
{
|
||||
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new AvatarInfoConfiguration());
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Shapes;
|
||||
|
||||
@@ -11,7 +12,7 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// <summary>
|
||||
/// Make ContentDialog's SmokeLayerBackground dsiplay over custom titleBar
|
||||
/// </summary>
|
||||
public class ContentDialogBehavior : BehaviorBase<FrameworkElement>
|
||||
public class ContentDialogBehavior : BehaviorBase<ContentDialog>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
@@ -37,4 +38,4 @@ public class ContentDialogBehavior : BehaviorBase<FrameworkElement>
|
||||
sender.ClearValue(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
src/Snap.Hutao/Snap.Hutao/Control/ValueConverterBase.cs
Normal file
59
src/Snap.Hutao/Snap.Hutao/Control/ValueConverterBase.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,15 +12,15 @@ 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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
namespace Snap.Hutao.Core.Exception;
|
||||
|
||||
/// <summary>
|
||||
/// 异常记录器
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,4 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
|
||||
if (typeToConvert.GetGenericTypeDefinition() != typeof(IDictionary<,>))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -40,14 +40,11 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
|
||||
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)
|
||||
{
|
||||
valueConverter = (JsonConverter<TValue>)options.GetConverter(typeof(TValue));
|
||||
|
||||
// Cache the key and value types.
|
||||
keyType = typeof(TKey);
|
||||
valueType = typeof(TValue);
|
||||
@@ -77,22 +74,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 +99,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();
|
||||
|
||||
@@ -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>
|
||||
/// 是否成功
|
||||
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
|
||||
@@ -107,7 +107,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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,24 +10,16 @@ namespace Snap.Hutao;
|
||||
/// 主窗体
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
[Injection(InjectAs.Singleton, typeof(Window))] // This is for IEnumerable<Window>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
190
src/Snap.Hutao/Snap.Hutao/Migrations/20220924135810_AddAvatarInfo.Designer.cs
generated
Normal file
190
src/Snap.Hutao/Snap.Hutao/Migrations/20220924135810_AddAvatarInfo.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -8,11 +8,6 @@ namespace Snap.Hutao.Model.Binding.Gacha;
|
||||
/// </summary>
|
||||
public class GachaStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认的空祈愿统计
|
||||
/// </summary>
|
||||
public static readonly GachaStatistics Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// 角色活动
|
||||
/// </summary>
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
// 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.Auth;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using EntityUser = Snap.Hutao.Model.Entity.User;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding;
|
||||
|
||||
@@ -12,7 +15,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 +24,7 @@ public class User : Observable
|
||||
/// 构造一个新的绑定视图用户
|
||||
/// </summary>
|
||||
/// <param name="user">用户实体</param>
|
||||
private User(Entity.User user)
|
||||
private User(EntityUser user)
|
||||
{
|
||||
inner = user;
|
||||
}
|
||||
@@ -45,14 +48,14 @@ 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"/>
|
||||
/// <inheritdoc cref="EntityUser.Cookie"/>
|
||||
public string? Cookie
|
||||
{
|
||||
get => inner.Cookie;
|
||||
@@ -62,7 +65,7 @@ public class User : Observable
|
||||
/// <summary>
|
||||
/// 内部的用户实体
|
||||
/// </summary>
|
||||
public Entity.User Entity { get => inner; }
|
||||
public EntityUser Entity { get => inner; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否初始化完成
|
||||
@@ -70,27 +73,172 @@ public class User : Observable
|
||||
public bool IsInitialized { get => isInitialized; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化用户
|
||||
/// 将cookie的字符串形式转换为字典
|
||||
/// </summary>
|
||||
/// <param name="inner">用户实体</param>
|
||||
/// <param name="cookie">cookie的字符串形式</param>
|
||||
/// <returns>包含cookie信息的字典</returns>
|
||||
public static IDictionary<string, string> ParseCookie(string cookie)
|
||||
{
|
||||
SortedDictionary<string, string> cookieDictionary = new();
|
||||
|
||||
string[] values = cookie.TrimEnd(';').Split(';');
|
||||
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
|
||||
{
|
||||
string cookieName = parts[0].Trim();
|
||||
string cookieValue = parts.Length == 1 ? string.Empty : parts[1].Trim();
|
||||
|
||||
cookieDictionary.Add(cookieName, cookieValue);
|
||||
}
|
||||
|
||||
return cookieDictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库恢复用户
|
||||
/// </summary>
|
||||
/// <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.ResumeInternalAsync(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="authClient">授权客户端</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="false"/> </returns>
|
||||
internal static async Task<User?> CreateAsync(
|
||||
IDictionary<string, string> cookie,
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
AuthClient authClient,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
string simplifiedCookie = ToCookieString(cookie);
|
||||
EntityUser inner = EntityUser.Create(simplifiedCookie);
|
||||
User user = new(inner);
|
||||
bool successful = await user.CreateInternalAsync(cookie, userClient, userGameRoleClient, authClient, token).ConfigureAwait(false);
|
||||
return successful ? user : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试升级到Stoken
|
||||
/// </summary>
|
||||
/// <param name="addition">额外的token</param>
|
||||
/// <param name="authClient">验证客户端</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否升级成功</returns>
|
||||
internal async Task<bool> TryUpgradeAsync(IDictionary<string, string> addition, AuthClient authClient, CancellationToken token)
|
||||
{
|
||||
IDictionary<string, string> cookie = ParseCookie(Cookie!);
|
||||
if (addition.TryGetValue(CookieKeys.LOGIN_TICKET, out string? loginTicket))
|
||||
{
|
||||
cookie[CookieKeys.LOGIN_TICKET] = loginTicket;
|
||||
}
|
||||
|
||||
if (addition.TryGetValue(CookieKeys.LOGIN_UID, out string? loginUid))
|
||||
{
|
||||
cookie[CookieKeys.LOGIN_UID] = loginUid;
|
||||
}
|
||||
|
||||
bool result = await TryAddStokenToCookieAsync(cookie, authClient, token).ConfigureAwait(false);
|
||||
|
||||
if (result)
|
||||
{
|
||||
Cookie = ToCookieString(cookie);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ToCookieString(IDictionary<string, string> cookie)
|
||||
{
|
||||
return string.Join(';', cookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||
}
|
||||
|
||||
private static async Task<bool> TryAddStokenToCookieAsync(IDictionary<string, string> cookie, AuthClient authClient, CancellationToken token)
|
||||
{
|
||||
if (cookie.TryGetValue(CookieKeys.LOGIN_TICKET, out string? loginTicket))
|
||||
{
|
||||
string? loginUid = cookie.GetValueOrDefault(CookieKeys.LOGIN_UID) ?? cookie.GetValueOrDefault(CookieKeys.LTUID);
|
||||
|
||||
if (loginUid != null)
|
||||
{
|
||||
Dictionary<string, string> stokens = await authClient
|
||||
.GetMultiTokenByLoginTicketAsync(loginTicket, loginUid, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (stokens.TryGetValue(CookieKeys.STOKEN, out string? stoken) && stokens.TryGetValue(CookieKeys.LTOKEN, out string? ltoken))
|
||||
{
|
||||
cookie[CookieKeys.STOKEN] = stoken;
|
||||
cookie[CookieKeys.LTOKEN] = ltoken;
|
||||
cookie[CookieKeys.STUID] = cookie[CookieKeys.LTUID];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> ResumeInternalAsync(
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await PrepareUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
|
||||
private async Task<bool> CreateInternalAsync(
|
||||
IDictionary<string, string> cookie,
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
AuthClient authClient,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await TryAddStokenToCookieAsync(cookie, authClient, token).ConfigureAwait(false))
|
||||
{
|
||||
Cookie = ToCookieString(cookie);
|
||||
}
|
||||
|
||||
await PrepareUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
|
||||
private async Task PrepareUserInfoAndUserGameRolesAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token)
|
||||
{
|
||||
UserInfo = await userClient
|
||||
.GetUserFullInfoAsync(this, token)
|
||||
.ConfigureAwait(false);
|
||||
@@ -100,9 +248,5 @@ public class User : Observable
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SelectedUserGameRole = UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
}
|
||||
31
src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs
Normal file
31
src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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!;
|
||||
}
|
||||
@@ -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>>();
|
||||
}
|
||||
}
|
||||
@@ -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)!)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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)!);
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
/// <summary>
|
||||
/// 角色
|
||||
/// </summary>
|
||||
public class Avatar : IStatisticsItemSource
|
||||
public class Avatar : IStatisticsItemSource, 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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,41 +10,26 @@ 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)
|
||||
public override IList<LevelParam<string, ParameterInfo>> Convert(DescParam rawDescParam)
|
||||
{
|
||||
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]);
|
||||
})
|
||||
.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 };
|
||||
})
|
||||
.Select(param => new LevelParam<string, ParameterInfo>(
|
||||
param.Level.ToString(),
|
||||
GetParameterInfos(rawDescParam, param.Parameters)))
|
||||
.ToList();
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
private static IList<ParameterInfo> GetParameterInfos(DescParam rawDescParam, IList<double> param)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
IList<DescFormat> formats = rawDescParam.Descriptions
|
||||
.Select(desc => new DescFormat(desc))
|
||||
.ToList();
|
||||
|
||||
private static IList<ParameterInfo> GetFormattedParameters(IList<DescFormat> formats, IList<double> param)
|
||||
{
|
||||
List<ParameterInfo> results = new();
|
||||
|
||||
for (int index = 0; index < formats.Count; index++)
|
||||
@@ -63,25 +48,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 +61,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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
return IconNameToUri(from);
|
||||
}
|
||||
}
|
||||
@@ -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,41 +11,30 @@ 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)
|
||||
public override IList<LevelParam<string, ParameterInfo>> Convert(PropertyInfo from)
|
||||
{
|
||||
if (value is PropertyInfo rawDescParam)
|
||||
{
|
||||
IList<LevelParam<string, ParameterInfo>> parameters = rawDescParam.Parameters
|
||||
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();
|
||||
FormatMethod method = properties[index].GetFormatMethod();
|
||||
|
||||
string valueFormatted = method switch
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
// 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)
|
||||
public override Uri Convert(string from)
|
||||
{
|
||||
string target = (string)value;
|
||||
|
||||
if (target.StartsWith("UI_Talent_"))
|
||||
if (from.StartsWith("UI_Talent_"))
|
||||
{
|
||||
return new Uri(string.Format(TalentUrl, target));
|
||||
return new Uri(string.Format(TalentUrl, from));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Uri(string.Format(SkillUrl, target));
|
||||
return new Uri(string.Format(SkillUrl, from));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
45
src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs
Normal file
45
src/Snap.Hutao/Snap.Hutao/Model/Metadata/ParameterFormat.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Weapon;
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public class Weapon : IStatisticsItemSource
|
||||
public class Weapon : IStatisticsItemSource, INameQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
@@ -54,6 +54,10 @@ public class Weapon : IStatisticsItemSource
|
||||
/// </summary>
|
||||
public PropertyInfo Property { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public ItemQuality Quality => RankLevel;
|
||||
|
||||
/// <summary>
|
||||
/// 转换为基础物品
|
||||
/// </summary>
|
||||
@@ -63,7 +67,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 +83,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 +102,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,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.1.0.0" />
|
||||
Version="1.1.3.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
|
||||
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_None.png
Normal file
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_None.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -5,10 +5,13 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.InterChange.GachaLog;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.GachaLog.Factory;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
@@ -46,8 +49,11 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
|
||||
private readonly Dictionary<string, ItemBase> itemBaseCache = new();
|
||||
|
||||
private Dictionary<string, Model.Metadata.Avatar.Avatar>? avatarMap;
|
||||
private Dictionary<string, Model.Metadata.Weapon.Weapon>? weaponMap;
|
||||
private Dictionary<string, Model.Metadata.Avatar.Avatar>? nameAvatarMap;
|
||||
private Dictionary<string, Model.Metadata.Weapon.Weapon>? nameWeaponMap;
|
||||
|
||||
private Dictionary<int, Model.Metadata.Avatar.Avatar>? idAvatarMap;
|
||||
private Dictionary<int, Model.Metadata.Weapon.Weapon>? idWeaponMap;
|
||||
private ObservableCollection<GachaArchive>? archiveCollection;
|
||||
|
||||
/// <summary>
|
||||
@@ -89,6 +95,26 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
/// <inheritdoc/>
|
||||
public bool IsInitialized { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<UIGF> ExportToUIGFAsync(GachaArchive archive)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "祈愿记录服务未能正常初始化");
|
||||
|
||||
var list = appDbContext.GachaItems
|
||||
.Where(i => i.ArchiveId == archive.InnerId)
|
||||
.AsEnumerable()
|
||||
.Select(i => i.ToUIGFItem(GetNameQualityByItemId(i.ItemId)))
|
||||
.ToList();
|
||||
|
||||
UIGF uigf = new()
|
||||
{
|
||||
Info = UIGFInfo.Create(archive.Uid),
|
||||
List = list,
|
||||
};
|
||||
|
||||
return Task.FromResult(uigf);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GachaArchive> GetArchiveCollection()
|
||||
{
|
||||
@@ -100,8 +126,11 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
{
|
||||
if (await metadataService.InitializeAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
avatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
weaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
@@ -128,7 +157,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
}
|
||||
else
|
||||
{
|
||||
return Task.FromResult(GachaStatistics.Default);
|
||||
return Must.Fault<GachaStatistics>("没有选中的存档");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +167,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
return option switch
|
||||
{
|
||||
RefreshOption.WebCache => urlProviders.Single(p => p.Name == nameof(GachaLogUrlWebCacheProvider)),
|
||||
RefreshOption.Stoken => urlProviders.Single(p => p.Name == nameof(GachaLogUrlStokenProvider)),
|
||||
RefreshOption.ManualInput => urlProviders.Single(p => p.Name == nameof(GachaLogUrlManualInputProvider)),
|
||||
_ => null,
|
||||
};
|
||||
@@ -150,7 +180,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RefreshGachaLogAsync(string query, RefreshStrategy strategy, IProgress<FetchState> progress, CancellationToken token)
|
||||
public async Task<bool> RefreshGachaLogAsync(string query, RefreshStrategy strategy, IProgress<FetchState> progress, CancellationToken token)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "祈愿记录服务未能正常初始化");
|
||||
|
||||
@@ -161,8 +191,9 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
|
||||
GachaArchive? result = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false);
|
||||
(bool authkeyValid, GachaArchive? result) = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false);
|
||||
CurrentArchive = result ?? CurrentArchive;
|
||||
return authkeyValid;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -206,7 +237,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
CurrentArchive = archive;
|
||||
}
|
||||
|
||||
private async Task<GachaArchive?> FetchGachaLogsAsync(string query, bool isLazy, IProgress<FetchState> progress, CancellationToken token)
|
||||
private async Task<ValueResult<bool, GachaArchive?>> FetchGachaLogsAsync(string query, bool isLazy, IProgress<FetchState> progress, CancellationToken token)
|
||||
{
|
||||
GachaArchive? archive = null;
|
||||
FetchState state = new();
|
||||
@@ -274,7 +305,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
await RandomDelayAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return archive;
|
||||
return new(!state.AuthKeyTimeout, archive);
|
||||
}
|
||||
|
||||
private void SkipOrInitArchive([NotNull] ref GachaArchive? archive, string uid)
|
||||
@@ -320,8 +351,8 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
{
|
||||
return item.ItemType switch
|
||||
{
|
||||
"角色" => avatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0,
|
||||
"武器" => weaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0,
|
||||
"角色" => nameAvatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0,
|
||||
"武器" => nameWeaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
@@ -332,8 +363,8 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
{
|
||||
result = type switch
|
||||
{
|
||||
"角色" => avatarMap![name].ToItemBase(),
|
||||
"武器" => weaponMap![name].ToItemBase(),
|
||||
"角色" => nameAvatarMap![name].ToItemBase(),
|
||||
"武器" => nameWeaponMap![name].ToItemBase(),
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
|
||||
@@ -343,6 +374,16 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
return result;
|
||||
}
|
||||
|
||||
private INameQuality GetNameQualityByItemId(int id)
|
||||
{
|
||||
return id.Place() switch
|
||||
{
|
||||
8 => idAvatarMap![id],
|
||||
5 => idWeaponMap![id],
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
}
|
||||
|
||||
private void SaveGachaItems(List<GachaItem> itemsToAdd, bool isLazy, GachaArchive? archive, long endId)
|
||||
{
|
||||
if (itemsToAdd.Count > 0)
|
||||
|
||||
@@ -18,6 +18,13 @@ internal interface IGachaLogService
|
||||
/// </summary>
|
||||
GachaArchive? CurrentArchive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 导出为一个新的UIGF对象
|
||||
/// </summary>
|
||||
/// <param name="archive">存档</param>
|
||||
/// <returns>UIGF对象</returns>
|
||||
Task<UIGF> ExportToUIGFAsync(GachaArchive archive);
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用于绑定的存档集合
|
||||
/// </summary>
|
||||
@@ -61,8 +68,8 @@ internal interface IGachaLogService
|
||||
/// <param name="strategy">刷新策略</param>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>任务</returns>
|
||||
Task RefreshGachaLogAsync(string query, RefreshStrategy strategy, IProgress<FetchState> progress, CancellationToken token);
|
||||
/// <returns>验证密钥是否可用</returns>
|
||||
Task<bool> RefreshGachaLogAsync(string query, RefreshStrategy strategy, IProgress<FetchState> progress, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// 删除存档
|
||||
|
||||
@@ -20,6 +20,11 @@ public enum RefreshOption
|
||||
/// </summary>
|
||||
WebCache,
|
||||
|
||||
/// <summary>
|
||||
/// 通过Stoken刷新
|
||||
/// </summary>
|
||||
Stoken,
|
||||
|
||||
/// <summary>
|
||||
/// 手动输入Url刷新
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog;
|
||||
|
||||
/// <summary>
|
||||
/// 使用Stokn提供祈愿Url
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IGachaLogUrlProvider))]
|
||||
internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
|
||||
{
|
||||
private readonly IUserService userService;
|
||||
private readonly BindingClient2 bindingClient2;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的提供器
|
||||
/// </summary>
|
||||
/// <param name="userService">用户服务</param>
|
||||
/// <param name="bindingClient2">绑定客户端</param>
|
||||
public GachaLogUrlStokenProvider(IUserService userService, BindingClient2 bindingClient2)
|
||||
{
|
||||
this.userService = userService;
|
||||
this.bindingClient2 = bindingClient2;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get => nameof(GachaLogUrlStokenProvider); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ValueResult<bool, string>> GetQueryAsync()
|
||||
{
|
||||
Model.Binding.User? user = userService.CurrentUser;
|
||||
if (user != null)
|
||||
{
|
||||
if (user.Cookie!.Contains(CookieKeys.STOKEN) && user.SelectedUserGameRole != null)
|
||||
{
|
||||
PlayerUid uid = (PlayerUid)user.SelectedUserGameRole;
|
||||
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(uid);
|
||||
|
||||
GameAuthKey? authkey = await bindingClient2.GenerateAuthenticationKeyAsync(user, data).ConfigureAwait(false);
|
||||
if (authkey != null)
|
||||
{
|
||||
return new(true, GachaLogConfigration.AsQuery(data, authkey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new(false, null!);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog;
|
||||
|
||||
@@ -47,7 +46,6 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
{
|
||||
using (BinaryReader reader = new(fileStream))
|
||||
{
|
||||
Regex urlMatch = new("(https.+?game_biz=hk4e.+?)&", RegexOptions.Compiled);
|
||||
string url = string.Empty;
|
||||
while (!reader.EndOfStream())
|
||||
{
|
||||
@@ -58,7 +56,7 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
byte[] chars = ReadBytesUntilZero(reader);
|
||||
string result = Encoding.UTF8.GetString(chars.AsSpan());
|
||||
|
||||
if (urlMatch.Match(result).Success)
|
||||
if (result.Contains("&auth_appid=webview_gacha"))
|
||||
{
|
||||
url = result;
|
||||
}
|
||||
@@ -101,4 +99,4 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ internal interface IGachaLogUrlProvider : INamed
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步获取包含验证密钥的查询语句
|
||||
/// 查询语句可以仅包含?后的内容
|
||||
/// </summary>
|
||||
/// <returns>包含验证密钥的查询语句</returns>
|
||||
Task<ValueResult<bool, string>> GetQueryAsync();
|
||||
|
||||
@@ -78,7 +78,7 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
|
||||
{
|
||||
using (SafeFileHandle snapshot = CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId))
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
|
||||
|
||||
MODULEENTRY32 entry = StructMarshal.MODULEENTRY32();
|
||||
bool found = false;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Automation.Provider;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
@@ -33,6 +35,14 @@ public interface IUserService
|
||||
/// <returns>用户初始化是否成功</returns>
|
||||
Task<UserAddResult> TryAddUserAsync(User user, string uid);
|
||||
|
||||
/// <summary>
|
||||
/// 尝试使用 login_ticket 升级用户
|
||||
/// </summary>
|
||||
/// <param name="cookie">额外的Cookie</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否升级成功</returns>
|
||||
Task<ValueResult<bool, string>> TryUpgradeUserAsync(IDictionary<string, string> addiition, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步移除用户
|
||||
/// </summary>
|
||||
@@ -40,17 +50,11 @@ public interface IUserService
|
||||
/// <returns>任务</returns>
|
||||
Task RemoveUserAsync(User user);
|
||||
|
||||
/// <summary>
|
||||
/// 将cookie的字符串形式转换为字典
|
||||
/// </summary>
|
||||
/// <param name="cookie">cookie的字符串形式</param>
|
||||
/// <returns>包含cookie信息的字典</returns>
|
||||
IDictionary<string, string> ParseCookie(string cookie);
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的绑定用户
|
||||
/// 若存在 login_ticket 与 login_uid 则 自动获取 stoken
|
||||
/// </summary>
|
||||
/// <param name="cookie">cookie的字符串形式</param>
|
||||
/// <returns>新的绑定用户</returns>
|
||||
Task<User?> CreateUserAsync(string cookie);
|
||||
Task<User?> CreateUserAsync(IDictionary<string, string> cookie);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.ObjectModel;
|
||||
using BindingUser = Snap.Hutao.Model.Binding.User;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
|
||||
@@ -20,11 +23,12 @@ internal class UserService : IUserService
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly UserClient userClient;
|
||||
private readonly UserGameRoleClient userGameRoleClient;
|
||||
private readonly BindingClient userGameRoleClient;
|
||||
private readonly AuthClient authClient;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
private User? currentUser;
|
||||
private ObservableCollection<User>? userCollection = null;
|
||||
private BindingUser? currentUser;
|
||||
private ObservableCollection<BindingUser>? userCollection;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户服务
|
||||
@@ -32,17 +36,24 @@ internal class UserService : IUserService
|
||||
/// <param name="appDbContext">应用程序数据库上下文</param>
|
||||
/// <param name="userClient">用户客户端</param>
|
||||
/// <param name="userGameRoleClient">角色客户端</param>
|
||||
/// <param name="authClient">验证客户端</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public UserService(AppDbContext appDbContext, UserClient userClient, UserGameRoleClient userGameRoleClient, IMessenger messenger)
|
||||
public UserService(
|
||||
AppDbContext appDbContext,
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
AuthClient authClient,
|
||||
IMessenger messenger)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.userClient = userClient;
|
||||
this.userGameRoleClient = userGameRoleClient;
|
||||
this.authClient = authClient;
|
||||
this.messenger = messenger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public User? CurrentUser
|
||||
public BindingUser? CurrentUser
|
||||
{
|
||||
get => currentUser;
|
||||
set
|
||||
@@ -80,12 +91,12 @@ internal class UserService : IUserService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<UserAddResult> TryAddUserAsync(User newUser, string uid)
|
||||
public async Task<UserAddResult> TryAddUserAsync(BindingUser newUser, string uid)
|
||||
{
|
||||
Must.NotNull(userCollection!);
|
||||
|
||||
// 查找是否有相同的uid
|
||||
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is User userWithSameUid)
|
||||
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
|
||||
{
|
||||
// Prevent users from adding a completely same cookie.
|
||||
if (userWithSameUid.Cookie == newUser.Cookie)
|
||||
@@ -118,7 +129,7 @@ internal class UserService : IUserService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RemoveUserAsync(User user)
|
||||
public Task RemoveUserAsync(BindingUser user)
|
||||
{
|
||||
Must.NotNull(userCollection!);
|
||||
|
||||
@@ -131,16 +142,16 @@ internal class UserService : IUserService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ObservableCollection<User>> GetUserCollectionAsync()
|
||||
public async Task<ObservableCollection<BindingUser>> GetUserCollectionAsync()
|
||||
{
|
||||
if (userCollection == null)
|
||||
{
|
||||
List<User> users = new();
|
||||
List<BindingUser> users = new();
|
||||
|
||||
foreach (Model.Entity.User entity in appDbContext.Users)
|
||||
{
|
||||
User? initialized = await User
|
||||
.CreateAsync(entity, userClient, userGameRoleClient)
|
||||
BindingUser? initialized = await BindingUser
|
||||
.ResumeAsync(entity, userClient, userGameRoleClient)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (initialized != null)
|
||||
@@ -163,25 +174,30 @@ internal class UserService : IUserService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<User?> CreateUserAsync(string cookie)
|
||||
public Task<BindingUser?> CreateUserAsync(IDictionary<string, string> cookie)
|
||||
{
|
||||
return User.CreateAsync(Model.Entity.User.Create(cookie), userClient, userGameRoleClient);
|
||||
return BindingUser.CreateAsync(cookie, userClient, userGameRoleClient, authClient);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDictionary<string, string> ParseCookie(string cookie)
|
||||
public async Task<ValueResult<bool, string>> TryUpgradeUserAsync(IDictionary<string, string> addition, CancellationToken token = default)
|
||||
{
|
||||
SortedDictionary<string, string> cookieDictionary = new();
|
||||
|
||||
string[] values = cookie.TrimEnd(';').Split(';');
|
||||
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
|
||||
Must.NotNull(userCollection!);
|
||||
if (addition.TryGetValue(CookieKeys.LOGIN_UID, out string? uid))
|
||||
{
|
||||
string cookieName = parts[0].Trim();
|
||||
string cookieValue = parts.Length == 1 ? string.Empty : parts[1].Trim();
|
||||
|
||||
cookieDictionary.Add(cookieName, cookieValue);
|
||||
// 查找是否有相同的uid
|
||||
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
|
||||
{
|
||||
// Update user cookie here.
|
||||
if (await userWithSameUid.TryUpgradeAsync(addition, authClient, token))
|
||||
{
|
||||
appDbContext.Users.Update(userWithSameUid.Entity);
|
||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
return new(true, uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cookieDictionary;
|
||||
return new(false, string.Empty);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@
|
||||
<None Remove="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
|
||||
<None Remove="Resource\Icon\UI_BtnIcon_Gacha.png" />
|
||||
<None Remove="Resource\Icon\UI_Icon_Achievement.png" />
|
||||
<None Remove="Resource\Icon\UI_Icon_None.png" />
|
||||
<None Remove="Resource\Icon\UI_ItemIcon_201.png" />
|
||||
<None Remove="Resource\Segoe Fluent Icons.ttf" />
|
||||
<None Remove="stylecop.json" />
|
||||
@@ -49,6 +50,7 @@
|
||||
<None Remove="View\Dialog\AchievementImportDialog.xaml" />
|
||||
<None Remove="View\Dialog\GachaLogImportDialog.xaml" />
|
||||
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserAutoCookieDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserDialog.xaml" />
|
||||
<None Remove="View\MainView.xaml" />
|
||||
<None Remove="View\Page\AchievementPage.xaml" />
|
||||
@@ -81,6 +83,7 @@
|
||||
<Content Include="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
|
||||
<Content Include="Resource\Icon\UI_BtnIcon_Gacha.png" />
|
||||
<Content Include="Resource\Icon\UI_Icon_Achievement.png" />
|
||||
<Content Include="Resource\Icon\UI_Icon_None.png" />
|
||||
<Content Include="Resource\Icon\UI_ItemIcon_201.png" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -103,7 +106,7 @@
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.5" />
|
||||
<PackageReference Include="MiniExcel" Version="1.26.7" />
|
||||
<PackageReference Include="MiniExcel" Version="1.28.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -125,6 +128,11 @@
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\UserAutoCookieDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\AchievementArchiveCreateDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -205,9 +213,6 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Model\Annotation\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\GachaLogImportDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.View.Control;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
xmlns:shmbg="using:Snap.Hutao.Model.Binding.Gacha"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance shmbg:TypedWishSummary}">
|
||||
@@ -54,6 +53,8 @@
|
||||
Visibility="{Binding IsUp,Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
|
||||
<TextBlock
|
||||
Width="20"
|
||||
TextAlignment="Center"
|
||||
Text="{Binding LastPull}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
@@ -94,16 +95,45 @@
|
||||
<TextBlock Margin="12,0,0,12" Text="抽" VerticalAlignment="Bottom"/>
|
||||
</StackPanel>
|
||||
|
||||
<ProgressBar
|
||||
Margin="0,0,0,0"
|
||||
Value="{Binding LastOrangePull}"
|
||||
Maximum="90"
|
||||
Foreground="{StaticResource OrangeBrush}"/>
|
||||
<ProgressBar
|
||||
Margin="0,6,0,0"
|
||||
Value="{Binding LastPurplePull}"
|
||||
Maximum="10"
|
||||
Foreground="{StaticResource PurpleBrush}"/>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ProgressBar
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Value="{Binding LastOrangePull}"
|
||||
Maximum="{Binding GuarenteeOrangeThreshold}"
|
||||
Foreground="{StaticResource OrangeBrush}"/>
|
||||
<TextBlock
|
||||
Width="20"
|
||||
TextAlignment="Center"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding LastOrangePull}"
|
||||
Foreground="{StaticResource OrangeBrush}"/>
|
||||
<ProgressBar
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Value="{Binding LastPurplePull}"
|
||||
Maximum="{Binding GuarenteePurpleThreshold}"
|
||||
Foreground="{StaticResource PurpleBrush}"/>
|
||||
<TextBlock
|
||||
Width="20"
|
||||
TextAlignment="Center"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding LastPurplePull}"
|
||||
Foreground="{StaticResource PurpleBrush}"/>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Text="AuthKey 已失效,请重新获取"
|
||||
Text="祈愿记录Url已失效,请重新获取"
|
||||
Visibility="{x:Bind State.AuthKeyTimeout,Converter={StaticResource BoolToVisibilityConverter},Mode=OneWay}"/>
|
||||
<cwucont:HeaderedItemsControl
|
||||
x:Name="GachaItemsPresenter"
|
||||
@@ -47,5 +47,4 @@
|
||||
</cwucont:HeaderedItemsControl.ItemsPanel>
|
||||
</cwucont:HeaderedItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
</ContentDialog>
|
||||
|
||||
@@ -44,7 +44,9 @@ public sealed partial class GachaLogRefreshProgressDialog : ContentDialog
|
||||
public void OnReport(FetchState state)
|
||||
{
|
||||
State = state;
|
||||
GachaItemsPresenter.Header = state.ConfigType.GetDescription();
|
||||
GachaItemsPresenter.Header = state.AuthKeyTimeout
|
||||
? null
|
||||
: (object)$"正在获取 {state.ConfigType.GetDescription()}";
|
||||
|
||||
// Binding not working here.
|
||||
GachaItemsPresenter.Items.Clear();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<ContentDialog
|
||||
x:Class="Snap.Hutao.View.Dialog.UserAutoCookieDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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"
|
||||
mc:Ignorable="d"
|
||||
Title="登录米哈游通行证"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonText="继续"
|
||||
CloseButtonText="取消"
|
||||
Style="{StaticResource DefaultContentDialogStyle}">
|
||||
<ContentDialog.Resources>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">1600</x:Double>
|
||||
<x:Double x:Key="ContentDialogMinHeight">200</x:Double>
|
||||
<x:Double x:Key="ContentDialogMaxHeight">1200</x:Double>
|
||||
</ContentDialog.Resources>
|
||||
<Grid Loaded="OnRootLoaded">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
Text="请在 成功登录米哈游通行证 后点击 [继续] 按钮"/>
|
||||
<WebView2
|
||||
Grid.Row="1"
|
||||
Width="640"
|
||||
Height="400"
|
||||
x:Name="WebView"/>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
|
||||
namespace Snap.Hutao.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 用户自动Cookie对话框
|
||||
/// </summary>
|
||||
public sealed partial class UserAutoCookieDialog : ContentDialog
|
||||
{
|
||||
private IDictionary<string, string>? cookieString;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户自动Cookie对话框
|
||||
/// </summary>
|
||||
/// <param name="window">依赖窗口</param>
|
||||
public UserAutoCookieDialog(Window window)
|
||||
{
|
||||
InitializeComponent();
|
||||
XamlRoot = window.Content.XamlRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取输入的Cookie
|
||||
/// </summary>
|
||||
/// <returns>输入的结果</returns>
|
||||
public async Task<ValueResult<bool, IDictionary<string, string>>> GetInputCookieAsync()
|
||||
{
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
return new(result == ContentDialogResult.Primary && cookieString != null, cookieString!);
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
private async void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await WebView.EnsureCoreWebView2Async();
|
||||
WebView.CoreWebView2.SourceChanged += OnCoreWebView2SourceChanged;
|
||||
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
|
||||
foreach (var item in cookies)
|
||||
{
|
||||
manager.DeleteCookie(item);
|
||||
}
|
||||
|
||||
WebView.CoreWebView2.Navigate("https://user.mihoyo.com/#/login/password");
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
private async void OnCoreWebView2SourceChanged(CoreWebView2 sender, CoreWebView2SourceChangedEventArgs args)
|
||||
{
|
||||
if (sender != null)
|
||||
{
|
||||
if (sender.Source.ToString() == "https://user.mihoyo.com/#/account/home")
|
||||
{
|
||||
try
|
||||
{
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
|
||||
cookieString = cookies.ToDictionary(c => c.Name, c => c.Value);
|
||||
|
||||
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,20 +22,21 @@
|
||||
<TextBox
|
||||
Margin="0,0,0,8"
|
||||
x:Name="InputText"
|
||||
TextChanged="InputTextChanged"
|
||||
TextChanged="InputTextChanged"
|
||||
PlaceholderText="在此处输入"
|
||||
VerticalAlignment="Top"/>
|
||||
<settings:Setting
|
||||
Margin="0,8,0,0"
|
||||
Icon=""
|
||||
Header="手动获取"
|
||||
Description="进入我们的文档页面并按指示操作"
|
||||
HorizontalAlignment="Stretch">
|
||||
<HyperlinkButton
|
||||
Margin="12,0,0,0"
|
||||
Padding="4"
|
||||
Content="立即前往"
|
||||
NavigateUri="https://www.snapgenshin.com/documents/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/>
|
||||
</settings:Setting>
|
||||
<settings:SettingsGroup Margin="0,-48,0,0">
|
||||
<settings:Setting
|
||||
Icon=""
|
||||
Header="操作文档"
|
||||
Description="进入我们的文档页面并按指示操作"
|
||||
HorizontalAlignment="Stretch">
|
||||
<HyperlinkButton
|
||||
Margin="12,0,0,0"
|
||||
Padding="4"
|
||||
Content="立即前往"
|
||||
NavigateUri="https://www.snapgenshin.com/documents/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/>
|
||||
</settings:Setting>
|
||||
</settings:SettingsGroup>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
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:cwu="using:CommunityToolkit.WinUI.UI"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
xmlns:shv="using:Snap.Hutao.View"
|
||||
xmlns:shvh="using:Snap.Hutao.View.Helper"
|
||||
xmlns:shvp="using:Snap.Hutao.View.Page"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<Thickness x:Key="NavigationViewContentMargin">0,44,0,0</Thickness>
|
||||
@@ -26,21 +25,26 @@
|
||||
Content="活动"
|
||||
shvh:NavHelper.NavigateTo="shvp:AnnouncementPage"
|
||||
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BtnIcon_ActivityEntry.png}"/>
|
||||
|
||||
<NavigationViewItemHeader Content="工具"/>
|
||||
|
||||
<NavigationViewItem
|
||||
Content="祈愿记录"
|
||||
shvh:NavHelper.NavigateTo="shvp:GachaLogPage"
|
||||
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BtnIcon_Gacha.png}"/>
|
||||
|
||||
<NavigationViewItem
|
||||
Content="成就"
|
||||
shvh:NavHelper.NavigateTo="shvp:AchievementPage"
|
||||
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_Icon_Achievement.png}"/>
|
||||
|
||||
<NavigationViewItemHeader Content="WIKI"/>
|
||||
|
||||
<NavigationViewItem
|
||||
Content="角色"
|
||||
shvh:NavHelper.NavigateTo="shvp:WikiAvatarPage"
|
||||
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/>
|
||||
|
||||
<NavigationViewItem
|
||||
Content="祈愿记录"
|
||||
shvh:NavHelper.NavigateTo="shvp:GachaLogPage"
|
||||
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BtnIcon_Gacha.png}"/>
|
||||
|
||||
</NavigationView.MenuItems>
|
||||
|
||||
<NavigationView.PaneFooter>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
xmlns:shv="using:Snap.Hutao.ViewModel"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Control" xmlns:image="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Control"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance shv:GachaLogViewModel}"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
@@ -45,15 +46,19 @@
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Text="从缓存刷新"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Command="{Binding RefreshByWebCacheCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
Text="Stoken刷新"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Command="{Binding RefreshByStokenCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
Text="手动输入Url"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Command="{Binding RefreshByManualInputCommand}"/>
|
||||
<ToggleMenuFlyoutItem
|
||||
Text="全量刷新"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
IsChecked="{Binding IsAggressiveRefresh}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
@@ -157,7 +162,7 @@
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="2">
|
||||
<image:CachedImage
|
||||
<shci:CachedImage
|
||||
Width="32"
|
||||
Height="32"
|
||||
Source="{Binding Icon}"/>
|
||||
@@ -187,7 +192,7 @@
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="2">
|
||||
<image:CachedImage
|
||||
<shci:CachedImage
|
||||
Width="32"
|
||||
Height="32"
|
||||
Source="{Binding Icon}"/>
|
||||
|
||||
@@ -45,9 +45,7 @@
|
||||
<shvc:DescParamComboBox
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Source="{Binding Proud,Converter={StaticResource DescParamDescriptor}}"/>
|
||||
|
||||
|
||||
Source="{Binding Proud,Mode=OneWay,Converter={StaticResource DescParamDescriptor}}"/>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
xmlns:mxim="using:Microsoft.Xaml.Interactions.Media"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:shc="using:Snap.Hutao.Control"
|
||||
mc:Ignorable="d">
|
||||
xmlns:shvm="using:Snap.Hutao.ViewModel"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance shvm:UserViewModel}">
|
||||
<mxi:Interaction.Behaviors>
|
||||
<mxic:EventTriggerBehavior EventName="Loaded">
|
||||
<mxic:InvokeCommandAction Command="{Binding OpenUICommand}"/>
|
||||
@@ -191,10 +193,18 @@
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<TextBlock
|
||||
Margin="10,6,0,6"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="Cookie"/>
|
||||
<CommandBar DefaultLabelPosition="Right">
|
||||
<AppBarButton
|
||||
Icon="Add"
|
||||
Label="添加新用户"
|
||||
Label="升级Stoken"
|
||||
Command="{Binding UpgradeToStokenCommand}"/>
|
||||
<AppBarButton
|
||||
Icon="Add"
|
||||
Label="手动添加"
|
||||
Command="{Binding AddUserCommand}"/>
|
||||
</CommandBar>
|
||||
</StackPanel>
|
||||
|
||||
@@ -377,12 +377,16 @@ internal class AchievementViewModel
|
||||
|
||||
FileOpenPicker picker = pickerFactory.GetFileOpenPicker();
|
||||
picker.SuggestedStartLocation = PickerLocationId.Desktop;
|
||||
picker.CommitButtonText = "导入";
|
||||
picker.FileTypeFilter.Add(".json");
|
||||
|
||||
if (await picker.PickSingleFileAsync() is StorageFile file)
|
||||
{
|
||||
if (await file.DeserializeJsonAsync<UIAF>(options, ex => infoBarService?.Error(ex)).ConfigureAwait(false) is UIAF uiaf)
|
||||
(bool isOk, UIAF? uiaf) = await file.DeserializeFromJsonAsync<UIAF>(options).ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
Must.NotNull(uiaf!);
|
||||
await TryImportUIAFInternalAsync(achievementService.CurrentArchive, uiaf).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -76,9 +76,9 @@ internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
try
|
||||
{
|
||||
Announcement = await announcementService.GetAnnouncementsAsync(CancellationToken);
|
||||
Announcement = await announcementService.GetAnnouncementsAsync(CancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogInformation($"{nameof(OpenUIAsync)} cancelled");
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.GachaLog;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Pickers;
|
||||
using Windows.Storage.Pickers.Provider;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.ViewModel;
|
||||
|
||||
@@ -60,6 +63,7 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
RefreshByWebCacheCommand = asyncRelayCommandFactory.Create(RefreshByWebCacheAsync);
|
||||
RefreshByStokenCommand = asyncRelayCommandFactory.Create(RefreshByStokenAsync);
|
||||
RefreshByManualInputCommand = asyncRelayCommandFactory.Create(RefreshByManualInputAsync);
|
||||
ImportFromUIGFExcelCommand = asyncRelayCommandFactory.Create(ImportFromUIGFExcelAsync);
|
||||
ImportFromUIGFJsonCommand = asyncRelayCommandFactory.Create(ImportFromUIGFJsonAsync);
|
||||
@@ -83,17 +87,7 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
public GachaArchive? SelectedArchive
|
||||
{
|
||||
get => selectedArchive;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref selectedArchive, value))
|
||||
{
|
||||
gachaLogService.CurrentArchive = selectedArchive;
|
||||
if (selectedArchive != null)
|
||||
{
|
||||
UpdateStatisticsAsync(selectedArchive).SafeForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
set => SetSelectedArchiveAndUpdateStatistics(value, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -131,6 +125,11 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
/// </summary>
|
||||
public ICommand RefreshByWebCacheCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stoken 刷新命令
|
||||
/// </summary>
|
||||
public ICommand RefreshByStokenCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 手动输入Url刷新命令
|
||||
/// </summary>
|
||||
@@ -162,11 +161,11 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
public ICommand RemoveArchiveCommand { get; }
|
||||
|
||||
[ThreadAccess(ThreadAccessState.MainThread)]
|
||||
private static Task<ContentDialogResult> ShowImportFailDialogAsync(string message)
|
||||
private static Task<ContentDialogResult> ShowImportResultDialogAsync(string title, string message)
|
||||
{
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
Title = "导入失败",
|
||||
Title = title,
|
||||
Content = message,
|
||||
PrimaryButtonText = "确认",
|
||||
DefaultButton = ContentDialogButton.Primary,
|
||||
@@ -195,6 +194,11 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
return RefreshInternalAsync(RefreshOption.WebCache);
|
||||
}
|
||||
|
||||
private Task RefreshByStokenAsync()
|
||||
{
|
||||
return RefreshInternalAsync(RefreshOption.Stoken);
|
||||
}
|
||||
|
||||
private Task RefreshByManualInputAsync()
|
||||
{
|
||||
return RefreshInternalAsync(RefreshOption.ManualInput);
|
||||
@@ -213,14 +217,23 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge;
|
||||
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
GachaLogRefreshProgressDialog dialog = new(mainWindow);
|
||||
await using (await dialog.BlockAsync().ConfigureAwait(false))
|
||||
{
|
||||
Progress<FetchState> progress = new(dialog.OnReport);
|
||||
await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, default).ConfigureAwait(false);
|
||||
IAsyncDisposable dialogHider = await dialog.BlockAsync().ConfigureAwait(false);
|
||||
Progress<FetchState> progress = new(dialog.OnReport);
|
||||
bool authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, default).ConfigureAwait(false);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedArchive = gachaLogService.CurrentArchive;
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
if (authkeyValid)
|
||||
{
|
||||
SetSelectedArchiveAndUpdateStatistics(gachaLogService.CurrentArchive, true);
|
||||
await dialogHider.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
dialog.Title = "获取祈愿记录失败";
|
||||
dialog.PrimaryButtonText = "确认";
|
||||
dialog.DefaultButton = ContentDialogButton.Primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,18 +248,21 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
FileOpenPicker picker = pickerFactory.GetFileOpenPicker();
|
||||
picker.SuggestedStartLocation = PickerLocationId.Desktop;
|
||||
picker.CommitButtonText = "导入";
|
||||
picker.FileTypeFilter.Add(".json");
|
||||
|
||||
if (await picker.PickSingleFileAsync() is StorageFile file)
|
||||
{
|
||||
if (await file.DeserializeJsonAsync<UIGF>(options, ex => infoBarService?.Error(ex)).ConfigureAwait(false) is UIGF uigf)
|
||||
(bool isOk, UIGF? uigf) = await file.DeserializeFromJsonAsync<UIGF>(options).ConfigureAwait(false);
|
||||
if (isOk)
|
||||
{
|
||||
Must.NotNull(uigf!);
|
||||
await TryImportUIGFInternalAsync(uigf).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await ShowImportFailDialogAsync("数据格式不正确").ConfigureAwait(false);
|
||||
await ShowImportResultDialogAsync("导入失败", "文件的数据格式不正确").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +274,32 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
private async Task ExportToUIGFJsonAsync()
|
||||
{
|
||||
await Task.Yield();
|
||||
if (SelectedArchive == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FileSavePicker picker = pickerFactory.GetFileSavePicker();
|
||||
picker.SuggestedStartLocation = PickerLocationId.Desktop;
|
||||
picker.SuggestedFileName = SelectedArchive.Uid;
|
||||
picker.CommitButtonText = "导出";
|
||||
picker.FileTypeChoices.Add("UIGF Json 文件", new List<string>() { ".json" });
|
||||
|
||||
if (await picker.PickSaveFileAsync() is StorageFile file)
|
||||
{
|
||||
UIGF uigf = await gachaLogService.ExportToUIGFAsync(SelectedArchive).ConfigureAwait(false);
|
||||
bool isOk = await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
if (isOk)
|
||||
{
|
||||
await ShowImportResultDialogAsync("导出成功", "成功保存到指定位置").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowImportResultDialogAsync("导出失败", "写入文件时遇到问题").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveArchiveAsync()
|
||||
@@ -287,6 +328,31 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadAccess(ThreadAccessState.MainThread)]
|
||||
private void SetSelectedArchiveAndUpdateStatistics(GachaArchive? archive, bool forceUpdate = false)
|
||||
{
|
||||
bool changed = false;
|
||||
if (selectedArchive != archive)
|
||||
{
|
||||
selectedArchive = archive;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
gachaLogService.CurrentArchive = archive;
|
||||
OnPropertyChanged(nameof(SelectedArchive));
|
||||
}
|
||||
|
||||
if (changed || forceUpdate)
|
||||
{
|
||||
if (archive != null)
|
||||
{
|
||||
UpdateStatisticsAsync(archive).SafeForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadAccess(ThreadAccessState.MainThread)]
|
||||
private async Task UpdateStatisticsAsync(GachaArchive? archive)
|
||||
{
|
||||
@@ -317,14 +383,14 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
infoBarService.Success($"导入完成");
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedArchive = gachaLogService.CurrentArchive;
|
||||
SetSelectedArchiveAndUpdateStatistics(gachaLogService.CurrentArchive, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await ShowImportFailDialogAsync("数据的 UIGF 版本过低,无法导入").ConfigureAwait(false);
|
||||
await ShowImportResultDialogAsync("导入失败", "数据的 UIGF 版本过低,无法导入").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Net;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Snap.Hutao.ViewModel;
|
||||
@@ -39,6 +42,7 @@ internal class UserViewModel : ObservableObject
|
||||
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
AddUserCommand = asyncRelayCommandFactory.Create(AddUserAsync);
|
||||
UpgradeToStokenCommand = asyncRelayCommandFactory.Create(UpgradeToStokenAsync);
|
||||
RemoveUserCommand = asyncRelayCommandFactory.Create<User>(RemoveUserAsync);
|
||||
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
|
||||
}
|
||||
@@ -73,6 +77,11 @@ internal class UserViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand AddUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 升级到Stoken命令
|
||||
/// </summary>
|
||||
public ICommand UpgradeToStokenCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 移除用户命令
|
||||
/// </summary>
|
||||
@@ -91,11 +100,15 @@ internal class UserViewModel : ObservableObject
|
||||
|
||||
foreach ((string key, string value) in map)
|
||||
{
|
||||
if (key == AccountIdKey || key == "cookie_token" || key == "ltoken" || key == "ltuid")
|
||||
if (key == CookieKeys.COOKIE_TOKEN || key == CookieKeys.ACCOUNT_ID || key == CookieKeys.LTOKEN || key == CookieKeys.LTUID)
|
||||
{
|
||||
validFlag--;
|
||||
filter.Add(key, value);
|
||||
}
|
||||
else if (key == CookieKeys.STOKEN || key == CookieKeys.STUID || key == CookieKeys.LOGIN_TICKET || key == CookieKeys.LOGIN_UID)
|
||||
{
|
||||
filter.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (validFlag == 0)
|
||||
@@ -112,7 +125,7 @@ internal class UserViewModel : ObservableObject
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
Users = await userService.GetUserCollectionAsync();
|
||||
Users = await userService.GetUserCollectionAsync().ConfigureAwait(true);
|
||||
SelectedUser = userService.CurrentUser;
|
||||
}
|
||||
|
||||
@@ -120,18 +133,16 @@ internal class UserViewModel : ObservableObject
|
||||
{
|
||||
// Get cookie from user input
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
(bool isOk, string cookie) = await new UserDialog(mainWindow).GetInputCookieAsync();
|
||||
ValueResult<bool, string> result = await new UserDialog(mainWindow).GetInputCookieAsync().ConfigureAwait(false);
|
||||
|
||||
// User confirms the input
|
||||
if (isOk)
|
||||
if (result.IsOk)
|
||||
{
|
||||
if (TryValidateCookie(userService.ParseCookie(cookie), out IDictionary<string, string>? filteredCookie))
|
||||
if (TryValidateCookie(User.ParseCookie(result.Value), out IDictionary<string, string>? filteredCookie))
|
||||
{
|
||||
string simplifiedCookie = string.Join(';', filteredCookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||
|
||||
if (await userService.CreateUserAsync(simplifiedCookie) is User user)
|
||||
if (await userService.CreateUserAsync(filteredCookie).ConfigureAwait(false) is User user)
|
||||
{
|
||||
switch (await userService.TryAddUserAsync(user, filteredCookie[AccountIdKey]))
|
||||
switch (await userService.TryAddUserAsync(user, filteredCookie[AccountIdKey]).ConfigureAwait(false))
|
||||
{
|
||||
case UserAddResult.Added:
|
||||
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 添加成功");
|
||||
@@ -158,10 +169,31 @@ internal class UserViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpgradeToStokenAsync()
|
||||
{
|
||||
// Get cookie from user input
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
(bool isOk, IDictionary<string, string> addition) = await new UserAutoCookieDialog(mainWindow).GetInputCookieAsync().ConfigureAwait(false);
|
||||
|
||||
// User confirms the input
|
||||
if (isOk)
|
||||
{
|
||||
(bool isUpgradeSucceed, string uid) = await userService.TryUpgradeUserAsync(addition).ConfigureAwait(false);
|
||||
if (isUpgradeSucceed)
|
||||
{
|
||||
infoBarService.Information($"用户 [{uid}] 的 Cookie 已成功添加 Stoken");
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning("请先添加对应用户的米游社Cookie");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveUserAsync(User? user)
|
||||
{
|
||||
Verify.Operation(user != null, "待删除的用户不应为 null");
|
||||
await userService.RemoveUserAsync(user);
|
||||
await userService.RemoveUserAsync(user).ConfigureAwait(false);
|
||||
infoBarService.Success($"用户 [{user.UserInfo?.Nickname}] 成功移除");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Snap.Hutao.Web.Enka;
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class EnkaClient
|
||||
{
|
||||
private const string EnkaAPI = "https://enka.shinshin.moe/u/{0}/__data.json";
|
||||
private const string EnkaAPIHutaoForward = "https://enka-api.hut.ao/{0}";
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
@@ -28,6 +29,17 @@ internal class EnkaClient
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取转发的 Enka API 响应
|
||||
/// </summary>
|
||||
/// <param name="playerUid">玩家Uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>Enka API 响应</returns>
|
||||
public Task<EnkaResponse?> GetForwardDataAsync(PlayerUid playerUid, CancellationToken token)
|
||||
{
|
||||
return httpClient.GetFromJsonAsync<EnkaResponse>(string.Format(EnkaAPIHutaoForward, playerUid.Value), token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取 Enka API 响应
|
||||
/// </summary>
|
||||
@@ -36,6 +48,6 @@ internal class EnkaClient
|
||||
/// <returns>Enka API 响应</returns>
|
||||
public Task<EnkaResponse?> GetDataAsync(PlayerUid playerUid, CancellationToken token)
|
||||
{
|
||||
return httpClient.GetFromJsonAsync<EnkaResponse>(string.Format(EnkaAPIHutaoForward, playerUid.Value), token);
|
||||
return httpClient.GetFromJsonAsync<EnkaResponse>(string.Format(EnkaAPI, playerUid.Value), token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Web.Enka.Model;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -89,16 +89,36 @@ internal static class ApiEndpoints
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UserGameRole
|
||||
#region Binding
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// </summary>
|
||||
public const string UserGameRoles = $"{ApiTaKumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_cn";
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// </summary>
|
||||
public const string GenAuthKey = $"{ApiTaKumiBindingApi}/genAuthKey";
|
||||
#endregion
|
||||
|
||||
#region Auth
|
||||
|
||||
/// <summary>
|
||||
/// 获取 stoken 与 ltoken
|
||||
/// </summary>
|
||||
/// <param name="loginTicket">登录票证</param>
|
||||
/// <param name="loginUid">uid</param>
|
||||
/// <returns>Url</returns>
|
||||
public static string AuthMultiToken(string loginTicket, string loginUid)
|
||||
{
|
||||
return $"{ApiTakumiAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
|
||||
}
|
||||
#endregion
|
||||
|
||||
// consts
|
||||
private const string ApiTakumi = "https://api-takumi.mihoyo.com";
|
||||
private const string ApiTakumiAuthApi = $"{ApiTakumi}/auth/api";
|
||||
private const string ApiTaKumiBindingApi = $"{ApiTakumi}/binding/api";
|
||||
private const string ApiTakumiRecord = "https://api-takumi-record.mihoyo.com";
|
||||
private const string ApiTakumiRecordApi = $"{ApiTakumiRecord}/game_record/app/genshin/api";
|
||||
@@ -111,4 +131,4 @@ internal static class ApiEndpoints
|
||||
private const string Hk4eApiGachaInfoApi = $"{Hk4eApi}/event/gacha_info/api";
|
||||
|
||||
private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000";
|
||||
}
|
||||
}
|
||||
21
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/CookieKeys.cs
Normal file
21
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/CookieKeys.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
/// <summary>
|
||||
/// Cookie的键
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1310")]
|
||||
[SuppressMessage("", "SA1600")]
|
||||
internal static class CookieKeys
|
||||
{
|
||||
public const string ACCOUNT_ID = "account_id";
|
||||
public const string COOKIE_TOKEN = "cookie_token";
|
||||
public const string LOGIN_TICKET = "login_ticket";
|
||||
public const string LOGIN_UID = "login_uid";
|
||||
public const string LTOKEN = "ltoken";
|
||||
public const string LTUID = "ltuid";
|
||||
public const string STOKEN = "stoken";
|
||||
public const string STUID = "stuid";
|
||||
}
|
||||
@@ -51,7 +51,7 @@ internal class DynamicSecretHttpClient<TValue> : IDynamicSecretHttpClient<TValue
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly string url;
|
||||
private readonly TValue? data = null;
|
||||
private readonly TValue data;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的使用动态密钥2的Http客户端默认实现的实例
|
||||
@@ -60,7 +60,7 @@ internal class DynamicSecretHttpClient<TValue> : IDynamicSecretHttpClient<TValue
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="url">url</param>
|
||||
/// <param name="data">请求的数据</param>
|
||||
public DynamicSecretHttpClient(HttpClient httpClient, JsonSerializerOptions options, string url, TValue? data)
|
||||
public DynamicSecretHttpClient(HttpClient httpClient, JsonSerializerOptions options, string url, TValue data)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
@@ -75,4 +75,11 @@ internal class DynamicSecretHttpClient<TValue> : IDynamicSecretHttpClient<TValue
|
||||
{
|
||||
return httpClient.PostAsJsonAsync(url, data, options, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<TResult?> TryCatchPostAsJsonAsync<TResult>(ILogger logger, CancellationToken token = default(CancellationToken))
|
||||
where TResult : class
|
||||
{
|
||||
return httpClient.TryCatchPostAsJsonAsync<TValue, TResult>(url, data, options, logger, token);
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,14 @@ internal interface IDynamicSecretHttpClient<TValue>
|
||||
/// <param name="token">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
Task<HttpResponseMessage> PostAsJsonAsync(CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a POST request to the specified Uri containing the value serialized as JSON in the request body.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">值的类型</typeparam>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="token">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <returns>结果</returns>
|
||||
Task<TResult?> TryCatchPostAsJsonAsync<TResult>(ILogger logger, CancellationToken token = default)
|
||||
where TResult : class;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Request.QueryString;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
@@ -15,6 +16,18 @@ public struct GachaLogConfigration
|
||||
/// </summary>
|
||||
public const int Size = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Below keys are required:
|
||||
/// authkey_ver
|
||||
/// auth_appid
|
||||
/// authkey
|
||||
/// sign_type
|
||||
/// Below keys used as control:
|
||||
/// lang
|
||||
/// gacha_type
|
||||
/// size
|
||||
/// end_id
|
||||
/// </summary>
|
||||
private readonly QueryString innerQuery;
|
||||
|
||||
/// <summary>
|
||||
@@ -46,6 +59,23 @@ public struct GachaLogConfigration
|
||||
set => innerQuery.Set("end_id", value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到查询字符串
|
||||
/// </summary>
|
||||
/// <param name="genAuthKeyData">生成信息</param>
|
||||
/// <param name="gameAuthKey">验证包装</param>
|
||||
/// <returns>查询</returns>
|
||||
public static string AsQuery(GenAuthKeyData genAuthKeyData, GameAuthKey gameAuthKey)
|
||||
{
|
||||
QueryString queryString = new();
|
||||
queryString.Set("auth_appid", genAuthKeyData.AuthAppId);
|
||||
queryString.Set("authkey", Uri.EscapeDataString(gameAuthKey.AuthKey));
|
||||
queryString.Set("authkey_ver", gameAuthKey.AuthKeyVersion);
|
||||
queryString.Set("sign_type", gameAuthKey.SignType);
|
||||
|
||||
return queryString.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到查询字符串
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using MiniExcelLibs.Attributes;
|
||||
using Snap.Hutao.Core.Json.Converter;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
@@ -32,15 +33,15 @@ public class GachaLogItem
|
||||
[ExcelColumn(Name = "item_id")]
|
||||
[Obsolete("API clear this property")]
|
||||
[JsonPropertyName("item_id")]
|
||||
public string ItemId { get; set; } = default!;
|
||||
public string ItemId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 个数 一般为 1
|
||||
/// </summary>
|
||||
[ExcelColumn(Name = "count")]
|
||||
[JsonPropertyName("count")]
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||
public int Count { get; set; }
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public int? Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间
|
||||
@@ -76,7 +77,7 @@ public class GachaLogItem
|
||||
/// </summary>
|
||||
[ExcelColumn(Name = "rank_type")]
|
||||
[JsonPropertyName("rank_type")]
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
[JsonConverter(typeof(EnumStringValueConverter<ItemQuality>))]
|
||||
public ItemQuality Rank { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -29,6 +29,22 @@ internal static class HttpClientExtensions
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="HttpClientJsonExtensions.PostAsJsonAsync{TValue}(HttpClient, string?, TValue, JsonSerializerOptions?, CancellationToken)"/>
|
||||
internal static async Task<TResult?> TryCatchPostAsJsonAsync<TValue, TResult>(this HttpClient httpClient, string requestUri, TValue value, JsonSerializerOptions options, ILogger logger, CancellationToken token = default)
|
||||
where TResult : class
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpResponseMessage message = await httpClient.PostAsJsonAsync(requestUri, value, options, token).ConfigureAwait(false);
|
||||
return await message.Content.ReadFromJsonAsync<TResult>(options, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
logger.LogWarning(EventIds.HttpException, ex, "请求异常已忽略");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置用户的Cookie
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 授权客户端
|
||||
/// </summary>
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class AuthClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<BindingClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的授权客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">Http客户端</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public AuthClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<BindingClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 MultiToken
|
||||
/// </summary>
|
||||
/// <param name="loginTicket">登录票证</param>
|
||||
/// <param name="loginUid">uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>包含token的字典</returns>
|
||||
public async Task<Dictionary<string, string>> GetMultiTokenByLoginTicketAsync(string loginTicket, string loginUid, CancellationToken token)
|
||||
{
|
||||
Response<ListWrapper<NameToken>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<ListWrapper<NameToken>>>(ApiEndpoints.AuthMultiToken(loginTicket, loginUid), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (resp?.Data != null)
|
||||
{
|
||||
return resp.Data.List.ToDictionary(n => n.Name, n => n.Token);
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 名称与令牌
|
||||
/// </summary>
|
||||
public sealed class NameToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Token名称
|
||||
/// stoken
|
||||
/// ltoken
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 令牌
|
||||
/// </summary>
|
||||
[JsonPropertyName("token")]
|
||||
public string Token { get; set; } = default!;
|
||||
}
|
||||
@@ -10,23 +10,22 @@ using System.Net.Http;
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色提供器
|
||||
/// 绑定客户端
|
||||
/// </summary>
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class UserGameRoleClient
|
||||
internal class BindingClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<UserGameRoleClient> logger;
|
||||
private readonly ILogger<BindingClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户游戏角色提供器
|
||||
/// </summary>
|
||||
/// <param name="userService">用户服务</param>
|
||||
/// <param name="httpClient">请求器</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public UserGameRoleClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<UserGameRoleClient> logger)
|
||||
public BindingClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<BindingClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
@@ -48,4 +47,4 @@ internal class UserGameRoleClient
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data?.List);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// Stoken绑定客户端
|
||||
/// </summary>
|
||||
[HttpClient(HttpClientConfigration.XRpc)]
|
||||
internal class BindingClient2
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<BindingClient2> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户游戏角色提供器
|
||||
/// </summary>
|
||||
/// <param name="httpClient">请求器</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public BindingClient2(HttpClient httpClient, JsonSerializerOptions options, ILogger<BindingClient2> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步生成祈愿验证密钥
|
||||
/// 需要stoken
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="data">提交数据</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户角色信息</returns>
|
||||
public async Task<GameAuthKey?> GenerateAuthenticationKeyAsync(User user, GenAuthKeyData data, CancellationToken token = default)
|
||||
{
|
||||
Response<GameAuthKey>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.SetReferer("https://app.mihoyo.com")
|
||||
.UsingDynamicSecret()
|
||||
.TryCatchPostAsJsonAsync<GenAuthKeyData, Response<GameAuthKey>>(ApiEndpoints.GenAuthKey, data, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// 验证密钥
|
||||
/// </summary>
|
||||
public class GameAuthKey
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证密钥
|
||||
/// </summary>
|
||||
[JsonPropertyName("authkey")]
|
||||
public string AuthKey { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 验证密钥版本
|
||||
/// </summary>
|
||||
[JsonPropertyName("authkey_ver")]
|
||||
public int AuthKeyVersion { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 签名类型
|
||||
/// </summary>
|
||||
[JsonPropertyName("sign_type")]
|
||||
public int SignType { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// 验证密钥提交数据
|
||||
/// </summary>
|
||||
public sealed class GenAuthKeyData
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的验证密钥提交数据
|
||||
/// </summary>
|
||||
/// <param name="authAppId">AppId</param>
|
||||
/// <param name="gameBiz">游戏代号</param>
|
||||
/// <param name="uid">uid</param>
|
||||
public GenAuthKeyData(string authAppId, string gameBiz, PlayerUid uid)
|
||||
{
|
||||
AuthAppId = authAppId;
|
||||
GameBiz = gameBiz;
|
||||
GameUid = int.Parse(uid.Value);
|
||||
Region = uid.Region;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// App Id
|
||||
/// </summary>
|
||||
[JsonPropertyName("auth_appid")]
|
||||
|
||||
public string AuthAppId { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏代号
|
||||
/// </summary>
|
||||
[JsonPropertyName("game_biz")]
|
||||
public string GameBiz { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Uid
|
||||
/// </summary>
|
||||
[JsonPropertyName("game_uid")]
|
||||
public int GameUid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域
|
||||
/// </summary>
|
||||
[JsonPropertyName("region")]
|
||||
public string Region { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 创建为祈愿记录验证密钥提交数据
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>验证密钥提交数据</returns>
|
||||
public static GenAuthKeyData CreateForWebViewGacha(PlayerUid uid)
|
||||
{
|
||||
return new("webview_gacha", "hk4e_cn", uid);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
|
||||
@@ -19,17 +18,19 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
internal class GameRecordClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions jsonSerializerOptions;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<GameRecordClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏记录提供器
|
||||
/// </summary>
|
||||
/// <param name="httpClient">请求器</param>
|
||||
/// <param name="jsonSerializerOptions">json序列化选项</param>
|
||||
public GameRecordClient(HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions)
|
||||
/// <param name="options">json序列化选项</param>
|
||||
public GameRecordClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<GameRecordClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.jsonSerializerOptions = jsonSerializerOptions;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,7 +56,7 @@ internal class GameRecordClient
|
||||
{
|
||||
Response<PlayerInfo>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.UsingDynamicSecret(jsonSerializerOptions, ApiEndpoints.GameRecordIndex(uid.Value, uid.Region))
|
||||
.UsingDynamicSecret(options, ApiEndpoints.GameRecordIndex(uid.Value, uid.Region))
|
||||
.GetFromJsonAsync<Response<PlayerInfo>>(token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -87,7 +88,7 @@ internal class GameRecordClient
|
||||
{
|
||||
Response<SpiralAbyss.SpiralAbyss>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.UsingDynamicSecret(jsonSerializerOptions, ApiEndpoints.GameRecordSpiralAbyss(schedule, uid))
|
||||
.UsingDynamicSecret(options, ApiEndpoints.GameRecordSpiralAbyss(schedule, uid))
|
||||
.GetFromJsonAsync<Response<SpiralAbyss.SpiralAbyss>>(token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -119,14 +120,10 @@ internal class GameRecordClient
|
||||
{
|
||||
CharacterData data = new(uid, playerInfo.Avatars.Select(x => x.Id));
|
||||
|
||||
HttpResponseMessage? response = await httpClient
|
||||
Response<CharacterWrapper>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.UsingDynamicSecret(jsonSerializerOptions, ApiEndpoints.GameRecordCharacter, data)
|
||||
.PostAsJsonAsync(token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Response<CharacterWrapper>? resp = await response.Content
|
||||
.ReadFromJsonAsync<Response<CharacterWrapper>>(jsonSerializerOptions, token)
|
||||
.UsingDynamicSecret(options, ApiEndpoints.GameRecordCharacter, data)
|
||||
.TryCatchPostAsJsonAsync<Response<CharacterWrapper>>(logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data?.Avatars);
|
||||
|
||||
@@ -43,6 +43,11 @@ public enum KnownReturnCode : int
|
||||
/// </summary>
|
||||
VIsitTooFrequently = -110,
|
||||
|
||||
/// <summary>
|
||||
/// 应用Id错误
|
||||
/// </summary>
|
||||
AppIdError = -109,
|
||||
|
||||
/// <summary>
|
||||
/// 验证密钥过期
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user