gacha part 2

This commit is contained in:
DismissedLight
2022-09-19 23:24:11 +08:00
parent 409041f3f0
commit eeb72209ef
85 changed files with 3346 additions and 356 deletions

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 有名称的对象
/// 指示该对象可通过名称区分
/// </summary>
internal interface INamed
{

View File

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

View File

@@ -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 版本

View 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);
}
}

View File

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

View File

@@ -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.");
}
}
}

View File

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

View File

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

View 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);
}
}

View 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
}
}
}

View File

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

View 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
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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!;
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
/// 法器

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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!;
}

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"profiles": {
"Snap.Hutao (Package)": {
"commandName": "MsixPackage",
"nativeDebugging": true
"nativeDebugging": false
},
"Snap.Hutao (Unpackaged)": {
"commandName": "Project"

View File

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

View File

@@ -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>
/// 异步移除存档

View File

@@ -4,9 +4,9 @@
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 导入选项
/// 导入策略
/// </summary>
public enum ImportOption
public enum ImportStrategy
{
/// <summary>
/// 贪婪合并

View File

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

View File

@@ -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(),
};
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View 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,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,7 +54,7 @@
<DataTemplate>
<Border
CornerRadius="{StaticResource CompatCornerRadius}"
Background="{ThemeResource SystemControlPageBackgroundAltHighBrush}"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
cwu:UIElementExtensions.ClipToBounds="True">
<Grid>
<Grid.RowDefinitions>

View File

@@ -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=&#xE72C;}">
<AppBarButton.Flyout>
<MenuFlyout Placement="Bottom">
<MenuFlyoutItem
Text="从缓存刷新"
Icon="{shcm:FontIcon Glyph=&#xE721;}"
Command="{Binding RefreshByWebCacheCommand}"/>
<MenuFlyoutItem
Text="手动输入Url"
Icon="{shcm:FontIcon Glyph=&#xE765;}"
Command="{Binding RefreshByManualInputCommand}"/>
</MenuFlyout>
</AppBarButton.Flyout>
</AppBarButton>
<AppBarSeparator/>
<AppBarButton Label="导入" Icon="{shcm:FontIcon Glyph=&#xE8B5;}">
<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=&#xE72C;}">
<AppBarButton.Flyout>
<MenuFlyout Placement="Bottom">
<MenuFlyoutItem
Text="从缓存刷新"
Icon="{shcm:FontIcon Glyph=&#xE721;}"
Command="{Binding RefreshByWebCacheCommand}"/>
<MenuFlyoutItem
Text="手动输入Url"
Icon="{shcm:FontIcon Glyph=&#xE765;}"
Command="{Binding RefreshByManualInputCommand}"/>
<ToggleMenuFlyoutItem
Text="全量刷新"
Icon="{shcm:FontIcon Glyph=&#xEA37;}"
IsChecked="{Binding IsAggressiveRefresh}"/>
</MenuFlyout>
</AppBarButton.Flyout>
</AppBarButton>
<AppBarSeparator/>
<AppBarButton Label="导入" Icon="{shcm:FontIcon Glyph=&#xE8B5;}">
<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=&#xEDE1;}">
<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=&#xEDE1;}">
<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>

View File

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

View File

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

View File

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

View File

@@ -68,6 +68,11 @@ internal static class ApiEndpoints
#region UserFullInfo
/// <summary>
/// BBS 指向引用
/// </summary>
public const string BbsReferer = "https://bbs.mihoyo.com/";
/// <summary>
/// 用户详细信息
/// </summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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&amp;b=5".
/// URL decoding of keys/values is automatically performed.
/// Also supports query strings that are serialized using ; instead of &amp;, 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)

View File

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