mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
make gachalog great again
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// 删除存档
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.GachaLog;
|
||||
/// 刷新策略
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal enum RefreshStrategy
|
||||
internal enum RefreshStrategyKind
|
||||
{
|
||||
/// <summary>
|
||||
/// 无策略 用于切换存档时使用
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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}"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// 五星角色
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user