[skip ci] uigf 4 support part 1

This commit is contained in:
DismissedLight
2024-07-12 17:06:59 +08:00
parent 83f5f25324
commit 05d0faf131
39 changed files with 328 additions and 290 deletions

View File

@@ -1,28 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao;
/// <summary>
/// 应用程序资源提供器
/// </summary>
[Injection(InjectAs.Transient, typeof(IAppResourceProvider))]
internal sealed class AppResourceProvider : IAppResourceProvider
{
private readonly App app;
/// <summary>
/// 构造一个新的应用程序资源提供器
/// </summary>
/// <param name="app">应用</param>
public AppResourceProvider(App app)
{
this.app = app;
}
/// <inheritdoc/>
public T GetResource<T>(string name)
{
return (T)app.Resources[name];
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Globalization;
namespace Snap.Hutao.Core.Json.Converter;
internal sealed class DateTimeConverter : JsonConverter<DateTime>
{
private const string Format = "yyyy-MM-dd HH:mm:ss";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is { } dataTimeString)
{
return DateTime.ParseExact(dataTimeString, Format, CultureInfo.InvariantCulture);
}
return default;
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format, CultureInfo.InvariantCulture));
}
}

View File

@@ -3,33 +3,14 @@
namespace Snap.Hutao.Core.Threading.Abstraction;
/// <summary>
/// 表示一个可等待对象,如果一个方法返回此类型的实例,则此方法可以使用 <see langword="await"/> 异步等待。
/// </summary>
/// <typeparam name="TAwaiter">用于给 await 确定返回时机的 IAwaiter 的实例。</typeparam>
internal interface IAwaitable<out TAwaiter>
where TAwaiter : IAwaiter
{
/// <summary>
/// 获取一个可用于 await 关键字异步等待的异步等待对象。
/// 此方法会被编译器自动调用。
/// </summary>
/// <returns>等待器</returns>
TAwaiter GetAwaiter();
}
/// <summary>
/// 表示一个包含返回值的可等待对象,如果一个方法返回此类型的实例,则此方法可以使用 <see langword="await"/> 异步等待返回值。
/// </summary>
/// <typeparam name="TAwaiter">用于给 await 确定返回时机的 <see cref="IAwaiter{TResult}"/> 的实例。</typeparam>
/// <typeparam name="TResult">异步返回的返回值类型。</typeparam>
internal interface IAwaitable<out TAwaiter, out TResult>
where TAwaiter : IAwaiter<TResult>
{
/// <summary>
/// 获取一个可用于 await 关键字异步等待的异步等待对象。
/// 此方法会被编译器自动调用。
/// </summary>
/// <returns>等待器</returns>
TAwaiter GetAwaiter();
}

View File

@@ -5,38 +5,16 @@ using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Threading.Abstraction;
/// <summary>
/// 用于给 await 确定异步返回的时机。
/// </summary>
internal interface IAwaiter : INotifyCompletion
{
/// <summary>
/// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常);此状态会被编译器自动调用。
/// 在实现中,为了达到各种效果,可以灵活应用其值:可以始终为 true或者始终为 false。
/// </summary>
bool IsCompleted { get; }
/// <summary>
/// 此方法会被编译器在 await 结束时自动调用以获取返回状态(包括异常)。
/// </summary>
void GetResult();
}
/// <summary>
/// 用于给 await 确定异步返回的时机,并获取到返回值。
/// </summary>
/// <typeparam name="TResult">异步返回的返回值类型。</typeparam>
internal interface IAwaiter<out TResult> : INotifyCompletion
{
/// <summary>
/// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常);此状态会被编译器自动调用。
/// 在实现中,为了达到各种效果,可以灵活应用其值:可以始终为 true或者始终为 false。
/// </summary>
bool IsCompleted { get; }
/// <summary>
/// 获取此异步等待操作的返回值,此方法会被编译器在 await 结束时自动调用以获取返回值(包括异常)。
/// </summary>
/// <returns>异步操作的返回值。</returns>
TResult GetResult();
}

View File

@@ -5,19 +5,10 @@ using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Threading.Abstraction;
/// <summary>
/// 当执行关键代码(此代码中的错误可能给应用程序中的其他状态造成负面影响)时,
/// 用于给 await 确定异步返回的时机。
/// </summary>
internal interface ICriticalAwaiter : IAwaiter, ICriticalNotifyCompletion
{
}
/// <summary>
/// 当执行关键代码(此代码中的错误可能给应用程序中的其他状态造成负面影响)时,
/// 用于给 await 确定异步返回的时机,并获取到返回值。
/// </summary>
/// <typeparam name="TResult">异步返回的返回值类型。</typeparam>
internal interface ICriticalAwaiter<out TResult> : IAwaiter<TResult>, ICriticalNotifyCompletion
{
}

View File

@@ -18,6 +18,7 @@ internal readonly struct DispatcherQueueSwitchOperation : IAwaitable<DispatcherQ
public bool IsCompleted
{
// Only yields when we are not on the DispatcherQueue thread.
get => dispatherQueue.HasThreadAccess;
}

View File

@@ -18,7 +18,7 @@ internal readonly struct ThreadPoolSwitchOperation : IAwaitable<ThreadPoolSwitch
public bool IsCompleted
{
// 如果已经处于后台就不再切换新的线程
// Only yields when we are on the DispatcherQueue thread.
get => !dispatherQueue.HasThreadAccess;
}

View File

@@ -1,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao;
/// <summary>
/// 应用程序资源提供器
/// </summary>
internal interface IAppResourceProvider
{
/// <summary>
/// 获取资源
/// </summary>
/// <typeparam name="T">资源的类型</typeparam>
/// <param name="key">资源的名称</param>
/// <returns>资源</returns>
T GetResource<T>(string key);
}

View File

@@ -17,8 +17,8 @@ namespace Snap.Hutao.Model.Entity;
[Table("gacha_items")]
internal sealed partial class GachaItem
: IDbMappingForeignKeyFrom<GachaItem, GachaLogItem, uint>,
IDbMappingForeignKeyFrom<GachaItem, UIGFItem, uint>,
IDbMappingForeignKeyFrom<GachaItem, UIGFItem>,
IDbMappingForeignKeyFrom<GachaItem, LegacyUIGFItem, uint>,
IDbMappingForeignKeyFrom<GachaItem, LegacyUIGFItem>,
IDbMappingForeignKeyFrom<GachaItem, Web.Hutao.GachaLog.GachaItem>
{
/// <summary>
@@ -93,7 +93,7 @@ internal sealed partial class GachaItem
/// <param name="item">祈愿物品</param>
/// <param name="itemId">物品Id</param>
/// <returns>新的祈愿物品</returns>
public static GachaItem From(in Guid archiveId, in UIGFItem item, in uint itemId)
public static GachaItem From(in Guid archiveId, in LegacyUIGFItem item, in uint itemId)
{
return new()
{
@@ -112,7 +112,7 @@ internal sealed partial class GachaItem
/// <param name="archiveId">存档Id</param>
/// <param name="item">祈愿物品</param>
/// <returns>新的祈愿物品</returns>
public static GachaItem From(in Guid archiveId, in UIGFItem item)
public static GachaItem From(in Guid archiveId, in LegacyUIGFItem item)
{
return new()
{

View File

@@ -12,7 +12,8 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// https://uigf.org/standards/UIGF.html
/// </summary>
[HighQuality]
internal sealed class UIGF : IJsonOnSerializing, IJsonOnDeserialized
[Obsolete]
internal sealed class LegacyUIGF : IJsonOnSerializing, IJsonOnDeserialized
{
/// <summary>
/// 当前版本
@@ -24,19 +25,19 @@ internal sealed class UIGF : IJsonOnSerializing, IJsonOnDeserialized
/// </summary>
[JsonRequired]
[JsonPropertyName("info")]
public UIGFInfo Info { get; set; } = default!;
public LegacyUIGFInfo Info { get; set; } = default!;
/// <summary>
/// 列表
/// </summary>
[JsonPropertyName("list")]
public List<UIGFItem> List { get; set; } = default!;
public List<LegacyUIGFItem> List { get; set; } = default!;
public void OnSerializing()
{
TimeSpan offset = GetRegionTimeZoneUtcOffset();
foreach (UIGFItem item in List)
foreach (LegacyUIGFItem item in List)
{
item.Time = item.Time.ToOffset(offset);
}
@@ -47,30 +48,30 @@ internal sealed class UIGF : IJsonOnSerializing, IJsonOnDeserialized
// Adjust items timezone
TimeSpan offset = GetRegionTimeZoneUtcOffset();
foreach (UIGFItem item in List)
foreach (LegacyUIGFItem item in List)
{
item.Time = UnsafeDateTimeOffset.AdjustOffsetOnly(item.Time, offset);
}
}
public bool IsCurrentVersionSupported(out UIGFVersion version)
public bool IsCurrentVersionSupported(out LegacyUIGFVersion version)
{
version = Info.UIGFVersion switch
{
"v2.1" => UIGFVersion.Major2Minor2OrLower,
"v2.2" => UIGFVersion.Major2Minor2OrLower,
"v2.3" => UIGFVersion.Major2Minor3OrHigher,
"v2.4" => UIGFVersion.Major2Minor3OrHigher,
"v3.0" => UIGFVersion.Major2Minor3OrHigher,
_ => UIGFVersion.NotSupported,
"v2.1" => LegacyUIGFVersion.Major2Minor2OrLower,
"v2.2" => LegacyUIGFVersion.Major2Minor2OrLower,
"v2.3" => LegacyUIGFVersion.Major2Minor3OrHigher,
"v2.4" => LegacyUIGFVersion.Major2Minor3OrHigher,
"v3.0" => LegacyUIGFVersion.Major2Minor3OrHigher,
_ => LegacyUIGFVersion.NotSupported,
};
return version != UIGFVersion.NotSupported;
return version != LegacyUIGFVersion.NotSupported;
}
public bool IsMajor2Minor2OrLowerListValid([NotNullWhen(false)] out long id)
{
foreach (ref readonly UIGFItem item in CollectionsMarshal.AsSpan(List))
foreach (ref readonly LegacyUIGFItem item in CollectionsMarshal.AsSpan(List))
{
if (item.ItemType != SH.ModelInterchangeUIGFItemTypeAvatar && item.ItemType != SH.ModelInterchangeUIGFItemTypeWeapon)
{
@@ -85,7 +86,7 @@ internal sealed class UIGF : IJsonOnSerializing, IJsonOnDeserialized
public bool IsMajor2Minor3OrHigherListValid([NotNullWhen(false)] out long id)
{
foreach (ref readonly UIGFItem item in CollectionsMarshal.AsSpan(List))
foreach (ref readonly LegacyUIGFItem item in CollectionsMarshal.AsSpan(List))
{
if (string.IsNullOrEmpty(item.ItemId))
{

View File

@@ -12,7 +12,8 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// UIGF格式的信息
/// </summary>
[HighQuality]
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, CultureOptions, string>
[Obsolete]
internal sealed class LegacyUIGFInfo : IMappingFrom<LegacyUIGFInfo, RuntimeOptions, CultureOptions, string>
{
/// <summary>
/// 用户Uid
@@ -65,7 +66,7 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, CultureO
[JsonPropertyName("region_time_zone")]
public int? RegionTimeZone { get; set; } = default!;
public static UIGFInfo From(RuntimeOptions runtimeOptions, CultureOptions cultureOptions, string uid)
public static LegacyUIGFInfo From(RuntimeOptions runtimeOptions, CultureOptions cultureOptions, string uid)
{
return new()
{
@@ -74,7 +75,7 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, CultureO
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = runtimeOptions.Version.ToString(),
UIGFVersion = UIGF.CurrentVersion,
UIGFVersion = LegacyUIGF.CurrentVersion,
RegionTimeZone = PlayerUid.GetRegionTimeZoneUtcOffsetForUid(uid).Hours,
};
}

View File

@@ -13,7 +13,8 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// UIGF物品
/// </summary>
[HighQuality]
internal sealed class UIGFItem : GachaLogItem, IMappingFrom<UIGFItem, GachaItem, INameQualityAccess>
[Obsolete]
internal sealed class LegacyUIGFItem : GachaLogItem, IMappingFrom<LegacyUIGFItem, GachaItem, INameQualityAccess>
{
/// <summary>
/// 额外祈愿映射
@@ -22,7 +23,7 @@ internal sealed class UIGFItem : GachaLogItem, IMappingFrom<UIGFItem, GachaItem,
[JsonEnum(JsonSerializeType.NumberString)]
public GachaType UIGFGachaType { get; set; } = default!;
public static UIGFItem From(GachaItem item, INameQualityAccess nameQuality)
public static LegacyUIGFItem From(GachaItem item, INameQualityAccess nameQuality)
{
return new()
{
@@ -32,7 +33,7 @@ internal sealed class UIGFItem : GachaLogItem, IMappingFrom<UIGFItem, GachaItem,
Time = item.Time,
Name = nameQuality.Name,
ItemType = GetItemTypeStringByItemId(item.ItemId),
Rank = nameQuality.Quality,
RankType = nameQuality.Quality,
Id = item.Id,
UIGFGachaType = item.QueryType,
};

View File

@@ -6,7 +6,8 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// <summary>
/// UIGF版本
/// </summary>
internal enum UIGFVersion
[Obsolete]
internal enum LegacyUIGFVersion
{
/// <summary>
/// 不支持的版本

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Json.Annotation;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
namespace Snap.Hutao.Model.InterChange;
internal sealed class Hk4eItem : IMappingFrom<Hk4eItem, GachaItem>
{
[JsonPropertyName("uigf_gacha_type")]
[JsonEnum(JsonSerializeType.NumberString)]
public required GachaType UIGFGachaType { get; set; }
[JsonPropertyName("gacha_type")]
[JsonEnum(JsonSerializeType.NumberString)]
public required GachaType GachaType { get; set; }
[JsonPropertyName("item_id")]
public required uint ItemId { get; set; }
[JsonPropertyName("time")]
[JsonConverter(typeof(Core.Json.Converter.DateTimeConverter))]
public required DateTime Time { get; set; }
[JsonPropertyName("id")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public required long Id { get; set; } = default!;
public static Hk4eItem From(GachaItem item)
{
return new()
{
UIGFGachaType = item.QueryType,
GachaType = item.GachaType,
ItemId = item.ItemId,
Time = item.Time.UtcDateTime,
Id = item.Id,
};
}
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
internal sealed class HutaoReserved
{
public required uint Version { get; set; }
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
internal sealed class UIGF
{
[JsonPropertyName("info")]
public required UIGFInfo Info { get; set; }
[JsonPropertyName("hk4e")]
public List<UIGFEntry<Hk4eItem>>? Hk4e { get; set; }
[JsonPropertyName("snap_hutao_reserved")]
public HutaoReserved? HutaoReserved { get; set; }
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
internal sealed class UIGFEntry<TItem>
{
[JsonPropertyName("uid")]
public required string Uid { get; set; }
[JsonPropertyName("timezone")]
public required int TimeZone { get; set; }
[JsonPropertyName("lang")]
public string? Language { get; set; }
[JsonPropertyName("list")]
public required List<TItem> List { get; set; }
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
internal sealed class UIGFInfo
{
[JsonPropertyName("export_timestamp")]
public required long ExportTimestamp { get; set; }
[JsonPropertyName("export_app")]
public required string ExportApp { get; set; }
[JsonPropertyName("export_app_version")]
public required string ExportAppVersion { get; set; }
[JsonPropertyName("version")]
public required string Version { get; set; }
}

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.InterChange;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.ObjectModel;

View File

@@ -85,12 +85,12 @@ internal sealed partial class GachaLogService : IGachaLogService
return statistics;
}
public ValueTask<UIGF> ExportToUIGFAsync(GachaArchive archive)
public ValueTask<LegacyUIGF> ExportToUIGFAsync(GachaArchive archive)
{
return gachaLogExportService.ExportAsync(context, archive);
}
public async ValueTask ImportFromUIGFAsync(UIGF uigf)
public async ValueTask ImportFromUIGFAsync(LegacyUIGF uigf)
{
ArgumentNullException.ThrowIfNull(Archives);
await gachaLogImportService.ImportAsync(context, uigf, Archives).ConfigureAwait(false);

View File

@@ -24,7 +24,7 @@ internal interface IGachaLogService
/// </summary>
/// <param name="archive">存档</param>
/// <returns>UIGF对象</returns>
ValueTask<UIGF> ExportToUIGFAsync(GachaArchive archive);
ValueTask<LegacyUIGF> ExportToUIGFAsync(GachaArchive archive);
/// <summary>
/// 获得对应的祈愿统计
@@ -45,7 +45,7 @@ internal interface IGachaLogService
/// </summary>
/// <param name="uigf">信息</param>
/// <returns>任务</returns>
ValueTask ImportFromUIGFAsync(UIGF uigf);
ValueTask ImportFromUIGFAsync(LegacyUIGF uigf);
/// <summary>
/// 异步初始化

View File

@@ -17,5 +17,5 @@ internal interface IUIGFExportService
/// <param name="context">元数据上下文</param>
/// <param name="archive">存档</param>
/// <returns>UIGF</returns>
ValueTask<UIGF> ExportAsync(GachaLogServiceMetadataContext context, GachaArchive archive);
ValueTask<LegacyUIGF> ExportAsync(GachaLogServiceMetadataContext context, GachaArchive archive);
}

View File

@@ -12,5 +12,5 @@ namespace Snap.Hutao.Service.GachaLog;
/// </summary>
internal interface IUIGFImportService
{
ValueTask ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, AdvancedDbCollectionView<GachaArchive> archives);
ValueTask ImportAsync(GachaLogServiceMetadataContext context, LegacyUIGF uigf, AdvancedDbCollectionView<GachaArchive> archives);
}

View File

@@ -20,15 +20,15 @@ internal sealed partial class UIGFExportService : IUIGFExportService
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async ValueTask<UIGF> ExportAsync(GachaLogServiceMetadataContext context, GachaArchive archive)
public async ValueTask<LegacyUIGF> ExportAsync(GachaLogServiceMetadataContext context, GachaArchive archive)
{
await taskContext.SwitchToBackgroundAsync();
List<GachaItem> entities = gachaLogDbService.GetGachaItemListByArchiveId(archive.InnerId);
List<UIGFItem> list = entities.SelectList(i => UIGFItem.From(i, context.GetNameQualityByItemId(i.ItemId)));
List<LegacyUIGFItem> list = entities.SelectList(i => LegacyUIGFItem.From(i, context.GetNameQualityByItemId(i.ItemId)));
UIGF uigf = new()
LegacyUIGF uigf = new()
{
Info = UIGFInfo.From(runtimeOptions, cultureOptions, archive.Uid),
Info = LegacyUIGFInfo.From(runtimeOptions, cultureOptions, archive.Uid),
List = list,
};

View File

@@ -18,11 +18,11 @@ internal sealed partial class UIGFImportService : IUIGFImportService
private readonly CultureOptions cultureOptions;
private readonly ITaskContext taskContext;
public async ValueTask ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, AdvancedDbCollectionView<GachaArchive> archives)
public async ValueTask ImportAsync(GachaLogServiceMetadataContext context, LegacyUIGF uigf, AdvancedDbCollectionView<GachaArchive> archives)
{
await taskContext.SwitchToBackgroundAsync();
if (!uigf.IsCurrentVersionSupported(out UIGFVersion version))
if (!uigf.IsCurrentVersionSupported(out LegacyUIGFVersion version))
{
HutaoException.InvalidOperation(SH.ServiceUIGFImportUnsupportedVersion);
}
@@ -30,7 +30,7 @@ internal sealed partial class UIGFImportService : IUIGFImportService
// v2.3+ supports any locale
// v2.2 only supports matched locale
// v2.1 only supports CHS
if (version is UIGFVersion.Major2Minor2OrLower)
if (version is LegacyUIGFVersion.Major2Minor2OrLower)
{
if (!cultureOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
{
@@ -45,7 +45,7 @@ internal sealed partial class UIGFImportService : IUIGFImportService
}
}
if (version is UIGFVersion.Major2Minor3OrHigher)
if (version is LegacyUIGFVersion.Major2Minor3OrHigher)
{
if (!uigf.IsMajor2Minor3OrHigherListValid(out long id))
{
@@ -65,12 +65,12 @@ internal sealed partial class UIGFImportService : IUIGFImportService
List<GachaItem> currentTypedList = version switch
{
UIGFVersion.Major2Minor3OrHigher => uigf.List
LegacyUIGFVersion.Major2Minor3OrHigher => uigf.List
.Where(item => item.UIGFGachaType == queryType && item.Id < trimId)
.OrderByDescending(item => item.Id)
.Select(item => GachaItem.From(archiveId, item))
.ToList(),
UIGFVersion.Major2Minor2OrLower => uigf.List
LegacyUIGFVersion.Major2Minor2OrLower => uigf.List
.Where(item => item.UIGFGachaType == queryType && item.Id < trimId)
.OrderByDescending(item => item.Id)
.Select(item => GachaItem.From(archiveId, item, context.GetItemId(item)))

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.UIGF;
internal interface IUIGFExportService
{
ValueTask<bool> ExportAsync(UIGFExportOptions exportOptions, CancellationToken token);
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.UIGF;
internal interface IUIGFService
{
ValueTask<bool> ExportAsync(UIGFExportOptions exportOptions, CancellationToken token);
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange;
using Snap.Hutao.Service.GachaLog;
using System.IO;
namespace Snap.Hutao.Service.UIGF;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IUIGFExportService), Key = UIGFVersion.UIGF40)]
internal sealed partial class UIGF40ExportService : IUIGFExportService
{
private readonly IGachaLogDbService gachaLogDbService;
private readonly JsonSerializerOptions jsonOptions;
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
public async ValueTask<bool> ExportAsync(UIGFExportOptions exportOptions, CancellationToken token)
{
await taskContext.SwitchToBackgroundAsync();
Model.InterChange.UIGF uigf = new()
{
Info = new()
{
ExportApp = "Snap Hutao",
ExportAppVersion = $"{runtimeOptions.Version}",
ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),
Version = "v4.0",
},
HutaoReserved = new()
{
Version = 1,
},
};
ExportGachaArchives(uigf, exportOptions.GachaArchiveIds);
using (FileStream stream = File.Create(exportOptions.FilePath))
{
await JsonSerializer.SerializeAsync(stream, uigf, jsonOptions, token).ConfigureAwait(false);
}
return true;
}
private void ExportGachaArchives(Model.InterChange.UIGF uigf, List<Guid> archiveIds)
{
List<UIGFEntry<Hk4eItem>> hk4eEntries = [];
foreach (Guid archiveId in archiveIds)
{
GachaArchive? archive = gachaLogDbService.GetGachaArchiveById(archiveId);
ArgumentNullException.ThrowIfNull(archive);
List<GachaItem> dbItems = gachaLogDbService.GetGachaItemListByArchiveId(archiveId);
UIGFEntry<Hk4eItem> entry = new()
{
Uid = archive.Uid,
TimeZone = 0,
List = [.. dbItems.Select(Hk4eItem.From)],
};
hk4eEntries.Add(entry);
}
uigf.Hk4e = hk4eEntries;
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.UIGF;
internal sealed class UIGFExportOptions
{
public string FilePath { get; set; } = default!;
public List<Guid> GachaArchiveIds { get; set; } = [];
public List<Guid> ReservedAchievementArchiveIds { get; set; } = [];
public List<string> ReservedAvatarInfoUids { get; set; } = [];
public List<Guid> ReservedCultivationProjectIds { get; set; } = [];
public List<string> ReservedSpiralAbyssUids { get; set; } = [];
public List<Guid> ReservedUserIds { get; set; } = [];
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.UIGF;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IUIGFService))]
internal sealed partial class UIGFService : IUIGFService
{
private readonly IServiceProvider serviceProvider;
public ValueTask<bool> ExportAsync(UIGFExportOptions exportOptions, CancellationToken token)
{
IUIGFExportService exportService = serviceProvider.GetRequiredKeyedService<IUIGFExportService>(UIGFVersion.UIGF40);
return exportService.ExportAsync(exportOptions, token);
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.UIGF;
internal enum UIGFVersion
{
None,
UIGF40,
}

View File

@@ -158,7 +158,6 @@
<None Remove="UI\Xaml\View\Dialog\CultivatePromotionDeltaDialog.xaml" />
<None Remove="UI\Xaml\View\Dialog\DailyNoteNotificationDialog.xaml" />
<None Remove="UI\Xaml\View\Dialog\DailyNoteWebhookDialog.xaml" />
<None Remove="UI\Xaml\View\Dialog\GachaLogImportDialog.xaml" />
<None Remove="UI\Xaml\View\Dialog\GachaLogRefreshProgressDialog.xaml" />
<None Remove="UI\Xaml\View\Dialog\GachaLogUrlDialog.xaml" />
<None Remove="UI\Xaml\View\Dialog\GeetestCustomUrlDialog.xaml" />

View File

@@ -697,7 +697,8 @@ internal sealed partial class AutoSuggestTokenBox : ListViewBase
// This code is only fires during an edgecase where an item is in the process of being deleted and the user inputs a character before the focus has been redirected to a string container.
if (innerItemsSource[^1] is ITokenStringContainer textEdit)
{
if (ContainerFromIndex(Items.Count - 1) is AutoSuggestTokenBoxItem last) // Should be our last text box
// Should be our last text box
if (ContainerFromIndex(Items.Count - 1) is AutoSuggestTokenBoxItem last)
{
ArgumentNullException.ThrowIfNull(last.AutoSuggestTextBox);
string text = last.AutoSuggestTextBox.Text;

View File

@@ -10,7 +10,9 @@ namespace Snap.Hutao.UI.Xaml.Markup;
[MarkupExtensionReturnType(ReturnType = typeof(string))]
internal sealed class ResourceStringExtension : MarkupExtension
{
public string? Name { get; set; }
private string? name;
public string? Name { get => name; set => name = value is null ? null : string.Intern(value); }
public string? CultureName { get; set; }

View File

@@ -1,70 +0,0 @@
<ContentDialog
x:Class="Snap.Hutao.UI.Xaml.View.Dialog.GachaLogImportDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup"
Title="{shuxm:ResourceString Name=ViewDialogGachaLogImportTitle}"
CloseButtonText="{shuxm:ResourceString Name=ContentDialogCancelCloseButtonText}"
DefaultButton="Primary"
PrimaryButtonText="{shuxm:ResourceString Name=ContentDialogConfirmPrimaryButtonText}"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<cwc:UniformGrid
Grid.Row="0"
ColumnSpacing="16"
Columns="3"
RowSpacing="16">
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportExportApp}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.ExportApp, Mode=OneWay, TargetNullValue=未知}"/>
</cwc:HeaderedContentControl>
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportExportTime}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.ExportDateTime.LocalDateTime, Mode=OneWay, TargetNullValue=未知}"/>
</cwc:HeaderedContentControl>
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportExportAppVersion}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.ExportAppVersion, Mode=OneWay, TargetNullValue=未知}"/>
</cwc:HeaderedContentControl>
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportUIGFExportUIGFVersion}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.UIGFVersion, Mode=OneWay, TargetNullValue=未知}"/>
</cwc:HeaderedContentControl>
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportUIGFExportListCount}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.List.Count, Mode=OneWay, TargetNullValue=未知}"/>
</cwc:HeaderedContentControl>
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportUIGFExportUid}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.Uid, Mode=OneWay, TargetNullValue=未知}"/>
</cwc:HeaderedContentControl>
</cwc:UniformGrid>
</Grid>
</ContentDialog>

View File

@@ -1,41 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Model.InterChange.GachaLog;
namespace Snap.Hutao.UI.Xaml.View.Dialog;
/// <summary>
/// 祈愿记录导入对话框
/// </summary>
[HighQuality]
[DependencyProperty("UIGF", typeof(UIGF))]
internal sealed partial class GachaLogImportDialog : ContentDialog
{
private readonly ITaskContext taskContext;
/// <summary>
/// 构造一个新的祈愿记录导入对话框
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="uigf">uigf数据</param>
public GachaLogImportDialog(IServiceProvider serviceProvider, UIGF uigf)
{
InitializeComponent();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
UIGF = uigf;
}
/// <summary>
/// 异步获取导入选项
/// </summary>
/// <returns>是否导入</returns>
public async ValueTask<bool> GetShouldImportAsync()
{
await taskContext.SwitchToMainThreadAsync();
return await ShowAsync() == ContentDialogResult.Primary;
}
}

View File

@@ -317,4 +317,4 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
infoBarService.Warning(SH.ViewModelAvatatPropertyExportTextError);
}
}
}
}

View File

@@ -241,8 +241,8 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
return;
}
ValueResult<bool, UIGF?> result = await file.DeserializeFromJsonAsync<UIGF>(options).ConfigureAwait(false);
if (result.TryGetValue(out UIGF? uigf))
ValueResult<bool, LegacyUIGF?> result = await file.DeserializeFromJsonAsync<LegacyUIGF>(options).ConfigureAwait(false);
if (result.TryGetValue(out LegacyUIGF? uigf))
{
await TryImportUIGFInternalAsync(uigf).ConfigureAwait(false);
}
@@ -270,7 +270,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
return;
}
UIGF uigf = await gachaLogService.ExportToUIGFAsync(Archives.CurrentItem).ConfigureAwait(false);
LegacyUIGF uigf = await gachaLogService.ExportToUIGFAsync(Archives.CurrentItem).ConfigureAwait(false);
if (await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false))
{
infoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage);
@@ -348,7 +348,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
}
}
private async ValueTask<bool> TryImportUIGFInternalAsync(UIGF uigf)
private async ValueTask<bool> TryImportUIGFInternalAsync(LegacyUIGF uigf)
{
if (!uigf.IsCurrentVersionSupported(out _))
{

View File

@@ -12,68 +12,37 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
[HighQuality]
internal class GachaLogItem
{
/// <summary>
/// 玩家Uid
/// </summary>
[JsonPropertyName("uid")]
public string Uid { get; set; } = default!;
/// <summary>
/// 祈愿类型
/// </summary>
[JsonPropertyName("gacha_type")]
[JsonEnum(JsonSerializeType.NumberString)]
public GachaType GachaType { get; set; } = default!;
/// <summary>
/// 总为 <see cref="string.Empty"/>
/// v2.3 使用了此值
/// </summary>
[JsonPropertyName("item_id")]
public string ItemId { get; set; } = default!;
/// <summary>
/// 个数 一般为 1
/// </summary>
[JsonPropertyName("count")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public int? Count { get; set; }
/// <summary>
/// 时间
/// </summary>
[JsonPropertyName("time")]
[JsonConverter(typeof(Core.Json.Converter.DateTimeOffsetConverter))]
public DateTimeOffset Time { get; set; }
/// <summary>
/// 物品名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 语言
/// </summary>
[JsonPropertyName("lang")]
public string Language { get; set; } = default!;
/// <summary>
/// 物品类型
/// </summary>
[JsonPropertyName("item_type")]
public string ItemType { get; set; } = default!;
/// <summary>
/// 物品稀有等级
/// </summary>
[JsonPropertyName("rank_type")]
[JsonEnum(JsonSerializeType.NumberString)]
public QualityType Rank { get; set; } = default!;
public QualityType RankType { get; set; } = default!;
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public long Id { get; set; } = default!;