From dc6dc94b4590b977f664bd48e6f9e332d12a9a19 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Sun, 30 Jun 2024 16:11:47 +0800 Subject: [PATCH] make gachalog great again --- .../Core/LifeCycle/AppActivation.cs | 19 +- .../ContentDialog/ContentDialogFactory.cs | 3 +- .../Snap.Hutao/Model/Entity/GachaArchive.cs | 13 +- .../Snap.Hutao/Resource/Localization/SH.resx | 3 +- .../Achievement/AchievementDbService.cs | 2 +- .../Factory/GachaStatisticsFactory.cs | 15 +- .../Service/GachaLog/GachaArchiveOperation.cs | 9 +- .../Service/GachaLog/GachaLogFetchContext.cs | 83 +---- .../Service/GachaLog/GachaLogService.cs | 140 ++++---- .../GachaLog/GachaLogTypedQueryOptions.cs | 42 +++ .../Service/GachaLog/IGachaLogService.cs | 15 +- .../Service/GachaLog/IUIGFImportService.cs | 11 +- .../GachaLog/QueryProvider/GachaLogQuery.cs | 4 +- .../GachaLogQueryManualInputProvider.cs | 42 +-- .../GachaLogQuerySTokenProvider.cs | 34 +- .../GachaLogQueryWebCacheProvider.cs | 24 +- ...reshStrategy.cs => RefreshStrategyKind.cs} | 2 +- .../Service/GachaLog/UIGFImportService.cs | 34 +- .../UI/Xaml/View/Page/GachaLogPage.xaml | 24 +- .../ViewModel/GachaLog/GachaLogViewModel.cs | 300 ++++++++---------- .../ViewModel/GachaLog/GachaStatistics.cs | 4 +- .../ViewModel/GachaLog/HistoryWish.cs | 12 +- .../Hk4e/Event/GachaInfo/GachaInfoClient.cs | 3 +- .../Event/GachaInfo/GachaLogQueryOptions.cs | 76 ----- .../Web/Hutao/Log/HutaoLogUploadClient.cs | 1 - .../Snap.Hutao/Win32/Foundation/HRESULT.cs | 1 + 26 files changed, 371 insertions(+), 545 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogTypedQueryOptions.cs rename src/Snap.Hutao/Snap.Hutao/Service/GachaLog/{RefreshStrategy.cs => RefreshStrategyKind.cs} (92%) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogQueryOptions.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs index fd0bed4d..977b3d75 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs @@ -39,13 +39,14 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi private readonly ICurrentXamlWindowReference currentWindowReference; private readonly IServiceProvider serviceProvider; + private readonly ILogger 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().ClearAsync().SafeForget(); + serviceProvider.GetRequiredService().ClearAsync().SafeForget(logger); serviceProvider.GetRequiredService().UnregisterAllTasks(); } @@ -109,7 +110,7 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi return; } - serviceProvider.GetRequiredService().RunAsync().SafeForget(); + serviceProvider.GetRequiredService().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(); } - serviceProvider.GetRequiredService().SetNormalActivityAsync().SafeForget(); - serviceProvider.GetRequiredService().StartAsync().SafeForget(); + serviceProvider.GetRequiredService().SetNormalActivityAsync().SafeForget(logger); + serviceProvider.GetRequiredService().StartAsync().SafeForget(logger); if (serviceProvider.GetRequiredService() is IMetadataServiceInitialization metadataServiceInitialization) { - metadataServiceInitialization.InitializeInternalAsync().SafeForget(); + metadataServiceInitialization.InitializeInternalAsync().SafeForget(logger); } if (serviceProvider.GetRequiredService() is IHutaoUserServiceInitialization hutaoUserServiceInitialization) { - hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget(); + hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget(logger); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs index f0f3d958..e6c6f146 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs @@ -13,6 +13,7 @@ namespace Snap.Hutao.Factory.ContentDialog; internal sealed partial class ContentDialogFactory : IContentDialogFactory { private readonly ICurrentXamlWindowReference currentWindowReference; + private readonly ILogger 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); } }); diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs index a796da99..22587557 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs @@ -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; /// [HighQuality] [Table("gacha_archives")] -internal sealed partial class GachaArchive : ISelectable, IMappingFrom +internal sealed partial class GachaArchive : ISelectable, + IAdvancedCollectionViewItem, + IMappingFrom { /// /// 内部Id @@ -39,4 +42,12 @@ internal sealed partial class GachaArchive : ISelectable, IMappingFrom default, + }; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 618dfda6..f6bbfdf8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -905,7 +905,8 @@ 未正确提供原神路径,或当前设置的路径不正确 - 找不到原神内置浏览器缓存路径:\n{0} + 找不到原神内置浏览器缓存路径: +{0} 未找到可用的 Url diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs index e67fa126..b52e88bf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementDbService.cs @@ -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); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs index d76c89f2..79901450 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs @@ -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 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(historyWishes, true)), // avatars OrangeAvatars = orangeAvatarCounter.ToStatisticsList(), diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveOperation.cs index 70b9f085..b82c1a86 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchiveOperation.cs @@ -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; -/// -/// 祈愿存档初始化上下文 -/// internal static class GachaArchiveOperation { - public static void GetOrAdd(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, string uid, ObservableCollection archives, [NotNull] out GachaArchive? archive) + public static void GetOrAdd(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, string uid, AdvancedDbCollectionView 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) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs index 76c9137d..f94ba495 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchContext.cs @@ -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; -/// -/// 祈愿记录获取上下文 -/// internal struct GachaLogFetchContext { - /// - /// 当前处理的存档 - /// public GachaArchive? TargetArchive; - - /// - /// 当前的获取状态 - /// public GachaLogFetchStatus FetchStatus = default!; - - /// - /// 当前的数据库 End Id - /// public long? DbEndId; - - /// - /// 查询选项 - /// - public GachaLogQueryOptions QueryOptions; - - /// - /// 待加入数据库的物品 - /// + public GachaLogTypedQueryOptions TypedQueryOptions; public List ItemsToAdd = default!; - - /// - /// 当前类型增加物品是否结束 - /// public bool CurrentTypeAddingCompleted; - - /// - /// 当前类型 - /// public GachaType CurrentType; private readonly GachaLogServiceMetadataContext serviceContext; @@ -61,36 +31,22 @@ internal struct GachaLogFetchContext this.isLazy = isLazy; } - /// - /// 为下一个卡池类型重置 - /// - /// 卡池类型 - /// 查询 public void ResetForProcessingType(GachaType configType, in GachaLogQuery query) { DbEndId = null; CurrentType = configType; ItemsToAdd = []; FetchStatus = new(configType); - QueryOptions = new(query, configType); + TypedQueryOptions = new(query, configType); } - /// - /// 为下一个物品页面重置 - /// public void ResetForProcessingPage() { FetchStatus = new(CurrentType); CurrentTypeAddingCompleted = false; } - /// - /// 确保 存档 与 EndId 不为空 - /// - /// 物品 - /// 存档集合 - /// 祈愿记录数据库服务 - public void EnsureArchiveAndEndId(GachaLogItem item, ObservableCollection archives, IGachaLogDbService gachaLogDbService) + public void EnsureArchiveAndEndId(GachaLogItem item, AdvancedDbCollectionView archives, IGachaLogDbService gachaLogDbService) { if (TargetArchive is null) { @@ -100,41 +56,24 @@ internal struct GachaLogFetchContext DbEndId ??= gachaLogDbService.GetNewestGachaItemIdByArchiveIdAndQueryType(TargetArchive.InnerId, CurrentType); } - /// - /// 判断是否应添加 - /// - /// 物品 - /// 是否应添加 public readonly bool ShouldAddItem(GachaLogItem item) { return !isLazy || item.Id > DbEndId; } - /// - /// 判断当前类型已经处理完成 - /// - /// 物品集合 - /// 当前类型已经处理完成 public readonly bool ItemsHaveReachEnd(List items) { - return CurrentTypeAddingCompleted || items.Count < GachaLogQueryOptions.Size; + return CurrentTypeAddingCompleted || items.Count < GachaLogTypedQueryOptions.Size; } - /// - /// 添加物品 - /// - /// 物品 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; } - /// - /// 保存物品 - /// 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); } } - /// - /// 完成添加 - /// public void CompleteAdding() { CurrentTypeAddingCompleted = true; } - /// - /// 反馈进度 - /// - /// 进度 - /// 验证密钥是否过期 public readonly void Report(IProgress progress, bool isAuthKeyTimeout = false) { FetchStatus.AuthKeyTimeout = isAuthKeyTimeout; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs index d43b7bb7..fd73c1cf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs @@ -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; -/// -/// 祈愿记录服务 -/// -[HighQuality] [ConstructorGenerated] [Injection(InjectAs.Scoped, typeof(IGachaLogService))] internal sealed partial class GachaLogService : IGachaLogService { - private readonly ScopedDbCurrent 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 logger; private readonly GachaInfoClient gachaInfoClient; - private readonly IGachaLogDbService gachaLogDbService; private readonly ITaskContext taskContext; private GachaLogServiceMetadataContext context; - private ObservableCollection? archiveCollection; + private AdvancedDbCollectionView? archives; - /// - public GachaArchive? CurrentArchive + public AdvancedDbCollectionView? Archives { - get => dbCurrent.Current; - set => dbCurrent.Current = value; + get => archives; + private set => archives = value; } - /// - public ObservableCollection? ArchiveCollection - { - get => archiveCollection; - private set => archiveCollection = value; - } - - /// public async ValueTask 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(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 } } - /// - public async ValueTask GetStatisticsAsync(GachaArchive? archive) + public async ValueTask GetStatisticsAsync(GachaArchive archive) { - archive ??= CurrentArchive; - archive ??= ArchiveCollection?.FirstOrDefault(); - ArgumentNullException.ThrowIfNull(archive); - - // Return statistics using (ValueStopwatch.MeasureExecution(logger)) { List items = await gachaLogDbService.GetGachaItemListByArchiveIdAsync(archive.InnerId).ConfigureAwait(false); @@ -88,14 +69,13 @@ internal sealed partial class GachaLogService : IGachaLogService } } - /// public async ValueTask> GetStatisticsSlimListAsync(CancellationToken token = default) { await InitializeAsync(token).ConfigureAwait(false); - ArgumentNullException.ThrowIfNull(ArchiveCollection); + ArgumentNullException.ThrowIfNull(Archives); List statistics = []; - foreach (GachaArchive archive in ArchiveCollection) + foreach (GachaArchive archive in Archives) { List 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; } - /// public ValueTask ExportToUIGFAsync(GachaArchive archive) { return gachaLogExportService.ExportAsync(context, archive); } - /// 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); } - /// - public async ValueTask RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token) + public async ValueTask RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategyKind kind, IProgress 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; } - /// 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 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> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress 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 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 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 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); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogTypedQueryOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogTypedQueryOptions.cs new file mode 100644 index 00000000..7469b64b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogTypedQueryOptions.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs index 68ae9a42..a41f7a2b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs @@ -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 { - /// - /// 当前存档 - /// - GachaArchive? CurrentArchive { get; set; } - - /// - /// 获取可用于绑定的存档集合 - /// - ObservableCollection? ArchiveCollection { get; } + AdvancedDbCollectionView? Archives { get; } ValueTask EnsureArchiveInCollectionAsync(Guid archiveId, CancellationToken token = default(CancellationToken)); @@ -39,7 +32,7 @@ internal interface IGachaLogService /// /// 存档 /// 祈愿统计 - ValueTask GetStatisticsAsync(GachaArchive? archive); + ValueTask GetStatisticsAsync(GachaArchive archive); /// /// 异步获取简化的祈愿统计列表 @@ -71,7 +64,7 @@ internal interface IGachaLogService /// 进度 /// 取消令牌 /// 验证密钥是否有效 - ValueTask RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token); + ValueTask RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategyKind strategy, IProgress progress, CancellationToken token); /// /// 删除存档 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IUIGFImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IUIGFImportService.cs index e1e75e0e..65f1dda0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IUIGFImportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IUIGFImportService.cs @@ -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; /// internal interface IUIGFImportService { - /// - /// 异步从 UIGF 导入 - /// - /// 祈愿记录服务上下文 - /// 数据 - /// 存档集合 - /// 存档 - ValueTask ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, ObservableCollection archives); + ValueTask ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, AdvancedDbCollectionView archives); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs index 231800af..85379cf4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuery.cs @@ -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); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs index 73f2b9b2..d8638d25 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -8,10 +8,6 @@ using System.Web; namespace Snap.Hutao.Service.GachaLog.QueryProvider; -/// -/// 手动输入方法提供器 -/// -[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().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)); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuerySTokenProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuerySTokenProvider.cs index f884a6ce..0aea9184 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuerySTokenProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQuerySTokenProvider.cs @@ -25,29 +25,25 @@ internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvid /// public async ValueTask> 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 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 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) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs index ea947d59..f933fe24 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs @@ -12,10 +12,6 @@ using System.Web; namespace Snap.Hutao.Service.GachaLog.QueryProvider; -/// -/// 浏览器缓存方法 -/// -[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; - /// - /// 获取缓存文件路径 - /// - /// 游戏路径 - /// 缓存文件路径 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)); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategy.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategyKind.cs similarity index 92% rename from src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategy.cs rename to src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategyKind.cs index af7dc4dd..c7bcbe01 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategy.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshStrategyKind.cs @@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.GachaLog; /// 刷新策略 /// [HighQuality] -internal enum RefreshStrategy +internal enum RefreshStrategyKind { /// /// 无策略 用于切换存档时使用 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs index a14fe939..5513f723 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs @@ -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; -/// -/// 祈愿记录导入服务 -/// [ConstructorGenerated] [Injection(InjectAs.Scoped, typeof(IUIGFImportService))] internal sealed partial class UIGFImportService : IUIGFImportService { + private readonly IGachaLogDbService gachaLogDbService; private readonly ILogger logger; private readonly CultureOptions cultureOptions; - private readonly IGachaLogDbService gachaLogDbService; private readonly ITaskContext taskContext; - /// - public async ValueTask ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, ObservableCollection archives) + public async ValueTask ImportAsync(GachaLogServiceMetadataContext context, UIGF uigf, AdvancedDbCollectionView 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 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 currentTypeToAdd) + private static void ThrowIfContainsInvalidItem(List 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)); } diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/GachaLogPage.xaml b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/GachaLogPage.xaml index b733d8e6..f55e51e7 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/GachaLogPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/GachaLogPage.xaml @@ -35,7 +35,7 @@ - + @@ -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}"/> @@ -243,7 +243,7 @@ @@ -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}"/> @@ -343,7 +343,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="{ThemeResource ControlCornerRadius}" - Source="{Binding SelectedHistoryWish.BannerImage}" + Source="{Binding Statistics.HistoryWishes.CurrentItem.BannerImage}" Stretch="UniformToFill"/> + Visibility="{Binding Statistics.HistoryWishes.CurrentItem.OrangeList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/> + Visibility="{Binding Statistics.HistoryWishes.CurrentItem.PurpleList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/> + Visibility="{Binding Statistics.HistoryWishes.CurrentItem.BlueList.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/> @@ -487,7 +487,7 @@ Spacing="16" Visibility="{Binding HutaoCloudStatisticsViewModel.IsInitialized, Converter={StaticResource BoolToVisibilityConverter}}"> - + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs index 5a70e988..84bb407d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs @@ -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; -/// -/// 祈愿记录视图模型 -/// -[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 logger; private readonly IProgressFactory progressFactory; private readonly IGachaLogService gachaLogService; private readonly IInfoBarService infoBarService; private readonly JsonSerializerOptions options; private readonly ITaskContext taskContext; - private ObservableCollection? archives; - private GachaArchive? selectedArchive; + private AdvancedDbCollectionView? archives; private GachaStatistics? statistics; private bool isAggressiveRefresh; - private HistoryWish? selectedHistoryWish; - /// - /// 存档集合 - /// - public ObservableCollection? Archives { get => archives; set => SetProperty(ref archives, value); } - - /// - /// 选中的存档 - /// 切换存档时异步获取对应的统计 - /// - public GachaArchive? SelectedArchive + public AdvancedDbCollectionView? 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; + } + } } - /// - /// 当前统计信息 - /// 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(); } } } - /// - /// 选中的历史祈愿 - /// - public HistoryWish? SelectedHistoryWish { get => selectedHistoryWish; set => SetProperty(ref selectedHistoryWish, value); } - - /// - /// 是否为贪婪刷新 - /// public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); } - /// - /// 胡桃云服务视图 - /// public HutaoCloudViewModel HutaoCloudViewModel { get => hutaoCloudViewModel; } - /// - /// 胡桃云祈愿统计试图 - /// public HutaoCloudStatisticsViewModel HutaoCloudStatisticsViewModel { get => hutaoCloudStatisticsViewModel; } protected override async ValueTask InitializeOverrideAsync() @@ -101,15 +83,13 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel { if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(false)) { - ArgumentNullException.ThrowIfNull(gachaLogService.ArchiveCollection); - ObservableCollection 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().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 progress = progressFactory.CreateForMainThread(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().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 progress = progressFactory.CreateForMainThread(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 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 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; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaStatistics.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaStatistics.cs index d0512416..bd3f8e91 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaStatistics.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaStatistics.cs @@ -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; /// @@ -32,7 +34,7 @@ internal sealed class GachaStatistics /// /// 历史卡池 /// - public List HistoryWishes { get; set; } = default!; + public AdvancedCollectionView HistoryWishes { get; set; } = default!; /// /// 五星角色 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HistoryWish.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HistoryWish.cs index 2c35e738..93d8424a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HistoryWish.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HistoryWish.cs @@ -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; /// /// 历史卡池概览 /// [HighQuality] -internal sealed class HistoryWish : Wish +internal sealed class HistoryWish : Wish, IAdvancedCollectionViewItem { /// /// 版本 @@ -43,4 +45,12 @@ internal sealed class HistoryWish : Wish /// 三星Up /// public List BlueList { get; set; } = default!; + + public object? GetPropertyValue(string name) + { + return name switch + { + _ => default, + }; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs index 9a9371b0..f72a7ba7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs @@ -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 /// 查询 /// 取消令牌 /// 单个祈愿记录页面 - public async ValueTask> GetGachaLogPageAsync(GachaLogQueryOptions options, CancellationToken token = default) + public async ValueTask> GetGachaLogPageAsync(GachaLogTypedQueryOptions options, CancellationToken token = default) { string query = options.ToQueryString(); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogQueryOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogQueryOptions.cs deleted file mode 100644 index e8c33611..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogQueryOptions.cs +++ /dev/null @@ -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; - -/// -/// 祈愿记录请求配置 -/// -[HighQuality] -internal struct GachaLogQueryOptions -{ - /// - /// 尺寸 - /// - public const int Size = 20; - - /// - /// 是否为国际服 - /// - public readonly bool IsOversea; - - /// - /// 结束Id - /// 控制API返回的分页 - /// 米哈游使用了 keyset pagination 来实现这一目标 - /// https://learn.microsoft.com/en-us/ef/core/querying/pagination#keyset-pagination - /// - public long EndId; - - public GachaType Type; - - /// - /// Keys required: - /// authkey_ver - /// auth_appid - /// authkey - /// sign_type - /// Keys used as control: - /// lang - /// gacha_type - /// size - /// end_id - /// - private readonly NameValueCollection innerQuery; - - /// - /// 构造一个新的祈愿记录请求配置 - /// - /// 原始查询字符串 - /// 祈愿类型 - /// 终止Id - 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; - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs index fc930a75..3e755fc8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Win32/Foundation/HRESULT.cs b/src/Snap.Hutao/Snap.Hutao/Win32/Foundation/HRESULT.cs index 23898cd3..96053e93 100644 --- a/src/Snap.Hutao/Snap.Hutao/Win32/Foundation/HRESULT.cs +++ b/src/Snap.Hutao/Snap.Hutao/Win32/Foundation/HRESULT.cs @@ -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);