mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
gacha part 2
This commit is contained in:
@@ -21,6 +21,8 @@
|
||||
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
|
||||
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
|
||||
|
||||
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
|
||||
|
||||
<CornerRadius x:Key="CompatCornerRadius">6</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusTop">6,6,0,0</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusRight">0,6,6,0</CornerRadius>
|
||||
|
||||
@@ -5,7 +5,6 @@ using CommunityToolkit.WinUI.UI.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.Exception;
|
||||
using Snap.Hutao.Extension;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Storage;
|
||||
@@ -34,21 +33,21 @@ public class CachedImage : ImageEx
|
||||
try
|
||||
{
|
||||
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
|
||||
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri);
|
||||
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
|
||||
|
||||
// check token state to determine whether the operation should be canceled.
|
||||
Must.ThrowOnCanceled(token, "Image source has changed.");
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
return new BitmapImage(new(file.Path));
|
||||
}
|
||||
catch (COMException ex) when (ex.Is(COMError.WINCODEC_ERR_COMPONENTNOTFOUND))
|
||||
catch (COMException)
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
|
||||
return null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// task was explicitly canceled
|
||||
return null;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
@@ -39,6 +40,21 @@ public class ScopedPage : Page
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步通知接收器
|
||||
/// </summary>
|
||||
/// <param name="extra">额外内容</param>
|
||||
/// <returns>任务</returns>
|
||||
public async Task NotifyRecipentAsync(INavigationData extra)
|
||||
{
|
||||
if (extra.Data != null && DataContext is INavigationRecipient recipient)
|
||||
{
|
||||
await recipient.ReceiveAsync(extra).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
extra.NotifyNavigationCompleted();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
@@ -48,4 +64,16 @@ public class ScopedPage : Page
|
||||
// Try dispose scope when page is not presented
|
||||
serviceScope.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
protected override async void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
if (e.Parameter is INavigationData extra)
|
||||
{
|
||||
await NotifyRecipentAsync(extra).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 有名称的对象
|
||||
/// 指示该对象可通过名称区分
|
||||
/// </summary>
|
||||
internal interface INamed
|
||||
{
|
||||
|
||||
@@ -18,8 +18,7 @@ internal abstract class Md5Convert
|
||||
/// <returns>计算的结果</returns>
|
||||
public static string ToHexString(string source)
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(source);
|
||||
byte[] hash = MD5.HashData(bytes);
|
||||
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
|
||||
return System.Convert.ToHexString(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,21 @@ namespace Snap.Hutao.Core;
|
||||
internal static class CoreEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// 动态密钥的盐
|
||||
/// 动态密钥1的盐
|
||||
/// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
|
||||
/// </summary>
|
||||
public const string DynamicSecretSalt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
|
||||
public const string DynamicSecret1Salt = "n0KjuIrKgLHh08LWSCYP0WXlVXaYvV64";
|
||||
|
||||
/// <summary>
|
||||
/// 动态密钥2的盐
|
||||
/// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
|
||||
/// </summary>
|
||||
public const string DynamicSecret2Salt = "YVEIkzDFNHLeKXLxzqCA9TzxCpWwbIbk";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabUA = $"miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
|
||||
28
src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs
Normal file
28
src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.DataTransfer;
|
||||
|
||||
/// <summary>
|
||||
/// 剪贴板
|
||||
/// </summary>
|
||||
internal static class Clipboard
|
||||
{
|
||||
/// <summary>
|
||||
/// 从剪贴板文本中反序列化
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标类型</typeparam>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <returns>实例</returns>
|
||||
public static async Task<T?> DeserializeTextAsync<T>(JsonSerializerOptions options)
|
||||
where T : class
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
|
||||
string json = await view.GetTextAsync();
|
||||
return JsonSerializer.Deserialize<T>(json, options);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ internal static class EventIds
|
||||
/// Xaml绑定错误
|
||||
/// </summary>
|
||||
public static readonly EventId UnobservedTaskException = 100006;
|
||||
|
||||
/// <summary>
|
||||
/// Xaml绑定错误
|
||||
/// </summary>
|
||||
public static readonly EventId HttpException = 100007;
|
||||
#endregion
|
||||
|
||||
#region 服务
|
||||
|
||||
@@ -103,19 +103,4 @@ public static class Must
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试抛出任务取消异常
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <param name="message">取消消息</param>
|
||||
/// <exception cref="TaskCanceledException">任务被取消</exception>
|
||||
[SuppressMessage("", "CA1068")]
|
||||
public static void ThrowOnCanceled(CancellationToken token, string message)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
throw new TaskCanceledException("Image source has changed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,11 @@ public class SystemBackdrop
|
||||
configuration.IsInputActive = true;
|
||||
SetConfigurationSourceTheme(configuration);
|
||||
|
||||
backdropController = new();
|
||||
|
||||
// Mica Alt
|
||||
backdropController.Kind = MicaKind.BaseAlt;
|
||||
backdropController = new()
|
||||
{
|
||||
// Mica Alt
|
||||
Kind = MicaKind.BaseAlt
|
||||
};
|
||||
backdropController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>());
|
||||
backdropController.SetSystemBackdropConfiguration(configuration);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
/// <summary>
|
||||
@@ -8,6 +10,26 @@ namespace Snap.Hutao.Extension;
|
||||
/// </summary>
|
||||
public static partial class EnumerableExtensions
|
||||
{
|
||||
/// <inheritdoc cref="Enumerable.Average(IEnumerable{int})"/>
|
||||
public static double AverageNoThrow(this List<int> source)
|
||||
{
|
||||
Span<int> span = CollectionsMarshal.AsSpan(source);
|
||||
|
||||
if (span.IsEmpty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
long sum = 0;
|
||||
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
sum += span[i];
|
||||
}
|
||||
|
||||
return (double)sum / span.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计数
|
||||
/// </summary>
|
||||
@@ -76,6 +98,26 @@ public static partial class EnumerableExtensions
|
||||
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取值或默认值
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <typeparam name="TValue">值类型</typeparam>
|
||||
/// <param name="dictionary">字典</param>
|
||||
/// <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!)
|
||||
where TKey : notnull
|
||||
{
|
||||
if (dictionary.TryGetValue(key, out TValue? value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除表中首个满足条件的项
|
||||
/// </summary>
|
||||
@@ -97,6 +139,20 @@ public static partial class EnumerableExtensions
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
|
||||
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
where TKey : notnull
|
||||
{
|
||||
Dictionary<TKey, TSource> dictionary = new();
|
||||
|
||||
foreach (TSource value in source)
|
||||
{
|
||||
dictionary[keySelector(value)] = value;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个对 <see cref="TItem"/> 类型的计数器
|
||||
/// </summary>
|
||||
|
||||
34
src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs
Normal file
34
src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
/// <summary>
|
||||
/// 数高性能扩展
|
||||
/// </summary>
|
||||
public static class NumberExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 计算给定整数的位数
|
||||
/// </summary>
|
||||
/// <param name="x">给定的整数</param>
|
||||
/// <returns>位数</returns>
|
||||
public static int Place(this int x)
|
||||
{
|
||||
// Benchmarked and compared as a most optimized solution
|
||||
return (int)(MathF.Log10(x) + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算给定整数的位数
|
||||
/// </summary>
|
||||
/// <param name="x">给定的整数</param>
|
||||
/// <returns>位数</returns>
|
||||
public static int Place(this long x)
|
||||
{
|
||||
// Benchmarked and compared as a most optimized solution
|
||||
return (int)(MathF.Log10(x) + 1);
|
||||
}
|
||||
}
|
||||
171
src/Snap.Hutao/Snap.Hutao/Migrations/20220914131149_AddGachaQueryType.Designer.cs
generated
Normal file
171
src/Snap.Hutao/Snap.Hutao/Migrations/20220914131149_AddGachaQueryType.Designer.cs
generated
Normal file
@@ -0,0 +1,171 @@
|
||||
// <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("20220914131149_AddGachaQueryType")]
|
||||
partial class AddGachaQueryType
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.8");
|
||||
|
||||
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.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("GachaArchives");
|
||||
});
|
||||
|
||||
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("GachaItems");
|
||||
});
|
||||
|
||||
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,27 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
public partial class AddGachaQueryType : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "QueryType",
|
||||
table: "GachaItems",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "QueryType",
|
||||
table: "GachaItems");
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/Snap.Hutao/Snap.Hutao/Migrations/20220918062300_RenameGachaTable.Designer.cs
generated
Normal file
171
src/Snap.Hutao/Snap.Hutao/Migrations/20220918062300_RenameGachaTable.Designer.cs
generated
Normal file
@@ -0,0 +1,171 @@
|
||||
// <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("20220918062300_RenameGachaTable")]
|
||||
partial class RenameGachaTable
|
||||
{
|
||||
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.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,102 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
public partial class RenameGachaTable : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GachaItems_GachaArchives_ArchiveId",
|
||||
table: "GachaItems");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_GachaItems",
|
||||
table: "GachaItems");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_GachaArchives",
|
||||
table: "GachaArchives");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "GachaItems",
|
||||
newName: "gacha_items");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "GachaArchives",
|
||||
newName: "gacha_archives");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_GachaItems_ArchiveId",
|
||||
table: "gacha_items",
|
||||
newName: "IX_gacha_items_ArchiveId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_gacha_items",
|
||||
table: "gacha_items",
|
||||
column: "InnerId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_gacha_archives",
|
||||
table: "gacha_archives",
|
||||
column: "InnerId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_gacha_items_gacha_archives_ArchiveId",
|
||||
table: "gacha_items",
|
||||
column: "ArchiveId",
|
||||
principalTable: "gacha_archives",
|
||||
principalColumn: "InnerId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_gacha_items_gacha_archives_ArchiveId",
|
||||
table: "gacha_items");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_gacha_items",
|
||||
table: "gacha_items");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_gacha_archives",
|
||||
table: "gacha_archives");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "gacha_items",
|
||||
newName: "GachaItems");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "gacha_archives",
|
||||
newName: "GachaArchives");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_gacha_items_ArchiveId",
|
||||
table: "GachaItems",
|
||||
newName: "IX_GachaItems_ArchiveId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_GachaItems",
|
||||
table: "GachaItems",
|
||||
column: "InnerId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_GachaArchives",
|
||||
table: "GachaArchives",
|
||||
column: "InnerId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GachaItems_GachaArchives_ArchiveId",
|
||||
table: "GachaItems",
|
||||
column: "ArchiveId",
|
||||
principalTable: "GachaArchives",
|
||||
principalColumn: "InnerId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.8");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
@@ -78,7 +78,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("GachaArchives");
|
||||
b.ToTable("gacha_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
@@ -99,6 +99,9 @@ namespace Snap.Hutao.Migrations
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QueryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -106,7 +109,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("GachaItems");
|
||||
b.ToTable("gacha_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Annotation;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举的文本描述特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
|
||||
internal class DescriptionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的枚举的文本描述特性
|
||||
/// </summary>
|
||||
/// <param name="description">描述</param>
|
||||
public DescriptionAttribute(string description)
|
||||
{
|
||||
Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文本描述
|
||||
/// </summary>
|
||||
public string Description { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 物品基类
|
||||
/// </summary>
|
||||
public class ItemBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 物品名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 主图标
|
||||
/// </summary>
|
||||
public Uri Icon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 小图标
|
||||
/// </summary>
|
||||
public Uri Badge { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 星级
|
||||
/// </summary>
|
||||
public ItemQuality Quality { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿基类
|
||||
/// </summary>
|
||||
public abstract class WishBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 卡池名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 统计开始时间
|
||||
/// </summary>
|
||||
public DateTimeOffset From { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 统计结束时间
|
||||
/// </summary>
|
||||
public DateTimeOffset To { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 统计开始时间
|
||||
/// </summary>
|
||||
public string FromFormatted
|
||||
{
|
||||
get => $"{From:yyyy.MM.dd}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统计开始时间
|
||||
/// </summary>
|
||||
public string ToFormatted
|
||||
{
|
||||
get => $"{To:yyyy.MM.dd}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 总数
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Gacha;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿统计
|
||||
/// </summary>
|
||||
public class GachaStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认的空祈愿统计
|
||||
/// </summary>
|
||||
public static readonly GachaStatistics Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// 角色活动
|
||||
/// </summary>
|
||||
public TypedWishSummary AvatarWish { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 神铸赋形
|
||||
/// </summary>
|
||||
public TypedWishSummary WeaponWish { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 奔行世间
|
||||
/// </summary>
|
||||
public TypedWishSummary PermanentWish { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 历史
|
||||
/// </summary>
|
||||
public List<HistoryWish> HistoryWishes { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 五星角色
|
||||
/// </summary>
|
||||
public List<StatisticsItem> OrangeAvatars { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 四星角色
|
||||
/// </summary>
|
||||
public List<StatisticsItem> PurpleAvatars { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 五星武器
|
||||
/// </summary>
|
||||
public List<StatisticsItem> OrangeWeapons { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 四星武器
|
||||
/// </summary>
|
||||
public List<StatisticsItem> PurpleWeapons { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 三星武器
|
||||
/// </summary>
|
||||
public List<StatisticsItem> BlueWeapons { get; set; } = default!;
|
||||
}
|
||||
38
src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/HistoryWish.cs
Normal file
38
src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/HistoryWish.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Gacha;
|
||||
|
||||
/// <summary>
|
||||
/// 历史卡池概览
|
||||
/// </summary>
|
||||
public class HistoryWish : WishBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 五星Up
|
||||
/// </summary>
|
||||
public List<StatisticsItem> OrangeUpList { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 四星Up
|
||||
/// </summary>
|
||||
public List<StatisticsItem> PurpleUpList { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 五星Up
|
||||
/// </summary>
|
||||
public List<StatisticsItem> OrangeList { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 四星Up
|
||||
/// </summary>
|
||||
public List<StatisticsItem> PurpleList { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 三星Up
|
||||
/// </summary>
|
||||
public List<StatisticsItem> BlueList { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Gacha;
|
||||
|
||||
/// <summary>
|
||||
/// 历史物品
|
||||
/// </summary>
|
||||
public class StatisticsItem : ItemBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取物品的个数
|
||||
/// </summary>
|
||||
public int Count { get; set; }
|
||||
}
|
||||
45
src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/SummaryItem.cs
Normal file
45
src/Snap.Hutao/Snap.Hutao/Model/Binding/Gacha/SummaryItem.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Gacha;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿卡池列表物品
|
||||
/// </summary>
|
||||
public class SummaryItem : ItemBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 据上次
|
||||
/// </summary>
|
||||
public int LastPull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为Up物品
|
||||
/// </summary>
|
||||
public bool IsUp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为大保底
|
||||
/// </summary>
|
||||
public bool IsGuarentee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取时间
|
||||
/// </summary>
|
||||
public DateTimeOffset Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取时间
|
||||
/// </summary>
|
||||
public string TimeFormatted
|
||||
{
|
||||
get => $"{Time:yyy.MM.dd HH:mm:ss}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 颜色
|
||||
/// </summary>
|
||||
public Windows.UI.Color Color { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Gacha;
|
||||
|
||||
/// <summary>
|
||||
/// 类型化的祈愿概览
|
||||
/// </summary>
|
||||
public class TypedWishSummary : WishBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 最大五星抽数
|
||||
/// </summary>
|
||||
public int MaxOrangePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大五星抽数
|
||||
/// </summary>
|
||||
public string MaxOrangePullFormatted
|
||||
{
|
||||
get => $"最非 {MaxOrangePull} 抽";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最小五星抽数
|
||||
/// </summary>
|
||||
public int MinOrangePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大五星抽数
|
||||
/// </summary>
|
||||
public string MinOrangePullFormatted
|
||||
{
|
||||
get => $"最欧 {MinOrangePull} 抽";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 据上个五星抽数
|
||||
/// </summary>
|
||||
public int LastOrangePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 五星保底阈值
|
||||
/// </summary>
|
||||
public int GuarenteeOrangeThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 据上个四星抽数
|
||||
/// </summary>
|
||||
public int LastPurplePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 四星保底阈值
|
||||
/// </summary>
|
||||
public int GuarenteePurpleThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 五星总数
|
||||
/// </summary>
|
||||
public int TotalOrangePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 五星总百分比
|
||||
/// </summary>
|
||||
public double TotalOrangePercent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 五星格式化字符串
|
||||
/// </summary>
|
||||
public string TotalOrangeFormatted
|
||||
{
|
||||
get => $"{TotalOrangePull} [{TotalOrangePercent,6:p2}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 四星总数
|
||||
/// </summary>
|
||||
public int TotalPurplePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 四星总百分比
|
||||
/// </summary>
|
||||
public double TotalPurplePercent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 四星格式化字符串
|
||||
/// </summary>
|
||||
public string TotalPurpleFormatted
|
||||
{
|
||||
get => $"{TotalPurplePull} [{TotalPurplePercent,6:p2}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 三星总数
|
||||
/// </summary>
|
||||
public int TotalBluePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 三星总百分比
|
||||
/// </summary>
|
||||
public double TotalBluePercent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 三星格式化字符串
|
||||
/// </summary>
|
||||
public string TotalBlueFormatted
|
||||
{
|
||||
get => $"{TotalBluePull} [{TotalBluePercent,6:p2}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平均五星抽数
|
||||
/// </summary>
|
||||
public double AverageOrangePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 平均五星抽数
|
||||
/// </summary>
|
||||
public string AverageOrangePullFormatted
|
||||
{
|
||||
get => $"{AverageOrangePull:f2} 抽";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平均Up五星抽数
|
||||
/// </summary>
|
||||
public double AverageUpOrangePull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 平均Up五星抽数
|
||||
/// </summary>
|
||||
public string AverageUpOrangePullFormatted
|
||||
{
|
||||
get => $"{AverageUpOrangePull:f2} 抽";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 五星列表
|
||||
/// </summary>
|
||||
public List<SummaryItem> OrangeList { get; set; } = default!;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿统计
|
||||
/// </summary>
|
||||
public class GachaStatistics
|
||||
{
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// <summary>
|
||||
/// 祈愿记录存档
|
||||
/// </summary>
|
||||
[Table("gacha_archives")]
|
||||
public class GachaArchive : ISelectable
|
||||
{
|
||||
/// <summary>
|
||||
@@ -26,4 +27,14 @@ public class GachaArchive : ISelectable
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的卡池存档
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>新的卡池存档</returns>
|
||||
public static GachaArchive Create(string uid)
|
||||
{
|
||||
return new() { Uid = uid };
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// <summary>
|
||||
/// 抽卡记录物品
|
||||
/// </summary>
|
||||
[Table("gacha_items")]
|
||||
public class GachaItem
|
||||
{
|
||||
/// <summary>
|
||||
@@ -34,6 +35,13 @@ public class GachaItem
|
||||
/// </summary>
|
||||
public GachaConfigType GachaType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿记录查询分类
|
||||
/// 合并保底的卡池使用此属性
|
||||
/// 仅4种(不含400)
|
||||
/// </summary>
|
||||
public GachaConfigType QueryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 物品Id
|
||||
/// </summary>
|
||||
@@ -48,4 +56,38 @@ public class GachaItem
|
||||
/// 物品
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的数据库祈愿物品
|
||||
/// </summary>
|
||||
/// <param name="archiveId">存档Id</param>
|
||||
/// <param name="item">祈愿物品</param>
|
||||
/// <param name="itemId">物品Id</param>
|
||||
/// <returns>新的祈愿物品</returns>
|
||||
public static GachaItem Create(Guid archiveId, GachaLogItem item, int itemId)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ArchiveId = archiveId,
|
||||
GachaType = item.GachaType,
|
||||
QueryType = ToQueryType(item.GachaType),
|
||||
ItemId = itemId,
|
||||
Time = item.Time,
|
||||
Id = item.Id,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将祈愿配置类型转换到祈愿查询类型
|
||||
/// </summary>
|
||||
/// <param name="configType">配置类型</param>
|
||||
/// <returns>祈愿查询类型</returns>
|
||||
public static GachaConfigType ToQueryType(GachaConfigType configType)
|
||||
{
|
||||
return configType switch
|
||||
{
|
||||
GachaConfigType.AvatarEventWish2 => GachaConfigType.AvatarEventWish,
|
||||
_ => configType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace Snap.Hutao.Model.Intrinsic;
|
||||
/// 武器类型
|
||||
/// https://github.com/Grasscutters/Grasscutter/blob/development/src/main/java/emu/grasscutter/game/props/WeaponType.java
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1124")]
|
||||
public enum WeaponType
|
||||
{
|
||||
/// <summary>
|
||||
@@ -19,6 +20,7 @@ public enum WeaponType
|
||||
/// </summary>
|
||||
WEAPON_SWORD_ONE_HAND = 1,
|
||||
|
||||
#region Not Used
|
||||
/// <summary>
|
||||
/// ?
|
||||
/// </summary>
|
||||
@@ -66,6 +68,7 @@ public enum WeaponType
|
||||
/// </summary>
|
||||
[Obsolete("尚未发现使用")]
|
||||
WEAPON_SHIELD_SMALL = 9,
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 法器
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 指示该类为统计物品的源
|
||||
/// </summary>
|
||||
public interface IStatisticsItemSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 转换到统计物品
|
||||
/// </summary>
|
||||
/// <param name="count">个数</param>
|
||||
/// <returns>统计物品</returns>
|
||||
StatisticsItem ToStatisticsItem(int count);
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 角色
|
||||
/// </summary>
|
||||
public class Avatar
|
||||
public class Avatar : IStatisticsItemSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
@@ -71,7 +75,7 @@ public class Avatar
|
||||
public SkillDepot SkillDepot { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 好感信息
|
||||
/// 好感信息/基本信息
|
||||
/// </summary>
|
||||
public FetterInfo FetterInfo { get; set; } = default!;
|
||||
|
||||
@@ -79,4 +83,57 @@ public class Avatar
|
||||
/// 皮肤
|
||||
/// </summary>
|
||||
public IEnumerable<Costume> Costumes { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 转换为基础物品
|
||||
/// </summary>
|
||||
/// <returns>基础物品</returns>
|
||||
public ItemBase ToItemBase()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = Name,
|
||||
Icon = AvatarIconConverter.NameToUri(Icon),
|
||||
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
|
||||
Quality = Quality,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到统计物品
|
||||
/// </summary>
|
||||
/// <param name="count">个数</param>
|
||||
/// <returns>统计物品</returns>
|
||||
public StatisticsItem ToStatisticsItem(int count)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = Name,
|
||||
Icon = AvatarIconConverter.NameToUri(Icon),
|
||||
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
|
||||
Quality = Quality,
|
||||
Count = count,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到简述统计物品
|
||||
/// </summary>
|
||||
/// <param name="lastPull">距上个五星</param>
|
||||
/// <param name="time">时间</param>
|
||||
/// <param name="isUp">是否为Up物品</param>
|
||||
/// <returns>简述统计物品</returns>
|
||||
public SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = Name,
|
||||
Icon = AvatarIconConverter.NameToUri(Icon),
|
||||
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
|
||||
Quality = Quality,
|
||||
Time = time,
|
||||
LastPull = lastPull,
|
||||
IsUp = isUp,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,20 @@ internal class AvatarIconConverter : IValueConverter
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/AvatarIcon/{0}.png";
|
||||
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>链接</returns>
|
||||
public static Uri NameToUri(string name)
|
||||
{
|
||||
return new Uri(string.Format(BaseUrl, name));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return new Uri(string.Format(BaseUrl, value));
|
||||
return NameToUri((string)value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -12,10 +12,14 @@ internal class ElementNameIconConverter : IValueConverter
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/IconElement/UI_Icon_Element_{0}.png";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
/// <summary>
|
||||
/// 将中文元素名称转换为图标链接
|
||||
/// </summary>
|
||||
/// <param name="elementName">元素名称</param>
|
||||
/// <returns>图标链接</returns>
|
||||
public static Uri ElementNameToIconUri(string elementName)
|
||||
{
|
||||
string element = (string)value switch
|
||||
string element = elementName switch
|
||||
{
|
||||
"雷" => "Electric",
|
||||
"火" => "Fire",
|
||||
@@ -30,6 +34,12 @@ internal class ElementNameIconConverter : IValueConverter
|
||||
return new Uri(string.Format(BaseUrl, element));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return ElementNameToIconUri((string)value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 武器图片转换器
|
||||
/// </summary>
|
||||
internal class EquipIconConverter : IValueConverter
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/EquipIcon/{0}.png";
|
||||
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>链接</returns>
|
||||
public static Uri NameToUri(string name)
|
||||
{
|
||||
return new Uri(string.Format(BaseUrl, name));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return NameToUri((string)value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,14 @@ internal class WeaponTypeIconConverter : IValueConverter
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/Skill/Skill_A_{0}.png";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
/// <summary>
|
||||
/// 将武器类型转换为图标链接
|
||||
/// </summary>
|
||||
/// <param name="type">武器类型</param>
|
||||
/// <returns>图标链接</returns>
|
||||
public static Uri WeaponTypeToIconUri(WeaponType type)
|
||||
{
|
||||
string element = (WeaponType)value switch
|
||||
string element = type switch
|
||||
{
|
||||
WeaponType.WEAPON_SWORD_ONE_HAND => "01",
|
||||
WeaponType.WEAPON_BOW => "02",
|
||||
@@ -29,6 +33,12 @@ internal class WeaponTypeIconConverter : IValueConverter
|
||||
return new Uri(string.Format(BaseUrl, element));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return WeaponTypeToIconUri((WeaponType)value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
|
||||
42
src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs
Normal file
42
src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿卡池配置
|
||||
/// </summary>
|
||||
public class GachaEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 卡池名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
public DateTimeOffset From { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
public DateTimeOffset To { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 卡池类型
|
||||
/// </summary>
|
||||
public GachaConfigType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 五星列表
|
||||
/// </summary>
|
||||
public List<string> UpOrangeList { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 四星列表
|
||||
/// </summary>
|
||||
public List<string> UpPurpleList { get; set; } = default!;
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Weapon;
|
||||
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public class Weapon
|
||||
public class Weapon : IStatisticsItemSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
@@ -49,4 +53,57 @@ public class Weapon
|
||||
/// 属性
|
||||
/// </summary>
|
||||
public PropertyInfo Property { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 转换为基础物品
|
||||
/// </summary>
|
||||
/// <returns>基础物品</returns>
|
||||
public ItemBase ToItemBase()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = Name,
|
||||
Icon = EquipIconConverter.NameToUri(Icon),
|
||||
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
|
||||
Quality = RankLevel,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到统计物品
|
||||
/// </summary>
|
||||
/// <param name="count">个数</param>
|
||||
/// <returns>统计物品</returns>
|
||||
public StatisticsItem ToStatisticsItem(int count)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = Name,
|
||||
Icon = EquipIconConverter.NameToUri(Icon),
|
||||
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
|
||||
Quality = RankLevel,
|
||||
Count = count,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到简述统计物品
|
||||
/// </summary>
|
||||
/// <param name="lastPull">距上个五星</param>
|
||||
/// <param name="time">时间</param>
|
||||
/// <param name="isUp">是否为Up物品</param>
|
||||
/// <returns>简述统计物品</returns>
|
||||
public SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = Name,
|
||||
Icon = EquipIconConverter.NameToUri(Icon),
|
||||
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
|
||||
Time = time,
|
||||
Quality = RankLevel,
|
||||
LastPull = lastPull,
|
||||
IsUp = isUp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.0.33.0" />
|
||||
Version="1.0.34.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"profiles": {
|
||||
"Snap.Hutao (Package)": {
|
||||
"commandName": "MsixPackage",
|
||||
"nativeDebugging": true
|
||||
"nativeDebugging": false
|
||||
},
|
||||
"Snap.Hutao (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
|
||||
@@ -122,25 +122,25 @@ internal class AchievementService : IAchievementService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImportResult ImportFromUIAF(EntityArchive archive, List<UIAFItem> list, ImportOption option)
|
||||
public ImportResult ImportFromUIAF(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy)
|
||||
{
|
||||
Guid archiveId = archive.InnerId;
|
||||
|
||||
switch (option)
|
||||
switch (strategy)
|
||||
{
|
||||
case ImportOption.AggressiveMerge:
|
||||
case ImportStrategy.AggressiveMerge:
|
||||
{
|
||||
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
|
||||
return achievementDbOperation.Merge(archiveId, orederedUIAF, true);
|
||||
}
|
||||
|
||||
case ImportOption.LazyMerge:
|
||||
case ImportStrategy.LazyMerge:
|
||||
{
|
||||
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
|
||||
return achievementDbOperation.Merge(archiveId, orederedUIAF, false);
|
||||
}
|
||||
|
||||
case ImportOption.Overwrite:
|
||||
case ImportStrategy.Overwrite:
|
||||
{
|
||||
IEnumerable<EntityAchievement> orederedUIAF = list
|
||||
.Select(uiaf => EntityAchievement.Create(archiveId, uiaf))
|
||||
@@ -154,9 +154,9 @@ internal class AchievementService : IAchievementService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportOption option)
|
||||
public Task<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy)
|
||||
{
|
||||
return Task.Run(() => ImportFromUIAF(archive, list, option));
|
||||
return Task.Run(() => ImportFromUIAF(archive, list, strategy));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -39,18 +39,18 @@ internal interface IAchievementService
|
||||
/// </summary>
|
||||
/// <param name="archive">用户</param>
|
||||
/// <param name="list">UIAF数据</param>
|
||||
/// <param name="option">选项</param>
|
||||
/// <param name="strategy">选项</param>
|
||||
/// <returns>导入结果</returns>
|
||||
ImportResult ImportFromUIAF(EntityArchive archive, List<UIAFItem> list, ImportOption option);
|
||||
ImportResult ImportFromUIAF(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy);
|
||||
|
||||
/// <summary>
|
||||
/// 异步导入UIAF数据
|
||||
/// </summary>
|
||||
/// <param name="archive">用户</param>
|
||||
/// <param name="list">UIAF数据</param>
|
||||
/// <param name="option">选项</param>
|
||||
/// <param name="strategy">选项</param>
|
||||
/// <returns>导入结果</returns>
|
||||
Task<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportOption option);
|
||||
Task<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy);
|
||||
|
||||
/// <summary>
|
||||
/// 异步移除存档
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
/// <summary>
|
||||
/// 导入选项
|
||||
/// 导入策略
|
||||
/// </summary>
|
||||
public enum ImportOption
|
||||
public enum ImportStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// 贪婪合并
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 统计拓展
|
||||
/// </summary>
|
||||
public static class GachaStatisticsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 值域压缩
|
||||
/// </summary>
|
||||
/// <param name="b">源</param>
|
||||
/// <returns>压缩值</returns>
|
||||
public static byte HalfRange(this byte b)
|
||||
{
|
||||
// [0,256] -> [0,128]-> [64,172]
|
||||
return (byte)((b / 2) + 64);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 求平均值
|
||||
/// </summary>
|
||||
/// <param name="span">跨度</param>
|
||||
/// <returns>平均值</returns>
|
||||
public static byte Average(this Span<byte> span)
|
||||
{
|
||||
int sum = 0;
|
||||
int count = 0;
|
||||
foreach (byte b in span)
|
||||
{
|
||||
sum += b;
|
||||
count++;
|
||||
}
|
||||
|
||||
return unchecked((byte)(sum / count));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
|
||||
where TKey : notnull
|
||||
{
|
||||
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>是否存在键值</returns>
|
||||
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
|
||||
where TKey : notnull
|
||||
{
|
||||
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
|
||||
if (!Unsafe.IsNullRef(ref value))
|
||||
{
|
||||
++value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将计数器转换为统计物品列表
|
||||
/// </summary>
|
||||
/// <param name="dict">计数器</param>
|
||||
/// <returns>统计物品列表</returns>
|
||||
public static List<StatisticsItem> ToStatisticsList(this Dictionary<IStatisticsItemSource, int> dict)
|
||||
{
|
||||
return dict.Select(kvp => kvp.Key.ToStatisticsItem(kvp.Value)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将计数器转换为统计物品列表
|
||||
/// </summary>
|
||||
/// <param name="dict">计数器</param>
|
||||
/// <returns>统计物品列表</returns>
|
||||
public static List<StatisticsItem> ToStatisticsList(this Dictionary<Avatar, int> dict)
|
||||
{
|
||||
return dict.Select(kvp => kvp.Key.ToStatisticsItem(kvp.Value)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将计数器转换为统计物品列表
|
||||
/// </summary>
|
||||
/// <param name="dict">计数器</param>
|
||||
/// <returns>统计物品列表</returns>
|
||||
public static List<StatisticsItem> ToStatisticsList(this Dictionary<Weapon, int> dict)
|
||||
{
|
||||
return dict.Select(kvp => kvp.Key.ToStatisticsItem(kvp.Value)).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Convert;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿统计工厂
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IGachaStatisticsFactory))]
|
||||
internal class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的祈愿统计工厂
|
||||
/// </summary>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
public GachaStatisticsFactory(IMetadataService metadataService)
|
||||
{
|
||||
this.metadataService = metadataService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<GachaStatistics> CreateAsync(IEnumerable<GachaItem> items)
|
||||
{
|
||||
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
|
||||
Dictionary<string, Avatar> nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false);
|
||||
Dictionary<string, Weapon> nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false);
|
||||
|
||||
List<GachaEvent> gachaevents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
|
||||
List<HistoryWishBuilder> historyWishBuilders = gachaevents.Select(g => new HistoryWishBuilder(g, nameAvatarMap, nameWeaponMap)).ToList();
|
||||
|
||||
IOrderedEnumerable<GachaItem> orderedItems = items.OrderBy(i => i.Id);
|
||||
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static GachaStatistics CreateCore(
|
||||
IOrderedEnumerable<GachaItem> items,
|
||||
List<HistoryWishBuilder> historyWishBuilders,
|
||||
Dictionary<int, Avatar> avatarMap,
|
||||
Dictionary<int, Weapon> weaponMap)
|
||||
{
|
||||
TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.PermanentWish, 90, 10);
|
||||
TypedWishSummaryBuilder avatarWishBuilder = new("角色活动", TypedWishSummaryBuilder.AvatarEventWish, 90, 10);
|
||||
TypedWishSummaryBuilder weaponWishBuilder = new("神铸赋形", TypedWishSummaryBuilder.WeaponEventWish, 80, 10);
|
||||
|
||||
Dictionary<Avatar, int> orangeAvatarCounter = new();
|
||||
Dictionary<Avatar, int> purpleAvatarCounter = new();
|
||||
Dictionary<Weapon, int> orangeWeaponCounter = new();
|
||||
Dictionary<Weapon, int> purpleWeaponCounter = new();
|
||||
Dictionary<Weapon, int> blueWeaponCounter = new();
|
||||
|
||||
// Items are ordered by precise time
|
||||
// first is oldest
|
||||
foreach (GachaItem item in items)
|
||||
{
|
||||
// Find target history wish to operate.
|
||||
// TODO: improve performance.
|
||||
HistoryWishBuilder? targetHistoryWishBuilder = historyWishBuilders
|
||||
.Where(w => w.ConfigType == item.GachaType)
|
||||
.SingleOrDefault(w => w.From <= item.Time && w.To >= item.Time);
|
||||
|
||||
// It's an avatar
|
||||
if (item.ItemId.Place() == 8)
|
||||
{
|
||||
Avatar avatar = avatarMap[item.ItemId];
|
||||
|
||||
bool isUp = false;
|
||||
switch (avatar.Quality)
|
||||
{
|
||||
case ItemQuality.QUALITY_ORANGE:
|
||||
orangeAvatarCounter.Increase(avatar);
|
||||
isUp = targetHistoryWishBuilder?.IncreaseOrangeAvatar(avatar) ?? false;
|
||||
break;
|
||||
case ItemQuality.QUALITY_PURPLE:
|
||||
purpleAvatarCounter.Increase(avatar);
|
||||
targetHistoryWishBuilder?.IncreasePurpleAvatar(avatar);
|
||||
break;
|
||||
}
|
||||
|
||||
permanentWishBuilder.TrackAvatar(item, avatar, isUp);
|
||||
avatarWishBuilder.TrackAvatar(item, avatar, isUp);
|
||||
weaponWishBuilder.TrackAvatar(item, avatar, isUp);
|
||||
}
|
||||
|
||||
// It's a weapon
|
||||
else if (item.ItemId.Place() == 5)
|
||||
{
|
||||
Weapon weapon = weaponMap[item.ItemId];
|
||||
|
||||
bool isUp = false;
|
||||
switch (weapon.RankLevel)
|
||||
{
|
||||
case ItemQuality.QUALITY_ORANGE:
|
||||
isUp = targetHistoryWishBuilder?.IncreaseOrangeWeapon(weapon) ?? false;
|
||||
orangeWeaponCounter.Increase(weapon);
|
||||
break;
|
||||
case ItemQuality.QUALITY_PURPLE:
|
||||
targetHistoryWishBuilder?.IncreasePurpleWeapon(weapon);
|
||||
purpleWeaponCounter.Increase(weapon);
|
||||
break;
|
||||
case ItemQuality.QUALITY_BLUE:
|
||||
targetHistoryWishBuilder?.IncreaseBlueWeapon(weapon);
|
||||
blueWeaponCounter.Increase(weapon);
|
||||
break;
|
||||
}
|
||||
|
||||
permanentWishBuilder.TrackWeapon(item, weapon, isUp);
|
||||
avatarWishBuilder.TrackWeapon(item, weapon, isUp);
|
||||
weaponWishBuilder.TrackWeapon(item, weapon, isUp);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ItemId place not correct.
|
||||
Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
// history
|
||||
HistoryWishes = historyWishBuilders.Select(builder => builder.ToHistoryWish()).ToList(),
|
||||
|
||||
// avatars
|
||||
OrangeAvatars = orangeAvatarCounter.ToStatisticsList(),
|
||||
PurpleAvatars = purpleAvatarCounter.ToStatisticsList(),
|
||||
|
||||
// weapons
|
||||
OrangeWeapons = orangeWeaponCounter.ToStatisticsList(),
|
||||
PurpleWeapons = purpleWeaponCounter.ToStatisticsList(),
|
||||
BlueWeapons = blueWeaponCounter.ToStatisticsList(),
|
||||
|
||||
PermanentWish = permanentWishBuilder.ToTypedWishSummary(),
|
||||
AvatarWish = avatarWishBuilder.ToTypedWishSummary(),
|
||||
WeaponWish = weaponWishBuilder.ToTypedWishSummary(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 卡池历史记录建造器
|
||||
/// </summary>
|
||||
internal class HistoryWishBuilder
|
||||
{
|
||||
private readonly GachaEvent gachaEvent;
|
||||
private readonly GachaConfigType configType;
|
||||
|
||||
private readonly Dictionary<IStatisticsItemSource, int> orangeUpCounter = new();
|
||||
private readonly Dictionary<IStatisticsItemSource, int> purpleUpCounter = new();
|
||||
private readonly Dictionary<IStatisticsItemSource, int> orangeCounter = new();
|
||||
private readonly Dictionary<IStatisticsItemSource, int> purpleCounter = new();
|
||||
private readonly Dictionary<IStatisticsItemSource, int> blueCounter = new();
|
||||
|
||||
private int totalCountTracker;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的卡池历史记录建造器
|
||||
/// </summary>
|
||||
/// <param name="gachaEvent">卡池配置</param>
|
||||
/// <param name="nameAvatarMap">命名角色映射</param>
|
||||
/// <param name="nameWeaponMap">命名武器映射</param>
|
||||
public HistoryWishBuilder(GachaEvent gachaEvent, Dictionary<string, Avatar> nameAvatarMap, Dictionary<string, Weapon> nameWeaponMap)
|
||||
{
|
||||
this.gachaEvent = gachaEvent;
|
||||
configType = gachaEvent.Type;
|
||||
|
||||
if (configType == GachaConfigType.AvatarEventWish || configType == GachaConfigType.AvatarEventWish2)
|
||||
{
|
||||
orangeUpCounter = gachaEvent.UpOrangeList.Select(name => nameAvatarMap[name]).ToDictionary(a => (IStatisticsItemSource)a, a => 0);
|
||||
purpleUpCounter = gachaEvent.UpPurpleList.Select(name => nameAvatarMap[name]).ToDictionary(a => (IStatisticsItemSource)a, a => 0);
|
||||
}
|
||||
else if (configType == GachaConfigType.WeaponEventWish)
|
||||
{
|
||||
orangeUpCounter = gachaEvent.UpOrangeList.Select(name => nameWeaponMap[name]).ToDictionary(w => (IStatisticsItemSource)w, w => 0);
|
||||
purpleUpCounter = gachaEvent.UpPurpleList.Select(name => nameWeaponMap[name]).ToDictionary(w => (IStatisticsItemSource)w, w => 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿配置类型
|
||||
/// </summary>
|
||||
public GachaConfigType ConfigType { get => configType; }
|
||||
|
||||
/// <inheritdoc cref="GachaEvent.From"/>
|
||||
public DateTimeOffset From => gachaEvent.From;
|
||||
|
||||
/// <inheritdoc cref="GachaEvent.To"/>
|
||||
public DateTimeOffset To => gachaEvent.To;
|
||||
|
||||
/// <summary>
|
||||
/// 计数五星角色
|
||||
/// </summary>
|
||||
/// <param name="avatar">角色</param>
|
||||
/// <returns>是否为Up角色</returns>
|
||||
public bool IncreaseOrangeAvatar(Avatar avatar)
|
||||
{
|
||||
orangeCounter.Increase(avatar);
|
||||
++totalCountTracker;
|
||||
|
||||
return orangeUpCounter.TryIncrease(avatar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计数四星角色
|
||||
/// </summary>
|
||||
/// <param name="avatar">角色</param>
|
||||
public void IncreasePurpleAvatar(Avatar avatar)
|
||||
{
|
||||
purpleUpCounter.TryIncrease(avatar);
|
||||
purpleCounter.Increase(avatar);
|
||||
++totalCountTracker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计数五星武器
|
||||
/// </summary>
|
||||
/// <param name="weapon">武器</param>
|
||||
/// <returns>是否为Up武器</returns>
|
||||
public bool IncreaseOrangeWeapon(Weapon weapon)
|
||||
{
|
||||
orangeCounter.Increase(weapon);
|
||||
++totalCountTracker;
|
||||
return orangeUpCounter.TryIncrease(weapon);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计数四星武器
|
||||
/// </summary>
|
||||
/// <param name="weapon">武器</param>
|
||||
public void IncreasePurpleWeapon(Weapon weapon)
|
||||
{
|
||||
purpleUpCounter.TryIncrease(weapon);
|
||||
purpleCounter.Increase(weapon);
|
||||
++totalCountTracker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计数三星武器
|
||||
/// </summary>
|
||||
/// <param name="weapon">武器</param>
|
||||
public void IncreaseBlueWeapon(Weapon weapon)
|
||||
{
|
||||
blueCounter.Increase(weapon);
|
||||
++totalCountTracker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到卡池历史记录
|
||||
/// </summary>
|
||||
/// <returns>卡池历史记录</returns>
|
||||
public HistoryWish ToHistoryWish()
|
||||
{
|
||||
HistoryWish historyWish = new()
|
||||
{
|
||||
// base
|
||||
Name = gachaEvent.Name,
|
||||
From = gachaEvent.From,
|
||||
To = gachaEvent.To,
|
||||
TotalCount = totalCountTracker,
|
||||
|
||||
// fill
|
||||
OrangeUpList = orangeUpCounter.ToStatisticsList(),
|
||||
PurpleUpList = purpleUpCounter.ToStatisticsList(),
|
||||
OrangeList = orangeCounter.ToStatisticsList(),
|
||||
PurpleList = purpleCounter.ToStatisticsList(),
|
||||
BlueList = blueCounter.ToStatisticsList(),
|
||||
};
|
||||
|
||||
return historyWish;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿统计工厂
|
||||
/// </summary>
|
||||
public interface IGachaStatisticsFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步创建祈愿统计对象
|
||||
/// </summary>
|
||||
/// <param name="items">物品列表</param>
|
||||
/// <returns>祈愿统计对象</returns>
|
||||
Task<GachaStatistics> CreateAsync(IEnumerable<GachaItem> items);
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 类型化祈愿统计信息构建器
|
||||
/// </summary>
|
||||
internal class TypedWishSummaryBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// 常驻祈愿
|
||||
/// </summary>
|
||||
public static readonly Func<GachaConfigType, bool> PermanentWish = type => type is GachaConfigType.PermanentWish;
|
||||
|
||||
/// <summary>
|
||||
/// 角色活动
|
||||
/// </summary>
|
||||
public static readonly Func<GachaConfigType, bool> AvatarEventWish = type => type is GachaConfigType.AvatarEventWish or GachaConfigType.AvatarEventWish2;
|
||||
|
||||
/// <summary>
|
||||
/// 武器活动
|
||||
/// </summary>
|
||||
public static readonly Func<GachaConfigType, bool> WeaponEventWish = type => type is GachaConfigType.WeaponEventWish;
|
||||
|
||||
private readonly string name;
|
||||
private readonly int guarenteeOrangeThreshold;
|
||||
private readonly int guarenteePurpleThreshold;
|
||||
private readonly Func<GachaConfigType, bool> typeEvaluator;
|
||||
|
||||
private readonly List<int> averageOrangePullTracker = new();
|
||||
private readonly List<int> averageUpOrangePullTracker = new();
|
||||
private readonly List<SummaryItem> summaryItemCache = new();
|
||||
|
||||
private int maxOrangePullTracker;
|
||||
private int minOrangePullTracker;
|
||||
private int lastOrangePullTracker;
|
||||
private int lastUpOrangePullTracker;
|
||||
private int lastPurplePullTracker;
|
||||
private int totalCountTracker;
|
||||
private int totalOrangePullTracker;
|
||||
private int totalPurplePullTracker;
|
||||
private int totalBluePullTracker;
|
||||
|
||||
private DateTimeOffset fromTimeTracker = DateTimeOffset.MaxValue;
|
||||
private DateTimeOffset toTimeTracker = DateTimeOffset.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的类型化祈愿统计信息构建器
|
||||
/// </summary>
|
||||
/// <param name="name">祈愿配置</param>
|
||||
/// <param name="typeEvaluator">祈愿类型判断器</param>
|
||||
/// <param name="guarenteeOrangeThreshold">五星保底</param>
|
||||
/// <param name="guarenteePurpleThreshold">四星保底</param>
|
||||
public TypedWishSummaryBuilder(string name, Func<GachaConfigType, bool> typeEvaluator, int guarenteeOrangeThreshold, int guarenteePurpleThreshold)
|
||||
{
|
||||
this.name = name;
|
||||
this.typeEvaluator = typeEvaluator;
|
||||
this.guarenteeOrangeThreshold = guarenteeOrangeThreshold;
|
||||
this.guarenteePurpleThreshold = guarenteePurpleThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追踪物品
|
||||
/// </summary>
|
||||
/// <param name="item">祈愿物品</param>
|
||||
/// <param name="avatar">对应角色</param>
|
||||
/// <param name="isUp">是否为Up物品</param>
|
||||
public void TrackAvatar(GachaItem item, Avatar avatar, bool isUp)
|
||||
{
|
||||
if (typeEvaluator(item.GachaType))
|
||||
{
|
||||
++lastOrangePullTracker;
|
||||
++lastPurplePullTracker;
|
||||
++lastUpOrangePullTracker;
|
||||
|
||||
// track total pulls
|
||||
++totalCountTracker;
|
||||
|
||||
switch (avatar.Quality)
|
||||
{
|
||||
case ItemQuality.QUALITY_ORANGE:
|
||||
{
|
||||
TrackMinMaxOrangePull(lastOrangePullTracker);
|
||||
TrackFromToTime(item.Time);
|
||||
averageOrangePullTracker.Add(lastOrangePullTracker);
|
||||
|
||||
if (isUp)
|
||||
{
|
||||
averageUpOrangePullTracker.Add(lastUpOrangePullTracker);
|
||||
lastUpOrangePullTracker = 0;
|
||||
}
|
||||
|
||||
summaryItemCache.Add(avatar.ToSummaryItem(lastOrangePullTracker, item.Time, isUp));
|
||||
|
||||
lastOrangePullTracker = 0;
|
||||
++totalOrangePullTracker;
|
||||
break;
|
||||
}
|
||||
|
||||
case ItemQuality.QUALITY_PURPLE:
|
||||
{
|
||||
lastPurplePullTracker = 0;
|
||||
++totalPurplePullTracker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追踪物品
|
||||
/// </summary>
|
||||
/// <param name="item">祈愿物品</param>
|
||||
/// <param name="weapon">对应武器</param>
|
||||
/// <param name="isUp">是否为Up物品</param>
|
||||
public void TrackWeapon(GachaItem item, Weapon weapon, bool isUp)
|
||||
{
|
||||
if (typeEvaluator(item.GachaType))
|
||||
{
|
||||
++lastOrangePullTracker;
|
||||
++lastPurplePullTracker;
|
||||
++lastUpOrangePullTracker;
|
||||
|
||||
// track total pulls
|
||||
++totalCountTracker;
|
||||
|
||||
switch (weapon.RankLevel)
|
||||
{
|
||||
case ItemQuality.QUALITY_ORANGE:
|
||||
{
|
||||
TrackMinMaxOrangePull(lastOrangePullTracker);
|
||||
TrackFromToTime(item.Time);
|
||||
averageOrangePullTracker.Add(lastOrangePullTracker);
|
||||
|
||||
if (isUp)
|
||||
{
|
||||
averageUpOrangePullTracker.Add(lastUpOrangePullTracker);
|
||||
lastUpOrangePullTracker = 0;
|
||||
}
|
||||
|
||||
summaryItemCache.Add(weapon.ToSummaryItem(lastOrangePullTracker, item.Time, isUp));
|
||||
|
||||
lastOrangePullTracker = 0;
|
||||
++totalOrangePullTracker;
|
||||
break;
|
||||
}
|
||||
|
||||
case ItemQuality.QUALITY_PURPLE:
|
||||
{
|
||||
lastPurplePullTracker = 0;
|
||||
++totalPurplePullTracker;
|
||||
break;
|
||||
}
|
||||
|
||||
case ItemQuality.QUALITY_BLUE:
|
||||
{
|
||||
++totalBluePullTracker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到类型化祈愿统计信息
|
||||
/// </summary>
|
||||
/// <returns>类型化祈愿统计信息</returns>
|
||||
public TypedWishSummary ToTypedWishSummary()
|
||||
{
|
||||
CompleteSummaryItems(summaryItemCache);
|
||||
double totalCountDouble = totalCountTracker;
|
||||
|
||||
return new()
|
||||
{
|
||||
// base
|
||||
Name = name,
|
||||
From = fromTimeTracker,
|
||||
To = toTimeTracker,
|
||||
TotalCount = totalCountTracker,
|
||||
|
||||
// TypedWishSummary
|
||||
MaxOrangePull = maxOrangePullTracker,
|
||||
MinOrangePull = minOrangePullTracker,
|
||||
LastOrangePull = lastOrangePullTracker,
|
||||
GuarenteeOrangeThreshold = guarenteeOrangeThreshold,
|
||||
LastPurplePull = lastPurplePullTracker,
|
||||
GuarenteePurpleThreshold = guarenteePurpleThreshold,
|
||||
TotalOrangePull = totalOrangePullTracker,
|
||||
TotalPurplePull = totalPurplePullTracker,
|
||||
TotalBluePull = totalBluePullTracker,
|
||||
TotalOrangePercent = totalOrangePullTracker / totalCountDouble,
|
||||
TotalPurplePercent = totalPurplePullTracker / totalCountDouble,
|
||||
TotalBluePercent = totalBluePullTracker / totalCountDouble,
|
||||
AverageOrangePull = averageOrangePullTracker.AverageNoThrow(),
|
||||
AverageUpOrangePull = averageUpOrangePullTracker.AverageNoThrow(),
|
||||
OrangeList = summaryItemCache,
|
||||
};
|
||||
}
|
||||
|
||||
private static void CompleteSummaryItems(List<SummaryItem> summaryItems)
|
||||
{
|
||||
// we can't trust first item's prev state.
|
||||
bool isPreviousUp = true;
|
||||
|
||||
// mark the IsGuarentee
|
||||
foreach (SummaryItem item in summaryItems)
|
||||
{
|
||||
if (item.IsUp && (!isPreviousUp))
|
||||
{
|
||||
item.IsGuarentee = true;
|
||||
}
|
||||
|
||||
isPreviousUp = item.IsUp;
|
||||
item.Color = GetColorByName(item.Name);
|
||||
}
|
||||
|
||||
// reverse items
|
||||
summaryItems.Reverse();
|
||||
}
|
||||
|
||||
private static Color GetColorByName(string name)
|
||||
{
|
||||
byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
|
||||
Span<byte> first = new(codes, 0, 5);
|
||||
Span<byte> second = new(codes, 5, 5);
|
||||
Span<byte> third = new(codes, 10, 5);
|
||||
Color color = Color.FromArgb(255, first.Average()/*.HalfRange()*/, second.Average()/*.HalfRange()*/, third.Average()/*.HalfRange()*/);
|
||||
return color;
|
||||
}
|
||||
|
||||
private void TrackMinMaxOrangePull(int lastOrangePull)
|
||||
{
|
||||
if (lastOrangePull < minOrangePullTracker || minOrangePullTracker == 0)
|
||||
{
|
||||
minOrangePullTracker = lastOrangePull;
|
||||
}
|
||||
|
||||
if (lastOrangePull > maxOrangePullTracker || maxOrangePullTracker == 0)
|
||||
{
|
||||
maxOrangePullTracker = lastOrangePull;
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackFromToTime(DateTimeOffset time)
|
||||
{
|
||||
if (time < fromTimeTracker)
|
||||
{
|
||||
fromTimeTracker = time;
|
||||
}
|
||||
|
||||
if (time > toTimeTracker)
|
||||
{
|
||||
toTimeTracker = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Snap.Hutao/Snap.Hutao/Service/GachaLog/FetchState.cs
Normal file
36
src/Snap.Hutao/Snap.Hutao/Service/GachaLog/FetchState.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog;
|
||||
|
||||
/// <summary>
|
||||
/// 获取状态
|
||||
/// </summary>
|
||||
public class FetchState
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个新的获取状态
|
||||
/// </summary>
|
||||
public FetchState()
|
||||
{
|
||||
Items = new(20);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证密钥是否过期
|
||||
/// </summary>
|
||||
public bool AuthKeyTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 卡池类型
|
||||
/// </summary>
|
||||
public GachaConfigType ConfigType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前获取的物品
|
||||
/// </summary>
|
||||
public List<ItemBase> Items { get; set; }
|
||||
}
|
||||
@@ -3,8 +3,17 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.GachaLog.Factory;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog;
|
||||
@@ -13,21 +22,59 @@ namespace Snap.Hutao.Service.GachaLog;
|
||||
/// 祈愿记录服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IGachaLogService))]
|
||||
internal class GachaLogService : IGachaLogService
|
||||
internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
{
|
||||
/// <summary>
|
||||
/// 祈愿记录查询的类型
|
||||
/// </summary>
|
||||
private static readonly ImmutableList<GachaConfigType> QueryTypes = ImmutableList.Create(new GachaConfigType[]
|
||||
{
|
||||
GachaConfigType.NoviceWish,
|
||||
GachaConfigType.PermanentWish,
|
||||
GachaConfigType.AvatarEventWish,
|
||||
GachaConfigType.WeaponEventWish,
|
||||
});
|
||||
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly IEnumerable<IGachaLogUrlProvider> urlProviders;
|
||||
private readonly GachaInfoClient gachaInfoClient;
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly IGachaStatisticsFactory gachaStatisticsFactory;
|
||||
private readonly DbCurrent<GachaArchive, Message.GachaArchiveChangedMessage> dbCurrent;
|
||||
|
||||
private readonly Dictionary<string, ItemBase> itemBaseCache = new();
|
||||
|
||||
private Dictionary<string, Model.Metadata.Avatar.Avatar>? avatarMap;
|
||||
private Dictionary<string, Model.Metadata.Weapon.Weapon>? weaponMap;
|
||||
private ObservableCollection<GachaArchive>? archiveCollection;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的祈愿记录服务
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="urlProviders">Url提供器集合</param>
|
||||
/// <param name="gachaInfoClient">祈愿记录客户端</param>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
/// <param name="infoBarService">信息条服务</param>
|
||||
/// <param name="gachaStatisticsFactory">祈愿统计工厂</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public GachaLogService(AppDbContext appDbContext, IMessenger messenger)
|
||||
public GachaLogService(
|
||||
AppDbContext appDbContext,
|
||||
IEnumerable<IGachaLogUrlProvider> urlProviders,
|
||||
GachaInfoClient gachaInfoClient,
|
||||
IMetadataService metadataService,
|
||||
IInfoBarService infoBarService,
|
||||
IGachaStatisticsFactory gachaStatisticsFactory,
|
||||
IMessenger messenger)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.urlProviders = urlProviders;
|
||||
this.gachaInfoClient = gachaInfoClient;
|
||||
this.metadataService = metadataService;
|
||||
this.infoBarService = infoBarService;
|
||||
this.gachaStatisticsFactory = gachaStatisticsFactory;
|
||||
|
||||
dbCurrent = new(appDbContext, appDbContext.GachaArchives, messenger);
|
||||
}
|
||||
|
||||
@@ -38,9 +85,235 @@ internal class GachaLogService : IGachaLogService
|
||||
set => dbCurrent.Current = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInitialized { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GachaArchive> GetArchiveCollection()
|
||||
{
|
||||
return archiveCollection ??= new(appDbContext.GachaArchives.ToList());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeAsync(CancellationToken token = default)
|
||||
{
|
||||
if (await metadataService.InitializeAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
avatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
weaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsInitialized = false;
|
||||
}
|
||||
|
||||
return IsInitialized;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<GachaStatistics> GetStatisticsAsync(GachaArchive? archive = null)
|
||||
{
|
||||
archive ??= CurrentArchive;
|
||||
|
||||
// Return statistics
|
||||
if (archive != null)
|
||||
{
|
||||
IQueryable<GachaItem> items = appDbContext.GachaItems
|
||||
.Where(i => i.ArchiveId == archive.InnerId);
|
||||
|
||||
return gachaStatisticsFactory.CreateAsync(items);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Task.FromResult(GachaStatistics.Default);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGachaLogUrlProvider? GetGachaLogUrlProvider(RefreshOption option)
|
||||
{
|
||||
return option switch
|
||||
{
|
||||
RefreshOption.WebCache => urlProviders.Single(p => p.Name == nameof(GachaLogUrlWebCacheProvider)),
|
||||
RefreshOption.ManualInput => urlProviders.Single(p => p.Name == nameof(GachaLogUrlManualInputProvider)),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RefreshGachaLogAsync(string query, RefreshStrategy strategy, IProgress<FetchState> progress, CancellationToken token)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "祈愿记录服务未能正常初始化");
|
||||
|
||||
bool isLazy = strategy switch
|
||||
{
|
||||
RefreshStrategy.AggressiveMerge => false,
|
||||
RefreshStrategy.LazyMerge => true,
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
|
||||
GachaArchive? result = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false);
|
||||
CurrentArchive = result ?? CurrentArchive;
|
||||
}
|
||||
|
||||
private static Task RandomDelayAsync(CancellationToken token)
|
||||
{
|
||||
return Task.Delay(TimeSpan.FromSeconds(Random.Shared.NextDouble() + 1), token);
|
||||
}
|
||||
|
||||
private async Task<GachaArchive?> FetchGachaLogsAsync(string query, bool isLazy, IProgress<FetchState> progress, CancellationToken token)
|
||||
{
|
||||
GachaArchive? archive = null;
|
||||
FetchState state = new();
|
||||
|
||||
foreach (GachaConfigType configType in QueryTypes)
|
||||
{
|
||||
state.ConfigType = configType;
|
||||
long? dbEndId = null;
|
||||
GachaLogConfigration configration = new(query, configType);
|
||||
List<GachaItem> itemsToAdd = new();
|
||||
|
||||
do
|
||||
{
|
||||
Response<GachaLogPage>? response = await gachaInfoClient.GetGachaLogPageAsync(configration, token).ConfigureAwait(false);
|
||||
|
||||
if (response?.Data is GachaLogPage page)
|
||||
{
|
||||
state.Items.Clear();
|
||||
List<GachaLogItem> items = page.List;
|
||||
bool completedCurrentTypeAdding = false;
|
||||
|
||||
foreach (GachaLogItem item in items)
|
||||
{
|
||||
SkipOrInitArchive(ref archive, item.Uid);
|
||||
dbEndId ??= GetEndId(archive, configType);
|
||||
|
||||
if ((!isLazy) || item.Id > dbEndId)
|
||||
{
|
||||
itemsToAdd.Add(GachaItem.Create(archive.InnerId, item, GetItemId(item)));
|
||||
state.Items.Add(GetItemBaseByName(item.Name, item.ItemType));
|
||||
configration.EndId = item.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
completedCurrentTypeAdding = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(state);
|
||||
|
||||
if (completedCurrentTypeAdding || items.Count < GachaLogConfigration.Size)
|
||||
{
|
||||
// exit current type fetch loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.AuthKeyTimeout = true;
|
||||
progress.Report(state);
|
||||
break;
|
||||
}
|
||||
|
||||
await RandomDelayAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
while (true);
|
||||
|
||||
if (state.AuthKeyTimeout)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
SaveGachaItems(itemsToAdd, isLazy, archive, configration.EndId);
|
||||
await RandomDelayAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
private void SkipOrInitArchive([NotNull] ref GachaArchive? archive, string uid)
|
||||
{
|
||||
if (archive == null)
|
||||
{
|
||||
archive = appDbContext.GachaArchives.SingleOrDefault(a => a.Uid == uid);
|
||||
|
||||
if (archive == null)
|
||||
{
|
||||
GachaArchive created = GachaArchive.Create(uid);
|
||||
appDbContext.GachaArchives.Add(created);
|
||||
appDbContext.SaveChanges();
|
||||
|
||||
archive = appDbContext.GachaArchives.Single(a => a.Uid == uid);
|
||||
GachaArchive temp = archive;
|
||||
Program.DispatcherQueue!.TryEnqueue(() => archiveCollection!.Add(temp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long GetEndId(GachaArchive? archive, GachaConfigType configType)
|
||||
{
|
||||
GachaItem? item = null;
|
||||
|
||||
if (archive != null)
|
||||
{
|
||||
item = appDbContext.GachaItems
|
||||
.Where(i => i.ArchiveId == archive.InnerId)
|
||||
.Where(i => i.QueryType == configType)
|
||||
|
||||
// MaxBy should be supported by .NET 7
|
||||
.AsEnumerable()
|
||||
.MaxBy(i => i.Id);
|
||||
}
|
||||
|
||||
return item?.Id ?? 0L;
|
||||
}
|
||||
|
||||
private int GetItemId(GachaLogItem item)
|
||||
{
|
||||
return item.ItemType switch
|
||||
{
|
||||
"角色" => avatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0,
|
||||
"武器" => weaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
private ItemBase GetItemBaseByName(string name, string type)
|
||||
{
|
||||
if (!itemBaseCache.TryGetValue(name, out ItemBase? result))
|
||||
{
|
||||
result = type switch
|
||||
{
|
||||
"角色" => avatarMap![name].ToItemBase(),
|
||||
"武器" => weaponMap![name].ToItemBase(),
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
|
||||
itemBaseCache[name] = result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SaveGachaItems(List<GachaItem> itemsToAdd, bool isLazy, GachaArchive? archive, long endId)
|
||||
{
|
||||
if (itemsToAdd.Count > 0)
|
||||
{
|
||||
if ((!isLazy) && archive != null)
|
||||
{
|
||||
IQueryable<GachaItem> toRemove = appDbContext.GachaItems
|
||||
.Where(i => i.ArchiveId == archive.InnerId)
|
||||
.Where(i => i.Id >= endId);
|
||||
|
||||
appDbContext.GachaItems.RemoveRange(toRemove);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
|
||||
appDbContext.GachaItems.AddRange(itemsToAdd);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
@@ -21,4 +22,36 @@ internal interface IGachaLogService
|
||||
/// </summary>
|
||||
/// <returns>存档集合</returns>
|
||||
ObservableCollection<GachaArchive> GetArchiveCollection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取祈愿日志Url提供器
|
||||
/// </summary>
|
||||
/// <param name="option">刷新模式</param>
|
||||
/// <returns>祈愿日志Url提供器</returns>
|
||||
IGachaLogUrlProvider? GetGachaLogUrlProvider(RefreshOption option);
|
||||
|
||||
/// <summary>
|
||||
/// 获得对应的祈愿统计
|
||||
/// </summary>
|
||||
/// <param name="archive">存档</param>
|
||||
/// <returns>祈愿统计</returns>
|
||||
Task<GachaStatistics> GetStatisticsAsync(GachaArchive? archive = null);
|
||||
|
||||
/// <summary>
|
||||
/// 异步初始化
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否初始化成功</returns>
|
||||
ValueTask<bool> InitializeAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新祈愿记录
|
||||
/// 切换选中的存档
|
||||
/// </summary>
|
||||
/// <param name="query">查询语句</param>
|
||||
/// <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);
|
||||
}
|
||||
27
src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs
Normal file
27
src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新选项
|
||||
/// </summary>
|
||||
public enum RefreshOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 无模式刷新
|
||||
/// 用于返回新的统计数据
|
||||
/// 或者切换存档后的刷新
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// 通过浏览器缓存刷新
|
||||
/// </summary>
|
||||
WebCache,
|
||||
|
||||
/// <summary>
|
||||
/// 手动输入Url刷新
|
||||
/// </summary>
|
||||
ManualInput,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新策略
|
||||
/// </summary>
|
||||
public enum RefreshStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// 无策略 用于切换存档时使用
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 贪婪合并
|
||||
/// </summary>
|
||||
AggressiveMerge = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 懒惰合并
|
||||
/// </summary>
|
||||
LazyMerge = 2,
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Locator;
|
||||
|
||||
@@ -19,7 +18,7 @@ internal class RegistryLauncherLocator : IGameLocator
|
||||
/// <inheritdoc/>
|
||||
public Task<ValueResult<bool, string>> LocateGamePathAsync()
|
||||
{
|
||||
return Task.FromResult(LocateInternal("InstallPath", "YuanShen.exe"));
|
||||
return Task.FromResult(LocateInternal("InstallPath", "\\Genshin Impact Game\\YuanShen.exe"));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -28,16 +27,16 @@ internal class RegistryLauncherLocator : IGameLocator
|
||||
return Task.FromResult(LocateInternal("DisplayIcon"));
|
||||
}
|
||||
|
||||
private static ValueResult<bool, string> LocateInternal(string key, string? combine = null)
|
||||
private static ValueResult<bool, string> LocateInternal(string key, string? append = null)
|
||||
{
|
||||
RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神");
|
||||
if (uninstallKey != null)
|
||||
{
|
||||
if (uninstallKey.GetValue(key) is string path)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(combine))
|
||||
if (!string.IsNullOrEmpty(append))
|
||||
{
|
||||
path = Path.Combine(combine);
|
||||
path += append;
|
||||
}
|
||||
|
||||
return new(true, path);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Model.Metadata.Achievement;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Reliquary;
|
||||
@@ -41,6 +42,41 @@ internal interface IMetadataService
|
||||
/// <returns>角色列表</returns>
|
||||
ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取卡池配置列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>卡池配置列表</returns>
|
||||
ValueTask<List<GachaEvent>> GetGachaEventsAsync(CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取Id到角色的字典
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>Id到角色的字典</returns>
|
||||
ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取ID到武器的字典
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>Id到武器的字典</returns>
|
||||
ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取名称到角色的字典
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>名称到角色的字典</returns>
|
||||
ValueTask<Dictionary<string, Avatar>> GetNameToAvatarMapAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取名称到武器的字典
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>名称到武器的字典</returns>
|
||||
ValueTask<Dictionary<string, Weapon>> GetNameToWeaponMapAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取圣遗物列表
|
||||
/// </summary>
|
||||
|
||||
@@ -7,6 +7,8 @@ using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Model.Metadata.Achievement;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Reliquary;
|
||||
@@ -108,6 +110,36 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
|
||||
return FromCacheOrFileAsync<List<Avatar>>("Avatar", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<GachaEvent>> GetGachaEventsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<GachaEvent>>("GachaEvent", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, Avatar>("Avatar", a => a.Id, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, Weapon>("Weapon", w => w.Id, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<string, Avatar>> GetNameToAvatarMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<string, Avatar>("Avatar", a => a.Name, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<string, Weapon>> GetNameToWeaponMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<string, Weapon>("Weapon", w => w.Name, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
|
||||
{
|
||||
@@ -132,7 +164,7 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
|
||||
return FromCacheOrFileAsync<List<Weapon>>("Weapon", token);
|
||||
}
|
||||
|
||||
private async Task<bool> TryUpdateMetadataAsync(CancellationToken token = default)
|
||||
private async Task<bool> TryUpdateMetadataAsync(CancellationToken token)
|
||||
{
|
||||
IDictionary<string, string>? metaMd5Map = null;
|
||||
try
|
||||
@@ -238,7 +270,7 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
|
||||
|
||||
if (memoryCache.TryGetValue(cacheKey, out object? value))
|
||||
{
|
||||
return Must.NotNull((value as T)!);
|
||||
return Must.NotNull((T)value!);
|
||||
}
|
||||
|
||||
using (Stream fileStream = metadataContext.OpenRead($"{fileName}.json"))
|
||||
@@ -247,4 +279,20 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
|
||||
return memoryCache.Set(cacheKey, Must.NotNull(result!));
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<Dictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TKey, TValue>(string fileName, Func<TValue, TKey> keySelector, CancellationToken token)
|
||||
where TKey : notnull
|
||||
{
|
||||
Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败");
|
||||
string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}.Map.{typeof(TKey).Name}";
|
||||
|
||||
if (memoryCache.TryGetValue(cacheKey, out object? value))
|
||||
{
|
||||
return Must.NotNull((Dictionary<TKey, TValue>)value!);
|
||||
}
|
||||
|
||||
List<TValue> list = await FromCacheOrFileAsync<List<TValue>>(fileName, token).ConfigureAwait(false);
|
||||
Dictionary<TKey, TValue> dict = list.ToDictionaryOverride(keySelector);
|
||||
return memoryCache.Set(cacheKey, dict);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
@@ -137,18 +138,32 @@ internal class NavigationService : INavigationService
|
||||
{
|
||||
NavigationResult result = Navigate<TPage>(data, syncNavigationViewItem);
|
||||
|
||||
if (result is NavigationResult.Succeed)
|
||||
switch (result)
|
||||
{
|
||||
try
|
||||
{
|
||||
await data
|
||||
.WaitForCompletionAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
return NavigationResult.Failed;
|
||||
}
|
||||
case NavigationResult.Succeed:
|
||||
{
|
||||
try
|
||||
{
|
||||
await data.WaitForCompletionAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(EventIds.NavigationFailed, ex, "异步导航时发生异常");
|
||||
return NavigationResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NavigationResult.AlreadyNavigatedTo:
|
||||
{
|
||||
if (Frame!.Content is ScopedPage scopedPage)
|
||||
{
|
||||
await scopedPage.NotifyRecipentAsync((INavigationData)data).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -44,8 +44,10 @@
|
||||
<None Remove="View\Control\DescParamComboBox.xaml" />
|
||||
<None Remove="View\Control\ItemIcon.xaml" />
|
||||
<None Remove="View\Control\SkillPivot.xaml" />
|
||||
<None Remove="View\Control\StatisticsCard.xaml" />
|
||||
<None Remove="View\Dialog\AchievementArchiveCreateDialog.xaml" />
|
||||
<None Remove="View\Dialog\AchievementImportDialog.xaml" />
|
||||
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserDialog.xaml" />
|
||||
<None Remove="View\MainView.xaml" />
|
||||
<None Remove="View\Page\AchievementPage.xaml" />
|
||||
@@ -86,9 +88,9 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||
<!-- The PrivateAssets & IncludeAssets of Microsoft.EntityFrameworkCore.Tools should be remove to prevent multiple deps files-->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
||||
@@ -99,7 +101,7 @@
|
||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.64" />
|
||||
<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.4" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.5" />
|
||||
<PackageReference Include="MiniExcel" Version="1.26.7" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -203,8 +205,19 @@
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Model\Annotation\" />
|
||||
<Folder Include="Web\Hoyolab\Takumi\Event\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Control\StatisticsCard.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\GachaLogRefreshProgressDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\GachaLogPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<UserControl
|
||||
x:Class="Snap.Hutao.View.Control.ItemIcon"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Snap.Hutao.View.Control"
|
||||
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:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
|
||||
@@ -11,7 +10,6 @@
|
||||
Width="80"
|
||||
Height="80">
|
||||
<UserControl.Resources>
|
||||
<shmmc:AvatarIconConverter x:Key="IconConverter"/>
|
||||
<shmmc:QualityConverter x:Key="QualityConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
@@ -22,6 +20,13 @@
|
||||
Source="https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png"/>
|
||||
<shci:CachedImage
|
||||
Source="{x:Bind Icon,Mode=OneWay}"/>
|
||||
<shci:CachedImage
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Margin="2"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Source="{x:Bind Badge,Mode=OneWay}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -15,6 +15,7 @@ public sealed partial class ItemIcon : UserControl
|
||||
{
|
||||
private static readonly DependencyProperty QualityProperty = Property<ItemIcon>.Depend(nameof(Quality), ItemQuality.QUALITY_NONE);
|
||||
private static readonly DependencyProperty IconProperty = Property<ItemIcon>.Depend<Uri>(nameof(Icon));
|
||||
private static readonly DependencyProperty BadgeProperty = Property<ItemIcon>.Depend<Uri>(nameof(Badge));
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的物品图标
|
||||
@@ -41,4 +42,13 @@ public sealed partial class ItemIcon : UserControl
|
||||
get => (Uri)GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角标
|
||||
/// </summary>
|
||||
public Uri Badge
|
||||
{
|
||||
get => (Uri)GetValue(BadgeProperty);
|
||||
set => SetValue(BadgeProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shct="using:Snap.Hutao.Control.Text"
|
||||
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -16,7 +14,7 @@
|
||||
|
||||
<Thickness x:Key="PivotHeaderItemMargin">0,0,16,0</Thickness>
|
||||
<Thickness x:Key="PivotItemMargin">0</Thickness>
|
||||
|
||||
|
||||
<Style TargetType="PivotHeaderItem" BasedOn="{StaticResource DefaultPivotHeaderItemStyle}">
|
||||
<Setter Property="Height" Value="80"/>
|
||||
</Style>
|
||||
|
||||
207
src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml
Normal file
207
src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml
Normal file
@@ -0,0 +1,207 @@
|
||||
<UserControl
|
||||
x:Class="Snap.Hutao.View.Control.StatisticsCard"
|
||||
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"
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shmbg="using:Snap.Hutao.Model.Binding.Gacha"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance shmbg:TypedWishSummary}">
|
||||
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="BlueBrush" Color="#FF5180CB"/>
|
||||
<SolidColorBrush x:Key="PurpleBrush" Color="#FFA156E0"/>
|
||||
<SolidColorBrush x:Key="OrangeBrush" Color="#FFBC6932"/>
|
||||
|
||||
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
|
||||
<DataTemplate x:Key="OrangeListTemplate" x:DataType="shmbg:SummaryItem">
|
||||
<Grid Margin="0,4,4,0" Background="Transparent" >
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{Binding TimeFormatted}"/>
|
||||
</ToolTipService.ToolTip>
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<shci:CachedImage
|
||||
Source="{Binding Icon}"
|
||||
Height="32" Width="32"/>
|
||||
<TextBlock
|
||||
Text="{Binding Name}"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush Color="{Binding Color}"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
Foreground="#FF0063FF"
|
||||
Text="保底"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsGuarentee,Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
Text="UP"
|
||||
Foreground="#FFFFA400"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsUp,Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding LastPull}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</DataTemplate>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border
|
||||
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{StaticResource CompatCornerRadius}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Expander
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
Padding="16,0,16,0"
|
||||
BorderBrush="{x:Null}"
|
||||
IsExpanded="True">
|
||||
<Expander.Header>
|
||||
<Grid Grid.Row="0">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Name}"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"/>
|
||||
</Grid>
|
||||
</Expander.Header>
|
||||
<StackPanel>
|
||||
<StackPanel Grid.Row="1" Margin="0,0,0,12">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0,4,0,4" FontFamily="Consolas" Text="{Binding TotalCount}" FontSize="48"/>
|
||||
<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}"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{Binding FromFormatted}"/>
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
Text="-"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{Binding ToFormatted}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<MenuFlyoutSeparator Margin="-12,0"/>
|
||||
<StackPanel Grid.Row="3" Margin="0,12,0,0">
|
||||
<Grid>
|
||||
<TextBlock
|
||||
Text="五星"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Foreground="{StaticResource OrangeBrush}"/>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontFamily="Consolas"
|
||||
Foreground="{StaticResource OrangeBrush}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{Binding TotalOrangeFormatted}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,2,0,0">
|
||||
<TextBlock
|
||||
Text="四星"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Foreground="{StaticResource PurpleBrush}"/>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontFamily="Consolas"
|
||||
Foreground="{StaticResource PurpleBrush}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{Binding TotalPurpleFormatted}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,2,0,0">
|
||||
<TextBlock
|
||||
Text="三星"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Foreground="{StaticResource BlueBrush}"/>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontFamily="Consolas"
|
||||
Foreground="{StaticResource BlueBrush}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{Binding TotalBlueFormatted}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="0,2,0,0">
|
||||
<TextBlock
|
||||
Text="五星平均抽数"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontFamily="Consolas,MicroSoft YaHei UI"
|
||||
Text="{Binding AverageOrangePullFormatted}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,2,0,0">
|
||||
<TextBlock
|
||||
Text="UP 平均抽数"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontFamily="Consolas,MicroSoft YaHei UI"
|
||||
Text="{Binding AverageUpOrangePullFormatted}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,2,0,0">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Left"
|
||||
FontFamily="Consolas,MicroSoft YaHei UI"
|
||||
Text="{Binding MaxOrangePullFormatted}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontFamily="Consolas,MicroSoft YaHei UI"
|
||||
Text="{Binding MinOrangePullFormatted}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<MenuFlyoutSeparator Margin="-12,12"/>
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
<ScrollViewer Grid.Row="2" Margin="12,-4,12,8" VerticalScrollBarVisibility="Hidden">
|
||||
<ItemsControl
|
||||
ItemsSource="{Binding OrangeList}"
|
||||
ItemTemplate="{StaticResource OrangeListTemplate}"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.View.Control;
|
||||
|
||||
/// <summary>
|
||||
/// 统计卡片
|
||||
/// </summary>
|
||||
public sealed partial class StatisticsCard : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的统计卡片
|
||||
/// </summary>
|
||||
public StatisticsCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -42,13 +42,13 @@ public sealed partial class AchievementImportDialog : ContentDialog
|
||||
/// 异步获取导入选项
|
||||
/// </summary>
|
||||
/// <returns>导入选项</returns>
|
||||
public async Task<ValueResult<bool, ImportOption>> GetImportOptionAsync()
|
||||
public async Task<ValueResult<bool, ImportStrategy>> GetImportStrategyAsync()
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
ImportOption option = (ImportOption)ImportModeSelector.SelectedIndex;
|
||||
ImportStrategy strategy = (ImportStrategy)ImportModeSelector.SelectedIndex;
|
||||
|
||||
return new(result == ContentDialogResult.Primary, option);
|
||||
return new(result == ContentDialogResult.Primary, strategy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<ContentDialog
|
||||
x:Class="Snap.Hutao.View.Dialog.GachaLogRefreshProgressDialog"
|
||||
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"
|
||||
xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:cwuconv="using:CommunityToolkit.WinUI.UI.Converters"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Control"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
mc:Ignorable="d"
|
||||
Style="{StaticResource DefaultContentDialogStyle}"
|
||||
Title="获取祈愿物品中">
|
||||
|
||||
<ContentDialog.Resources>
|
||||
<cwuconv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
<DataTemplate x:Key="GachaItemDataTemplate">
|
||||
<Grid
|
||||
Width="40"
|
||||
Height="40">
|
||||
<shvc:ItemIcon
|
||||
Icon="{Binding Icon}"
|
||||
Badge="{Binding Badge}"
|
||||
Quality="{Binding Quality}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ContentDialog.Resources>
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:ContentDialogBehavior/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Text="AuthKey 已失效,请重新获取"
|
||||
Visibility="{x:Bind State.AuthKeyTimeout,Converter={StaticResource BoolToVisibilityConverter},Mode=OneWay}"/>
|
||||
<cwucont:HeaderedItemsControl
|
||||
x:Name="GachaItemsPresenter"
|
||||
Padding="0,8,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
ItemTemplate="{StaticResource GachaItemDataTemplate}">
|
||||
<cwucont:HeaderedItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cwucont:UniformGrid Columns="5" ColumnSpacing="4" RowSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
</cwucont:HeaderedItemsControl.ItemsPanel>
|
||||
</cwucont:HeaderedItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Service.GachaLog;
|
||||
using Snap.Hutao.View.Control;
|
||||
|
||||
namespace Snap.Hutao.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿记录刷新进度对话框
|
||||
/// </summary>
|
||||
public sealed partial class GachaLogRefreshProgressDialog : ContentDialog
|
||||
{
|
||||
private static readonly DependencyProperty StateProperty = Property<GachaLogRefreshProgressDialog>.Depend<FetchState>(nameof(State));
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的对话框
|
||||
/// </summary>
|
||||
/// <param name="window">窗体</param>
|
||||
public GachaLogRefreshProgressDialog(Window window)
|
||||
{
|
||||
InitializeComponent();
|
||||
XamlRoot = window.Content.XamlRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新状态
|
||||
/// </summary>
|
||||
public FetchState State
|
||||
{
|
||||
get { return (FetchState)GetValue(StateProperty); }
|
||||
set { SetValue(StateProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接收进度更新
|
||||
/// </summary>
|
||||
/// <param name="state">状态</param>
|
||||
public void OnReport(FetchState state)
|
||||
{
|
||||
State = state;
|
||||
GachaItemsPresenter.Header = state.ConfigType.GetDescription();
|
||||
|
||||
// Binding not working here.
|
||||
GachaItemsPresenter.Items.Clear();
|
||||
foreach (ItemBase item in state.Items)
|
||||
{
|
||||
GachaItemsPresenter.Items.Add(new ItemIcon
|
||||
{
|
||||
Width = 60,
|
||||
Height = 60,
|
||||
Quality = item.Quality,
|
||||
Icon = item.Icon,
|
||||
Badge = item.Badge,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,26 +60,54 @@
|
||||
MaxWidth="640"
|
||||
VerticalAlignment="Bottom">
|
||||
<StackPanel.Resources>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarErrorSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#442726"
|
||||
FallbackColor="#442726"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarWarningSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#433519"
|
||||
FallbackColor="#433519"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarSuccessSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#393D1B"
|
||||
FallbackColor="#393D1B"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarInformationalSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#34424d"
|
||||
FallbackColor="#34424d"/>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarErrorSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#FDE7E9"
|
||||
FallbackColor="#FDE7E9"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarWarningSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#FFF4CE"
|
||||
FallbackColor="#FFF4CE"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarSuccessSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#DFF6DD"
|
||||
FallbackColor="#DFF6DD"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarInformationalSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#80F6F6F6"
|
||||
FallbackColor="#80F6F6F6"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarErrorSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#442726"
|
||||
FallbackColor="#442726"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarWarningSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#433519"
|
||||
FallbackColor="#433519"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarSuccessSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#393D1B"
|
||||
FallbackColor="#393D1B"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarInformationalSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#34424d"
|
||||
FallbackColor="#34424d"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
</StackPanel.Resources>
|
||||
<StackPanel.Transitions>
|
||||
<TransitionCollection>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.ViewModel;
|
||||
|
||||
namespace Snap.Hutao.View.Page;
|
||||
@@ -21,21 +19,4 @@ public sealed partial class AchievementPage : ScopedPage
|
||||
InitializeWith<AchievementViewModel>();
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
protected override async void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
if (e.Parameter is INavigationData extra)
|
||||
{
|
||||
if (extra.Data != null)
|
||||
{
|
||||
await ((INavigationRecipient)DataContext).ReceiveAsync(extra).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
extra.NotifyNavigationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@
|
||||
<DataTemplate>
|
||||
<Border
|
||||
CornerRadius="{StaticResource CompatCornerRadius}"
|
||||
Background="{ThemeResource SystemControlPageBackgroundAltHighBrush}"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
cwu:UIElementExtensions.ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
|
||||
@@ -3,68 +3,113 @@
|
||||
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:shcm="using:Snap.Hutao.Control.Markup"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:shc="using:Snap.Hutao.Control"
|
||||
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"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance shv:GachaLogViewModel}"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
|
||||
<shc:ScopedPage.Resources>
|
||||
<Thickness x:Key="PivotHeaderItemMargin">8,0,8,0</Thickness>
|
||||
<Thickness x:Key="PivotItemMargin">0</Thickness>
|
||||
</shc:ScopedPage.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<CommandBar DefaultLabelPosition="Right">
|
||||
<CommandBar.Background>
|
||||
<SolidColorBrush Color="{ThemeResource CardBackgroundFillColorSecondary}"/>
|
||||
</CommandBar.Background>
|
||||
<CommandBar.Content>
|
||||
<Rectangle
|
||||
Height="48"
|
||||
VerticalAlignment="Top"
|
||||
Fill="{StaticResource CardBackgroundFillColorSecondary}"/>
|
||||
<Pivot Grid.RowSpan="2">
|
||||
<Pivot.LeftHeader>
|
||||
<ComboBox
|
||||
MinWidth="120"
|
||||
Height="36"
|
||||
Margin="2,6,3,6"/>
|
||||
</CommandBar.Content>
|
||||
<AppBarButton Label="刷新" Icon="{shcm:FontIcon Glyph=}">
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Text="从缓存刷新"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Command="{Binding RefreshByWebCacheCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
Text="手动输入Url"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Command="{Binding RefreshByManualInputCommand}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
<AppBarSeparator/>
|
||||
<AppBarButton Label="导入" Icon="{shcm:FontIcon Glyph=}">
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Margin="16,6,12,6"
|
||||
DisplayMemberPath="Uid"
|
||||
SelectedItem="{Binding SelectedArchive,Mode=TwoWay}"
|
||||
ItemsSource="{Binding Archives}"/>
|
||||
</Pivot.LeftHeader>
|
||||
<Pivot.RightHeader>
|
||||
<CommandBar DefaultLabelPosition="Right">
|
||||
<AppBarButton Label="刷新" Icon="{shcm:FontIcon Glyph=}">
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Text="从缓存刷新"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Command="{Binding RefreshByWebCacheCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
Text="手动输入Url"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Command="{Binding RefreshByManualInputCommand}"/>
|
||||
<ToggleMenuFlyoutItem
|
||||
Text="全量刷新"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
IsChecked="{Binding IsAggressiveRefresh}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
<AppBarSeparator/>
|
||||
<AppBarButton Label="导入" Icon="{shcm:FontIcon Glyph=}">
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Text="从 UIGF Json 文件导入"
|
||||
Command="{Binding ImportFromUIGFJsonCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
<MenuFlyoutItem
|
||||
Text="从 UIGF Excel 文件导入"
|
||||
Command="{Binding ImportFromUIGFExcelCommand}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
<AppBarButton Label="导出" Icon="{shcm:FontIcon Glyph=}">
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Text="导出到 UIGF Json 文件"
|
||||
Command="{Binding ExportToUIGFJsonCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
Text="导出到 UIGF Excel 文件"
|
||||
Command="{Binding ExportToUIGFExcelCommand}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
</CommandBar>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
<AppBarButton Label="导出" Icon="{shcm:FontIcon Glyph=}">
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Text="导出到 UIGF Json 文件"
|
||||
Command="{Binding ExportToUIGFJsonCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
Text="导出到 UIGF Excel 文件"
|
||||
Command="{Binding ExportToUIGFExcelCommand}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
</CommandBar>
|
||||
</Pivot.RightHeader>
|
||||
<PivotItem Header="总览">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition MaxWidth="320"/>
|
||||
<ColumnDefinition MaxWidth="320"/>
|
||||
<ColumnDefinition MaxWidth="320"/>
|
||||
<ColumnDefinition Width="auto" MinWidth="16"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shvc:StatisticsCard
|
||||
Margin="16,16,0,16"
|
||||
Grid.Column="0"
|
||||
DataContext="{Binding Statistics.AvatarWish}"/>
|
||||
<shvc:StatisticsCard
|
||||
Margin="16,16,0,16"
|
||||
Grid.Column="1"
|
||||
DataContext="{Binding Statistics.WeaponWish}"/>
|
||||
<shvc:StatisticsCard
|
||||
Margin="16,16,0,16"
|
||||
Grid.Column="2"
|
||||
DataContext="{Binding Statistics.PermanentWish}"/>
|
||||
</Grid>
|
||||
</PivotItem>
|
||||
<PivotItem Header="历史"/>
|
||||
<PivotItem Header="角色"/>
|
||||
<PivotItem Header="武器"/>
|
||||
</Pivot>
|
||||
</Grid>
|
||||
</shc:ScopedPage>
|
||||
|
||||
@@ -8,6 +8,7 @@ using CommunityToolkit.WinUI.UI;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.IO.DataTransfer;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Core.Threading.CodeAnalysis;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
@@ -21,8 +22,6 @@ using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Pickers;
|
||||
using Windows.Storage.Streams;
|
||||
@@ -398,29 +397,15 @@ internal class AchievementViewModel
|
||||
[ThreadAccess(ThreadAccessState.AnyThread)]
|
||||
private async Task<UIAF?> GetUIAFFromClipboardAsync()
|
||||
{
|
||||
UIAF? uiaf = null;
|
||||
string json;
|
||||
try
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
json = await Clipboard.GetContent().GetTextAsync();
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
infoBarService?.Error(ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
uiaf = JsonSerializer.Deserialize<UIAF>(json, options);
|
||||
return await Clipboard.DeserializeTextAsync<UIAF>(options).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
infoBarService?.Error(ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
return uiaf;
|
||||
}
|
||||
|
||||
[ThreadAccess(ThreadAccessState.AnyThread)]
|
||||
@@ -452,7 +437,7 @@ internal class AchievementViewModel
|
||||
{
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
(bool isOk, ImportOption option) = await new AchievementImportDialog(mainWindow, uiaf).GetImportOptionAsync().ConfigureAwait(true);
|
||||
(bool isOk, ImportStrategy strategy) = await new AchievementImportDialog(mainWindow, uiaf).GetImportStrategyAsync().ConfigureAwait(true);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
@@ -466,7 +451,7 @@ internal class AchievementViewModel
|
||||
ImportResult result;
|
||||
await using (await importingDialog.InitializeWithWindow(mainWindow).BlockAsync().ConfigureAwait(false))
|
||||
{
|
||||
result = await achievementService.ImportFromUIAFAsync(archive, uiaf.List, option).ConfigureAwait(false);
|
||||
result = await achievementService.ImportFromUIAFAsync(archive, uiaf.List, strategy).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
infoBarService.Success(result.ToString());
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Core.Threading.CodeAnalysis;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.GachaLog;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.ViewModel;
|
||||
@@ -20,28 +22,25 @@ namespace Snap.Hutao.ViewModel;
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly IGachaLogService gachaLogService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
|
||||
private ObservableCollection<GachaArchive>? archives;
|
||||
private GachaArchive? selectedArchive;
|
||||
private GachaStatistics? statistics;
|
||||
private bool isAggressiveRefresh;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的祈愿记录视图模型
|
||||
/// </summary>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
/// <param name="gachaLogService">祈愿记录服务</param>
|
||||
/// <param name="infoBarService">信息</param>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
public GachaLogViewModel(
|
||||
IMetadataService metadataService,
|
||||
IGachaLogService gachaLogService,
|
||||
IInfoBarService infoBarService,
|
||||
IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
||||
{
|
||||
this.metadataService = metadataService;
|
||||
this.gachaLogService = gachaLogService;
|
||||
this.infoBarService = infoBarService;
|
||||
|
||||
@@ -64,14 +63,30 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
/// <summary>
|
||||
/// 选中的存档
|
||||
/// 切换存档时异步获取对应的统计
|
||||
/// </summary>
|
||||
public GachaArchive? SelectedArchive { get => selectedArchive; set => SetProperty(ref selectedArchive, value); }
|
||||
public GachaArchive? SelectedArchive
|
||||
{
|
||||
get => selectedArchive;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref selectedArchive, value))
|
||||
{
|
||||
UpdateStatisticsAsync(selectedArchive).SafeForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前统计信息
|
||||
/// </summary>
|
||||
public GachaStatistics? Statistics { get => statistics; set => SetProperty(ref statistics, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为贪婪刷新
|
||||
/// </summary>
|
||||
public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 页面加载命令
|
||||
/// </summary>
|
||||
@@ -109,7 +124,7 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
if (await gachaLogService.InitializeAsync().ConfigureAwait(true))
|
||||
{
|
||||
Archives = gachaLogService.GetArchiveCollection();
|
||||
SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
|
||||
@@ -121,14 +136,40 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshByWebCacheAsync()
|
||||
private Task RefreshByWebCacheAsync()
|
||||
{
|
||||
//Statistics = await gachaLogService.RefreshAsync();
|
||||
return RefreshInternalAsync(RefreshOption.WebCache);
|
||||
}
|
||||
|
||||
private async Task RefreshByManualInputAsync()
|
||||
private Task RefreshByManualInputAsync()
|
||||
{
|
||||
return RefreshInternalAsync(RefreshOption.ManualInput);
|
||||
}
|
||||
|
||||
private async Task RefreshInternalAsync(RefreshOption option)
|
||||
{
|
||||
IGachaLogUrlProvider? provider = gachaLogService.GetGachaLogUrlProvider(option);
|
||||
|
||||
if (provider != null)
|
||||
{
|
||||
(bool isOk, string query) = await provider.GetQueryAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge;
|
||||
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
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);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedArchive = gachaLogService.CurrentArchive;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ImportFromUIGFExcelAsync()
|
||||
@@ -150,4 +191,12 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[ThreadAccess(ThreadAccessState.MainThread)]
|
||||
private async Task UpdateStatisticsAsync(GachaArchive? archive)
|
||||
{
|
||||
GachaStatistics temp = await gachaLogService.GetStatisticsAsync(archive).ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Statistics = temp;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +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;
|
||||
|
||||
@@ -36,6 +36,6 @@ internal class EnkaClient
|
||||
/// <returns>Enka API 响应</returns>
|
||||
public Task<EnkaResponse?> GetDataAsync(PlayerUid playerUid, CancellationToken token)
|
||||
{
|
||||
return httpClient.GetFromJsonAsync<EnkaResponse>(string.Format(EnkaAPI, playerUid.Value), token);
|
||||
return httpClient.GetFromJsonAsync<EnkaResponse>(string.Format(EnkaAPIHutaoForward, playerUid.Value), token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,11 @@ internal static class ApiEndpoints
|
||||
|
||||
#region UserFullInfo
|
||||
|
||||
/// <summary>
|
||||
/// BBS 指向引用
|
||||
/// </summary>
|
||||
public const string BbsReferer = "https://bbs.mihoyo.com/";
|
||||
|
||||
/// <summary>
|
||||
/// 用户详细信息
|
||||
/// </summary>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
|
||||
@@ -15,17 +14,20 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
internal class UserClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions jsonSerializerOptions;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<UserClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户信息客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="jsonSerializerOptions">Json序列化选项</param>
|
||||
public UserClient(HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions)
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public UserClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<UserClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.jsonSerializerOptions = jsonSerializerOptions;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,26 +39,10 @@ internal class UserClient
|
||||
public async Task<UserInfo?> GetUserFullInfoAsync(Model.Binding.User user, CancellationToken token = default)
|
||||
{
|
||||
Response<UserFullInfoWrapper>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.GetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiEndpoints.UserFullInfo, jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data?.UserInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取其他用户详细信息
|
||||
/// </summary>
|
||||
/// <param name="user">当前用户</param>
|
||||
/// <param name="uid">米游社Uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>详细信息</returns>
|
||||
public async Task<UserInfo?> GetUserFullInfoAsync(Model.Binding.User user, string uid, CancellationToken token = default)
|
||||
{
|
||||
Response<UserFullInfoWrapper>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.GetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiEndpoints.UserFullInfoQuery(uid), jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
.SetUser(user)
|
||||
.SetReferer(ApiEndpoints.BbsReferer)
|
||||
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiEndpoints.UserFullInfo, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data?.UserInfo;
|
||||
}
|
||||
|
||||
@@ -2,52 +2,43 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Convert;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
|
||||
/// <summary>
|
||||
/// 为MiHoYo接口请求器 <see cref="Requester"/> 提供2代动态密钥
|
||||
/// 为MiHoYo接口请求器 <see cref="Requester"/> 提供动态密钥
|
||||
/// </summary>
|
||||
internal abstract class DynamicSecretProvider : Md5Convert
|
||||
{
|
||||
private const string RandomRange = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
|
||||
/// <summary>
|
||||
/// 创建动态密钥
|
||||
/// </summary>
|
||||
/// <param name="options">json格式化器</param>
|
||||
/// <param name="queryUrl">查询url</param>
|
||||
/// <param name="postBody">请求体</param>
|
||||
/// <returns>密钥</returns>
|
||||
public static string Create(JsonSerializerOptions options, string queryUrl, object? postBody = null)
|
||||
public static string Create()
|
||||
{
|
||||
// unix timestamp
|
||||
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
// random
|
||||
int r = GetRandom();
|
||||
string r = GetRandomString();
|
||||
|
||||
// body
|
||||
string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options);
|
||||
|
||||
// query
|
||||
string q = string.Join("&", new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x));
|
||||
|
||||
// check
|
||||
string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecretSalt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();
|
||||
string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret1Salt}&t={t}&r={r}").ToLowerInvariant();
|
||||
|
||||
return $"{t},{r},{check}";
|
||||
}
|
||||
|
||||
private static int GetRandom()
|
||||
private static string GetRandomString()
|
||||
{
|
||||
// 原汁原味
|
||||
// v16 = time(0LL);
|
||||
// srand(v16);
|
||||
// v17 = (int)((double)rand() / 2147483650.0 * 100000.0 + 100000.0) % 1000000;
|
||||
// if (v17 >= 100001)
|
||||
// v18 = v17;
|
||||
// else
|
||||
// v18 = v17 + 542367;
|
||||
int rand = Random.Shared.Next(100000, 200000);
|
||||
return rand == 100000 ? 642367 : rand;
|
||||
StringBuilder sb = new(6);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
int pos = Random.Shared.Next(0, RandomRange.Length);
|
||||
sb.Append(RandomRange[pos]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Convert;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
|
||||
/// <summary>
|
||||
/// 为MiHoYo接口请求器 <see cref="Requester"/> 提供2代动态密钥
|
||||
/// </summary>
|
||||
internal abstract class DynamicSecretProvider2 : Md5Convert
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建动态密钥
|
||||
/// </summary>
|
||||
/// <param name="options">json格式化器</param>
|
||||
/// <param name="queryUrl">查询url</param>
|
||||
/// <param name="postBody">请求体</param>
|
||||
/// <returns>密钥</returns>
|
||||
public static string Create(JsonSerializerOptions options, string queryUrl, object? postBody = null)
|
||||
{
|
||||
// unix timestamp
|
||||
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
// random
|
||||
int r = GetRandom();
|
||||
|
||||
// body
|
||||
string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options);
|
||||
|
||||
// query
|
||||
string q = string.Join("&", new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x));
|
||||
|
||||
// check
|
||||
string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret2Salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();
|
||||
|
||||
return $"{t},{r},{check}";
|
||||
}
|
||||
|
||||
private static int GetRandom()
|
||||
{
|
||||
// 原汁原味
|
||||
// v16 = time(0LL);
|
||||
// srand(v16);
|
||||
// v17 = (int)((double)rand() / 2147483650.0 * 100000.0 + 100000.0) % 1000000;
|
||||
// if (v17 >= 100001)
|
||||
// v18 = v17;
|
||||
// else
|
||||
// v18 = v17 + 542367;
|
||||
int rand = Random.Shared.Next(100000, 200000);
|
||||
return rand == 100000 ? 642367 : rand;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Request;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using Snap.Hutao.Web.Request;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret.Http;
|
||||
|
||||
@@ -30,7 +30,7 @@ internal class DynamicSecretHttpClient : IDynamicSecretHttpClient
|
||||
this.options = options;
|
||||
this.url = url;
|
||||
|
||||
httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider.Create(options, url, null));
|
||||
httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider2.Create(options, url, null));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -67,7 +67,7 @@ internal class DynamicSecretHttpClient<TValue> : IDynamicSecretHttpClient<TValue
|
||||
this.url = url;
|
||||
this.data = data;
|
||||
|
||||
httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider.Create(options, url, data));
|
||||
httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider2.Create(options, url, data));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret.Http;
|
||||
using Snap.Hutao.Web.Request;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
@@ -11,6 +12,17 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
/// </summary>
|
||||
internal static class HttpClientDynamicSecretExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用一代动态密钥执行 GET 操作
|
||||
/// </summary>
|
||||
/// <param name="httpClient">请求器</param>
|
||||
/// <returns>响应</returns>
|
||||
public static HttpClient UsingDynamicSecret(this HttpClient httpClient)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider.Create());
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用二代动态密钥执行 GET 操作
|
||||
/// </summary>
|
||||
|
||||
@@ -11,25 +11,30 @@ public enum GachaConfigType
|
||||
/// <summary>
|
||||
/// 新手池
|
||||
/// </summary>
|
||||
[Description("新手祈愿")]
|
||||
NoviceWish = 100,
|
||||
|
||||
/// <summary>
|
||||
/// 常驻池
|
||||
/// </summary>
|
||||
[Description("常驻祈愿")]
|
||||
PermanentWish = 200,
|
||||
|
||||
/// <summary>
|
||||
/// 角色1池
|
||||
/// </summary>
|
||||
[Description("角色活动祈愿")]
|
||||
AvatarEventWish = 301,
|
||||
|
||||
/// <summary>
|
||||
/// 武器池
|
||||
/// </summary>
|
||||
[Description("武器活动祈愿")]
|
||||
WeaponEventWish = 302,
|
||||
|
||||
/// <summary>
|
||||
/// 角色2池
|
||||
/// </summary>
|
||||
[Description("角色活动祈愿-2")]
|
||||
AvatarEventWish2 = 400,
|
||||
}
|
||||
@@ -10,6 +10,11 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
/// </summary>
|
||||
public struct GachaLogConfigration
|
||||
{
|
||||
/// <summary>
|
||||
/// 尺寸
|
||||
/// </summary>
|
||||
public const int Size = 20;
|
||||
|
||||
private readonly QueryString innerQuery;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,37 +23,24 @@ public struct GachaLogConfigration
|
||||
/// <param name="query">原始查询字符串</param>
|
||||
/// <param name="type">祈愿类型</param>
|
||||
/// <param name="endId">终止Id</param>
|
||||
public GachaLogConfigration(string query, GachaConfigType type, ulong endId = 0UL)
|
||||
public GachaLogConfigration(string query, GachaConfigType type, long endId = 0L)
|
||||
{
|
||||
innerQuery = QueryString.Parse(query);
|
||||
innerQuery.Set("lang", "zh-cn");
|
||||
|
||||
Size = 20;
|
||||
Type = type;
|
||||
innerQuery.Set("lang", "zh-cn");
|
||||
innerQuery.Set("gacha_type", (int)type);
|
||||
innerQuery.Set("size", Size);
|
||||
|
||||
EndId = endId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尺寸
|
||||
/// </summary>
|
||||
public int Size
|
||||
{
|
||||
set => innerQuery.Set("size", value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 类型
|
||||
/// </summary>
|
||||
public GachaConfigType Type
|
||||
{
|
||||
set => innerQuery.Set("gacha_type", (int)value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结束Id
|
||||
/// 控制API返回的分页
|
||||
/// </summary>
|
||||
public ulong EndId
|
||||
public long EndId
|
||||
{
|
||||
get => long.Parse(innerQuery["end_id"]);
|
||||
set => innerQuery.Set("end_id", value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Web.Request;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpClient"/> 扩展
|
||||
/// </summary>
|
||||
internal static class HttpClientCookieExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置用户的Cookie
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="user">用户</param>
|
||||
/// <returns>客户端</returns>
|
||||
internal static HttpClient SetUser(this HttpClient httpClient, User user)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie);
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Web.Request;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpClient"/> 扩展
|
||||
/// </summary>
|
||||
internal static class HttpClientExtensions
|
||||
{
|
||||
/// <inheritdoc cref="HttpClientJsonExtensions.GetFromJsonAsync{TValue}(HttpClient, string?, JsonSerializerOptions?, CancellationToken)"/>
|
||||
internal static async Task<T?> TryCatchGetFromJsonAsync<T>(this HttpClient httpClient, string requestUri, JsonSerializerOptions options, ILogger logger, CancellationToken token = default)
|
||||
where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
return await httpClient.GetFromJsonAsync<T>(requestUri, options, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
logger.LogWarning(EventIds.HttpException, ex, "请求异常已忽略");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置用户的Cookie
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="user">用户</param>
|
||||
/// <returns>客户端</returns>
|
||||
internal static HttpClient SetUser(this HttpClient httpClient, User user)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie);
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置Referer
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="referer">用户</param>
|
||||
/// <returns>客户端</returns>
|
||||
internal static HttpClient SetReferer(this HttpClient httpClient, string referer)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Set("Referer", referer);
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
|
||||
@@ -17,15 +16,21 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
internal class UserGameRoleClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<UserGameRoleClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户游戏角色提供器
|
||||
/// </summary>
|
||||
/// <param name="userService">用户服务</param>
|
||||
/// <param name="httpClient">请求器</param>
|
||||
public UserGameRoleClient(HttpClient httpClient)
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public UserGameRoleClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<UserGameRoleClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -38,9 +43,9 @@ internal class UserGameRoleClient
|
||||
{
|
||||
Response<ListWrapper<UserGameRole>>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
.GetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiEndpoints.UserGameRoles, token)
|
||||
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiEndpoints.UserGameRoles, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data?.List);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ public struct QueryString
|
||||
/// </summary>
|
||||
/// <param name="queryString">
|
||||
/// The query string to deserialize.
|
||||
/// This should NOT have a leading ? character.
|
||||
/// Valid input would be something like "a=1&b=5".
|
||||
/// URL decoding of keys/values is automatically performed.
|
||||
/// Also supports query strings that are serialized using ; instead of &, like "a=1;b=5"</param>
|
||||
@@ -56,6 +55,9 @@ public struct QueryString
|
||||
return new QueryString();
|
||||
}
|
||||
|
||||
int questionMarkIndex = queryString.IndexOf('?');
|
||||
queryString = queryString[(questionMarkIndex + 1)..];
|
||||
|
||||
string[] pairs = queryString.Split('&', ';');
|
||||
QueryString answer = new();
|
||||
foreach (string pair in pairs)
|
||||
|
||||
@@ -39,15 +39,20 @@ public enum KnownReturnCode : int
|
||||
RET_NEED_AIGIS = -3101,
|
||||
|
||||
/// <summary>
|
||||
/// 尚未登录
|
||||
/// 访问过于频繁
|
||||
/// </summary>
|
||||
RET_TOKEN_INVALID = -100,
|
||||
VIsitTooFrequently = -110,
|
||||
|
||||
/// <summary>
|
||||
/// 验证密钥过期
|
||||
/// </summary>
|
||||
AuthKeyTimeOut = -101,
|
||||
|
||||
/// <summary>
|
||||
/// 尚未登录
|
||||
/// </summary>
|
||||
RET_TOKEN_INVALID = -100,
|
||||
|
||||
/// <summary>
|
||||
/// Ok
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user