refactor gacha service

This commit is contained in:
Lightczx
2023-07-27 17:23:28 +08:00
parent e843c84374
commit 53044b0dda
23 changed files with 173 additions and 161 deletions

View File

@@ -5,6 +5,7 @@ namespace Snap.Hutao.Core.Threading;
/// <summary>
/// An asynchronous barrier that blocks the signaler until all other participants have signaled.
/// FIFO
/// </summary>
internal class AsyncBarrier
{
@@ -16,7 +17,7 @@ internal class AsyncBarrier
/// <summary>
/// The set of participants who have reached the barrier, with their awaiters that can resume those participants.
/// </summary>
private readonly Stack<TaskCompletionSource> waiters;
private readonly Queue<TaskCompletionSource> waiters;
/// <summary>
/// Initializes a new instance of the <see cref="AsyncBarrier"/> class.
@@ -24,7 +25,7 @@ internal class AsyncBarrier
/// <param name="participants">The number of participants.</param>
public AsyncBarrier(int participants)
{
Requires.Range(participants > 0, nameof(participants));
Must.Range(participants >= 1, "Participants of AsyncBarrier can not be less than 1");
participantCount = participants;
// Allocate the stack so no resizing is necessary.
@@ -47,7 +48,7 @@ internal class AsyncBarrier
// Unleash everyone that preceded this one.
while (waiters.Count > 0)
{
_ = Task.Factory.StartNew(state => ((TaskCompletionSource)state!).SetResult(), waiters.Pop(), default, TaskCreationOptions.None, TaskScheduler.Default);
_ = Task.Factory.StartNew(state => ((TaskCompletionSource)state!).SetResult(), waiters.Dequeue(), default, TaskCreationOptions.None, TaskScheduler.Default);
}
// And allow this one to continue immediately.
@@ -57,7 +58,7 @@ internal class AsyncBarrier
{
// We need more folks. So suspend this caller.
TaskCompletionSource tcs = new();
waiters.Push(tcs);
waiters.Enqueue(tcs);
return tcs.Task;
}
}

View File

@@ -22,19 +22,9 @@ internal static class StringExtension
return new(value);
}
/// <summary>
/// 移除结尾可能存在的字符串
/// </summary>
/// <param name="source">源</param>
/// <param name="value">值</param>
/// <returns>新的字符串</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string TrimEnd(this string source, string value)
{
while (source.EndsWith(value))
{
source = source[..^value.Length];
}
return source;
return source.AsSpan().TrimEnd(value).ToString();
}
}

View File

@@ -159,7 +159,7 @@ internal sealed partial class CultivationService : ICultivationService
}
/// <inheritdoc/>
public async Task<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Web.Hoyolab.Takumi.Event.Calculate.Item> items)
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Web.Hoyolab.Takumi.Event.Calculate.Item> items)
{
if (items.Count == 0)
{

View File

@@ -81,7 +81,7 @@ internal interface ICultivationService
/// <param name="itemId">主Id</param>
/// <param name="items">待存物品</param>
/// <returns>是否保存成功</returns>
Task<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Item> items);
ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Item> items);
/// <summary>
/// 保存养成物品状态

View File

@@ -26,11 +26,11 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
private readonly AppOptions options;
/// <inheritdoc/>
public async Task<GachaStatistics> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context)
public async ValueTask<GachaStatistics> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context)
{
await taskContext.SwitchToBackgroundAsync();
List<GachaEvent> gachaEvents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
List<HistoryWishBuilder> historyWishBuilders = gachaEvents.SelectList(g => new HistoryWishBuilder(g, context));
List<HistoryWishBuilder> historyWishBuilders = gachaEvents.SelectList(gachaEvent => new HistoryWishBuilder(gachaEvent, context));
return CreateCore(serviceProvider, items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible);
}
@@ -137,6 +137,8 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
}
}
AsyncBarrier barrier = new(3);
return new()
{
// history
@@ -157,9 +159,9 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
BlueWeapons = blueWeaponCounter.ToStatisticsList(),
// typed wish summary
StandardWish = standardWishBuilder.ToTypedWishSummary(),
AvatarWish = avatarWishBuilder.ToTypedWishSummary(),
WeaponWish = weaponWishBuilder.ToTypedWishSummary(),
StandardWish = standardWishBuilder.ToTypedWishSummary(barrier),
AvatarWish = avatarWishBuilder.ToTypedWishSummary(barrier),
WeaponWish = weaponWishBuilder.ToTypedWishSummary(barrier),
};
}
}

View File

@@ -19,7 +19,7 @@ internal sealed partial class GachaStatisticsSlimFactory : IGachaStatisticsSlimF
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async Task<GachaStatisticsSlim> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context)
public async ValueTask<GachaStatisticsSlim> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context)
{
await taskContext.SwitchToBackgroundAsync();

View File

@@ -18,5 +18,5 @@ internal interface IGachaStatisticsFactory
/// <param name="items">物品列表</param>
/// <param name="context">祈愿记录上下文</param>
/// <returns>祈愿统计对象</returns>
Task<GachaStatistics> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context);
ValueTask<GachaStatistics> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context);
}

View File

@@ -17,5 +17,5 @@ internal interface IGachaStatisticsSlimFactory
/// <param name="items">排序的物品</param>
/// <param name="context">祈愿记录服务上下文</param>
/// <returns>简化的祈愿统计</returns>
Task<GachaStatisticsSlim> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context);
ValueTask<GachaStatisticsSlim> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context);
}

View File

@@ -34,7 +34,7 @@ internal sealed class PullPrediction
this.distributionType = distributionType;
}
public async Task PredictAsync()
public async Task PredictAsync(AsyncBarrier barrier)
{
await taskContext.SwitchToBackgroundAsync();
HomaGachaLogClient gachaLogClient = serviceProvider.GetRequiredService<HomaGachaLogClient>();
@@ -43,9 +43,9 @@ internal sealed class PullPrediction
if (response.IsOk())
{
PredictResult result = PredictCore(response.Data.Distribution, typedWishSummary);
await barrier.SignalAndWaitAsync().ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
typedWishSummary.ProbabilityOfNextPullIsOrange = result.ProbabilityOfNextPullIsOrange;
typedWishSummary.ProbabilityOfPredictedPullLeftToOrange = result.ProbabilityOfPredictedPullLeftToOrange;
typedWishSummary.PredictedPullLeftToOrange = result.PredictedPullLeftToOrange;

View File

@@ -54,15 +54,6 @@ internal sealed class TypedWishSummaryBuilder
private DateTimeOffset fromTimeTracker = DateTimeOffset.MaxValue;
private DateTimeOffset toTimeTracker = DateTimeOffset.MinValue;
/// <summary>
/// 构造一个新的类型化祈愿统计信息构建器
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="name">祈愿配置</param>
/// <param name="typeEvaluator">祈愿类型判断器</param>
/// <param name="distributionType">分布类型</param>
/// <param name="guaranteeOrangeThreshold">五星保底</param>
/// <param name="guaranteePurpleThreshold">四星保底</param>
public TypedWishSummaryBuilder(
IServiceProvider serviceProvider,
string name,
@@ -140,7 +131,7 @@ internal sealed class TypedWishSummaryBuilder
/// 转换到类型化祈愿统计信息
/// </summary>
/// <returns>类型化祈愿统计信息</returns>
public TypedWishSummary ToTypedWishSummary()
public TypedWishSummary ToTypedWishSummary(AsyncBarrier barrier)
{
summaryItems.CompleteAdding(guaranteeOrangeThreshold);
double totalCount = totalCountTracker;
@@ -172,7 +163,7 @@ internal sealed class TypedWishSummaryBuilder
};
// TODO: barrier all predictions.
new PullPrediction(serviceProvider, summary, distributionType).PredictAsync().SafeForget();
new PullPrediction(serviceProvider, summary, distributionType).PredictAsync(barrier).SafeForget();
return summary;
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
@@ -17,13 +18,17 @@ internal static class GachaArchives
/// <summary>
/// 初始化存档集合
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="collection">集合</param>
public static void Initialize(AppDbContext appDbContext, out ObservableCollection<GachaArchive> collection)
public static void Initialize(IServiceProvider serviceProvider, out ObservableCollection<GachaArchive> collection)
{
try
{
collection = appDbContext.GachaArchives.ToObservableCollection();
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
collection = appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
}
catch (SqliteException ex)
{

View File

@@ -46,7 +46,7 @@ internal sealed partial class GachaLogService : IGachaLogService
}
/// <inheritdoc/>
public ObservableCollection<GachaArchive> ArchiveCollection
public ObservableCollection<GachaArchive>? ArchiveCollection
{
get => context.ArchiveCollection;
}
@@ -67,14 +67,10 @@ internal sealed partial class GachaLogService : IGachaLogService
Dictionary<string, Model.Metadata.Avatar.Avatar> nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
Dictionary<string, Model.Metadata.Weapon.Weapon> nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
GachaArchives.Initialize(appDbContext, out ObservableCollection<GachaArchive> collection);
GachaArchives.Initialize(serviceProvider, out ObservableCollection<GachaArchive> collection);
context = new(idAvatarMap, idWeaponMap, nameAvatarMap, nameWeaponMap, collection);
return true;
}
context = new(idAvatarMap, idWeaponMap, nameAvatarMap, nameWeaponMap, collection);
return true;
}
else
{
@@ -83,30 +79,24 @@ internal sealed partial class GachaLogService : IGachaLogService
}
/// <inheritdoc/>
public async Task<GachaStatistics> GetStatisticsAsync(GachaArchive? archive)
public async ValueTask<GachaStatistics> GetStatisticsAsync(GachaArchive? archive)
{
archive ??= CurrentArchive;
ArgumentNullException.ThrowIfNull(archive);
// Return statistics
if (archive != null)
using (ValueStopwatch.MeasureExecution(logger))
{
using (ValueStopwatch.MeasureExecution(logger))
using (IServiceScope scope = serviceProvider.CreateScope())
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IOrderedQueryable<GachaItem> items = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.OrderBy(i => i.Id);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IOrderedQueryable<GachaItem> items = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.OrderBy(i => i.Id);
return await gachaStatisticsFactory.CreateAsync(items, context).ConfigureAwait(false);
}
return await gachaStatisticsFactory.CreateAsync(items, context).ConfigureAwait(false);
}
}
else
{
throw Must.NeverHappen();
}
}
/// <inheritdoc/>
@@ -250,4 +240,15 @@ internal sealed partial class GachaLogService : IGachaLogService
return new(!fetchContext.FetchStatus.AuthKeyTimeout, fetchContext.TargetArchive);
}
}
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IGachaLogDbService))]
internal sealed partial class GachaLogDbService : IGachaLogDbService
{
}
internal interface IGachaLogDbService
{
}

View File

@@ -86,13 +86,17 @@ internal readonly struct GachaLogServiceContext
{
if (!ItemCache.TryGetValue(name, out Item? result))
{
result = type switch
if (type == SH.ModelInterchangeUIGFItemTypeAvatar)
{
"角色" => NameAvatarMap[name].ToItem(),
"武器" => NameWeaponMap[name].ToItem(),
_ => throw Must.NeverHappen(),
};
result = NameAvatarMap[name].ToItem();
}
if (type == SH.ModelInterchangeUIGFItemTypeWeapon)
{
result = NameWeaponMap[name].ToItem();
}
ArgumentNullException.ThrowIfNull(result);
ItemCache[name] = result;
}
@@ -123,11 +127,16 @@ internal readonly struct GachaLogServiceContext
/// <returns>物品 Id</returns>
public uint GetItemId(GachaLogItem item)
{
return item.ItemType switch
if (item.ItemType == SH.ModelInterchangeUIGFItemTypeAvatar)
{
"角色" => NameAvatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0,
"武器" => NameWeaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0,
_ => 0U,
};
return NameAvatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0;
}
if (item.ItemType == SH.ModelInterchangeUIGFItemTypeWeapon)
{
return NameWeaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0;
}
return 0U;
}
}

View File

@@ -23,7 +23,7 @@ internal interface IGachaLogService
/// <summary>
/// 获取可用于绑定的存档集合
/// </summary>
ObservableCollection<GachaArchive> ArchiveCollection { get; }
ObservableCollection<GachaArchive>? ArchiveCollection { get; }
/// <summary>
/// 导出为一个新的UIGF对象
@@ -37,7 +37,7 @@ internal interface IGachaLogService
/// </summary>
/// <param name="archive">存档</param>
/// <returns>祈愿统计</returns>
Task<GachaStatistics> GetStatisticsAsync(GachaArchive? archive);
ValueTask<GachaStatistics> GetStatisticsAsync(GachaArchive? archive);
/// <summary>
/// 异步获取简化的祈愿统计列表

View File

@@ -4,7 +4,7 @@
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// <summary>
/// 祈愿记录query
/// 祈愿记录 query
/// </summary>
[HighQuality]
internal readonly struct GachaLogQuery

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGachaLogQueryProvider))]
[Injection(InjectAs.Transient)]
internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryProvider
{
private readonly IServiceProvider serviceProvider;

View File

@@ -1,31 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// <summary>
/// 祈愿记录Url提供器拓展
/// </summary>
internal static class GachaLogQueryProviderExtension
{
/// <summary>
/// 选出对应的祈愿 Url 提供器
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="option">刷新选项</param>
/// <returns>对应的祈愿 Url 提供器</returns>
public static IGachaLogQueryProvider? PickProvider(this IServiceProvider serviceProvider, RefreshOption option)
{
IEnumerable<IGachaLogQueryProvider> providers = serviceProvider.GetServices<IGachaLogQueryProvider>();
string? name = option switch
{
RefreshOption.WebCache => nameof(GachaLogQueryWebCacheProvider),
RefreshOption.SToken => nameof(GachaLogQuerySTokenProvider),
RefreshOption.ManualInput => nameof(GachaLogQueryManualInputProvider),
_ => null,
};
return providers.SingleOrDefault(p => p.Name == name);
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGachaLogQueryProviderFactory))]
internal sealed partial class GachaLogQueryProviderFactory : IGachaLogQueryProviderFactory
{
private readonly IServiceProvider serviceProvider;
public IGachaLogQueryProvider Create(RefreshOption option)
{
return option switch
{
RefreshOption.SToken => serviceProvider.GetRequiredService<GachaLogQuerySTokenProvider>(),
RefreshOption.WebCache => serviceProvider.GetRequiredService<GachaLogQueryWebCacheProvider>(),
RefreshOption.ManualInput => serviceProvider.GetRequiredService<GachaLogQueryManualInputProvider>(),
_ => throw Must.NeverHappen("不支持的刷新选项"),
};
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGachaLogQueryProvider))]
[Injection(InjectAs.Transient)]
internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvider
{
private readonly BindingClient2 bindingClient2;

View File

@@ -17,7 +17,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGachaLogQueryProvider))]
[Injection(InjectAs.Transient)]
internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider
{
private readonly IGameService gameService;
@@ -38,8 +38,15 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
? GameConstants.GenshinImpactData
: GameConstants.YuanShenData;
// TODO: make sure how the cache file located.
return Path.Combine(Path.GetDirectoryName(path)!, dataFolder, @"webCaches\2.13.0.1\Cache\Cache_Data\data_2");
DirectoryInfo webCacheFolder = new(Path.Combine(Path.GetDirectoryName(path)!, dataFolder, "webCaches"));
Regex versionRegex = VersionRegex();
DirectoryInfo? lastestVersionCacheFolder = webCacheFolder
.EnumerateDirectories()
.Where(dir => versionRegex.IsMatch(dir.Name))
.MaxBy(dir => new Version(dir.Name));
lastestVersionCacheFolder ??= webCacheFolder;
return Path.Combine(lastestVersionCacheFolder.FullName, @"Cache\Cache_Data\data_2");
}
/// <inheritdoc/>
@@ -112,4 +119,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
return null;
}
[GeneratedRegex("^[1-9]+?\\.[0-9]+?\\.[0-9]+?\\.[0-9]+?$")]
private static partial Regex VersionRegex();
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
internal interface IGachaLogQueryProviderFactory
{
IGachaLogQueryProvider Create(RefreshOption option);
}

View File

@@ -146,65 +146,62 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
private async Task RefreshInternalAsync(RefreshOption option)
{
IGachaLogQueryProvider? provider = serviceProvider.PickProvider(option);
IGachaLogQueryProvider provider = serviceProvider.GetRequiredService<IGachaLogQueryProviderFactory>().Create(option);
if (provider != null)
(bool isOk, GachaLogQuery query) = await provider.GetQueryAsync().ConfigureAwait(false);
if (isOk)
{
(bool isOk, GachaLogQuery query) = await provider.GetQueryAsync().ConfigureAwait(false);
RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge;
if (isOk)
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
GachaLogRefreshProgressDialog dialog = serviceProvider.CreateInstance<GachaLogRefreshProgressDialog>();
IDisposable dialogHider = await dialog.BlockAsync(taskContext).ConfigureAwait(false);
Progress<GachaLogFetchStatus> progress = new(dialog.OnReport);
bool authkeyValid;
try
{
RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge;
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
GachaLogRefreshProgressDialog dialog = serviceProvider.CreateInstance<GachaLogRefreshProgressDialog>();
IDisposable dialogHider = await dialog.BlockAsync(taskContext).ConfigureAwait(false);
Progress<GachaLogFetchStatus> progress = new(dialog.OnReport);
bool authkeyValid;
try
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
try
{
try
{
authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, CancellationToken).ConfigureAwait(false);
}
catch (UserdataCorruptedException ex)
{
authkeyValid = false;
infoBarService.Error(ex);
}
authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, CancellationToken).ConfigureAwait(false);
}
catch (UserdataCorruptedException ex)
{
authkeyValid = false;
infoBarService.Error(ex);
}
}
catch (OperationCanceledException)
{
// We set true here in order to hide the dialog.
authkeyValid = true;
infoBarService.Warning(SH.ViewModelGachaLogRefreshOperationCancel);
}
}
catch (OperationCanceledException)
{
// We set true here in order to hide the dialog.
authkeyValid = true;
infoBarService.Warning(SH.ViewModelGachaLogRefreshOperationCancel);
}
await taskContext.SwitchToMainThreadAsync();
if (authkeyValid)
{
SetSelectedArchiveAndUpdateStatistics(gachaLogService.CurrentArchive, true);
dialogHider.Dispose();
}
else
{
dialog.Title = SH.ViewModelGachaLogRefreshFail;
dialog.PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText;
dialog.DefaultButton = ContentDialogButton.Primary;
}
await taskContext.SwitchToMainThreadAsync();
if (authkeyValid)
{
SetSelectedArchiveAndUpdateStatistics(gachaLogService.CurrentArchive, true);
dialogHider.Dispose();
}
else
{
if (!string.IsNullOrEmpty(query.Message))
{
infoBarService.Warning(query.Message);
}
dialog.Title = SH.ViewModelGachaLogRefreshFail;
dialog.PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText;
dialog.DefaultButton = ContentDialogButton.Primary;
}
}
else
{
if (!string.IsNullOrEmpty(query.Message))
{
infoBarService.Warning(query.Message);
}
}
}

View File

@@ -38,6 +38,12 @@ internal readonly struct QueryString
}
}
private static QueryString Parse(ReadOnlySpan<char> value)
{
// TODO: .NET 8 ReadOnlySpan Split
return default;
}
/// <summary>
/// Parses a query string into a <see cref="QueryString"/> object. Keys/values are automatically URL decoded.
/// </summary>