make gachalog great again

This commit is contained in:
DismissedLight
2024-06-30 16:11:47 +08:00
parent c0f7293921
commit dc6dc94b45
26 changed files with 371 additions and 545 deletions

View File

@@ -39,13 +39,14 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
private readonly ICurrentXamlWindowReference currentWindowReference;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<AppActivation> logger;
private readonly ITaskContext taskContext;
private readonly SemaphoreSlim activateSemaphore = new(1);
public void Activate(HutaoActivationArguments args)
{
HandleActivationExclusiveAsync(args).SafeForget();
HandleActivationExclusiveAsync(args).SafeForget(logger);
async ValueTask HandleActivationExclusiveAsync(HutaoActivationArguments args)
{
@@ -85,12 +86,12 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
public void NotificationInvoked(AppNotificationManager manager, AppNotificationActivatedEventArgs args)
{
HandleAppNotificationActivationAsync(args.Arguments, false).SafeForget();
HandleAppNotificationActivationAsync(args.Arguments, false).SafeForget(logger);
}
public void PostInitialization()
{
RunPostInitializationAsync().SafeForget();
RunPostInitializationAsync().SafeForget(logger);
async ValueTask RunPostInitializationAsync()
{
@@ -100,7 +101,7 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
{
// TODO: Introduced in 1.10.2, remove in later version
{
serviceProvider.GetRequiredService<IJumpListInterop>().ClearAsync().SafeForget();
serviceProvider.GetRequiredService<IJumpListInterop>().ClearAsync().SafeForget(logger);
serviceProvider.GetRequiredService<IScheduleTaskInterop>().UnregisterAllTasks();
}
@@ -109,7 +110,7 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
return;
}
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget(logger);
// RegisterHotKey should be called from main thread
await taskContext.SwitchToMainThreadAsync();
@@ -124,17 +125,17 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
serviceProvider.GetRequiredService<IDiscordService>().SetNormalActivityAsync().SafeForget();
serviceProvider.GetRequiredService<IQuartzService>().StartAsync().SafeForget();
serviceProvider.GetRequiredService<IDiscordService>().SetNormalActivityAsync().SafeForget(logger);
serviceProvider.GetRequiredService<IQuartzService>().StartAsync().SafeForget(logger);
if (serviceProvider.GetRequiredService<IMetadataService>() is IMetadataServiceInitialization metadataServiceInitialization)
{
metadataServiceInitialization.InitializeInternalAsync().SafeForget();
metadataServiceInitialization.InitializeInternalAsync().SafeForget(logger);
}
if (serviceProvider.GetRequiredService<IHutaoUserService>() is IHutaoUserServiceInitialization hutaoUserServiceInitialization)
{
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget();
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget(logger);
}
}
}

View File

@@ -13,6 +13,7 @@ namespace Snap.Hutao.Factory.ContentDialog;
internal sealed partial class ContentDialogFactory : IContentDialogFactory
{
private readonly ICurrentXamlWindowReference currentWindowReference;
private readonly ILogger<ContentDialogFactory> logger;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
@@ -123,7 +124,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
}
finally
{
ShowNextDialog().SafeForget();
ShowNextDialog().SafeForget(logger);
}
});

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.UI.Xaml.Data;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -13,7 +14,9 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("gacha_archives")]
internal sealed partial class GachaArchive : ISelectable, IMappingFrom<GachaArchive, string>
internal sealed partial class GachaArchive : ISelectable,
IAdvancedCollectionViewItem,
IMappingFrom<GachaArchive, string>
{
/// <summary>
/// 内部Id
@@ -39,4 +42,12 @@ internal sealed partial class GachaArchive : ISelectable, IMappingFrom<GachaArch
{
return new() { Uid = uid };
}
public object? GetPropertyValue(string name)
{
return name switch
{
_ => default,
};
}
}

View File

@@ -905,7 +905,8 @@
<value>未正确提供原神路径,或当前设置的路径不正确</value>
</data>
<data name="ServiceGachaLogUrlProviderCachePathNotFound" xml:space="preserve">
<value>找不到原神内置浏览器缓存路径:\n{0}</value>
<value>找不到原神内置浏览器缓存路径:
{0}</value>
</data>
<data name="ServiceGachaLogUrlProviderCacheUrlNotFound" xml:space="preserve">
<value>未找到可用的 Url</value>

View File

@@ -80,7 +80,7 @@ internal sealed partial class AchievementDbService : IAchievementDbService
public async ValueTask RemoveAchievementArchiveAsync(AchievementArchive archive, CancellationToken token = default)
{
// It will cascade deleted the achievements.
// It will cascade delete the achievements.
await this.DeleteAsync(archive, token).ConfigureAwait(false);
}

View File

@@ -7,6 +7,7 @@ using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.UI.Xaml.Data;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hutao.GachaLog;
@@ -201,15 +202,17 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
AsyncBarrier barrier = new(4);
List<HistoryWish> historyWishes = historyWishBuilders
.Where(b => appOptions.IsEmptyHistoryWishVisible || (!b.IsEmpty))
.OrderByDescending(builder => builder.From)
.ThenBy(builder => builder.ConfigType, GachaTypeComparer.Shared)
.Select(builder => builder.ToHistoryWish())
.ToList();
return new()
{
// history
HistoryWishes = historyWishBuilders
.Where(b => appOptions.IsEmptyHistoryWishVisible || (!b.IsEmpty))
.OrderByDescending(builder => builder.From)
.ThenBy(builder => builder.ConfigType, GachaTypeComparer.Shared)
.Select(builder => builder.ToHistoryWish())
.ToList(),
HistoryWishes = taskContext.InvokeOnMainThread(() => new AdvancedCollectionView<HistoryWish>(historyWishes, true)),
// avatars
OrangeAvatars = orangeAvatarCounter.ToStatisticsList(),

View File

@@ -1,19 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿存档初始化上下文
/// </summary>
internal static class GachaArchiveOperation
{
public static void GetOrAdd(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, string uid, ObservableCollection<GachaArchive> archives, [NotNull] out GachaArchive? archive)
public static void GetOrAdd(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, string uid, AdvancedDbCollectionView<GachaArchive> archives, [NotNull] out GachaArchive? archive)
{
archive = archives.SingleOrDefault(a => a.Uid == uid);
archive = archives.SourceCollection.SingleOrDefault(a => a.Uid == uid);
if (archive is not null)
{

View File

@@ -1,51 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿记录获取上下文
/// </summary>
internal struct GachaLogFetchContext
{
/// <summary>
/// 当前处理的存档
/// </summary>
public GachaArchive? TargetArchive;
/// <summary>
/// 当前的获取状态
/// </summary>
public GachaLogFetchStatus FetchStatus = default!;
/// <summary>
/// 当前的数据库 End Id
/// </summary>
public long? DbEndId;
/// <summary>
/// 查询选项
/// </summary>
public GachaLogQueryOptions QueryOptions;
/// <summary>
/// 待加入数据库的物品
/// </summary>
public GachaLogTypedQueryOptions TypedQueryOptions;
public List<GachaItem> ItemsToAdd = default!;
/// <summary>
/// 当前类型增加物品是否结束
/// </summary>
public bool CurrentTypeAddingCompleted;
/// <summary>
/// 当前类型
/// </summary>
public GachaType CurrentType;
private readonly GachaLogServiceMetadataContext serviceContext;
@@ -61,36 +31,22 @@ internal struct GachaLogFetchContext
this.isLazy = isLazy;
}
/// <summary>
/// 为下一个卡池类型重置
/// </summary>
/// <param name="configType">卡池类型</param>
/// <param name="query">查询</param>
public void ResetForProcessingType(GachaType configType, in GachaLogQuery query)
{
DbEndId = null;
CurrentType = configType;
ItemsToAdd = [];
FetchStatus = new(configType);
QueryOptions = new(query, configType);
TypedQueryOptions = new(query, configType);
}
/// <summary>
/// 为下一个物品页面重置
/// </summary>
public void ResetForProcessingPage()
{
FetchStatus = new(CurrentType);
CurrentTypeAddingCompleted = false;
}
/// <summary>
/// 确保 存档 与 EndId 不为空
/// </summary>
/// <param name="item">物品</param>
/// <param name="archives">存档集合</param>
/// <param name="gachaLogDbService">祈愿记录数据库服务</param>
public void EnsureArchiveAndEndId(GachaLogItem item, ObservableCollection<GachaArchive> archives, IGachaLogDbService gachaLogDbService)
public void EnsureArchiveAndEndId(GachaLogItem item, AdvancedDbCollectionView<GachaArchive> archives, IGachaLogDbService gachaLogDbService)
{
if (TargetArchive is null)
{
@@ -100,41 +56,24 @@ internal struct GachaLogFetchContext
DbEndId ??= gachaLogDbService.GetNewestGachaItemIdByArchiveIdAndQueryType(TargetArchive.InnerId, CurrentType);
}
/// <summary>
/// 判断是否应添加
/// </summary>
/// <param name="item">物品</param>
/// <returns>是否应添加</returns>
public readonly bool ShouldAddItem(GachaLogItem item)
{
return !isLazy || item.Id > DbEndId;
}
/// <summary>
/// 判断当前类型已经处理完成
/// </summary>
/// <param name="items">物品集合</param>
/// <returns>当前类型已经处理完成</returns>
public readonly bool ItemsHaveReachEnd(List<GachaLogItem> items)
{
return CurrentTypeAddingCompleted || items.Count < GachaLogQueryOptions.Size;
return CurrentTypeAddingCompleted || items.Count < GachaLogTypedQueryOptions.Size;
}
/// <summary>
/// 添加物品
/// </summary>
/// <param name="item">物品</param>
public void AddItem(GachaLogItem item)
{
ArgumentNullException.ThrowIfNull(TargetArchive);
ItemsToAdd.Add(GachaItem.From(TargetArchive.InnerId, item, serviceContext.GetItemId(item)));
FetchStatus.Items.Add(serviceContext.GetItemByNameAndType(item.Name, item.ItemType));
QueryOptions.EndId = item.Id;
TypedQueryOptions.EndId = item.Id;
}
/// <summary>
/// 保存物品
/// </summary>
public readonly void SaveItems()
{
// While no item is fetched, archive can be null.
@@ -148,26 +87,18 @@ internal struct GachaLogFetchContext
// 全量刷新
if (!isLazy)
{
gachaLogDbService.RemoveNewerGachaItemRangeByArchiveIdQueryTypeAndEndId(TargetArchive.InnerId, QueryOptions.Type, QueryOptions.EndId);
gachaLogDbService.RemoveNewerGachaItemRangeByArchiveIdQueryTypeAndEndId(TargetArchive.InnerId, TypedQueryOptions.Type, TypedQueryOptions.EndId);
}
gachaLogDbService.AddGachaItemRange(ItemsToAdd);
}
}
/// <summary>
/// 完成添加
/// </summary>
public void CompleteAdding()
{
CurrentTypeAddingCompleted = true;
}
/// <summary>
/// 反馈进度
/// </summary>
/// <param name="progress">进度</param>
/// <param name="isAuthKeyTimeout">验证密钥是否过期</param>
public readonly void Report(IProgress<GachaLogFetchStatus> progress, bool isAuthKeyTimeout = false)
{
FetchStatus.AuthKeyTimeout = isAuthKeyTimeout;

View File

@@ -13,47 +13,33 @@ using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿记录服务
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IGachaLogService))]
internal sealed partial class GachaLogService : IGachaLogService
{
private readonly ScopedDbCurrent<GachaArchive, Message.GachaArchiveChangedMessage> dbCurrent;
private readonly IGachaStatisticsSlimFactory gachaStatisticsSlimFactory;
private readonly IGachaStatisticsFactory gachaStatisticsFactory;
private readonly IUIGFExportService gachaLogExportService;
private readonly IUIGFImportService gachaLogImportService;
private readonly IGachaLogDbService gachaLogDbService;
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService;
private readonly ILogger<GachaLogService> logger;
private readonly GachaInfoClient gachaInfoClient;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
private GachaLogServiceMetadataContext context;
private ObservableCollection<GachaArchive>? archiveCollection;
private AdvancedDbCollectionView<GachaArchive>? archives;
/// <inheritdoc/>
public GachaArchive? CurrentArchive
public AdvancedDbCollectionView<GachaArchive>? Archives
{
get => dbCurrent.Current;
set => dbCurrent.Current = value;
get => archives;
private set => archives = value;
}
/// <inheritdoc/>
public ObservableCollection<GachaArchive>? ArchiveCollection
{
get => archiveCollection;
private set => archiveCollection = value;
}
/// <inheritdoc/>
public async ValueTask<bool> InitializeAsync(CancellationToken token = default)
{
if (context is { IsInitialized: true })
@@ -64,7 +50,8 @@ internal sealed partial class GachaLogService : IGachaLogService
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
context = await metadataService.GetContextAsync<GachaLogServiceMetadataContext>(token).ConfigureAwait(false);
ArchiveCollection = gachaLogDbService.GetGachaArchiveCollection();
await taskContext.SwitchToMainThreadAsync();
Archives = new(gachaLogDbService.GetGachaArchiveCollection(), serviceProvider);
return true;
}
else
@@ -73,14 +60,8 @@ internal sealed partial class GachaLogService : IGachaLogService
}
}
/// <inheritdoc/>
public async ValueTask<GachaStatistics> GetStatisticsAsync(GachaArchive? archive)
public async ValueTask<GachaStatistics> GetStatisticsAsync(GachaArchive archive)
{
archive ??= CurrentArchive;
archive ??= ArchiveCollection?.FirstOrDefault();
ArgumentNullException.ThrowIfNull(archive);
// Return statistics
using (ValueStopwatch.MeasureExecution(logger))
{
List<GachaItem> items = await gachaLogDbService.GetGachaItemListByArchiveIdAsync(archive.InnerId).ConfigureAwait(false);
@@ -88,14 +69,13 @@ internal sealed partial class GachaLogService : IGachaLogService
}
}
/// <inheritdoc/>
public async ValueTask<List<GachaStatisticsSlim>> GetStatisticsSlimListAsync(CancellationToken token = default)
{
await InitializeAsync(token).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(ArchiveCollection);
ArgumentNullException.ThrowIfNull(Archives);
List<GachaStatisticsSlim> statistics = [];
foreach (GachaArchive archive in ArchiveCollection)
foreach (GachaArchive archive in Archives)
{
List<GachaItem> items = await gachaLogDbService.GetGachaItemListByArchiveIdAsync(archive.InnerId).ConfigureAwait(false);
GachaStatisticsSlim slim = await gachaStatisticsSlimFactory.CreateAsync(context, items, archive.Uid).ConfigureAwait(false);
@@ -105,43 +85,39 @@ internal sealed partial class GachaLogService : IGachaLogService
return statistics;
}
/// <inheritdoc/>
public ValueTask<UIGF> ExportToUIGFAsync(GachaArchive archive)
{
return gachaLogExportService.ExportAsync(context, archive);
}
/// <inheritdoc/>
public async ValueTask ImportFromUIGFAsync(UIGF uigf)
{
ArgumentNullException.ThrowIfNull(ArchiveCollection);
CurrentArchive = await gachaLogImportService.ImportAsync(context, uigf, ArchiveCollection).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(Archives);
await gachaLogImportService.ImportAsync(context, uigf, Archives).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
public async ValueTask<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategyKind kind, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
{
bool isLazy = strategy switch
bool isLazy = kind switch
{
RefreshStrategy.AggressiveMerge => false,
RefreshStrategy.LazyMerge => true,
RefreshStrategyKind.AggressiveMerge => false,
RefreshStrategyKind.LazyMerge => true,
_ => throw HutaoException.NotSupported(),
};
(bool authkeyValid, GachaArchive? result) = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false);
(bool authkeyValid, GachaArchive? target) = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false);
if (result is not null)
if (target is not null && Archives is not null)
{
CurrentArchive = result;
Archives.CurrentItem = target;
}
return authkeyValid;
}
/// <inheritdoc/>
public async ValueTask RemoveArchiveAsync(GachaArchive archive)
{
ArgumentNullException.ThrowIfNull(archiveCollection);
ArgumentNullException.ThrowIfNull(archives);
// Sync database
await taskContext.SwitchToBackgroundAsync();
@@ -149,32 +125,32 @@ internal sealed partial class GachaLogService : IGachaLogService
// Sync cache
await taskContext.SwitchToMainThreadAsync();
archiveCollection.Remove(archive);
archives.Remove(archive);
}
public async ValueTask<GachaArchive> EnsureArchiveInCollectionAsync(Guid archiveId, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(ArchiveCollection);
ArgumentNullException.ThrowIfNull(Archives);
if (ArchiveCollection.SingleOrDefault(a => a.InnerId == archiveId) is { } archive)
if (Archives.SourceCollection.SingleOrDefault(a => a.InnerId == archiveId) is { } archive)
{
return archive;
}
else
{
// sync cache
GachaArchive? newArchive = await gachaLogDbService.GetGachaArchiveByIdAsync(archiveId, token).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(newArchive);
await taskContext.SwitchToMainThreadAsync();
ArchiveCollection.Add(newArchive);
Archives.Add(newArchive);
return newArchive;
}
}
private async ValueTask<ValueResult<bool, GachaArchive?>> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(ArchiveCollection);
ArgumentNullException.ThrowIfNull(Archives);
GachaLogFetchContext fetchContext = new(gachaLogDbService, taskContext, context, isLazy);
foreach (GachaType configType in GachaLog.QueryTypes)
@@ -184,44 +160,42 @@ internal sealed partial class GachaLogService : IGachaLogService
do
{
Response<GachaLogPage> response = await gachaInfoClient
.GetGachaLogPageAsync(fetchContext.QueryOptions, token)
.GetGachaLogPageAsync(fetchContext.TypedQueryOptions, token)
.ConfigureAwait(false);
if (response.TryGetData(out GachaLogPage? page))
if (!response.TryGetData(out GachaLogPage? page))
{
List<GachaLogItem> items = page.List;
fetchContext.ResetForProcessingPage();
foreach (GachaLogItem item in items)
{
fetchContext.EnsureArchiveAndEndId(item, ArchiveCollection, gachaLogDbService);
if (fetchContext.ShouldAddItem(item))
{
fetchContext.AddItem(item);
}
else
{
fetchContext.CompleteAdding();
break;
}
}
fetchContext.Report(progress);
if (fetchContext.ItemsHaveReachEnd(items))
{
// exit current type fetch loop
break;
}
}
else
{
fetchContext.Report(progress, true);
fetchContext.Report(progress, isAuthKeyTimeout: true);
break;
}
await Delay.RandomMilliSeconds(1000, 2000).ConfigureAwait(false);
List<GachaLogItem> items = page.List;
fetchContext.ResetForProcessingPage();
foreach (GachaLogItem item in items)
{
fetchContext.EnsureArchiveAndEndId(item, Archives, gachaLogDbService);
if (fetchContext.ShouldAddItem(item))
{
fetchContext.AddItem(item);
}
else
{
fetchContext.CompleteAdding();
break;
}
}
fetchContext.Report(progress);
if (fetchContext.ItemsHaveReachEnd(items))
{
// exit current type fetch loop
break;
}
await Task.Delay(Random.Shared.Next(1000, 2000), token).ConfigureAwait(false);
}
while (true);

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.Specialized;
using System.Web;
namespace Snap.Hutao.Service.GachaLog;
internal struct GachaLogTypedQueryOptions
{
public const int Size = 20;
public readonly bool IsOversea;
public readonly GachaType Type;
public long EndId;
private readonly NameValueCollection innerQuery;
public GachaLogTypedQueryOptions(in GachaLogQuery query, GachaType queryType)
{
IsOversea = query.IsOversea;
// 对于每个类型我们需要单独创建
// 对应类型的 GachaLogQueryOptions
Type = queryType;
innerQuery = HttpUtility.ParseQueryString(query.Query);
innerQuery.Set("gacha_type", $"{queryType:D}");
innerQuery.Set("size", $"{Size}");
}
public readonly string ToQueryString()
{
// Make the cached end id into query.
innerQuery.Set("end_id", $"{EndId:D}");
string? query = innerQuery.ToString();
ArgumentException.ThrowIfNullOrEmpty(query);
return query;
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service.GachaLog.QueryProvider;
@@ -15,15 +16,7 @@ namespace Snap.Hutao.Service.GachaLog;
[HighQuality]
internal interface IGachaLogService
{
/// <summary>
/// 当前存档
/// </summary>
GachaArchive? CurrentArchive { get; set; }
/// <summary>
/// 获取可用于绑定的存档集合
/// </summary>
ObservableCollection<GachaArchive>? ArchiveCollection { get; }
AdvancedDbCollectionView<GachaArchive>? Archives { get; }
ValueTask<GachaArchive> EnsureArchiveInCollectionAsync(Guid archiveId, CancellationToken token = default(CancellationToken));
@@ -39,7 +32,7 @@ internal interface IGachaLogService
/// </summary>
/// <param name="archive">存档</param>
/// <returns>祈愿统计</returns>
ValueTask<GachaStatistics> GetStatisticsAsync(GachaArchive? archive);
ValueTask<GachaStatistics> GetStatisticsAsync(GachaArchive archive);
/// <summary>
/// 异步获取简化的祈愿统计列表
@@ -71,7 +64,7 @@ internal interface IGachaLogService
/// <param name="progress">进度</param>
/// <param name="token">取消令牌</param>
/// <returns>验证密钥是否有效</returns>
ValueTask<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress<GachaLogFetchStatus> progress, CancellationToken token);
ValueTask<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategyKind strategy, IProgress<GachaLogFetchStatus> progress, CancellationToken token);
/// <summary>
/// 删除存档

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
@@ -12,12 +12,5 @@ namespace Snap.Hutao.Service.GachaLog;
/// </summary>
internal interface IUIGFImportService
{
/// <summary>
/// 异步从 UIGF 导入
/// </summary>
/// <param name="context">祈愿记录服务上下文</param>
/// <param name="uigf">数据</param>
/// <param name="archives">存档集合</param>
/// <returns>存档</returns>
ValueTask<GachaArchive> ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, ObservableCollection<GachaArchive> archives);
ValueTask ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, AdvancedDbCollectionView<GachaArchive> archives);
}

View File

@@ -41,8 +41,8 @@ internal readonly struct GachaLogQuery
Message = message;
}
public static implicit operator GachaLogQuery(string message)
public static GachaLogQuery Invalid(string message)
{
return new(default!, message);
return new(string.Empty, message);
}
}

View File

@@ -8,10 +8,6 @@ using System.Web;
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// <summary>
/// 手动输入方法提供器
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient)]
internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryProvider
@@ -25,31 +21,25 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP
GachaLogUrlDialog dialog = await contentDialogFactory.CreateInstanceAsync<GachaLogUrlDialog>().ConfigureAwait(false);
(bool isOk, string queryString) = await dialog.GetInputUrlAsync().ConfigureAwait(false);
if (isOk)
if (!isOk)
{
NameValueCollection query = HttpUtility.ParseQueryString(queryString);
return new(false, default);
}
if (query.TryGetSingleValue("auth_appid", out string? appId) && appId is "webview_gacha")
{
string? queryLanguageCode = query["lang"];
if (cultureOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
{
return new(true, new(queryString));
}
else
{
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, cultureOptions.LanguageCode);
return new(false, message);
}
}
else
{
return new(false, SH.ServiceGachaLogUrlProviderManualInputInvalid);
}
}
else
NameValueCollection query = HttpUtility.ParseQueryString(queryString);
if (!query.TryGetSingleValue("auth_appid", out string? appId) || appId is not "webview_gacha")
{
return new(false, string.Empty);
return new(false, GachaLogQuery.Invalid(SH.ServiceGachaLogUrlProviderManualInputInvalid));
}
string? queryLanguageCode = query["lang"];
if (!cultureOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
{
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, cultureOptions.LanguageCode);
return new(false, GachaLogQuery.Invalid(message));
}
return new(true, new(queryString));
}
}

View File

@@ -25,29 +25,25 @@ internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvid
/// <inheritdoc/>
public async ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (!UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
if (userAndUid.User.IsOversea)
{
return new(false, SH.ServiceGachaLogUrlProviderStokenUnsupported);
}
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(userAndUid.Uid);
Response<GameAuthKey> authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(userAndUid.User, data).ConfigureAwait(false);
if (authkeyResponse.IsOk())
{
return new(true, new(ComposeQueryString(data, authkeyResponse.Data, cultureOptions.LanguageCode)));
}
else
{
return new(false, SH.ServiceGachaLogUrlProviderAuthkeyRequestFailed);
}
return new(false, GachaLogQuery.Invalid(SH.MustSelectUserAndUid));
}
else
if (userAndUid.User.IsOversea)
{
return new(false, SH.MustSelectUserAndUid);
return new(false, GachaLogQuery.Invalid(SH.ServiceGachaLogUrlProviderStokenUnsupported));
}
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(userAndUid.Uid);
Response<GameAuthKey> authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(userAndUid.User, data).ConfigureAwait(false);
if (!authkeyResponse.IsOk())
{
return new(false, GachaLogQuery.Invalid(SH.ServiceGachaLogUrlProviderAuthkeyRequestFailed));
}
return new(true, new(ComposeQueryString(data, authkeyResponse.Data, cultureOptions.LanguageCode)));
}
private static string ComposeQueryString(GenAuthKeyData genAuthKeyData, GameAuthKey gameAuthKey, string lang)

View File

@@ -12,10 +12,6 @@ using System.Web;
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// <summary>
/// 浏览器缓存方法
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient)]
internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider
@@ -23,11 +19,6 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
private readonly IGameServiceFacade gameService;
private readonly CultureOptions cultureOptions;
/// <summary>
/// 获取缓存文件路径
/// </summary>
/// <param name="path">游戏路径</param>
/// <returns>缓存文件路径</returns>
public static string GetCacheFile(string path)
{
string exeName = Path.GetFileName(path);
@@ -62,7 +53,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
if (!isOk || string.IsNullOrEmpty(path))
{
return new(false, SH.ServiceGachaLogUrlProviderCachePathInvalid);
return new(false, GachaLogQuery.Invalid(SH.ServiceGachaLogUrlProviderCachePathInvalid));
}
string cacheFile = GetCacheFile(path);
@@ -70,8 +61,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
{
if (!tempFile.TryGetValue(out TempFile file))
{
string unescaped = Regex.Unescape(SH.ServiceGachaLogUrlProviderCachePathNotFound);
return new(false, string.Format(CultureInfo.CurrentCulture, unescaped, cacheFile));
return new(false, GachaLogQuery.Invalid(SH.FormatServiceGachaLogUrlProviderCachePathNotFound(cacheFile)));
}
using (FileStream fileStream = new(file.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
@@ -83,19 +73,19 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
if (string.IsNullOrEmpty(result))
{
return new(false, SH.ServiceGachaLogUrlProviderCacheUrlNotFound);
return new(false, GachaLogQuery.Invalid(SH.ServiceGachaLogUrlProviderCacheUrlNotFound));
}
NameValueCollection query = HttpUtility.ParseQueryString(result.TrimEnd("#/log"));
string? queryLanguageCode = query["lang"];
if (cultureOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
if (!cultureOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
{
return new(true, new(result));
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, cultureOptions.LanguageCode);
return new(false, GachaLogQuery.Invalid(message));
}
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, cultureOptions.LanguageCode);
return new(false, message);
return new(true, new(result));
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.GachaLog;
/// 刷新策略
/// </summary>
[HighQuality]
internal enum RefreshStrategy
internal enum RefreshStrategyKind
{
/// <summary>
/// 无策略 用于切换存档时使用

View File

@@ -1,28 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿记录导入服务
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IUIGFImportService))]
internal sealed partial class UIGFImportService : IUIGFImportService
{
private readonly IGachaLogDbService gachaLogDbService;
private readonly ILogger<UIGFImportService> logger;
private readonly CultureOptions cultureOptions;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async ValueTask<GachaArchive> ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, ObservableCollection<GachaArchive> archives)
public async ValueTask ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, AdvancedDbCollectionView<GachaArchive> archives)
{
await taskContext.SwitchToBackgroundAsync();
@@ -31,9 +27,9 @@ internal sealed partial class UIGFImportService : IUIGFImportService
HutaoException.InvalidOperation(SH.ServiceUIGFImportUnsupportedVersion);
}
// v2.3+ support any locale
// v2.2 only support matched locale
// v2.1 only support CHS
// v2.3+ supports any locale
// v2.2 only supports matched locale
// v2.1 only supports CHS
if (version is UIGFVersion.Major2Minor2OrLower)
{
if (!cultureOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
@@ -70,14 +66,14 @@ internal sealed partial class UIGFImportService : IUIGFImportService
List<GachaItem> currentTypedList = version switch
{
UIGFVersion.Major2Minor3OrHigher => uigf.List
.Where(i => i.UIGFGachaType == queryType && i.Id < trimId)
.OrderByDescending(i => i.Id)
.Select(i => GachaItem.From(archiveId, i))
.Where(item => item.UIGFGachaType == queryType && item.Id < trimId)
.OrderByDescending(item => item.Id)
.Select(item => GachaItem.From(archiveId, item))
.ToList(),
UIGFVersion.Major2Minor2OrLower => uigf.List
.Where(i => i.UIGFGachaType == queryType && i.Id < trimId)
.OrderByDescending(i => i.Id)
.Select(i => GachaItem.From(archiveId, i, context.GetItemId(i)))
.Where(item => item.UIGFGachaType == queryType && item.Id < trimId)
.OrderByDescending(item => item.Id)
.Select(item => GachaItem.From(archiveId, item, context.GetItemId(item)))
.ToList(),
_ => throw HutaoException.NotSupported(),
};
@@ -87,15 +83,15 @@ internal sealed partial class UIGFImportService : IUIGFImportService
}
await gachaLogDbService.AddGachaItemsAsync(fullItems).ConfigureAwait(false);
return archive;
archives.MoveCurrentTo(archive);
}
private static void ThrowIfContainsInvalidItem(List<GachaItem> currentTypeToAdd)
private static void ThrowIfContainsInvalidItem(List<GachaItem> list)
{
// 越早的记录手工导入的可能性越高
// 错误率相对来说会更高
// 因此从尾部开始查找
if (currentTypeToAdd.LastOrDefault(item => item.ItemId is 0U) is { } item)
if (list.LastOrDefault(item => item.ItemId is 0U) is { } item)
{
HutaoException.InvalidOperation(SH.FormatServiceGachaLogUIGFImportItemInvalidFormat(item.Id));
}

View File

@@ -35,7 +35,7 @@
<Flyout x:Key="HutaoCloudFlyout">
<Grid>
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding HutaoCloudViewModel.OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding HutaoCloudViewModel.LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Grid Visibility="{Binding HutaoCloudViewModel.IsInitialized, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid Visibility="{Binding HutaoCloudViewModel.Options.IsCloudServiceAllowed, Converter={StaticResource BoolToVisibilityConverter}}">
@@ -112,7 +112,7 @@
Grid.Row="2"
HorizontalAlignment="Stretch"
Command="{Binding HutaoCloudViewModel.UploadCommand}"
CommandParameter="{Binding SelectedArchive}"
CommandParameter="{Binding Archives.CurrentItem}"
Content="{shuxm:ResourceString Name=ViewPageGachaLogHutaoCloudUpload}"/>
</Grid>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}" Visibility="{Binding HutaoCloudViewModel.Options.IsCloudServiceAllowed, Converter={StaticResource BoolToVisibilityRevertConverter}}">
@@ -243,7 +243,7 @@
<ComboBox
DisplayMemberPath="Uid"
ItemsSource="{Binding Archives}"
SelectedItem="{Binding SelectedArchive, Mode=TwoWay}"
SelectedItem="{Binding Archives.CurrentItem, Mode=TwoWay}"
Style="{ThemeResource CommandBarComboBoxStyle}"/>
</shuxc:SizeRestrictedContentControl>
</Pivot.LeftHeader>
@@ -328,7 +328,7 @@
Padding="{ThemeResource ListViewInSplitPanePadding}"
ItemTemplate="{StaticResource HistoryWishListTemplate}"
ItemsSource="{Binding Statistics.HistoryWishes}"
SelectedItem="{Binding SelectedHistoryWish, Mode=TwoWay}"/>
SelectedItem="{Binding Statistics.HistoryWishes.CurrentItem, Mode=TwoWay}"/>
</SplitView.Pane>
<SplitView.Content>
<ScrollViewer>
@@ -343,7 +343,7 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="{ThemeResource ControlCornerRadius}"
Source="{Binding SelectedHistoryWish.BannerImage}"
Source="{Binding Statistics.HistoryWishes.CurrentItem.BannerImage}"
Stretch="UniformToFill"/>
</cwcont:ConstrainedBox>
<Border
@@ -358,30 +358,30 @@
Margin="0,8,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shuxm:ResourceString Name=ViewControlStatisticsCardOrangeText}"
Visibility="{Binding SelectedHistoryWish.OrangeList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
Visibility="{Binding Statistics.HistoryWishes.CurrentItem.OrangeList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<GridView
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
ItemsSource="{Binding SelectedHistoryWish.OrangeList}"
ItemsSource="{Binding Statistics.HistoryWishes.CurrentItem.OrangeList}"
SelectionMode="None"/>
<TextBlock
Margin="0,8,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shuxm:ResourceString Name=ViewControlStatisticsCardPurpleText}"
Visibility="{Binding SelectedHistoryWish.PurpleList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
Visibility="{Binding Statistics.HistoryWishes.CurrentItem.PurpleList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<GridView
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
ItemsSource="{Binding SelectedHistoryWish.PurpleList}"
ItemsSource="{Binding Statistics.HistoryWishes.CurrentItem.PurpleList}"
SelectionMode="None"/>
<TextBlock
Margin="0,8,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shuxm:ResourceString Name=ViewControlStatisticsCardBlueText}"
Visibility="{Binding SelectedHistoryWish.BlueList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
Visibility="{Binding Statistics.HistoryWishes.CurrentItem.BlueList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<GridView
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
ItemsSource="{Binding SelectedHistoryWish.BlueList}"
ItemsSource="{Binding Statistics.HistoryWishes.CurrentItem.BlueList}"
SelectionMode="None"/>
</StackPanel>
@@ -487,7 +487,7 @@
Spacing="16"
Visibility="{Binding HutaoCloudStatisticsViewModel.IsInitialized, Converter={StaticResource BoolToVisibilityConverter}}">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding HutaoCloudStatisticsViewModel.OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding HutaoCloudStatisticsViewModel.LoadCommand}"/>
</mxi:Interaction.Behaviors>
<shuxvs:HutaoStatisticsCard DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent}"/>
<shuxvs:HutaoStatisticsCard DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent2}"/>

View File

@@ -15,15 +15,11 @@ using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.UI.Xaml.Control;
using Snap.Hutao.UI.Xaml.View.Dialog;
using System.Collections.ObjectModel;
using Snap.Hutao.Win32.Foundation;
using System.Runtime.InteropServices;
namespace Snap.Hutao.ViewModel.GachaLog;
/// <summary>
/// 祈愿记录视图模型
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
@@ -33,36 +29,36 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
private readonly IContentDialogFactory contentDialogFactory;
private readonly HutaoCloudViewModel hutaoCloudViewModel;
private readonly ILogger<GachaLogViewModel> logger;
private readonly IProgressFactory progressFactory;
private readonly IGachaLogService gachaLogService;
private readonly IInfoBarService infoBarService;
private readonly JsonSerializerOptions options;
private readonly ITaskContext taskContext;
private ObservableCollection<GachaArchive>? archives;
private GachaArchive? selectedArchive;
private AdvancedDbCollectionView<GachaArchive>? archives;
private GachaStatistics? statistics;
private bool isAggressiveRefresh;
private HistoryWish? selectedHistoryWish;
/// <summary>
/// 存档集合
/// </summary>
public ObservableCollection<GachaArchive>? Archives { get => archives; set => SetProperty(ref archives, value); }
/// <summary>
/// 选中的存档
/// 切换存档时异步获取对应的统计
/// </summary>
public GachaArchive? SelectedArchive
public AdvancedDbCollectionView<GachaArchive>? Archives
{
get => selectedArchive;
set => SetSelectedArchiveAndUpdateStatisticsAsync(value).SafeForget();
get => archives;
set
{
if (Archives is not null)
{
Archives.CurrentChanged -= OnCurrentArchiveChanged;
}
SetProperty(ref archives, value);
if (value is not null)
{
value.CurrentChanged += OnCurrentArchiveChanged;
}
}
}
/// <summary>
/// 当前统计信息
/// </summary>
public GachaStatistics? Statistics
{
get => statistics;
@@ -70,29 +66,15 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
{
if (SetProperty(ref statistics, value))
{
SelectedHistoryWish = statistics?.HistoryWishes.FirstOrDefault();
statistics?.HistoryWishes.MoveCurrentToFirst();
}
}
}
/// <summary>
/// 选中的历史祈愿
/// </summary>
public HistoryWish? SelectedHistoryWish { get => selectedHistoryWish; set => SetProperty(ref selectedHistoryWish, value); }
/// <summary>
/// 是否为贪婪刷新
/// </summary>
public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); }
/// <summary>
/// 胡桃云服务视图
/// </summary>
public HutaoCloudViewModel HutaoCloudViewModel { get => hutaoCloudViewModel; }
/// <summary>
/// 胡桃云祈愿统计试图
/// </summary>
public HutaoCloudStatisticsViewModel HutaoCloudStatisticsViewModel { get => hutaoCloudStatisticsViewModel; }
protected override async ValueTask<bool> InitializeOverrideAsync()
@@ -101,15 +83,13 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
{
if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(false))
{
ArgumentNullException.ThrowIfNull(gachaLogService.ArchiveCollection);
ObservableCollection<GachaArchive> archives = gachaLogService.ArchiveCollection;
ArgumentNullException.ThrowIfNull(gachaLogService.Archives);
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await taskContext.SwitchToMainThreadAsync();
Archives = archives;
Archives = gachaLogService.Archives;
HutaoCloudViewModel.RetrieveCommand = RetrieveFromCloudCommand;
await SetSelectedArchiveAndUpdateStatisticsAsync(Archives.SelectedOrDefault(), true).ConfigureAwait(false);
Archives.MoveCurrentTo(Archives.SourceCollection.SelectedOrDefault());
}
return true;
@@ -122,96 +102,106 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
return false;
}
protected override void UninitializeOverride()
{
Archives?.Detach();
Archives = default;
}
private void OnCurrentArchiveChanged(object? sender, object? e)
{
UpdateStatisticsAsync(Archives?.CurrentItem).SafeForget(logger);
}
[Command("RefreshByWebCacheCommand")]
private Task RefreshByWebCacheAsync()
{
return RefreshInternalAsync(RefreshOption.WebCache).AsTask();
return RefreshCoreAsync(RefreshOption.WebCache).AsTask();
}
[Command("RefreshBySTokenCommand")]
private Task RefreshBySTokenAsync()
{
return RefreshInternalAsync(RefreshOption.SToken).AsTask();
return RefreshCoreAsync(RefreshOption.SToken).AsTask();
}
[Command("RefreshByManualInputCommand")]
private Task RefreshByManualInputAsync()
{
return RefreshInternalAsync(RefreshOption.ManualInput).AsTask();
return RefreshCoreAsync(RefreshOption.ManualInput).AsTask();
}
private async ValueTask RefreshInternalAsync(RefreshOption option)
private async ValueTask RefreshCoreAsync(RefreshOption option)
{
IGachaLogQueryProvider provider = gachaLogQueryProviderFactory.Create(option);
(bool isOk, GachaLogQuery query) = await provider.GetQueryAsync().ConfigureAwait(false);
if (isOk)
{
RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge;
GachaLogRefreshProgressDialog dialog = await contentDialogFactory.CreateInstanceAsync<GachaLogRefreshProgressDialog>().ConfigureAwait(false);
ContentDialogScope hideToken;
try
{
hideToken = await dialog.BlockAsync(taskContext).ConfigureAwait(false);
}
catch (COMException ex)
{
if (ex.HResult == unchecked((int)0x80000019))
{
infoBarService.Error(ex);
return;
}
throw;
}
IProgress<GachaLogFetchStatus> progress = progressFactory.CreateForMainThread<GachaLogFetchStatus>(dialog.OnReport);
bool authkeyValid;
try
{
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
try
{
authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, CancellationToken).ConfigureAwait(false);
}
catch (HutaoException ex)
{
authkeyValid = false;
infoBarService.Error(ex);
}
}
}
catch (OperationCanceledException)
{
// We set true here in order to hide the dialog.
authkeyValid = true;
infoBarService.Warning(SH.ViewModelGachaLogRefreshOperationCancel);
}
await taskContext.SwitchToMainThreadAsync();
if (authkeyValid)
{
await SetSelectedArchiveAndUpdateStatisticsAsync(gachaLogService.CurrentArchive, true).ConfigureAwait(false);
await hideToken.DisposeAsync().ConfigureAwait(false);
}
else
{
dialog.Title = SH.ViewModelGachaLogRefreshFail;
dialog.PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText;
dialog.DefaultButton = ContentDialogButton.Primary;
}
}
else
if (!isOk)
{
if (!string.IsNullOrEmpty(query.Message))
{
infoBarService.Warning(query.Message);
}
return;
}
RefreshStrategyKind strategy = IsAggressiveRefresh ? RefreshStrategyKind.AggressiveMerge : RefreshStrategyKind.LazyMerge;
GachaLogRefreshProgressDialog dialog = await contentDialogFactory.CreateInstanceAsync<GachaLogRefreshProgressDialog>().ConfigureAwait(false);
ContentDialogScope hideToken;
try
{
hideToken = await dialog.BlockAsync(taskContext).ConfigureAwait(false);
}
catch (COMException ex)
{
if (ex.HResult == HRESULT.E_ASYNC_OPERATION_NOT_STARTED)
{
infoBarService.Error(ex);
return;
}
throw;
}
IProgress<GachaLogFetchStatus> progress = progressFactory.CreateForMainThread<GachaLogFetchStatus>(dialog.OnReport);
bool authkeyValid;
try
{
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
try
{
authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, CancellationToken).ConfigureAwait(false);
}
catch (HutaoException ex)
{
authkeyValid = false;
infoBarService.Error(ex);
}
}
}
catch (OperationCanceledException)
{
// We set true here in order to hide the dialog.
authkeyValid = true;
infoBarService.Warning(SH.ViewModelGachaLogRefreshOperationCancel);
}
await taskContext.SwitchToMainThreadAsync();
if (authkeyValid)
{
await hideToken.DisposeAsync().ConfigureAwait(false);
}
else
{
// User needs to manually close the dialog
dialog.Title = SH.ViewModelGachaLogRefreshFail;
dialog.PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText;
dialog.DefaultButton = ContentDialogButton.Primary;
}
}
@@ -241,14 +231,14 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
[Command("ExportToUIGFJsonCommand")]
private async Task ExportToUIGFJsonAsync()
{
if (SelectedArchive is null)
if (Archives?.CurrentItem is null)
{
return;
}
(bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile(
SH.ViewModelGachaLogUIGFExportPickerTitle,
$"{SelectedArchive.Uid}.json",
$"{Archives.CurrentItem.Uid}.json",
[(SH.ViewModelGachaLogExportFileType, "*.json")]);
if (!isOk)
@@ -256,7 +246,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
return;
}
UIGF uigf = await gachaLogService.ExportToUIGFAsync(SelectedArchive).ConfigureAwait(false);
UIGF uigf = await gachaLogService.ExportToUIGFAsync(Archives.CurrentItem).ConfigureAwait(false);
if (await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false))
{
infoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage);
@@ -270,80 +260,62 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
[Command("RemoveArchiveCommand")]
private async Task RemoveArchiveAsync()
{
if (Archives is not null && SelectedArchive is not null)
if (Archives?.CurrentItem is null)
{
ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync(SH.FormatViewModelGachaLogRemoveArchiveTitle(SelectedArchive.Uid), SH.ViewModelGachaLogRemoveArchiveDescription)
.ConfigureAwait(false);
return;
}
if (result == ContentDialogResult.Primary)
{
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await gachaLogService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false);
ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync(
SH.FormatViewModelGachaLogRemoveArchiveTitle(Archives.CurrentItem.Uid),
SH.ViewModelGachaLogRemoveArchiveDescription)
.ConfigureAwait(false);
// reselect first archive
await taskContext.SwitchToMainThreadAsync();
await SetSelectedArchiveAndUpdateStatisticsAsync(Archives.FirstOrDefault(), false).ConfigureAwait(false);
}
}
if (result is not ContentDialogResult.Primary)
{
return;
}
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await gachaLogService.RemoveArchiveAsync(Archives.CurrentItem).ConfigureAwait(false);
// reselect first archive
await taskContext.SwitchToMainThreadAsync();
Archives.MoveCurrentToFirst();
}
}
[Command("RetrieveFromCloudCommand")]
private async Task RetrieveAsync(string? uid)
{
if (uid is not null)
{
ValueResult<bool, Guid> result = await HutaoCloudViewModel.RetrieveAsync(uid).ConfigureAwait(false);
if (result.TryGetValue(out Guid archiveId))
{
GachaArchive archive = await gachaLogService.EnsureArchiveInCollectionAsync(archiveId).ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
await SetSelectedArchiveAndUpdateStatisticsAsync(archive, true).ConfigureAwait(false);
}
}
}
private async ValueTask SetSelectedArchiveAndUpdateStatisticsAsync(GachaArchive? archive, bool forceUpdate = false)
{
if (IsViewDisposed)
if (uid is null)
{
return;
}
bool changed = SetProperty(ref selectedArchive, archive, nameof(SelectedArchive));
ValueResult<bool, Guid> result = await HutaoCloudViewModel.RetrieveAsync(uid).ConfigureAwait(false);
if (changed)
if (result.TryGetValue(out Guid archiveId))
{
gachaLogService.CurrentArchive = archive;
}
GachaArchive archive = await gachaLogService.EnsureArchiveInCollectionAsync(archiveId).ConfigureAwait(false);
if (forceUpdate || changed)
{
if (archive is not null)
{
await UpdateStatisticsAsync(archive).ConfigureAwait(false);
}
else
{
// 删光了存档或使用 Ctrl 取消了存档选择时触发
// 因此我们在这里额外判断一次是否删光了存档
if (Archives.IsNullOrEmpty())
{
Statistics = null;
}
}
await taskContext.SwitchToMainThreadAsync();
Archives?.MoveCurrentTo(archive);
}
}
private async ValueTask UpdateStatisticsAsync(GachaArchive? archive)
{
if (archive is null)
{
Statistics = default;
return;
}
try
{
GachaStatistics? statistics = await gachaLogService.GetStatisticsAsync(archive).ConfigureAwait(false);
GachaStatistics statistics = await gachaLogService.GetStatisticsAsync(archive).ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
Statistics = statistics;
@@ -391,8 +363,6 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
}
infoBarService.Success(SH.ViewModelGachaLogImportComplete);
await taskContext.SwitchToMainThreadAsync();
await SetSelectedArchiveAndUpdateStatisticsAsync(gachaLogService.CurrentArchive, true).ConfigureAwait(false);
return true;
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.UI.Xaml.Data;
namespace Snap.Hutao.ViewModel.GachaLog;
/// <summary>
@@ -32,7 +34,7 @@ internal sealed class GachaStatistics
/// <summary>
/// 历史卡池
/// </summary>
public List<HistoryWish> HistoryWishes { get; set; } = default!;
public AdvancedCollectionView<HistoryWish> HistoryWishes { get; set; } = default!;
/// <summary>
/// 五星角色

View File

@@ -1,13 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.UI.Xaml.Data;
namespace Snap.Hutao.ViewModel.GachaLog;
/// <summary>
/// 历史卡池概览
/// </summary>
[HighQuality]
internal sealed class HistoryWish : Wish
internal sealed class HistoryWish : Wish, IAdvancedCollectionViewItem
{
/// <summary>
/// 版本
@@ -43,4 +45,12 @@ internal sealed class HistoryWish : Wish
/// 三星Up
/// </summary>
public List<StatisticsItem> BlueList { get; set; } = default!;
public object? GetPropertyValue(string name)
{
return name switch
{
_ => default,
};
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Service.GachaLog;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
@@ -27,7 +28,7 @@ internal sealed partial class GachaInfoClient
/// <param name="options">查询</param>
/// <param name="token">取消令牌</param>
/// <returns>单个祈愿记录页面</returns>
public async ValueTask<Response<GachaLogPage>> GetGachaLogPageAsync(GachaLogQueryOptions options, CancellationToken token = default)
public async ValueTask<Response<GachaLogPage>> GetGachaLogPageAsync(GachaLogTypedQueryOptions options, CancellationToken token = default)
{
string query = options.ToQueryString();

View File

@@ -1,76 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.GachaLog.QueryProvider;
using System.Collections.Specialized;
using System.Web;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
/// <summary>
/// 祈愿记录请求配置
/// </summary>
[HighQuality]
internal struct GachaLogQueryOptions
{
/// <summary>
/// 尺寸
/// </summary>
public const int Size = 20;
/// <summary>
/// 是否为国际服
/// </summary>
public readonly bool IsOversea;
/// <summary>
/// 结束Id
/// 控制API返回的分页
/// 米哈游使用了 keyset pagination 来实现这一目标
/// https://learn.microsoft.com/en-us/ef/core/querying/pagination#keyset-pagination
/// </summary>
public long EndId;
public GachaType Type;
/// <summary>
/// Keys required:
/// authkey_ver
/// auth_appid
/// authkey
/// sign_type
/// Keys used as control:
/// lang
/// gacha_type
/// size
/// end_id
/// </summary>
private readonly NameValueCollection innerQuery;
/// <summary>
/// 构造一个新的祈愿记录请求配置
/// </summary>
/// <param name="query">原始查询字符串</param>
/// <param name="queryType">祈愿类型</param>
/// <param name="endId">终止Id</param>
public GachaLogQueryOptions(in GachaLogQuery query, GachaType queryType)
{
IsOversea = query.IsOversea;
// 对于每个类型我们需要单独创建
// 对应类型的 GachaLogQueryOptions
Type = queryType;
innerQuery = HttpUtility.ParseQueryString(query.Query);
innerQuery.Set("gacha_type", $"{queryType:D}");
innerQuery.Set("size", $"{Size}");
}
public readonly string ToQueryString()
{
// Make the cached end id into query.
innerQuery.Set("end_id", $"{EndId:D}");
string? query = innerQuery.ToString();
ArgumentException.ThrowIfNullOrEmpty(query);
return query;
}
}

View File

@@ -5,7 +5,6 @@ using Snap.Hutao.Core;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hutao.Log;

View File

@@ -9,6 +9,7 @@ namespace Snap.Hutao.Win32.Foundation;
internal readonly partial struct HRESULT
{
public static readonly HRESULT S_OK = unchecked((int)0x00000000);
public static readonly HRESULT E_ASYNC_OPERATION_NOT_STARTED = unchecked((int)0x80000019);
public static readonly HRESULT E_FAIL = unchecked((int)0x80004005);
public static readonly HRESULT DXGI_ERROR_NOT_FOUND = unchecked((int)0x887A0002);
public static readonly HRESULT DXGI_ERROR_DEVICE_REMOVED = unchecked((int)0x887A0005);