refactor viewmodel

This commit is contained in:
Lightczx
2023-08-16 17:19:43 +08:00
parent d08d2b406f
commit c4e4ffebd6
101 changed files with 814 additions and 733 deletions

View File

@@ -277,6 +277,12 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
}
}
if (syntax.Operand is DefaultExpressionSyntax expression)
{
return;
}
Location location = syntax.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location);
context.ReportDiagnostic(diagnostic);

View File

@@ -52,12 +52,15 @@ internal static class CompositionExtension
Mode = blendEffectMode,
};
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
using (effect)
{
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter(Background, background);
brush.SetSourceParameter(Foreground, foreground);
brush.SetSourceParameter(Background, background);
brush.SetSourceParameter(Foreground, foreground);
return brush;
return brush;
}
}
/// <summary>
@@ -75,11 +78,14 @@ internal static class CompositionExtension
Source = new CompositionEffectSourceParameter(Source),
};
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
using (effect)
{
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter(Source, source);
brush.SetSourceParameter(Source, source);
return brush;
return brush;
}
}
/// <summary>
@@ -97,11 +103,14 @@ internal static class CompositionExtension
Source = new CompositionEffectSourceParameter(Source),
};
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
using (effect)
{
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter(Source, sourceBrush);
brush.SetSourceParameter(Source, sourceBrush);
return brush;
return brush;
}
}
/// <summary>
@@ -116,18 +125,21 @@ internal static class CompositionExtension
CompositionBrush sourceBrush,
CompositionBrush alphaMask)
{
AlphaMaskEffect maskEffect = new()
AlphaMaskEffect effect = new()
{
AlphaMask = new CompositionEffectSourceParameter(AlphaMask),
Source = new CompositionEffectSourceParameter(Source),
};
CompositionEffectBrush brush = compositor.CreateEffectFactory(maskEffect).CreateBrush();
using (effect)
{
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter(AlphaMask, alphaMask);
brush.SetSourceParameter(Source, sourceBrush);
brush.SetSourceParameter(AlphaMask, alphaMask);
brush.SetSourceParameter(Source, sourceBrush);
return brush;
return brush;
}
}
/// <summary>

View File

@@ -17,4 +17,11 @@ internal interface IMappingFrom<TSelf, T1, T2>
{
[Pure]
static abstract TSelf From(T1 t1, T2 t2);
}
internal interface IMappingFrom<TSelf, T1, T2, T3>
where TSelf : IMappingFrom<TSelf, T1, T2, T3>
{
[Pure]
static abstract TSelf From(T1 t1, T2 t2, T3 t3);
}

View File

@@ -88,6 +88,7 @@ internal static partial class IocHttpClientConfiguration
/// HoYoLAB web
/// </summary>
/// <param name="client">配置后的客户端</param>
[SuppressMessage("", "IDE0051")]
private static void XRpc4Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;

View File

@@ -34,4 +34,10 @@ internal interface IContentDialogFactory
/// <param name="title">标题</param>
/// <returns>内容对话框</returns>
ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title);
TContentDialog CreateInstance<TContentDialog>(params object[] parameters)
where TContentDialog : ContentDialog;
ValueTask<TContentDialog> CreateInstanceAsync<TContentDialog>(params object[] parameters)
where TContentDialog : ContentDialog;
}

View File

@@ -3,27 +3,19 @@
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Factory.Abstraction;
using System.Security.Authentication;
namespace Snap.Hutao.Factory;
/// <inheritdoc cref="IContentDialogFactory"/>
[HighQuality]
[Injection(InjectAs.Transient, typeof(IContentDialogFactory))]
internal sealed class ContentDialogFactory : IContentDialogFactory
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IContentDialogFactory))]
internal sealed partial class ContentDialogFactory : IContentDialogFactory
{
private readonly MainWindow mainWindow;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
/// <summary>
/// 构造一个新的内容对话框工厂
/// </summary>
/// <param name="taskContext">任务上下文</param>
/// <param name="mainWindow">主窗体</param>
public ContentDialogFactory(ITaskContext taskContext, MainWindow mainWindow)
{
this.taskContext = taskContext;
this.mainWindow = mainWindow;
}
private readonly MainWindow mainWindow;
/// <inheritdoc/>
public async ValueTask<ContentDialogResult> CreateForConfirmAsync(string title, string content)
@@ -71,4 +63,17 @@ internal sealed class ContentDialogFactory : IContentDialogFactory
return dialog;
}
public async ValueTask<TContentDialog> CreateInstanceAsync<TContentDialog>(params object[] parameters)
where TContentDialog : ContentDialog
{
await taskContext.SwitchToMainThreadAsync();
return serviceProvider.CreateInstance<TContentDialog>();
}
public TContentDialog CreateInstance<TContentDialog>(params object[] parameters)
where TContentDialog : ContentDialog
{
return serviceProvider.CreateInstance<TContentDialog>();
}
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Service.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
namespace Snap.Hutao.Model.Entity;
/// <summary>
/// 操作部分
/// </summary>
internal sealed partial class GachaArchive
{
/// <summary>
/// 保存祈愿物品
/// </summary>
/// <param name="context">上下文</param>
[SuppressMessage("", "SH002")]
public void SaveItems(GachaItemSaveContext context)
{
if (context.ItemsToAdd.Count > 0)
{
// 全量刷新
if (!context.IsLazy)
{
context.GachaItems
.Where(i => i.ArchiveId == InnerId)
.Where(i => i.Id >= context.EndId)
.ExecuteDelete();
}
context.GachaItems.AddRangeAndSave(context.ItemsToAdd);
}
}
}

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// UIGF格式的信息
/// </summary>
[HighQuality]
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, IServiceProvider, string>
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, MetadataOptions, string>
{
/// <summary>
/// 用户Uid
@@ -58,17 +58,8 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, IServiceProvider, string
[JsonPropertyName("uigf_version")]
public string UIGFVersion { get; set; } = default!;
/// <summary>
/// 构造一个新的专用 UIGF 信息
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="uid">uid</param>
/// <returns>专用 UIGF 信息</returns>
public static UIGFInfo From(IServiceProvider serviceProvider, string uid)
public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid)
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
MetadataOptions metadataOptions = serviceProvider.GetRequiredService<MetadataOptions>();
return new()
{
Uid = uid,

View File

@@ -57,6 +57,7 @@ internal sealed partial class AchievementDbService : IAchievementDbService
}
}
[SuppressMessage("", "CA1305")]
public async ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take)
{
using (IServiceScope scope = serviceProvider.CreateScope())
@@ -66,7 +67,7 @@ internal sealed partial class AchievementDbService : IAchievementDbService
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.OrderByDescending(a => a.Time)
.OrderByDescending(a => a.Time.ToString())
.Take(take)
.ToListAsync()
.ConfigureAwait(false);

View File

@@ -25,8 +25,8 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
private readonly IAvatarInfoDbService avatarInfoDbService;
private readonly ILogger<AvatarInfoService> logger;
private readonly IMetadataService metadataService;
private readonly IServiceProvider serviceProvider;
private readonly ISummaryFactory summaryFactory;
private readonly EnkaClient enkaClient;
/// <inheritdoc/>
public async ValueTask<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default)
@@ -92,8 +92,6 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
private async ValueTask<EnkaResponse?> GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default)
{
EnkaClient enkaClient = serviceProvider.GetRequiredService<EnkaClient>();
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
}

View File

@@ -10,6 +10,7 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using Windows.Foundation.Metadata;
namespace Snap.Hutao.Service.DailyNote;
@@ -18,38 +19,26 @@ namespace Snap.Hutao.Service.DailyNote;
/// 实时便笺通知器
/// </summary>
[HighQuality]
internal sealed class DailyNoteNotificationOperation
[ConstructorGenerated]
[Injection(InjectAs.Singleton)]
internal sealed partial class DailyNoteNotificationOperation
{
private const string ToastHeaderIdArgument = "DAILYNOTE";
private const string ToastAttributionUnknown = "Unknown";
private readonly ITaskContext taskContext;
private readonly IServiceProvider serviceProvider;
private readonly DailyNoteEntry entry;
private readonly IGameService gameService;
private readonly BindingClient bindingClient;
private readonly DailyNoteOptions options;
/// <summary>
/// 构造一个新的实时便笺通知器
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="entry">实时便笺入口</param>
public DailyNoteNotificationOperation(IServiceProvider serviceProvider, DailyNoteEntry entry)
{
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
this.serviceProvider = serviceProvider;
this.entry = entry;
}
/// <summary>
/// 异步通知
/// </summary>
/// <returns>任务</returns>
public async ValueTask SendAsync()
public async ValueTask SendAsync(DailyNoteEntry entry)
{
if (entry.DailyNote is null)
{
return;
}
List<NotifyInfo> notifyInfos = new();
List<DailyNoteNotifyInfo> notifyInfos = new();
CheckNotifySuppressed(entry, notifyInfos);
@@ -58,92 +47,95 @@ internal sealed class DailyNoteNotificationOperation
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
Response<ListWrapper<UserGameRole>> rolesResponse = await bindingClient
.GetUserGameRolesOverseaAwareAsync(entry.User)
.ConfigureAwait(false);
if (rolesResponse.IsOk())
{
DailyNoteOptions options = scope.ServiceProvider.GetRequiredService<DailyNoteOptions>();
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(entry.User)
.ConfigureAwait(false);
if (rolesResponse.IsOk())
{
List<UserGameRole> roles = rolesResponse.Data.List;
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? ToastAttributionUnknown;
}
ToastContentBuilder builder = new ToastContentBuilder()
.AddHeader(ToastHeaderIdArgument, SH.ServiceDailyNoteNotifierTitle, ToastHeaderIdArgument)
.AddAttributionText(attribution)
.AddButton(new ToastButton()
.SetContent(SH.ServiceDailyNoteNotifierActionLaunchGameButton)
.AddArgument(Activation.Action, Activation.LaunchGame)
.AddArgument(Activation.Uid, entry.Uid))
.AddButton(new ToastButtonDismiss(SH.ServiceDailyNoteNotifierActionLaunchGameDismiss));
if (options.IsReminderNotification)
{
builder.SetToastScenario(ToastScenario.Reminder);
}
if (notifyInfos.Count > 2)
{
builder.AddText(SH.ServiceDailyNoteNotifierMultiValueReached);
// Desktop and Mobile started supporting adaptive toasts in API contract 3 (Anniversary Update)
if (UniversalApiContract.IsPresent(WindowsVersion.Windows10AnniversaryUpdate))
{
AdaptiveGroup group = new();
foreach (NotifyInfo info in notifyInfos)
{
AdaptiveSubgroup subgroup = new()
{
HintWeight = 1,
Children =
{
new AdaptiveImage() { Source = info.AdaptiveIcon, HintRemoveMargin = true, },
new AdaptiveText() { Text = info.AdaptiveHint, HintAlign = AdaptiveTextAlign.Center, },
new AdaptiveText() { Text = info.Title, HintAlign = AdaptiveTextAlign.Center, HintStyle = AdaptiveTextStyle.CaptionSubtle, },
},
};
group.Children.Add(subgroup);
}
builder.AddVisualChild(group);
}
}
else
{
foreach (NotifyInfo info in notifyInfos)
{
builder.AddText(info.Hint);
}
}
await taskContext.SwitchToMainThreadAsync();
builder.Show(toast => toast.SuppressPopup = ShouldSuppressPopup(options));
List<UserGameRole> roles = rolesResponse.Data.List;
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? ToastAttributionUnknown;
}
ToastContentBuilder builder = new ToastContentBuilder()
.AddHeader(ToastHeaderIdArgument, SH.ServiceDailyNoteNotifierTitle, ToastHeaderIdArgument)
.AddAttributionText(attribution)
.AddButton(new ToastButton()
.SetContent(SH.ServiceDailyNoteNotifierActionLaunchGameButton)
.AddArgument(Activation.Action, Activation.LaunchGame)
.AddArgument(Activation.Uid, entry.Uid))
.AddButton(new ToastButtonDismiss(SH.ServiceDailyNoteNotifierActionLaunchGameDismiss));
if (options.IsReminderNotification)
{
builder.SetToastScenario(ToastScenario.Reminder);
}
if (notifyInfos.Count > 2)
{
builder.AddText(SH.ServiceDailyNoteNotifierMultiValueReached);
// Desktop and Mobile started supporting adaptive toasts in API contract 3 (Anniversary Update)
if (UniversalApiContract.IsPresent(WindowsVersion.Windows10AnniversaryUpdate))
{
AdaptiveGroup group = new();
foreach (DailyNoteNotifyInfo info in notifyInfos)
{
AdaptiveSubgroup subgroup = new()
{
HintWeight = 1,
Children =
{
new AdaptiveImage() { Source = info.AdaptiveIcon, HintRemoveMargin = true, },
new AdaptiveText() { Text = info.AdaptiveHint, HintAlign = AdaptiveTextAlign.Center, },
new AdaptiveText() { Text = info.Title, HintAlign = AdaptiveTextAlign.Center, HintStyle = AdaptiveTextStyle.CaptionSubtle, },
},
};
group.Children.Add(subgroup);
}
builder.AddVisualChild(group);
}
}
else
{
foreach (DailyNoteNotifyInfo info in notifyInfos)
{
builder.AddText(info.Hint);
}
}
await taskContext.SwitchToMainThreadAsync();
builder.Show(toast => toast.SuppressPopup = ShouldSuppressPopup(options));
}
private static void CheckNotifySuppressed(DailyNoteEntry entry, List<NotifyInfo> notifyInfos)
private static void CheckNotifySuppressed(DailyNoteEntry entry, List<DailyNoteNotifyInfo> notifyInfos)
{
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast?tabs=uwp#adding-images
// Image limitation.
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast?tabs=uwp#adding-images
// NotifySuppressed judge
if (entry.DailyNote!.CurrentResin >= entry.ResinNotifyThreshold)
ChcekResinNotifySuppressed(entry, notifyInfos);
CheckHomeCoinNotifySuppressed(entry, notifyInfos);
CheckDailyTaskNotifySuppressed(entry, notifyInfos);
CheckTransformerNotifySuppressed(entry, notifyInfos);
CheckExpeditionNotifySuppressed(entry, notifyInfos);
}
private static void ChcekResinNotifySuppressed(DailyNoteEntry entry, List<DailyNoteNotifyInfo> notifyInfos)
{
ArgumentNullException.ThrowIfNull(entry.DailyNote);
if (entry.DailyNote.CurrentResin >= entry.ResinNotifyThreshold)
{
if (!entry.ResinNotifySuppressed)
{
notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierResin,
Web.Hoyolab.Images.UIItemIcon210,
Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_210.png"),
$"{entry.DailyNote.CurrentResin}",
string.Format(SH.ServiceDailyNoteNotifierResinCurrent, entry.DailyNote.CurrentResin)));
SH.ServiceDailyNoteNotifierResinCurrent.Format(entry.DailyNote.CurrentResin)));
entry.ResinNotifySuppressed = true;
}
}
@@ -151,16 +143,20 @@ internal sealed class DailyNoteNotificationOperation
{
entry.ResinNotifySuppressed = false;
}
}
private static void CheckHomeCoinNotifySuppressed(DailyNoteEntry entry, List<DailyNoteNotifyInfo> notifyInfos)
{
ArgumentNullException.ThrowIfNull(entry.DailyNote);
if (entry.DailyNote.CurrentHomeCoin >= entry.HomeCoinNotifyThreshold)
{
if (!entry.HomeCoinNotifySuppressed)
{
notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierHomeCoin,
Web.Hoyolab.Images.UIItemIcon204,
Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_204.png"),
$"{entry.DailyNote.CurrentHomeCoin}",
string.Format(SH.ServiceDailyNoteNotifierHomeCoinCurrent, entry.DailyNote.CurrentHomeCoin)));
SH.ServiceDailyNoteNotifierHomeCoinCurrent.Format(entry.DailyNote.CurrentHomeCoin)));
entry.HomeCoinNotifySuppressed = true;
}
}
@@ -168,14 +164,17 @@ internal sealed class DailyNoteNotificationOperation
{
entry.HomeCoinNotifySuppressed = false;
}
}
if (entry.DailyTaskNotify && !entry.DailyNote.IsExtraTaskRewardReceived)
private static void CheckDailyTaskNotifySuppressed(DailyNoteEntry entry, List<DailyNoteNotifyInfo> notifyInfos)
{
if (entry is { DailyTaskNotify: true, DailyNote.IsExtraTaskRewardReceived: false })
{
if (!entry.DailyTaskNotifySuppressed)
{
notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierDailyTask,
Web.Hoyolab.Images.UIMarkQuestEventsProce,
Web.HutaoEndpoints.StaticFile("Bg", "UI_MarkQuest_Events_Proce.png"),
SH.ServiceDailyNoteNotifierDailyTaskHint,
entry.DailyNote.ExtraTaskRewardDescription));
entry.DailyTaskNotifySuppressed = true;
@@ -185,14 +184,17 @@ internal sealed class DailyNoteNotificationOperation
{
entry.DailyTaskNotifySuppressed = false;
}
}
if (entry.TransformerNotify && entry.DailyNote.Transformer.Obtained && entry.DailyNote.Transformer.RecoveryTime.Reached)
private static void CheckTransformerNotifySuppressed(DailyNoteEntry entry, List<DailyNoteNotifyInfo> notifyInfos)
{
if (entry is { TransformerNotify: true, DailyNote.Transformer: { Obtained: true, RecoveryTime.Reached: true } })
{
if (!entry.TransformerNotifySuppressed)
{
notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierTransformer,
Web.Hoyolab.Images.UIItemIcon220021,
Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_220021.png"),
SH.ServiceDailyNoteNotifierTransformerAdaptiveHint,
SH.ServiceDailyNoteNotifierTransformerHint));
entry.TransformerNotifySuppressed = true;
@@ -202,14 +204,18 @@ internal sealed class DailyNoteNotificationOperation
{
entry.TransformerNotifySuppressed = false;
}
}
private static void CheckExpeditionNotifySuppressed(DailyNoteEntry entry, List<DailyNoteNotifyInfo> notifyInfos)
{
ArgumentNullException.ThrowIfNull(entry.DailyNote);
if (entry.ExpeditionNotify && entry.DailyNote.Expeditions.All(e => e.Status == ExpeditionStatus.Finished))
{
if (!entry.ExpeditionNotifySuppressed)
{
notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierExpedition,
Web.Hoyolab.Images.UIIconInteeExplore1,
Web.HutaoEndpoints.StaticFile("Bg", "UI_Icon_Intee_Explore_1.png"),
SH.ServiceDailyNoteNotifierExpeditionAdaptiveHint,
SH.ServiceDailyNoteNotifierExpeditionHint));
entry.ExpeditionNotifySuppressed = true;
@@ -224,22 +230,6 @@ internal sealed class DailyNoteNotificationOperation
private bool ShouldSuppressPopup(DailyNoteOptions options)
{
// Prevent notify when we are in game && silent mode.
return options.IsSilentWhenPlayingGame && serviceProvider.GetRequiredService<IGameService>().IsGameRunning();
}
private readonly struct NotifyInfo
{
public readonly string Title;
public readonly string AdaptiveIcon;
public readonly string AdaptiveHint;
public readonly string Hint;
public NotifyInfo(string title, string adaptiveIcon, string adaptiveHint, string hint)
{
Title = title;
AdaptiveIcon = adaptiveIcon;
AdaptiveHint = adaptiveHint;
Hint = hint;
}
return options.IsSilentWhenPlayingGame && gameService.IsGameRunning();
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.DailyNote;
internal readonly struct DailyNoteNotifyInfo
{
public readonly string Title;
public readonly string AdaptiveIcon;
public readonly string AdaptiveHint;
public readonly string Hint;
public DailyNoteNotifyInfo(string title, string adaptiveIcon, string adaptiveHint, string hint)
{
Title = title;
AdaptiveIcon = adaptiveIcon;
AdaptiveHint = adaptiveHint;
Hint = hint;
}
}

View File

@@ -26,6 +26,7 @@ namespace Snap.Hutao.Service.DailyNote;
[Injection(InjectAs.Singleton, typeof(IDailyNoteService))]
internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessage>
{
private readonly DailyNoteNotificationOperation dailyNoteNotificationOperation;
private readonly IServiceProvider serviceProvider;
private readonly IDailyNoteDbService dailyNoteDbService;
private readonly IUserService userService;
@@ -106,7 +107,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
entries?.SingleOrDefault(e => e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote);
// database
await new DailyNoteNotificationOperation(serviceProvider, entry).SendAsync().ConfigureAwait(false);
await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false);
entry.DailyNote = dailyNote;
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false);
}

View File

@@ -9,6 +9,7 @@ using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hutao;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -21,8 +22,8 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
[Injection(InjectAs.Scoped, typeof(IGachaStatisticsFactory))]
internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
{
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService;
private readonly HomaGachaLogClient homaGachaLogClient;
private readonly ITaskContext taskContext;
private readonly AppOptions options;
@@ -33,32 +34,25 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
List<GachaEvent> gachaEvents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
List<HistoryWishBuilder> historyWishBuilders = gachaEvents.SelectList(gachaEvent => new HistoryWishBuilder(gachaEvent, context));
return CreateCore(serviceProvider, items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible);
return CreateCore(taskContext, homaGachaLogClient, items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible);
}
private static GachaStatistics CreateCore(
IServiceProvider serviceProvider,
ITaskContext taskContext,
HomaGachaLogClient gachaLogClient,
List<GachaItem> items,
List<HistoryWishBuilder> historyWishBuilders,
in GachaLogServiceMetadataContext context,
bool isEmptyHistoryWishVisible)
{
TypedWishSummaryBuilder standardWishBuilder = new(
serviceProvider,
SH.ServiceGachaLogFactoryPermanentWishName,
TypedWishSummaryBuilder.IsStandardWish,
Web.Hutao.GachaLog.GachaDistributionType.Standard);
TypedWishSummaryBuilder avatarWishBuilder = new(
serviceProvider,
SH.ServiceGachaLogFactoryAvatarWishName,
TypedWishSummaryBuilder.IsAvatarEventWish,
Web.Hutao.GachaLog.GachaDistributionType.AvatarEvent);
TypedWishSummaryBuilder weaponWishBuilder = new(
serviceProvider,
SH.ServiceGachaLogFactoryWeaponWishName,
TypedWishSummaryBuilder.IsWeaponEventWish,
Web.Hutao.GachaLog.GachaDistributionType.WeaponEvent,
80);
TypedWishSummaryBuilderContext standardContext = TypedWishSummaryBuilderContext.StandardWish(taskContext, gachaLogClient);
TypedWishSummaryBuilder standardWishBuilder = new(standardContext);
TypedWishSummaryBuilderContext avatarContext = TypedWishSummaryBuilderContext.AvatarEventWish(taskContext, gachaLogClient);
TypedWishSummaryBuilder avatarWishBuilder = new(avatarContext);
TypedWishSummaryBuilderContext weaponContext = TypedWishSummaryBuilderContext.WeaponEventWish(taskContext, gachaLogClient);
TypedWishSummaryBuilder weaponWishBuilder = new(weaponContext);
Dictionary<Avatar, int> orangeAvatarCounter = new();
Dictionary<Avatar, int> purpleAvatarCounter = new();

View File

@@ -14,38 +14,26 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
/// </summary>
internal sealed class PullPrediction
{
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly TypedWishSummary typedWishSummary;
private readonly GachaDistributionType distributionType;
private readonly TypedWishSummaryBuilderContext context;
/// <summary>
/// 构造一个新的抽数预计
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="typedWishSummary">类型化祈愿统计信息</param>
/// <param name="distributionType">分布类型</param>
public PullPrediction(IServiceProvider serviceProvider, TypedWishSummary typedWishSummary, GachaDistributionType distributionType)
public PullPrediction(TypedWishSummary typedWishSummary, in TypedWishSummaryBuilderContext context)
{
this.serviceProvider = serviceProvider;
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
this.typedWishSummary = typedWishSummary;
this.distributionType = distributionType;
this.context = context;
}
public async ValueTask PredictAsync(AsyncBarrier barrier)
{
await taskContext.SwitchToBackgroundAsync();
HomaGachaLogClient gachaLogClient = serviceProvider.GetRequiredService<HomaGachaLogClient>();
Response<GachaDistribution> response = await gachaLogClient.GetGachaDistributionAsync(distributionType).ConfigureAwait(false);
await context.TaskContext.SwitchToBackgroundAsync();
Response<GachaDistribution> response = await context.GetGachaDistributionAsync().ConfigureAwait(false);
if (response.IsOk())
{
PredictResult result = PredictCore(response.Data.Distribution, typedWishSummary);
await barrier.SignalAndWaitAsync().ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
await context.TaskContext.SwitchToMainThreadAsync();
typedWishSummary.ProbabilityOfNextPullIsOrange = result.ProbabilityOfNextPullIsOrange;
typedWishSummary.ProbabilityOfPredictedPullLeftToOrange = result.ProbabilityOfPredictedPullLeftToOrange;
typedWishSummary.PredictedPullLeftToOrange = result.PredictedPullLeftToOrange;

View File

@@ -6,6 +6,7 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -30,12 +31,7 @@ internal sealed class TypedWishSummaryBuilder
/// </summary>
public static readonly Func<GachaConfigType, bool> IsWeaponEventWish = type => type is GachaConfigType.WeaponEventWish;
private readonly IServiceProvider serviceProvider;
private readonly string name;
private readonly int guaranteeOrangeThreshold;
private readonly int guaranteePurpleThreshold;
private readonly Func<GachaConfigType, bool> typeEvaluator;
private readonly Web.Hutao.GachaLog.GachaDistributionType distributionType;
private readonly TypedWishSummaryBuilderContext context;
private readonly List<int> averageOrangePullTracker = new();
private readonly List<int> averageUpOrangePullTracker = new();
@@ -54,20 +50,9 @@ internal sealed class TypedWishSummaryBuilder
private DateTimeOffset fromTimeTracker = DateTimeOffset.MaxValue;
private DateTimeOffset toTimeTracker = DateTimeOffset.MinValue;
public TypedWishSummaryBuilder(
IServiceProvider serviceProvider,
string name,
Func<GachaConfigType, bool> typeEvaluator,
Web.Hutao.GachaLog.GachaDistributionType distributionType,
int guaranteeOrangeThreshold = 90,
int guaranteePurpleThreshold = 10)
public TypedWishSummaryBuilder(in TypedWishSummaryBuilderContext context)
{
this.serviceProvider = serviceProvider;
this.name = name;
this.typeEvaluator = typeEvaluator;
this.guaranteeOrangeThreshold = guaranteeOrangeThreshold;
this.guaranteePurpleThreshold = guaranteePurpleThreshold;
this.distributionType = distributionType;
this.context = context;
}
/// <summary>
@@ -78,7 +63,7 @@ internal sealed class TypedWishSummaryBuilder
/// <param name="isUp">是否为Up物品</param>
public void Track(GachaItem item, ISummaryItemSource source, bool isUp)
{
if (typeEvaluator(item.GachaType))
if (context.TypeEvaluator(item.GachaType))
{
++lastOrangePullTracker;
++lastPurplePullTracker;
@@ -129,13 +114,13 @@ internal sealed class TypedWishSummaryBuilder
public TypedWishSummary ToTypedWishSummary(AsyncBarrier barrier)
{
summaryItems.CompleteAdding(guaranteeOrangeThreshold);
summaryItems.CompleteAdding(context.GuaranteeOrangeThreshold);
double totalCount = totalCountTracker;
TypedWishSummary summary = new()
{
// base
Name = name,
Name = context.Name,
From = fromTimeTracker,
To = toTimeTracker,
TotalCount = totalCountTracker,
@@ -144,9 +129,9 @@ internal sealed class TypedWishSummaryBuilder
MaxOrangePull = maxOrangePullTracker,
MinOrangePull = minOrangePullTracker,
LastOrangePull = lastOrangePullTracker,
GuaranteeOrangeThreshold = guaranteeOrangeThreshold,
GuaranteeOrangeThreshold = context.GuaranteeOrangeThreshold,
LastPurplePull = lastPurplePullTracker,
GuaranteePurpleThreshold = guaranteePurpleThreshold,
GuaranteePurpleThreshold = context.GuaranteePurpleThreshold,
TotalOrangePull = totalOrangePullTracker,
TotalPurplePull = totalPurplePullTracker,
TotalBluePull = totalBluePullTracker,
@@ -158,7 +143,7 @@ internal sealed class TypedWishSummaryBuilder
OrangeList = summaryItems,
};
new PullPrediction(serviceProvider, summary, distributionType).PredictAsync(barrier).SafeForget();
new PullPrediction(summary, context).PredictAsync(barrier).SafeForget();
return summary;
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.GachaLog;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.GachaLog.Factory;
internal readonly struct TypedWishSummaryBuilderContext
{
public readonly ITaskContext TaskContext;
public readonly HomaGachaLogClient GachaLogClient;
public readonly string Name;
public readonly int GuaranteeOrangeThreshold;
public readonly int GuaranteePurpleThreshold;
public readonly Func<GachaConfigType, bool> TypeEvaluator;
public readonly GachaDistributionType DistributionType;
private static readonly Func<GachaConfigType, bool> IsStandardWish = type => type is GachaConfigType.StandardWish;
private static readonly Func<GachaConfigType, bool> IsAvatarEventWish = type => type is GachaConfigType.AvatarEventWish or GachaConfigType.AvatarEventWish2;
private static readonly Func<GachaConfigType, bool> IsWeaponEventWish = type => type is GachaConfigType.WeaponEventWish;
public TypedWishSummaryBuilderContext(
ITaskContext taskContext,
HomaGachaLogClient gachaLogClient,
string name,
int guaranteeOrangeThreshold,
int guaranteePurpleThreshold,
Func<GachaConfigType, bool> typeEvaluator,
GachaDistributionType distributionType)
{
TaskContext = taskContext;
GachaLogClient = gachaLogClient;
Name = name;
GuaranteeOrangeThreshold = guaranteeOrangeThreshold;
GuaranteePurpleThreshold = guaranteePurpleThreshold;
TypeEvaluator = typeEvaluator;
DistributionType = distributionType;
}
public static TypedWishSummaryBuilderContext StandardWish(ITaskContext taskContext, HomaGachaLogClient gachaLogClient)
{
return new(taskContext, gachaLogClient, SH.ServiceGachaLogFactoryPermanentWishName, 90, 10, IsStandardWish, GachaDistributionType.Standard);
}
public static TypedWishSummaryBuilderContext AvatarEventWish(ITaskContext taskContext, HomaGachaLogClient gachaLogClient)
{
return new(taskContext, gachaLogClient, SH.ServiceGachaLogFactoryAvatarWishName, 90, 10, IsAvatarEventWish, GachaDistributionType.AvatarEvent);
}
public static TypedWishSummaryBuilderContext WeaponEventWish(ITaskContext taskContext, HomaGachaLogClient gachaLogClient)
{
return new(taskContext, gachaLogClient, SH.ServiceGachaLogFactoryWeaponWishName, 80, 10, IsWeaponEventWish, GachaDistributionType.WeaponEvent);
}
public ValueTask<Response<GachaDistribution>> GetGachaDistributionAsync()
{
return GachaLogClient.GetGachaDistributionAsync(DistributionType);
}
}

View File

@@ -11,22 +11,15 @@ namespace Snap.Hutao.Service.GachaLog;
/// </summary>
internal static class GachaArchiveOperation
{
public static void GetOrAdd(IServiceProvider serviceProvider, string uid, ObservableCollection<GachaArchive> archives, [NotNull] out GachaArchive? archive)
public static void GetOrAdd(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, string uid, ObservableCollection<GachaArchive> archives, [NotNull] out GachaArchive? archive)
{
archive = archives.SingleOrDefault(a => a.Uid == uid);
if (archive is null)
{
GachaArchive created = GachaArchive.From(uid);
using (IServiceScope scope = serviceProvider.CreateScope())
{
IGachaLogDbService gachaLogDbService = scope.ServiceProvider.GetRequiredService<IGachaLogDbService>();
gachaLogDbService.AddGachaArchive(created);
ITaskContext taskContext = scope.ServiceProvider.GetRequiredService<ITaskContext>();
taskContext.InvokeOnMainThread(() => archives.Add(created));
}
gachaLogDbService.AddGachaArchive(created);
taskContext.InvokeOnMainThread(() => archives.Add(created));
archive = created;
}
}

View File

@@ -29,20 +29,27 @@ internal readonly struct GachaItemSaveContext
/// <summary>
/// 数据集
/// </summary>
public readonly DbSet<GachaItem> GachaItems;
public readonly IGachaLogDbService GachaLogDbService;
/// <summary>
/// 构造一个新的祈愿物品
/// </summary>
/// <param name="itemsToAdd">待添加物品</param>
/// <param name="isLazy">是否懒惰</param>
/// <param name="endId">结尾 Id</param>
/// <param name="gachaItems">数据集</param>
public GachaItemSaveContext(List<GachaItem> itemsToAdd, bool isLazy, long endId, DbSet<GachaItem> gachaItems)
public GachaItemSaveContext(List<GachaItem> itemsToAdd, bool isLazy, long endId, IGachaLogDbService gachaLogDbService)
{
ItemsToAdd = itemsToAdd;
IsLazy = isLazy;
EndId = endId;
GachaItems = gachaItems;
GachaLogDbService = gachaLogDbService;
}
public void SaveItems(GachaArchive archive)
{
if (ItemsToAdd.Count > 0)
{
// 全量刷新
if (!IsLazy)
{
GachaLogDbService.DeleteNewerGachaItemsByArchiveIdAndEndId(archive.InnerId, EndId);
}
GachaLogDbService.AddGachaItems(ItemsToAdd);
}
}
}

View File

@@ -13,7 +13,7 @@ using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IGachaLogDbService))]
[Injection(InjectAs.Singleton, typeof(IGachaLogDbService))]
internal sealed partial class GachaLogDbService : IGachaLogDbService
{
private readonly IServiceProvider serviceProvider;
@@ -30,7 +30,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
}
catch (SqliteException ex)
{
string message = string.Format(SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage, ex.Message);
string message = SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage.Format(ex.Message);
throw ThrowHelper.UserdataCorrupted(message, ex);
}
}
@@ -202,4 +202,26 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
await appDbContext.GachaItems.AddRangeAndSaveAsync(items).ConfigureAwait(false);
}
}
}
public void AddGachaItems(List<GachaItem> items)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GachaItems.AddRangeAndSave(items);
}
}
public void DeleteNewerGachaItemsByArchiveIdAndEndId(Guid archiveId, long endId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.Id >= endId)
.ExecuteDelete();
}
}
}

View File

@@ -49,19 +49,15 @@ internal struct GachaLogFetchContext
/// </summary>
public GachaConfigType CurrentType;
private readonly IServiceProvider serviceProvider;
private readonly GachaLogServiceMetadataContext serviceContext;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
private readonly bool isLazy;
/// <summary>
/// 构造一个新的祈愿记录获取上下文
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="serviceContext">祈愿服务上下文</param>
/// <param name="isLazy">是否为懒惰模式</param>
public GachaLogFetchContext(IServiceProvider serviceProvider, in GachaLogServiceMetadataContext serviceContext, bool isLazy)
public GachaLogFetchContext(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, in GachaLogServiceMetadataContext serviceContext, bool isLazy)
{
this.serviceProvider = serviceProvider;
this.gachaLogDbService = gachaLogDbService;
this.taskContext = taskContext;
this.serviceContext = serviceContext;
this.isLazy = isLazy;
}
@@ -99,7 +95,7 @@ internal struct GachaLogFetchContext
{
if (TargetArchive is null)
{
GachaArchiveOperation.GetOrAdd(serviceProvider, item.Uid, archives, out TargetArchive);
GachaArchiveOperation.GetOrAdd(gachaLogDbService, taskContext, item.Uid, archives, out TargetArchive);
}
DbEndId ??= gachaLogDbService.GetNewestGachaItemIdByArchiveIdAndQueryType(TargetArchive.InnerId, CurrentType);
@@ -131,7 +127,8 @@ internal struct GachaLogFetchContext
/// <param name="item">物品</param>
public void AddItem(GachaLogItem item)
{
ItemsToAdd.Add(GachaItem.From(TargetArchive!.InnerId, item, serviceContext.GetItemId(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;
}
@@ -141,16 +138,11 @@ internal struct GachaLogFetchContext
/// </summary>
public readonly void SaveItems()
{
using (IServiceScope scope = serviceProvider.CreateScope())
// While no item is fetched, archive can be null.
if (TargetArchive is not null)
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// While no item is fetched, archive can be null.
if (TargetArchive is not null)
{
GachaItemSaveContext saveContext = new(ItemsToAdd, isLazy, QueryOptions.EndId, appDbContext.GachaItems);
TargetArchive.SaveItems(saveContext);
}
GachaItemSaveContext saveContext = new(ItemsToAdd, isLazy, QueryOptions.EndId, gachaLogDbService);
saveContext.SaveItems(TargetArchive);
}
}

View File

@@ -25,9 +25,9 @@ namespace Snap.Hutao.Service.GachaLog;
[Injection(InjectAs.Scoped, typeof(IGachaLogHutaoCloudService))]
internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudService
{
private readonly IMetadataService metadataService;
private readonly HomaGachaLogClient homaGachaLogClient;
private readonly IGachaLogDbService gachaLogDbService;
private readonly IServiceProvider serviceProvider;
/// <inheritdoc/>
public ValueTask<Response<List<GachaEntry>>> GetGachaEntriesAsync(CancellationToken token = default)
@@ -92,7 +92,6 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
Response<GachaEventStatistics> response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false);
if (response.IsOk())
{
IMetadataService metadataService = serviceProvider.GetRequiredService<IMetadataService>();
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);

View File

@@ -29,7 +29,6 @@ internal sealed partial class GachaLogService : IGachaLogService
private readonly IGachaStatisticsFactory gachaStatisticsFactory;
private readonly IUIGFExportService gachaLogExportService;
private readonly IUIGFImportService gachaLogImportService;
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService;
private readonly ILogger<GachaLogService> logger;
private readonly GachaInfoClient gachaInfoClient;
@@ -160,7 +159,7 @@ internal sealed partial class GachaLogService : IGachaLogService
private async ValueTask<ValueResult<bool, GachaArchive?>> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(ArchiveCollection);
GachaLogFetchContext fetchContext = new(serviceProvider, context, isLazy);
GachaLogFetchContext fetchContext = new(gachaLogDbService, taskContext, context, isLazy);
foreach (GachaConfigType configType in GachaLog.QueryTypes)
{
@@ -172,7 +171,7 @@ internal sealed partial class GachaLogService : IGachaLogService
.GetGachaLogPageAsync(fetchContext.QueryOptions, token)
.ConfigureAwait(false);
if (response.TryGetData(out GachaLogPage? page, serviceProvider))
if (response.TryGetData(out GachaLogPage? page))
{
List<GachaLogItem> items = page.List;
fetchContext.ResetForProcessingPage();

View File

@@ -121,12 +121,12 @@ internal readonly struct GachaLogServiceMetadataContext
{
if (item.ItemType == SH.ModelInterchangeUIGFItemTypeAvatar)
{
return NameAvatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0;
return NameAvatarMap.GetValueOrDefault(item.Name)?.Id ?? 0;
}
if (item.ItemType == SH.ModelInterchangeUIGFItemTypeWeapon)
{
return NameWeaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0;
return NameWeaponMap.GetValueOrDefault(item.Name)?.Id ?? 0;
}
return 0U;

View File

@@ -13,10 +13,14 @@ internal interface IGachaLogDbService
ValueTask AddGachaArchiveAsync(GachaArchive archive);
void AddGachaItems(List<GachaItem> items);
ValueTask AddGachaItemsAsync(List<GachaItem> items);
ValueTask DeleteGachaArchiveByIdAsync(Guid archiveId);
void DeleteNewerGachaItemsByArchiveIdAndEndId(Guid archiveId, long endId);
ValueTask<GachaArchive?> GetGachaArchiveByUidAsync(string uid, CancellationToken token);
ObservableCollection<GachaArchive> GetGachaArchiveCollection();

View File

@@ -31,7 +31,7 @@ internal readonly struct GachaLogQuery
public GachaLogQuery(string query)
{
Query = query;
IsOversea = query.Contains("hoyoverse.com");
IsOversea = query.Contains("hoyoverse.com", StringComparison.OrdinalIgnoreCase);
Message = string.Empty;
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Request.QueryString;
@@ -15,34 +16,29 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
[Injection(InjectAs.Transient)]
internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryProvider
{
private readonly IServiceProvider serviceProvider;
private readonly IContentDialogFactory contentDialogFactory;
private readonly MetadataOptions metadataOptions;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
GachaLogUrlDialog dialog = serviceProvider.CreateInstance<GachaLogUrlDialog>();
GachaLogUrlDialog dialog = await contentDialogFactory.CreateInstanceAsync<GachaLogUrlDialog>().ConfigureAwait(false);
(bool isOk, string queryString) = await dialog.GetInputUrlAsync().ConfigureAwait(false);
if (isOk)
{
QueryString query = QueryString.Parse(queryString);
string queryLanguageCode = query["lang"];
if (query["auth_appid"] == "webview_gacha")
{
string queryLanguageCode = query["lang"];
if (metadataOptions.IsCurrentLocale(queryLanguageCode))
{
return new(true, new(queryString));
}
else
{
string message = string.Format(
SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale,
queryLanguageCode,
metadataOptions.LanguageCode);
string message = SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale
.Format(queryLanguageCode, metadataOptions.LanguageCode);
return new(false, message);
}
}

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
[Injection(InjectAs.Transient, typeof(IGachaLogQueryProviderFactory))]
internal sealed partial class GachaLogQueryProviderFactory : IGachaLogQueryProviderFactory
{
[SuppressMessage("", "SH301")]
private readonly IServiceProvider serviceProvider;
public IGachaLogQueryProvider Create(RefreshOption option)

View File

@@ -35,7 +35,9 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
? GameConstants.GenshinImpactData
: GameConstants.YuanShenData;
DirectoryInfo webCacheFolder = new(Path.Combine(Path.GetDirectoryName(path)!, dataFolder, "webCaches"));
string? directory = Path.GetDirectoryName(path);
ArgumentNullException.ThrowIfNull(directory);
DirectoryInfo webCacheFolder = new(Path.Combine(directory, dataFolder, "webCaches"));
Regex versionRegex = VersionRegex();
DirectoryInfo? lastestVersionCacheFolder = webCacheFolder
.EnumerateDirectories()
@@ -51,53 +53,45 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
{
(bool isOk, string path) = await gameService.GetGamePathAsync().ConfigureAwait(false);
if (isOk && (!string.IsNullOrEmpty(path)))
{
string cacheFile = GetCacheFile(path);
using (TempFile? tempFile = TempFile.CopyFrom(cacheFile))
{
if (tempFile.TryGetValue(out TempFile file))
{
using (FileStream fileStream = new(file.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (MemoryStream memoryStream = new())
{
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
string? result = Match(memoryStream, cacheFile.Contains(GameConstants.GenshinImpactData));
if (!string.IsNullOrEmpty(result))
{
QueryString query = QueryString.Parse(result.TrimEnd("#/log"));
string queryLanguageCode = query["lang"];
if (metadataOptions.IsCurrentLocale(queryLanguageCode))
{
return new(true, new(result));
}
else
{
string message = string.Format(
SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale,
queryLanguageCode,
metadataOptions.LanguageCode);
return new(false, message);
}
}
else
{
return new(false, SH.ServiceGachaLogUrlProviderCacheUrlNotFound);
}
}
}
}
return new(false, string.Format(Regex.Unescape(SH.ServiceGachaLogUrlProviderCachePathNotFound), cacheFile));
}
}
else
if (!isOk || string.IsNullOrEmpty(path))
{
return new(false, SH.ServiceGachaLogUrlProviderCachePathInvalid);
}
string cacheFile = GetCacheFile(path);
using (TempFile? tempFile = TempFile.CopyFrom(cacheFile))
{
if (!tempFile.TryGetValue(out TempFile file))
{
return new(false, Regex.Unescape(SH.ServiceGachaLogUrlProviderCachePathNotFound).Format(cacheFile));
}
using (FileStream fileStream = new(file.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (MemoryStream memoryStream = new())
{
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
string? result = Match(memoryStream, cacheFile.Contains(GameConstants.GenshinImpactData, StringComparison.Ordinal));
if (string.IsNullOrEmpty(result))
{
return new(false, SH.ServiceGachaLogUrlProviderCacheUrlNotFound);
}
QueryString query = QueryString.Parse(result.TrimEnd("#/log"));
string queryLanguageCode = query["lang"];
if (metadataOptions.IsCurrentLocale(queryLanguageCode))
{
return new(true, new(result));
}
string message = SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale
.Format(queryLanguageCode, metadataOptions.LanguageCode);
return new(false, message);
}
}
}
}
private static string? Match(MemoryStream stream, bool isOversea)

View File

@@ -1,9 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service.Metadata;
namespace Snap.Hutao.Service.GachaLog;
@@ -14,8 +16,9 @@ namespace Snap.Hutao.Service.GachaLog;
[Injection(InjectAs.Scoped, typeof(IUIGFExportService))]
internal sealed partial class UIGFExportService : IUIGFExportService
{
private readonly IServiceProvider serviceProvider;
private readonly IGachaLogDbService gachaLogDbService;
private readonly RuntimeOptions runtimeOptions;
private readonly MetadataOptions metadataOptions;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
@@ -29,7 +32,7 @@ internal sealed partial class UIGFExportService : IUIGFExportService
UIGF uigf = new()
{
Info = UIGFInfo.From(serviceProvider, archive.Uid),
Info = UIGFInfo.From(runtimeOptions, metadataOptions, archive.Uid),
List = list,
};

View File

@@ -18,7 +18,6 @@ internal sealed partial class UIGFImportService : IUIGFImportService
{
private readonly ILogger<UIGFImportService> logger;
private readonly MetadataOptions metadataOptions;
private readonly IServiceProvider serviceProvider;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
@@ -29,11 +28,11 @@ internal sealed partial class UIGFImportService : IUIGFImportService
if (!metadataOptions.IsCurrentLocale(uigf.Info.Language))
{
string message = string.Format(SH.ServiceGachaUIGFImportLanguageNotMatch, uigf.Info.Language, metadataOptions.LanguageCode);
string message = SH.ServiceGachaUIGFImportLanguageNotMatch.Format(uigf.Info.Language, metadataOptions.LanguageCode);
ThrowHelper.InvalidOperation(message, null);
}
GachaArchiveOperation.GetOrAdd(serviceProvider, uigf.Info.Uid, archives, out GachaArchive? archive);
GachaArchiveOperation.GetOrAdd(gachaLogDbService, taskContext, uigf.Info.Uid, archives, out GachaArchive? archive);
Guid archiveId = archive.InnerId;
long trimId = gachaLogDbService.GetOldestGachaItemIdByArchiveId(archiveId);

View File

@@ -15,7 +15,7 @@ internal sealed class GameFileOperationException : Exception
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public GameFileOperationException(string message, Exception innerException)
: base(string.Format(SH.ServiceGameFileOperationExceptionMessage, message), innerException)
: base(SH.ServiceGameFileOperationExceptionMessage.Format(message), innerException)
{
}
}

View File

@@ -82,7 +82,7 @@ internal sealed partial class GameService : IGameService
}
else
{
return new(false, null!);
return new(false, default!);
}
}
@@ -112,9 +112,11 @@ internal sealed partial class GameService : IGameService
public bool SetChannelOptions(LaunchScheme scheme)
{
string gamePath = appOptions.GamePath;
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFileName);
string? directory = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
string configPath = Path.Combine(directory, ConfigFileName);
List<IniElement> elements = null!;
List<IniElement> elements = default!;
try
{
using (FileStream readStream = File.OpenRead(configPath))
@@ -124,11 +126,11 @@ internal sealed partial class GameService : IGameService
}
catch (FileNotFoundException ex)
{
ThrowHelper.GameFileOperation(string.Format(SH.ServiceGameSetMultiChannelConfigFileNotFound, configPath), ex);
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex);
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.GameFileOperation(string.Format(SH.ServiceGameSetMultiChannelConfigFileNotFound, configPath), ex);
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex);
}
catch (UnauthorizedAccessException ex)
{
@@ -168,7 +170,8 @@ internal sealed partial class GameService : IGameService
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
string gamePath = appOptions.GamePath;
string gameFolder = Path.GetDirectoryName(gamePath)!;
string? gameFolder = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(gameFolder);
string gameFileName = Path.GetFileName(gamePath);
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
@@ -241,39 +244,36 @@ internal sealed partial class GameService : IGameService
}
string gamePath = appOptions.GamePath;
if (string.IsNullOrWhiteSpace(gamePath))
ArgumentNullException.ThrowIfNullOrEmpty(gamePath);
using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath))
{
// TODO: throw exception
return;
}
Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath);
try
{
bool isFirstInstance = Interlocked.Increment(ref runningGamesCounter) == 1;
game.Start();
bool isAdvancedOptionsAllowed = runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled;
if (isAdvancedOptionsAllowed && launchOptions.MultipleInstances && !isFirstInstance)
try
{
ProcessInterop.DisableProtection(game, gamePath);
}
bool isFirstInstance = Interlocked.Increment(ref runningGamesCounter) == 1;
if (isAdvancedOptionsAllowed && launchOptions.UnlockFps)
{
await ProcessInterop.UnlockFpsAsync(serviceProvider, game, default).ConfigureAwait(false);
game.Start();
bool isAdvancedOptionsAllowed = runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled;
if (isAdvancedOptionsAllowed && launchOptions.MultipleInstances && !isFirstInstance)
{
ProcessInterop.DisableProtection(game, gamePath);
}
if (isAdvancedOptionsAllowed && launchOptions.UnlockFps)
{
await ProcessInterop.UnlockFpsAsync(serviceProvider, game, default).ConfigureAwait(false);
}
else
{
await game.WaitForExitAsync().ConfigureAwait(false);
}
}
else
finally
{
await game.WaitForExitAsync().ConfigureAwait(false);
Interlocked.Decrement(ref runningGamesCounter);
}
}
finally
{
Interlocked.Decrement(ref runningGamesCounter);
}
}
/// <inheritdoc/>
@@ -373,7 +373,8 @@ internal sealed partial class GameService : IGameService
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
gameAccounts!.Remove(gameAccount);
ArgumentNullException.ThrowIfNull(gameAccounts);
gameAccounts.Remove(gameAccount);
await taskContext.SwitchToBackgroundAsync();
await gameDbService.DeleteGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false);

View File

@@ -5,6 +5,7 @@ using Microsoft.UI.Windowing;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using System.Globalization;
using Windows.Graphics;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
@@ -140,12 +141,12 @@ internal sealed class LaunchOptions : DbStoreOptions
[AllowNull]
public NameValue<int> Monitor
{
get => GetOption(ref monitor, SettingEntry.LaunchMonitor, index => Monitors[int.Parse(index) - 1], Monitors[0]);
get => GetOption(ref monitor, SettingEntry.LaunchMonitor, index => Monitors[int.Parse(index, CultureInfo.InvariantCulture) - 1], Monitors[0]);
set
{
if (value is not null)
{
SetOption(ref monitor, SettingEntry.LaunchMonitor, value, selected => selected.Value.ToString());
SetOption(ref monitor, SettingEntry.LaunchMonitor, value, selected => selected.Value.ToString(CultureInfo.InvariantCulture));
}
}
}

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Service.Game.Locator;
[Injection(InjectAs.Transient, typeof(IGameLocatorFactory))]
internal sealed partial class GameLocatorFactory : IGameLocatorFactory
{
[SuppressMessage("", "SH301")]
private readonly IServiceProvider serviceProvider;
public IGameLocator Create(GameLocationSource source)

View File

@@ -39,6 +39,6 @@ internal sealed partial class ManualGameLocator : IGameLocator
}
}
return new(false, null!);
return new(false, default!);
}
}

View File

@@ -32,7 +32,8 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
else
{
string? path = Path.GetDirectoryName(result.Value);
string configPath = Path.Combine(path!, GameConstants.ConfigFileName);
ArgumentException.ThrowIfNullOrEmpty(path);
string configPath = Path.Combine(path, GameConstants.ConfigFileName);
string? escapedPath;
using (FileStream stream = File.OpenRead(configPath))
{
@@ -64,12 +65,12 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
}
else
{
return new(false, null!);
return new(false, default!);
}
}
else
{
return new(false, null!);
return new(false, default!);
}
}
}
@@ -80,10 +81,10 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
// 不包含中文
// Some one's folder might begin with 'u'
if (!hex4Result.Contains(@"\u"))
if (!hex4Result.Contains(@"\u", StringComparison.Ordinal))
{
// fix path with \
hex4Result = hex4Result.Replace(@"\", @"\\");
hex4Result = hex4Result.Replace(@"\", @"\\", StringComparison.Ordinal);
}
return Regex.Unescape(hex4Result);

View File

@@ -116,7 +116,9 @@ internal sealed partial class PackageConverter
if (entry.Length != 0)
{
string targetPath = Path.Combine(gameFolder, entry.FullName);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
string? directory = Path.GetDirectoryName(targetPath);
ArgumentException.ThrowIfNullOrEmpty(directory);
Directory.CreateDirectory(directory);
entry.ExtractToFile(targetPath, true);
}
}
@@ -180,7 +182,8 @@ internal sealed partial class PackageConverter
Regex dataFolderRegex = DataFolderRegex();
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } row && !string.IsNullOrEmpty(row))
{
VersionItem item = JsonSerializer.Deserialize<VersionItem>(row, options)!;
VersionItem? item = JsonSerializer.Deserialize<VersionItem>(row, options);
ArgumentNullException.ThrowIfNull(item);
item.RelativePath = dataFolderRegex.Replace(item.RelativePath, "{0}");
results.Add(item.RelativePath, item);
}
@@ -231,7 +234,7 @@ internal sealed partial class PackageConverter
private async ValueTask SkipOrDownloadAsync(ItemOperationInfo info, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
{
// 还原正确的远程地址
string remoteName = string.Format(info.Remote.RelativePath, context.ToDataFolderName);
string remoteName = info.Remote.RelativePath.Format(context.ToDataFolderName);
string cacheFile = context.GetServerCacheTargetFilePath(remoteName);
if (File.Exists(cacheFile))
@@ -276,7 +279,7 @@ internal sealed partial class PackageConverter
{
// System.IO.IOException: The response ended prematurely.
// System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
ThrowHelper.PackageConvert(string.Format(SH.ServiceGamePackageRequestScatteredFileFailed, remoteName), ex);
ThrowHelper.PackageConvert(SH.ServiceGamePackageRequestScatteredFileFailed.Format(remoteName), ex);
}
}
}
@@ -312,17 +315,21 @@ internal sealed partial class PackageConverter
if (backup)
{
string localFileName = string.Format(info.Local.RelativePath, context.FromDataFolder);
string localFileName = info.Local.RelativePath.Format(context.FromDataFolder);
string localFilePath = context.GetGameFolderFilePath(localFileName);
Directory.CreateDirectory(Path.GetDirectoryName(localFilePath)!);
string? directory = Path.GetDirectoryName(localFilePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
Directory.CreateDirectory(directory);
File.Move(localFilePath, context.GetServerCacheBackupFilePath(localFileName), true);
}
if (target)
{
string targetFileName = string.Format(info.Remote.RelativePath, context.ToDataFolder);
string targetFileName = info.Remote.RelativePath.Format(context.ToDataFolder);
string targetFilePath = context.GetGameFolderFilePath(targetFileName);
Directory.CreateDirectory(Path.GetDirectoryName(targetFilePath)!);
string? directory = Path.GetDirectoryName(targetFilePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
Directory.CreateDirectory(directory);
File.Move(context.GetServerCacheTargetFilePath(targetFileName), targetFilePath, true);
}
}

View File

@@ -88,7 +88,8 @@ internal static class RegistryInterop
private static string GetPowerShellLocation()
{
string paths = Environment.GetEnvironmentVariable("Path")!;
string? paths = Environment.GetEnvironmentVariable("Path");
ArgumentException.ThrowIfNullOrEmpty(paths);
foreach (StringSegment path in new StringTokenizer(paths, ';'.ToArray()))
{
@@ -101,6 +102,6 @@ internal static class RegistryInterop
}
}
throw ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, null!);
throw ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!);
}
}

View File

@@ -184,7 +184,7 @@ internal sealed partial class HutaoCache : IHutaoCache
AvatarAppearanceRanks = avatarAppearanceRanksRaw.SortByDescending(r => r.Floor).SelectList(rank => new AvatarRankView
{
Floor = string.Format(SH.ModelBindingHutaoComplexRankFloor, rank.Floor),
Floor = SH.ModelBindingHutaoComplexRankFloor.Format(rank.Floor),
Avatars = rank.Ranks.SortByDescending(r => r.Rate).SelectList(rank => new AvatarView(idAvatarMap[rank.Item], rank.Rate)),
});
}
@@ -201,7 +201,7 @@ internal sealed partial class HutaoCache : IHutaoCache
AvatarUsageRanks = avatarUsageRanksRaw.SortByDescending(r => r.Floor).SelectList(rank => new AvatarRankView
{
Floor = string.Format(SH.ModelBindingHutaoComplexRankFloor, rank.Floor),
Floor = SH.ModelBindingHutaoComplexRankFloor.Format(rank.Floor),
Avatars = rank.Ranks.SortByDescending(r => r.Rate).SelectList(rank => new AvatarView(idAvatarMap[rank.Item], rank.Rate)),
});
}

View File

@@ -19,9 +19,10 @@ namespace Snap.Hutao.Service.Hutao;
[Injection(InjectAs.Scoped, typeof(IHutaoService))]
internal sealed partial class HutaoService : IHutaoService
{
private static readonly TimeSpan CacheExpireTime = TimeSpan.FromHours(4);
private readonly TimeSpan cacheExpireTime = TimeSpan.FromHours(4);
private readonly IObjectCacheDbService objectCacheDbService;
private readonly HomaSpiralAbyssClient homaClient;
private readonly IServiceProvider serviceProvider;
private readonly JsonSerializerOptions options;
private readonly IMemoryCache memoryCache;
@@ -67,59 +68,30 @@ internal sealed partial class HutaoService : IHutaoService
return FromCacheOrWebAsync(nameof(TeamAppearance), homaClient.GetTeamCombinationsAsync);
}
private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, Func<CancellationToken, Task<Response<T>>> taskFunc)
where T : new()
private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, Func<CancellationToken, ValueTask<Response<T>>> taskFunc)
where T : class, new()
{
string key = $"{nameof(HutaoService)}.Cache.{typeName}";
if (memoryCache.TryGetValue(key, out object? cache))
{
return (T)cache!;
T? t = cache as T;
ArgumentNullException.ThrowIfNull(t);
return t;
}
using (IServiceScope scope = serviceProvider.CreateScope())
if (await objectCacheDbService.GetObjectOrDefaultAsync<T>(key).ConfigureAwait(false) is { } value)
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
if (appDbContext.ObjectCache.SingleOrDefault(e => e.Key == key) is { } entry)
{
if (entry.IsExpired)
{
await appDbContext.ObjectCache.RemoveAndSaveAsync(entry).ConfigureAwait(false);
}
else
{
T value = JsonSerializer.Deserialize<T>(entry.Value!, options)!;
return memoryCache.Set(key, value, TimeSpan.FromMinutes(30));
}
}
return memoryCache.Set(key, value, cacheExpireTime);
}
Response<T> webResponse = await taskFunc(default).ConfigureAwait(false);
T? data = webResponse.Data;
try
if (data is not null)
{
if (data is not null)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.ObjectCache.AddAndSaveAsync(new()
{
Key = key,
ExpireTime = DateTimeOffset.Now.Add(CacheExpireTime),
Value = JsonSerializer.Serialize(data, options),
}).ConfigureAwait(false);
}
}
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException)
{
// DbUpdateException: An error occurred while saving the entity changes.
// TODO: Not ignore it.
await objectCacheDbService.AddObjectCacheAsync(key, cacheExpireTime, data).ConfigureAwait(false);
}
return memoryCache.Set(key, data ?? new(), CacheExpireTime);
return memoryCache.Set(key, data ?? new(), cacheExpireTime);
}
}

View File

@@ -86,7 +86,7 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions<HutaoUserOpt
public void UpdateUserInfo(UserInfo userInfo)
{
IsLicensedDeveloper = userInfo.IsLicensedDeveloper;
GachaLogExpireAt = string.Format(Regex.Unescape(SH.ServiceHutaoUserGachaLogExpiredAt), userInfo.GachaLogExpireAt);
GachaLogExpireAt = Regex.Unescape(SH.ServiceHutaoUserGachaLogExpiredAt).Format(userInfo.GachaLogExpireAt);
IsCloudServiceAllowed = IsLicensedDeveloper || userInfo.GachaLogExpireAt > DateTimeOffset.Now;
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Hutao;
internal interface IObjectCacheDbService
{
ValueTask AddObjectCacheAsync<T>(string key, TimeSpan expire, T data)
where T : class;
ValueTask<T?> GetObjectOrDefaultAsync<T>(string key)
where T : class;
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Model;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.Hutao;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IObjectCacheDbService))]
internal sealed partial class ObjectCacheDbService : IObjectCacheDbService
{
private readonly IServiceProvider serviceProvider;
private readonly JsonSerializerOptions jsonSerializerOptions;
public async ValueTask AddObjectCacheAsync<T>(string key, TimeSpan expire, T data)
where T : class
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.ObjectCache.AddAndSaveAsync(new()
{
Key = key,
ExpireTime = DateTimeOffset.Now.Add(expire),
Value = JsonSerializer.Serialize(data, jsonSerializerOptions),
}).ConfigureAwait(false);
}
}
public async ValueTask<T?> GetObjectOrDefaultAsync<T>(string key)
where T : class
{
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
if (await appDbContext.ObjectCache.SingleOrDefaultAsync(e => e.Key == key).ConfigureAwait(false) is { } entry)
{
if (!entry.IsExpired)
{
ArgumentNullException.ThrowIfNull(entry.Value);
T? value = JsonSerializer.Deserialize<T>(entry.Value, jsonSerializerOptions);
return value;
}
await appDbContext.ObjectCache.RemoveAndSaveAsync(entry).ConfigureAwait(false);
}
}
}
catch (DbUpdateException ex)
{
ThrowHelper.DatabaseCorrupted($"无法存储 Key:{key} 对应的值到数据库缓存", ex);
}
return default!;
}
}

View File

@@ -8,6 +8,7 @@ namespace Snap.Hutao.Service.Metadata;
/// </summary>
internal partial class MetadataService
{
#pragma warning disable CA1823
private const string FileNameAchievement = "Achievement";
private const string FileNameAchievementGoal = "AchievementGoal";
private const string FileNameAvatar = "Avatar";
@@ -30,4 +31,5 @@ internal partial class MetadataService
private const string FileNameWeapon = "Weapon";
private const string FileNameWeaponCurve = "WeaponCurve";
private const string FileNameWeaponPromote = "WeaponPromote";
#pragma warning restore
}

View File

@@ -132,7 +132,7 @@ internal sealed class NavigationService : INavigationService, INavigationInitial
case NavigationResult.AlreadyNavigatedTo:
{
if (frame!.Content is ScopedPage scopedPage)
if (frame is { Content: ScopedPage scopedPage })
{
await scopedPage.NotifyRecipientAsync((INavigationData)data).ConfigureAwait(false);
}
@@ -158,11 +158,9 @@ internal sealed class NavigationService : INavigationService, INavigationInitial
{
taskContext.InvokeOnMainThread(() =>
{
bool canGoBack = frame?.CanGoBack ?? false;
if (canGoBack)
if (frame is { CanGoBack: true })
{
frame!.GoBack();
frame.GoBack();
SyncSelectedNavigationViewItemWith(frame.Content.GetType());
}
});
@@ -234,6 +232,7 @@ internal sealed class NavigationService : INavigationService, INavigationInitial
private void OnPaneStateChanged(NavigationView sender, object args)
{
LocalSetting.Set(SettingKeys.IsNavPaneOpen, NavigationView!.IsPaneOpen);
ArgumentNullException.ThrowIfNull(NavigationView);
LocalSetting.Set(SettingKeys.IsNavPaneOpen, NavigationView.IsPaneOpen);
}
}

View File

@@ -125,7 +125,8 @@ internal sealed class InfoBarService : IInfoBarService
};
infoBar.Closed += infobarClosedEventHandler;
collection!.Add(infoBar);
ArgumentNullException.ThrowIfNull(collection);
collection.Add(infoBar);
if (delay > 0)
{
@@ -137,7 +138,8 @@ internal sealed class InfoBarService : IInfoBarService
private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args)
{
taskContext.InvokeOnMainThread(() => collection!.Remove(sender));
ArgumentNullException.ThrowIfNull(collection);
taskContext.InvokeOnMainThread(() => collection.Remove(sender));
sender.Closed -= infobarClosedEventHandler;
}
}

View File

@@ -10,7 +10,7 @@ using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.SpiralAbyss;
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(ISpiralAbyssRecordDbService))]
[Injection(InjectAs.Singleton, typeof(ISpiralAbyssRecordDbService))]
internal sealed partial class SpiralAbyssRecordDbService : ISpiralAbyssRecordDbService
{
private readonly IServiceProvider serviceProvider;

View File

@@ -18,8 +18,8 @@ namespace Snap.Hutao.Service.SpiralAbyss;
[Injection(InjectAs.Scoped, typeof(ISpiralAbyssRecordService))]
internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordService
{
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory;
private readonly ISpiralAbyssRecordDbService spiralAbyssRecordDbService;
private string? uid;
@@ -50,8 +50,7 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
private async ValueTask RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule)
{
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);
@@ -60,7 +59,8 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
{
Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data;
if (spiralAbysses!.SingleOrDefault(s => s.ScheduleId == webSpiralAbyss.ScheduleId) is { } existEntry)
ArgumentNullException.ThrowIfNull(spiralAbysses);
if (spiralAbysses.SingleOrDefault(s => s.ScheduleId == webSpiralAbyss.ScheduleId) is { } existEntry)
{
await taskContext.SwitchToMainThreadAsync();
existEntry.UpdateSpiralAbyss(webSpiralAbyss);
@@ -73,7 +73,7 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
SpiralAbyssEntry newEntry = SpiralAbyssEntry.From(userAndUid.Uid.Value, webSpiralAbyss);
await taskContext.SwitchToMainThreadAsync();
spiralAbysses!.Insert(0, newEntry);
spiralAbysses.Insert(0, newEntry);
await taskContext.SwitchToBackgroundAsync();
await spiralAbyssRecordDbService.AddSpiralAbyssEntryAsync(newEntry).ConfigureAwait(false);

View File

@@ -47,7 +47,8 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
{
// Sync cache
await taskContext.SwitchToMainThreadAsync();
userCollection!.Remove(user);
ArgumentNullException.ThrowIfNull(userCollection);
userCollection.Remove(user);
userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid);
// Sync database
@@ -132,7 +133,8 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
}
// 检查 mid 对应用户是否存在
if (TryGetUser(userCollection!, mid, out BindingUser? user))
ArgumentNullException.ThrowIfNull(userCollection);
if (TryGetUser(userCollection, mid, out BindingUser? user))
{
if (cookie.TryGetSToken(isOversea, out Cookie? stoken))
{
@@ -172,7 +174,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
user.CookieToken ??= new();
// Sync ui and database
user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken!;
user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken;
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
return true;
@@ -216,7 +218,8 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
// Sync database
await taskContext.SwitchToBackgroundAsync();
await userDbService.AddUserAsync(newUser.Entity).ConfigureAwait(false);
return new(UserOptionResult.Added, newUser.UserInfo!.Uid);
ArgumentNullException.ThrowIfNull(newUser.UserInfo);
return new(UserOptionResult.Added, newUser.UserInfo.Uid);
}
else
{

View File

@@ -288,7 +288,6 @@
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
<None Include="Model\Entity\GachaArchive.Operation.cs" />
</ItemGroup>
<ItemGroup>

View File

@@ -16,6 +16,6 @@ internal sealed partial class BottomTextSmallControl : UserControl
{
public BottomTextSmallControl()
{
this.InitializeComponent();
InitializeComponent();
}
}

View File

@@ -24,6 +24,7 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="options">选项</param>
[SuppressMessage("", "SH002")]
public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, CalculableOptions options)
{
InitializeComponent();
@@ -40,37 +41,35 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
/// 异步获取提升差异
/// </summary>
/// <returns>提升差异</returns>
public async Task<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaAsync()
public async ValueTask<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaAsync()
{
await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
if (result == ContentDialogResult.Primary)
{
AvatarPromotionDelta delta = new()
{
AvatarId = Avatar?.AvatarId ?? 0,
AvatarLevelCurrent = Avatar?.LevelCurrent ?? 0,
AvatarLevelTarget = Avatar?.LevelTarget ?? 0,
SkillList = Avatar?.Skills.SelectList(s => new PromotionDelta()
{
Id = s.GroupId,
LevelCurrent = s.LevelCurrent,
LevelTarget = s.LevelTarget,
}),
Weapon = Weapon is null ? null : new PromotionDelta()
{
Id = Weapon.WeaponId,
LevelCurrent = Weapon.LevelCurrent,
LevelTarget = Weapon.LevelTarget,
},
};
return new(true, delta);
}
else
if (result != ContentDialogResult.Primary)
{
return new(false, default!);
}
AvatarPromotionDelta delta = new()
{
AvatarId = Avatar?.AvatarId ?? 0,
AvatarLevelCurrent = Avatar?.LevelCurrent ?? 0,
AvatarLevelTarget = Avatar?.LevelTarget ?? 0,
SkillList = Avatar?.Skills.SelectList(s => new PromotionDelta()
{
Id = s.GroupId,
LevelCurrent = s.LevelCurrent,
LevelTarget = s.LevelTarget,
}),
Weapon = Weapon is null ? null : new PromotionDelta()
{
Id = Weapon.WeaponId,
LevelCurrent = Weapon.LevelCurrent,
LevelTarget = Weapon.LevelTarget,
},
};
return new(true, delta);
}
}

View File

@@ -35,7 +35,7 @@ internal sealed partial class GachaLogImportDialog : ContentDialog
/// 异步获取导入选项
/// </summary>
/// <returns>是否导入</returns>
public async Task<bool> GetShouldImportAsync()
public async ValueTask<bool> GetShouldImportAsync()
{
await taskContext.SwitchToMainThreadAsync();
return await ShowAsync() == ContentDialogResult.Primary;

View File

@@ -38,7 +38,7 @@ internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog
// TODO: test new binding approach
GachaItemsPresenter.Header = state.AuthKeyTimeout
? SH.ViewDialogGachaLogRefreshProgressAuthkeyTimeout
: string.Format(SH.ViewDialogGachaLogRefreshProgressDescription, state.ConfigType.GetLocalizedDescription());
: SH.ViewDialogGachaLogRefreshProgressDescription.Format(state.ConfigType.GetLocalizedDescription());
// Binding not working here.
GachaItemsPresenter.Items.Clear();

View File

@@ -29,7 +29,7 @@ internal sealed partial class GachaLogUrlDialog : ContentDialog
/// 获取输入的Url
/// </summary>
/// <returns>输入的结果</returns>
public async Task<ValueResult<bool, string>> GetInputUrlAsync()
public async ValueTask<ValueResult<bool, string>> GetInputUrlAsync()
{
await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();

View File

@@ -29,7 +29,7 @@ internal sealed partial class LaunchGameAccountNameDialog : ContentDialog
/// 获取输入的Cookie
/// </summary>
/// <returns>输入的结果</returns>
public async Task<ValueResult<bool, string>> GetInputNameAsync()
public async ValueTask<ValueResult<bool, string>> GetInputNameAsync()
{
await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();

View File

@@ -36,7 +36,7 @@ internal sealed partial class SignInWebViewDialog : ContentDialog
InitializeAsync().SafeForget();
}
private async Task InitializeAsync()
private async ValueTask InitializeAsync()
{
await WebView.EnsureCoreWebView2Async();
CoreWebView2 coreWebView2 = WebView.CoreWebView2;

View File

@@ -29,7 +29,7 @@ internal sealed partial class UserDialog : ContentDialog
/// 获取输入的Cookie
/// </summary>
/// <returns>输入的结果</returns>
public async Task<ValueResult<bool, string>> GetInputCookieAsync()
public async ValueTask<ValueResult<bool, string>> GetInputCookieAsync()
{
await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();

View File

@@ -28,7 +28,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
InitializeComponent();
}
private static async Task<string> GetUidFromCookieAsync(IServiceProvider serviceProvider, Cookie cookie, CancellationToken token = default)
private static async ValueTask<string> GetUidFromCookieAsync(IServiceProvider serviceProvider, Cookie cookie, CancellationToken token = default)
{
JsonSerializerOptions options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
ILogger<LoginHoyoverseUserPage> logger = serviceProvider.GetRequiredService<ILogger<LoginHoyoverseUserPage>>();
@@ -40,9 +40,9 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
.TryCatchGetFromJsonAsync<WebApiResponse<AccountInfoWrapper>>(ApiOsEndpoints.WebApiOsAccountLoginByCookie, options, logger, token)
.ConfigureAwait(false);
if (resp != null)
if (resp is not null)
{
return resp.Data.AccountInfo.AccountId.ToString();
return $"{resp.Data.AccountInfo.AccountId}";
}
return string.Empty;

View File

@@ -36,7 +36,7 @@ internal sealed partial class TitleView : UserControl
#else
SH.AppNameAndVersion;
#endif
return string.Format(format, hutaoOptions.Version);
return format.Format(hutaoOptions.Version);
}
}

View File

@@ -23,9 +23,11 @@ namespace Snap.Hutao.ViewModel.Achievement;
[Injection(InjectAs.Scoped)]
internal sealed partial class AchievementImporter
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IAchievementService achievementService;
private readonly IServiceProvider serviceProvider;
private readonly IClipboardInterop clipboardInterop;
private readonly IInfoBarService infoBarService;
private readonly IPickerFactory pickerFactory;
private readonly JsonSerializerOptions options;
private readonly ITaskContext taskContext;
@@ -62,8 +64,7 @@ internal sealed partial class AchievementImporter
{
if (achievementService.CurrentArchive is { } archive)
{
ValueResult<bool, ValueFile> pickerResult = await serviceProvider
.GetRequiredService<IPickerFactory>()
ValueResult<bool, ValueFile> pickerResult = await pickerFactory
.GetFileOpenPicker(PickerLocationId.Desktop, SH.FilePickerImportCommit, ".json")
.TryPickSingleFileAsync()
.ConfigureAwait(false);
@@ -94,10 +95,7 @@ internal sealed partial class AchievementImporter
{
try
{
return await serviceProvider
.GetRequiredService<IClipboardInterop>()
.DeserializeFromJsonAsync<UIAF>()
.ConfigureAwait(false);
return await clipboardInterop.DeserializeFromJsonAsync<UIAF>().ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -110,16 +108,13 @@ internal sealed partial class AchievementImporter
{
if (uiaf.IsCurrentVersionSupported())
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
AchievementImportDialog importDialog = serviceProvider.CreateInstance<AchievementImportDialog>(uiaf);
AchievementImportDialog importDialog = await contentDialogFactory.CreateInstanceAsync<AchievementImportDialog>(uiaf).ConfigureAwait(false);
(bool isOk, ImportStrategy strategy) = await importDialog.GetImportStrategyAsync().ConfigureAwait(false);
if (isOk)
{
await taskContext.SwitchToMainThreadAsync();
ContentDialog dialog = await serviceProvider
.GetRequiredService<IContentDialogFactory>()
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelAchievementImportProgress)
.ConfigureAwait(false);

View File

@@ -28,13 +28,13 @@ namespace Snap.Hutao.ViewModel.Achievement;
[Injection(InjectAs.Scoped)]
internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INavigationRecipient
{
private static readonly SortDescription UncompletedItemsFirstSortDescription = new(nameof(AchievementView.IsChecked), SortDirection.Ascending);
private static readonly SortDescription CompletionTimeSortDescription = new(nameof(AchievementView.Time), SortDirection.Descending);
private readonly SortDescription uncompletedItemsFirstSortDescription = new(nameof(AchievementView.IsChecked), SortDirection.Ascending);
private readonly SortDescription completionTimeSortDescription = new(nameof(AchievementView.Time), SortDirection.Descending);
private readonly IContentDialogFactory contentDialogFactory;
private readonly IPickerFactory pickerFactory;
private readonly AchievementImporter achievementImporter;
private readonly IAchievementService achievementService;
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService;
private readonly IInfoBarService infoBarService;
private readonly JsonSerializerOptions options;
@@ -189,9 +189,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
{
if (Archives is null)
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
AchievementArchiveCreateDialog dialog = serviceProvider.CreateInstance<AchievementArchiveCreateDialog>();
AchievementArchiveCreateDialog dialog = await contentDialogFactory.CreateInstanceAsync<AchievementArchiveCreateDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputAsync().ConfigureAwait(false);
if (isOk)
@@ -203,13 +201,13 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
case ArchiveAddResult.Added:
await taskContext.SwitchToMainThreadAsync();
SelectedArchive = achievementService.CurrentArchive;
infoBarService.Success(string.Format(SH.ViewModelAchievementArchiveAdded, name));
infoBarService.Success(SH.ViewModelAchievementArchiveAdded.Format(name));
break;
case ArchiveAddResult.InvalidName:
infoBarService.Warning(SH.ViewModelAchievementArchiveInvalidName);
break;
case ArchiveAddResult.AlreadyExists:
infoBarService.Warning(string.Format(SH.ViewModelAchievementArchiveAlreadyExists, name));
infoBarService.Warning(SH.ViewModelAchievementArchiveAlreadyExists.Format(name));
break;
default:
throw Must.NeverHappen();
@@ -223,7 +221,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
{
if (Archives is not null && SelectedArchive is not null)
{
string title = string.Format(SH.ViewModelAchievementRemoveArchiveTitle, SelectedArchive.Name);
string title = SH.ViewModelAchievementRemoveArchiveTitle.Format(SelectedArchive.Name);
string content = SH.ViewModelAchievementRemoveArchiveContent;
ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync(title, content)
@@ -260,8 +258,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[SH.ViewModelAchievementExportFileType] = ".json".Enumerate().ToList(),
};
FileSavePicker picker = serviceProvider
.GetRequiredService<IPickerFactory>()
FileSavePicker picker = pickerFactory
.GetFileSavePicker(PickerLocationId.Desktop, fileName, SH.FilePickerExportCommit, fileTypes);
(bool isPickerOk, ValueFile file) = await picker.TryPickSaveFileAsync().ConfigureAwait(false);
@@ -343,8 +340,8 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
{
if (IsUncompletedItemsFirst)
{
Achievements.SortDescriptions.Add(UncompletedItemsFirstSortDescription);
Achievements.SortDescriptions.Add(CompletionTimeSortDescription);
Achievements.SortDescriptions.Add(uncompletedItemsFirstSortDescription);
Achievements.SortDescriptions.Add(completionTimeSortDescription);
}
else
{
@@ -387,7 +384,8 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
Achievements.Filter = obj =>
{
AchievementView view = (AchievementView)obj;
return view.Inner.Title.Contains(search) || view.Inner.Description.Contains(search);
return view.Inner.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase)
|| view.Inner.Description.Contains(search, StringComparison.CurrentCultureIgnoreCase);
};
}
}

View File

@@ -46,7 +46,7 @@ internal sealed class AvatarProperty : INameIcon
{
Name = name;
Value = value;
Icon = PropertyIcons.GetValueOrDefault(property)!;
Icon = PropertyIcons.GetValueOrDefault(property);
AddValue = addValue;
}
@@ -58,6 +58,7 @@ internal sealed class AvatarProperty : INameIcon
/// <summary>
/// 图标
/// </summary>
[AllowNull]
public Uri Icon { get; }
/// <summary>

View File

@@ -38,10 +38,16 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
[Injection(InjectAs.Scoped)]
internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage>
{
private readonly IServiceProvider serviceProvider;
private readonly IContentDialogFactory contentDialogFactory;
private readonly IAppResourceProvider appResourceProvider;
private readonly ICultivationService cultivationService;
private readonly IAvatarInfoService avatarInfoService;
private readonly IClipboardInterop clipboardInterop;
private readonly CalculatorClient calculatorClient;
private readonly ITaskContext taskContext;
private readonly IUserService userService;
private readonly IInfoBarService infoBarService;
private Summary? summary;
private AvatarView? selectedAvatar;
@@ -109,15 +115,13 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
ValueResult<RefreshResult, Summary?> summaryResult;
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
ContentDialog dialog = await serviceProvider
.GetRequiredService<IContentDialogFactory>()
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyFetch)
.ConfigureAwait(false);
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
summaryResult = await serviceProvider
.GetRequiredService<IAvatarInfoService>()
summaryResult = await avatarInfoService
.GetSummaryAsync(userAndUid, option, token)
.ConfigureAwait(false);
}
@@ -139,7 +143,8 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
break;
case RefreshResult.StatusCodeNotSucceed:
infoBarService.Warning(summary!.Message);
ArgumentNullException.ThrowIfNull(summary);
infoBarService.Warning(summary.Message);
break;
case RefreshResult.ShowcaseNotOpen:
@@ -167,49 +172,50 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
}
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable());
CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance<CultivatePromotionDeltaDialog>(options);
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
(bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (isOk)
if (!isOk)
{
Response<CalculatorConsumption> consumptionResponse = await serviceProvider
.GetRequiredService<CalculatorClient>()
.ComputeAsync(userService.Current.Entity, delta)
return;
}
Response<CalculatorConsumption> consumptionResponse = await calculatorClient
.ComputeAsync(userService.Current.Entity, delta)
.ConfigureAwait(false);
if (!consumptionResponse.IsOk())
{
return;
}
CalculatorConsumption consumption = consumptionResponse.Data;
List<CalculatorItem> items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
bool avatarSaved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items)
.ConfigureAwait(false);
try
{
// take a hot path if avatar is not saved.
bool avatarAndWeaponSaved = avatarSaved && await cultivationService
.SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull())
.ConfigureAwait(false);
if (consumptionResponse.IsOk())
if (avatarAndWeaponSaved)
{
ICultivationService cultivationService = serviceProvider.GetRequiredService<ICultivationService>();
CalculatorConsumption consumption = consumptionResponse.Data;
List<CalculatorItem> items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
bool avatarSaved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items)
.ConfigureAwait(false);
try
{
// take a hot path if avatar is not saved.
bool avatarAndWeaponSaved = avatarSaved && await cultivationService
.SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull())
.ConfigureAwait(false);
if (avatarAndWeaponSaved)
{
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
}
else
{
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
}
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
}
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
}
else
{
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
}
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
}
}
else
@@ -231,7 +237,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
bool clipboardOpened = false;
using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight))
{
Bgra32 tint = serviceProvider.GetRequiredService<IAppResourceProvider>().GetResource<Color>("CompatBackgroundColor");
Bgra32 tint = appResourceProvider.GetResource<Color>("CompatBackgroundColor");
softwareBitmap.NormalBlend(tint);
using (InMemoryRandomAccessStream memory = new())
{
@@ -239,7 +245,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
encoder.SetSoftwareBitmap(softwareBitmap);
await encoder.FlushAsync();
clipboardOpened = serviceProvider.GetRequiredService<IClipboardInterop>().SetBitmap(memory);
clipboardOpened = clipboardInterop.SetBitmap(memory);
}
}

View File

@@ -25,7 +25,7 @@ internal sealed class WeaponView : Equip, ICalculableSource<ICalculableWeapon>
/// <summary>
/// 精炼属性
/// </summary>
public string AffixLevel { get => string.Format(SH.ModelBindingAvatarPropertyWeaponAffixFormat, AffixLevelNumber); }
public string AffixLevel { get => SH.ModelBindingAvatarPropertyWeaponAffixFormat.Format(AffixLevelNumber); }
/// <summary>
/// 精炼名称

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.Primitives;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Web.Hutao.Model;
using System.Globalization;
namespace Snap.Hutao.ViewModel.Complex;
@@ -25,11 +26,11 @@ internal sealed class Team : List<AvatarView>
// TODO use Collection Literials
foreach (StringSegment item in new StringTokenizer(team.Item, new char[] { ',' }))
{
uint id = uint.Parse(item.AsSpan());
uint id = uint.Parse(item.AsSpan(), CultureInfo.InvariantCulture);
Add(new(idAvatarMap[id]));
}
Rate = string.Format(SH.ModelBindingHutaoTeamUpCountFormat, team.Rate);
Rate = SH.ModelBindingHutaoTeamUpCountFormat.Format(team.Rate);
}
/// <summary>

View File

@@ -20,7 +20,7 @@ internal sealed class TeamAppearanceView
/// <param name="idAvatarMap">映射</param>
public TeamAppearanceView(TeamAppearance teamRank, Dictionary<AvatarId, Avatar> idAvatarMap)
{
Floor = string.Format(SH.ModelBindingHutaoComplexRankFloor, teamRank.Floor);
Floor = SH.ModelBindingHutaoComplexRankFloor.Format(teamRank.Floor);
Up = teamRank.Up.SelectList(teamRate => new Team(teamRate, idAvatarMap));
Down = teamRank.Down.SelectList(teamRate => new Team(teamRate, idAvatarMap));
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Model.Primitive;
@@ -23,10 +24,12 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
{
private readonly ConcurrentCancellationTokenSource statisticsCancellationTokenSource = new();
private readonly IContentDialogFactory contentDialogFactory;
private readonly ICultivationService cultivationService;
private readonly ILogger<CultivationViewModel> logger;
private readonly IServiceProvider serviceProvider;
private readonly INavigationService navigationService;
private readonly IMetadataService metadataService;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
private ObservableCollection<CultivateProject>? projects;
@@ -89,15 +92,12 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
[Command("AddProjectCommand")]
private async Task AddProjectAsync()
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
CultivateProjectDialog dialog = serviceProvider.CreateInstance<CultivateProjectDialog>();
CultivateProjectDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivateProjectDialog>().ConfigureAwait(false);
(bool isOk, CultivateProject project) = await dialog.CreateProjectAsync().ConfigureAwait(false);
if (isOk)
{
ProjectAddResult result = await cultivationService.TryAddProjectAsync(project).ConfigureAwait(false);
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
switch (result)
{
@@ -214,10 +214,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
{
Type? pageType = Type.GetType(typeString);
ArgumentNullException.ThrowIfNull(pageType);
serviceProvider
.GetRequiredService<INavigationService>()
.Navigate(pageType, INavigationAwaiter.Default, true);
navigationService.Navigate(pageType, INavigationAwaiter.Default, true);
}
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Notification;
@@ -20,8 +21,8 @@ namespace Snap.Hutao.ViewModel.DailyNote;
[Injection(InjectAs.Scoped)]
internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IDailyNoteService dailyNoteService;
private readonly IServiceProvider serviceProvider;
private readonly IInfoBarService infoBarService;
private readonly DailyNoteOptions options;
private readonly ITaskContext taskContext;
@@ -113,7 +114,8 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
await serviceProvider.CreateInstance<DailyNoteNotificationDialog>(entry).ShowAsync();
DailyNoteNotificationDialog dialog = await contentDialogFactory.CreateInstanceAsync<DailyNoteNotificationDialog>(entry).ConfigureAwait(true);
await dialog.ShowAsync();
await taskContext.SwitchToBackgroundAsync();
await dailyNoteService.UpdateDailyNoteAsync(entry).ConfigureAwait(false);

View File

@@ -26,10 +26,10 @@ namespace Snap.Hutao.ViewModel.GachaLog;
[Injection(InjectAs.Scoped)]
internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
{
private readonly IGachaLogQueryProviderFactory gachaLogQueryProviderFactory;
private readonly IContentDialogFactory contentDialogFactory;
private readonly HutaoCloudStatisticsViewModel hutaoCloudStatisticsViewModel;
private readonly HutaoCloudViewModel hutaoCloudViewModel;
private readonly IServiceProvider serviceProvider;
private readonly IGachaLogService gachaLogService;
private readonly IInfoBarService infoBarService;
private readonly JsonSerializerOptions options;
@@ -150,7 +150,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
private async ValueTask RefreshInternalAsync(RefreshOption option)
{
IGachaLogQueryProvider provider = serviceProvider.GetRequiredService<IGachaLogQueryProviderFactory>().Create(option);
IGachaLogQueryProvider provider = gachaLogQueryProviderFactory.Create(option);
(bool isOk, GachaLogQuery query) = await provider.GetQueryAsync().ConfigureAwait(false);
@@ -158,10 +158,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
{
RefreshStrategy strategy = IsAggressiveRefresh ? RefreshStrategy.AggressiveMerge : RefreshStrategy.LazyMerge;
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
GachaLogRefreshProgressDialog dialog = serviceProvider.CreateInstance<GachaLogRefreshProgressDialog>();
GachaLogRefreshProgressDialog dialog = await contentDialogFactory.CreateInstanceAsync<GachaLogRefreshProgressDialog>().ConfigureAwait(false);
ContentDialogHideToken hideToken = await dialog.BlockAsync(taskContext).ConfigureAwait(false);
Progress<GachaLogFetchStatus> progress = new(dialog.OnReport);
bool authkeyValid;
@@ -268,7 +265,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
if (Archives is not null && SelectedArchive is not null)
{
ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync(string.Format(SH.ViewModelGachaLogRemoveArchiveTitle, SelectedArchive.Uid), SH.ViewModelGachaLogRemoveArchiveDescription)
.CreateForConfirmCancelAsync(SH.ViewModelGachaLogRemoveArchiveTitle.Format(SelectedArchive.Uid), SH.ViewModelGachaLogRemoveArchiveDescription)
.ConfigureAwait(false);
if (result == ContentDialogResult.Primary)
@@ -356,9 +353,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
{
if (uigf.IsCurrentVersionSupported(out UIGFVersion version))
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
GachaLogImportDialog importDialog = serviceProvider.CreateInstance<GachaLogImportDialog>(uigf);
GachaLogImportDialog importDialog = await contentDialogFactory.CreateInstanceAsync<GachaLogImportDialog>(uigf).ConfigureAwait(false);
if (await importDialog.GetShouldImportAsync().ConfigureAwait(false))
{
if (CanImport(version, uigf))

View File

@@ -22,10 +22,10 @@ namespace Snap.Hutao.ViewModel.GachaLog;
[Injection(InjectAs.Scoped)]
internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel
{
private readonly INavigationService navigationService;
private readonly IContentDialogFactory contentDialogFactory;
private readonly IGachaLogHutaoCloudService hutaoCloudService;
private readonly IHutaoUserService hutaoUserService;
private readonly IServiceProvider serviceProvider;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
private readonly HutaoUserOptions options;
@@ -131,9 +131,7 @@ internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel
[Command("NavigateToSpiralAbyssRecordCommand")]
private void NavigateToSpiralAbyssRecord()
{
serviceProvider
.GetRequiredService<INavigationService>()
.Navigate<View.Page.SpiralAbyssRecordPage>(INavigationAwaiter.Default);
navigationService.Navigate<View.Page.SpiralAbyssRecordPage>(INavigationAwaiter.Default);
}
private async ValueTask RefreshUidCollectionAsync()

View File

@@ -23,7 +23,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string MaxOrangePullFormatted
{
get => string.Format(SH.ModelBindingGachaTypedWishSummaryMaxOrangePullFormat, MaxOrangePull);
get => SH.ModelBindingGachaTypedWishSummaryMaxOrangePullFormat.Format(MaxOrangePull);
}
/// <summary>
@@ -31,7 +31,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string MinOrangePullFormatted
{
get => string.Format(SH.ModelBindingGachaTypedWishSummaryMinOrangePullFormat, MinOrangePull);
get => SH.ModelBindingGachaTypedWishSummaryMinOrangePullFormat.Format(MinOrangePull);
}
/// <summary>
@@ -83,7 +83,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string AverageOrangePullFormatted
{
get => string.Format(SH.ModelBindingGachaTypedWishSummaryAveragePullFormat, AverageOrangePull);
get => SH.ModelBindingGachaTypedWishSummaryAveragePullFormat.Format(AverageOrangePull);
}
/// <summary>
@@ -96,7 +96,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string AverageUpOrangePullFormatted
{
get => string.Format(SH.ModelBindingGachaTypedWishSummaryAveragePullFormat, AverageUpOrangePull);
get => SH.ModelBindingGachaTypedWishSummaryAveragePullFormat.Format(AverageUpOrangePull);
}
/// <summary>
@@ -104,7 +104,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string PredictedPullLeftToOrangeFormatted
{
get => string.Format(SH.ViewModelGachaLogPredictedPullLeftToOrange, PredictedPullLeftToOrange, ProbabilityOfPredictedPullLeftToOrange);
get => SH.ViewModelGachaLogPredictedPullLeftToOrange.Format(PredictedPullLeftToOrange, ProbabilityOfPredictedPullLeftToOrange);
}
/// <summary>
@@ -112,7 +112,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string ProbabilityOfNextPullIsOrangeFormatted
{
get => string.Format(SH.ViewModelGachaLogProbabilityOfNextPullIsOrange, ProbabilityOfNextPullIsOrange);
get => SH.ViewModelGachaLogProbabilityOfNextPullIsOrange.Format(ProbabilityOfNextPullIsOrange);
}
/// <summary>

View File

@@ -40,7 +40,7 @@ internal abstract class Wish
/// </summary>
public string TotalCountFormatted
{
get => string.Format(SH.ModelBindingGachaWishBaseTotalCountFormat, TotalCount);
get => SH.ModelBindingGachaWishBaseTotalCountFormat.Format(TotalCount);
}
/// <summary>

View File

@@ -6,6 +6,7 @@ using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Game;
@@ -33,10 +34,12 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
/// </summary>
public const string DesiredUid = nameof(DesiredUid);
private readonly IServiceProvider serviceProvider;
private readonly IContentDialogFactory contentDialogFactory;
private readonly INavigationService navigationService;
private readonly IInfoBarService infoBarService;
private readonly LaunchOptions launchOptions;
private readonly RuntimeOptions hutaoOptions;
private readonly ResourceClient resourceClient;
private readonly IUserService userService;
private readonly ITaskContext taskContext;
private readonly IGameService gameService;
@@ -102,8 +105,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
protected override async ValueTask<bool> InitializeUIAsync()
{
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (File.Exists(AppOptions.GamePath))
{
try
@@ -127,7 +128,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
}
else
{
infoBarService.Warning(string.Format(SH.ViewModelLaunchGameMultiChannelReadFail, options.ConfigFilePath));
infoBarService.Warning(SH.ViewModelLaunchGameMultiChannelReadFail.Format(options.ConfigFilePath));
}
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
@@ -153,7 +154,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{
infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid);
await taskContext.SwitchToMainThreadAsync();
await serviceProvider.GetRequiredService<INavigationService>()
await navigationService
.NavigateAsync<View.Page.SettingPage>(INavigationAwaiter.Default, true)
.ConfigureAwait(false);
}
@@ -164,8 +165,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private async ValueTask UpdateGameResourceAsync(LaunchScheme scheme)
{
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await serviceProvider
.GetRequiredService<ResourceClient>()
Web.Response.Response<GameResource> response = await resourceClient
.GetResourceAsync(scheme)
.ConfigureAwait(false);
@@ -179,8 +179,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
[Command("LaunchCommand", AllowConcurrentExecutions = true)]
private async Task LaunchAsync()
{
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (SelectedScheme is not null)
{
try
@@ -188,8 +186,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
if (gameService.SetChannelOptions(SelectedScheme))
{
// Channel changed, we need to change local file.
await taskContext.SwitchToMainThreadAsync();
LaunchGamePackageConvertDialog dialog = serviceProvider.CreateInstance<LaunchGamePackageConvertDialog>();
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
Progress<Service.Game.Package.PackageReplaceStatus> progress = new(state => dialog.State = state.Clone());
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
@@ -232,7 +229,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
}
catch (UserdataCorruptedException ex)
{
serviceProvider.GetRequiredService<IInfoBarService>().Error(ex);
infoBarService.Error(ex);
}
}

View File

@@ -24,6 +24,7 @@ internal sealed class DownloadSummary : ObservableObject
{
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly IImageCache imageCache;
private readonly HttpClient httpClient;
private readonly string fileName;
private readonly string fileUrl;
@@ -41,6 +42,7 @@ internal sealed class DownloadSummary : ObservableObject
{
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
httpClient = serviceProvider.GetRequiredService<HttpClient>();
imageCache = serviceProvider.GetRequiredService<IImageCache>();
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
@@ -72,7 +74,7 @@ internal sealed class DownloadSummary : ObservableObject
/// 异步下载并解压
/// </summary>
/// <returns>任务</returns>
public async Task<bool> DownloadAndExtractAsync()
public async ValueTask<bool> DownloadAndExtractAsync()
{
ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>();
try
@@ -116,13 +118,14 @@ internal sealed class DownloadSummary : ObservableObject
private void ExtractFiles(Stream stream)
{
IImageCacheFilePathOperation imageCache = serviceProvider.GetRequiredService<IImageCache>().As<IImageCacheFilePathOperation>()!;
IImageCacheFilePathOperation? imageCacheFilePathOperation = imageCache.As<IImageCacheFilePathOperation>();
ArgumentNullException.ThrowIfNull(imageCacheFilePathOperation);
using (ZipArchive archive = new(stream))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
string destPath = imageCache.GetFileFromCategoryAndName(fileName, entry.FullName);
string destPath = imageCacheFilePathOperation.GetFileFromCategoryAndName(fileName, entry.FullName);
entry.ExtractToFile(destPath, true);
}
}

View File

@@ -77,7 +77,7 @@ internal sealed partial class AnnouncementViewModel : Abstraction.ViewModel
}
else if (rand == 1)
{
GreetingText = string.Format(SH.ViewPageHomeGreetingTextCommon2, LocalSetting.Get(SettingKeys.LaunchTimes, 0));
GreetingText = SH.ViewPageHomeGreetingTextCommon2.Format(LocalSetting.Get(SettingKeys.LaunchTimes, 0));
}
}
}

View File

@@ -22,7 +22,6 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
private readonly HomaPassportClient homaPassportClient;
private readonly INavigationService navigationService;
private readonly HutaoUserOptions hutaoUserOptions;
private readonly IServiceProvider serviceProvider;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
@@ -142,7 +141,7 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
}
Response response = await homaPassportClient.VerifyAsync(UserName, isResetPassword).ConfigureAwait(false);
serviceProvider.GetRequiredService<IInfoBarService>().Information(response.Message);
infoBarService.Information(response.Message);
}
private void SaveUserNameAndPassword()

View File

@@ -19,7 +19,7 @@ internal sealed class BattleView
/// <param name="idAvatarMap">Id角色映射</param>
public BattleView(Battle battle, Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap)
{
Time = DateTimeOffset.FromUnixTimeSeconds(battle.Timestamp).ToLocalTime().ToString("yyyy.MM.dd HH:mm:ss");
Time = $"{DateTimeOffset.FromUnixTimeSeconds(battle.Timestamp).ToLocalTime():yyyy.MM.dd HH:mm:ss}";
Avatars = battle.Avatars.SelectList(a => AvatarView.From(idAvatarMap[a.Id]));
}

View File

@@ -18,7 +18,7 @@ internal sealed class FloorView
/// <param name="idAvatarMap">Id角色映射</param>
public FloorView(Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.Floor floor, Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap)
{
Index = string.Format(SH.ModelBindingHutaoComplexRankFloor, floor.Index);
Index = SH.ModelBindingHutaoComplexRankFloor.Format(floor.Index);
SettleTime = $"{DateTimeOffset.FromUnixTimeSeconds(floor.SettleTime).ToLocalTime():yyyy.MM.dd HH:mm:ss}";
Star = floor.Star;
Levels = floor.Levels.SelectList(l => new LevelView(l, idAvatarMap));

View File

@@ -18,7 +18,7 @@ internal sealed class LevelView
/// <param name="idAvatarMap">Id角色映射</param>
public LevelView(Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.Level level, Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap)
{
Index = string.Format(SH.ModelBindingHutaoComplexRankLevel, level.Index);
Index = SH.ModelBindingHutaoComplexRankLevel.Format(level.Index);
Star = level.Star;
Battles = level.Battles.SelectList(b => new BattleView(b, idAvatarMap));
}

View File

@@ -26,7 +26,7 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss;
internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage>
{
private readonly ISpiralAbyssRecordService spiralAbyssRecordService;
private readonly IServiceProvider serviceProvider;
private readonly HomaSpiralAbyssClient spiralAbyssClient;
private readonly IMetadataService metadataService;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
@@ -148,15 +148,12 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
[Command("UploadSpiralAbyssRecordCommand")]
private async Task UploadSpiralAbyssRecordAsync()
{
HomaSpiralAbyssClient homaClient = serviceProvider.GetRequiredService<HomaSpiralAbyssClient>();
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
SimpleRecord? record = await homaClient.GetPlayerRecordAsync(userAndUid).ConfigureAwait(false);
SimpleRecord? record = await spiralAbyssClient.GetPlayerRecordAsync(userAndUid).ConfigureAwait(false);
if (record is not null)
{
Web.Response.Response<string> response = await homaClient.UploadRecordAsync(record).ConfigureAwait(false);
Web.Response.Response<string> response = await spiralAbyssClient.UploadRecordAsync(record).ConfigureAwait(false);
if (response.IsOk())
{

View File

@@ -36,6 +36,7 @@ internal sealed class UserAndUid : IMappingFrom<UserAndUid, EntityUser, PlayerUi
/// </summary>
public PlayerUid Uid { get; private set; }
[SuppressMessage("", "SH002")]
public static UserAndUid From(EntityUser user, PlayerUid role)
{
return new(user, role);

View File

@@ -77,7 +77,7 @@ internal sealed partial class UserViewModel : ObservableObject
SelectedUser = Users.Single();
}
infoBarService.Success(string.Format(SH.ViewModelUserAdded, uid));
infoBarService.Success(SH.ViewModelUserAdded.Format(uid));
break;
case UserOptionResult.Incomplete:
infoBarService.Information(SH.ViewModelUserIncomplete);
@@ -86,7 +86,7 @@ internal sealed partial class UserViewModel : ObservableObject
infoBarService.Information(SH.ViewModelUserInvalid);
break;
case UserOptionResult.Updated:
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, uid));
infoBarService.Success(SH.ViewModelUserUpdated.Format(uid));
break;
default:
throw Must.NeverHappen();
@@ -173,7 +173,7 @@ internal sealed partial class UserViewModel : ObservableObject
try
{
await userService.RemoveUserAsync(user).ConfigureAwait(false);
infoBarService.Success(string.Format(SH.ViewModelUserRemoved, user.UserInfo?.Nickname));
infoBarService.Success(SH.ViewModelUserRemoved.Format(user.UserInfo?.Nickname));
}
catch (UserdataCorruptedException ex)
{
@@ -198,7 +198,7 @@ internal sealed partial class UserViewModel : ObservableObject
serviceProvider.GetRequiredService<IClipboardInterop>().SetText(cookieString);
ArgumentNullException.ThrowIfNull(user.UserInfo);
infoBarService.Success(string.Format(SH.ViewModelUserCookieCopied, user.UserInfo.Nickname));
infoBarService.Success(SH.ViewModelUserCookieCopied.Format(user.UserInfo.Nickname));
}
catch (Exception ex)
{

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Intrinsic;
@@ -34,11 +35,13 @@ namespace Snap.Hutao.ViewModel.Wiki;
[Injection(InjectAs.Scoped)]
internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
{
private readonly IServiceProvider serviceProvider;
private readonly IContentDialogFactory contentDialogFactory;
private readonly ICultivationService cultivationService;
private readonly IMetadataService metadataService;
private readonly ITaskContext taskContext;
private readonly IHutaoCache hutaoCache;
private readonly IInfoBarService infoBarService;
private readonly CalculateClient calculateClient;
private readonly IUserService userService;
private AdvancedCollectionView? avatars;
@@ -136,7 +139,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
CalculableOptions options = new(avatar.ToCalculable(), null);
CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance<CultivatePromotionDeltaDialog>(options);
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
(bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (!isOk)
@@ -144,8 +147,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
return;
}
Response<CalculateConsumption> consumptionResponse = await serviceProvider
.GetRequiredService<CalculateClient>()
Response<CalculateConsumption> consumptionResponse = await calculateClient
.ComputeAsync(userService.Current.Entity, delta)
.ConfigureAwait(false);
@@ -158,8 +160,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
List<CalculateItem> items = CalculateItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
try
{
bool saved = await serviceProvider
.GetRequiredService<ICultivationService>()
bool saved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items)
.ConfigureAwait(false);

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Intrinsic;
@@ -30,8 +31,10 @@ namespace Snap.Hutao.ViewModel.Wiki;
[Injection(InjectAs.Scoped)]
internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly CalculateClient calculateClient;
private readonly ICultivationService cultivationService;
private readonly ITaskContext taskContext;
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService;
private readonly IHutaoCache hutaoCache;
private readonly IInfoBarService infoBarService;
@@ -120,10 +123,8 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
return;
}
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
CalculableOptions options = new(null, weapon.ToCalculable());
CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance<CultivatePromotionDeltaDialog>(options);
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
(bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (!isOk)
@@ -131,8 +132,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
return;
}
Response<CalculateConsumption> consumptionResponse = await serviceProvider
.GetRequiredService<CalculateClient>()
Response<CalculateConsumption> consumptionResponse = await calculateClient
.ComputeAsync(userService.Current.Entity, delta)
.ConfigureAwait(false);
@@ -144,8 +144,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
CalculateConsumption consumption = consumptionResponse.Data;
try
{
bool saved = await serviceProvider
.GetRequiredService<ICultivationService>()
bool saved = await cultivationService
.SaveConsumptionAsync(CultivateType.Weapon, weapon.Id, consumption.WeaponConsume.EmptyIfNull())
.ConfigureAwait(false);

View File

@@ -47,17 +47,17 @@ internal static class CoreWebView2Extension
{
CoreWebView2CookieManager cookieManager = webView.CookieManager;
if (cookieToken != null)
if (cookieToken is not null)
{
cookieManager.AddMihoyoCookie(Cookie.ACCOUNT_ID, cookieToken, isOversea).AddMihoyoCookie(Cookie.COOKIE_TOKEN, cookieToken, isOversea);
}
if (lToken != null)
if (lToken is not null)
{
cookieManager.AddMihoyoCookie(Cookie.LTUID, lToken, isOversea).AddMihoyoCookie(Cookie.LTOKEN, lToken, isOversea);
}
if (sToken != null)
if (sToken is not null)
{
cookieManager.AddMihoyoCookie(Cookie.STUID, sToken, isOversea).AddMihoyoCookie(Cookie.STOKEN, sToken, isOversea);
}

View File

@@ -20,7 +20,6 @@ namespace Snap.Hutao.Web.Bridge;
/// </summary>
[HighQuality]
[SuppressMessage("", "CA1001")]
[SuppressMessage("", "SA1600")]
internal class MiHoYoJSInterface
{
private const string InitializeJsInterfaceScript2 = """
@@ -65,7 +64,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="jsParam">参数</param>
/// <returns>响应</returns>
public virtual async Task<IJsResult?> GetActionTicketAsync(JsParam<ActionTypePayload> jsParam)
public virtual async ValueTask<IJsResult?> GetActionTicketAsync(JsParam<ActionTypePayload> jsParam)
{
return await serviceProvider
.GetRequiredService<AuthClient>()
@@ -98,14 +97,13 @@ internal class MiHoYoJSInterface
/// <returns>响应</returns>
public virtual JsResult<Dictionary<string, string>> GetCookieInfo(JsParam param)
{
User user = serviceProvider.GetRequiredService<IUserService>().Current!;
ArgumentNullException.ThrowIfNull(userAndUid.User.LToken);
return new()
{
Data = new()
{
[Cookie.LTUID] = user.LToken![Cookie.LTUID],
[Cookie.LTOKEN] = user.LToken[Cookie.LTOKEN],
[Cookie.LTUID] = userAndUid.User.LToken[Cookie.LTUID],
[Cookie.LTOKEN] = userAndUid.User.LToken[Cookie.LTOKEN],
[Cookie.LOGIN_TICKET] = string.Empty,
},
};
@@ -116,6 +114,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[SuppressMessage("", "CA1308")]
public virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV1(JsParam param)
{
string salt = HoyolabOptions.Salts[SaltType.LK2];
@@ -146,9 +145,9 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[SuppressMessage("", "CA1308")]
public virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV2(JsParam<DynamicSecrect2Playload> param)
{
// TODO: Salt X4 for hoyolab user
string salt = HoyolabOptions.Salts[SaltType.X4];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
int r = GetRandom();
@@ -193,7 +192,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual async Task<JsResult<Dictionary<string, string>>> GetCookieTokenAsync(JsParam<CookieTokenPayload> param)
public virtual async ValueTask<JsResult<Dictionary<string, string>>> GetCookieTokenAsync(JsParam<CookieTokenPayload> param)
{
IUserService userService = serviceProvider.GetRequiredService<IUserService>();
User user = userService.Current!;
@@ -212,7 +211,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual async Task<IJsResult?> ClosePageAsync(JsParam param)
public virtual async ValueTask<IJsResult?> ClosePageAsync(JsParam param)
{
await taskContext.SwitchToMainThreadAsync();
if (webView.CanGoBack)
@@ -247,7 +246,7 @@ internal class MiHoYoJSInterface
return new() { Data = new() { ["statusBarHeight"] = 0 } };
}
public virtual async Task<IJsResult?> PushPageAsync(JsParam<PushPagePayload> param)
public virtual async ValueTask<IJsResult?> PushPageAsync(JsParam<PushPagePayload> param)
{
await taskContext.SwitchToMainThreadAsync();
webView.Navigate(param.Payload.Page);
@@ -267,15 +266,16 @@ internal class MiHoYoJSInterface
{
Data = new()
{
// TODO: replace with metadata options value
["language"] = appOptions.PreviousCulture.Name.ToLowerInvariant(),
["timeZone"] = "GMT+8",
},
};
}
public virtual Task<IJsResult?> ShowAlertDialogAsync(JsParam param)
public virtual ValueTask<IJsResult?> ShowAlertDialogAsync(JsParam param)
{
return Task.FromException<IJsResult?>(new NotImplementedException());
return ValueTask.FromException<IJsResult?>(new NotSupportedException());
}
public virtual IJsResult? StartRealPersonValidation(JsParam param)
@@ -308,7 +308,7 @@ internal class MiHoYoJSInterface
throw new NotImplementedException();
}
public virtual Task<IJsResult?> GetNotificationSettingsAsync(JsParam param)
public virtual ValueTask<IJsResult?> GetNotificationSettingsAsync(JsParam param)
{
throw new NotImplementedException();
}
@@ -318,7 +318,7 @@ internal class MiHoYoJSInterface
throw new NotImplementedException();
}
private async Task<string> ExecuteCallbackScriptAsync(string callback, string? payload = null)
private async ValueTask<string> ExecuteCallbackScriptAsync(string callback, string? payload = null)
{
if (string.IsNullOrEmpty(callback))
{
@@ -377,7 +377,7 @@ internal class MiHoYoJSInterface
return default;
}
private async Task<IJsResult?> TryGetJsResultFromJsParamAsync(JsParam param)
private async ValueTask<IJsResult?> TryGetJsResultFromJsParamAsync(JsParam param)
{
try
{

View File

@@ -25,6 +25,7 @@ internal sealed class DynamicSecrect2Playload
/// 获取排序后的的查询参数
/// </summary>
/// <returns>查询参数</returns>
[SuppressMessage("", "CA1308")]
public string GetQueryParam()
{
// TODO : improve here.

View File

@@ -61,7 +61,7 @@ internal sealed class JsParam<TPayload>
return new JsParam<TPayload>()
{
Method = jsParam.Method,
Payload = jsParam.Payload.HasValue ? jsParam.Payload.Value.Deserialize<TPayload>()! : default!,
Payload = JsonSerializer.Deserialize<TPayload>(jsParam.Payload ?? default) ?? default!,
Callback = jsParam.Callback,
};
}

View File

@@ -32,7 +32,7 @@ internal sealed partial class EnkaClient
/// <param name="playerUid">玩家Uid</param>
/// <param name="token">取消令牌</param>
/// <returns>Enka API 响应</returns>
public Task<EnkaResponse?> GetForwardDataAsync(in PlayerUid playerUid, CancellationToken token = default)
public ValueTask<EnkaResponse?> GetForwardDataAsync(in PlayerUid playerUid, CancellationToken token = default)
{
return TryGetEnkaResponseCoreAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), token);
}
@@ -43,12 +43,12 @@ internal sealed partial class EnkaClient
/// <param name="playerUid">玩家Uid</param>
/// <param name="token">取消令牌</param>
/// <returns>Enka API 响应</returns>
public Task<EnkaResponse?> GetDataAsync(in PlayerUid playerUid, CancellationToken token = default)
public ValueTask<EnkaResponse?> GetDataAsync(in PlayerUid playerUid, CancellationToken token = default)
{
return TryGetEnkaResponseCoreAsync(string.Format(EnkaAPI, playerUid.Value), token);
return TryGetEnkaResponseCoreAsync(EnkaAPI.Format(playerUid.Value), token);
}
private async Task<EnkaResponse?> TryGetEnkaResponseCoreAsync(string url, CancellationToken token = default)
private async ValueTask<EnkaResponse?> TryGetEnkaResponseCoreAsync(string url, CancellationToken token = default)
{
try
{

View File

@@ -34,7 +34,7 @@ internal sealed class EnkaResponse
public bool IsValid
{
[MemberNotNullWhen(true, nameof(PlayerInfo), nameof(AvatarInfoList))]
get => PlayerInfo != null && AvatarInfoList != null;
get => PlayerInfo is not null && AvatarInfoList is not null;
}
/// <summary>

View File

@@ -23,7 +23,7 @@ internal sealed partial class GeetestClient
/// </summary>
/// <param name="gt">gt</param>
/// <returns>类型</returns>
public async Task<GeetestResult<JsonElement>?> GetTypeAsync(string gt)
public async ValueTask<GeetestResult<JsonElement>?> GetTypeAsync(string gt)
{
string raw = await httpClient.GetStringAsync(ApiEndpoints.GeetestGetType(gt)).ConfigureAwait(false);
raw = raw[0] == '(' ? raw[1..^1] : raw; // remove surrounded ( )
@@ -37,7 +37,7 @@ internal sealed partial class GeetestClient
/// <param name="gt">gt</param>
/// <param name="challenge">验证流水号</param>
/// <returns>验证方式</returns>
public async Task<GeetestResult<GeetestData>?> GetAjaxAsync(string gt, string challenge)
public async ValueTask<GeetestResult<GeetestData>?> GetAjaxAsync(string gt, string challenge)
{
string raw = await httpClient.GetStringAsync(ApiEndpoints.GeetestAjax(gt, challenge)).ConfigureAwait(false);
raw = raw[0] == '(' ? raw[1..^1] : raw; // remove surrounded ( )
@@ -50,7 +50,7 @@ internal sealed partial class GeetestClient
/// </summary>
/// <param name="registration">验证注册</param>
/// <returns>验证方式</returns>
public async Task<GeetestResult<GeetestData>?> GetAjaxAsync(VerificationRegistration registration)
public async ValueTask<GeetestResult<GeetestData>?> GetAjaxAsync(VerificationRegistration registration)
{
try
{

View File

@@ -32,7 +32,7 @@ internal sealed partial class AccountClient
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)]
public async Task<Response<GameAuthKey>> GenerateAuthenticationKeyAsync(User user, GenAuthKeyData data, CancellationToken token = default)
public async ValueTask<Response<GameAuthKey>> GenerateAuthenticationKeyAsync(User user, GenAuthKeyData data, CancellationToken token = default)
{
Response<GameAuthKey>? resp = await httpClient
.SetUser(user, CookieType.SToken)

View File

@@ -16,5 +16,5 @@ internal interface IUserClient
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
Task<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default);
ValueTask<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default);
}

View File

@@ -29,15 +29,16 @@ internal sealed partial class UserClient : IUserClient
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)]
public async Task<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
public async ValueTask<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
{
ArgumentException.ThrowIfNullOrEmpty(user.Aid);
Response<UserFullInfoWrapper>? resp = await httpClient
// .SetUser(user, CookieType.SToken)
.SetReferer(ApiEndpoints.BbsReferer)
// .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiEndpoints.UserFullInfoQuery(user.Aid), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);

View File

@@ -28,11 +28,12 @@ internal sealed partial class UserClientOversea : IUserClient
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.None)]
public async Task<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
public async ValueTask<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
{
ArgumentException.ThrowIfNullOrEmpty(user.Aid);
Response<UserFullInfoWrapper>? resp = await httpClient
.SetUser(user, CookieType.LToken)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiOsEndpoints.UserFullInfoQuery(user.Aid), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab;
internal static class Images
{
public const string UIItemIcon204 = $"https://smms.app/image/x9psnPrcbYoCl6U";
public const string UIItemIcon210 = $"https://smms.app/image/n4gwxlFGPTX2j8p";
public const string UIItemIcon220021 = $"https://smms.app/image/kbh1a2YVXpxWuez";
public const string UIIconInteeExplore1 = $"https://smms.app/image/zJ4UYqKiD6uQlLc";
public const string UIMarkQuestEventsProce = $"https://smms.app/image/DQyTF3rv4aA8MZV";
}

View File

@@ -55,7 +55,7 @@ internal sealed class HomaGachaLogClient
/// <param name="distributionType">分布类型</param>
/// <param name="token">取消令牌</param>
/// <returns>祈愿分布</returns>
public async Task<Response<GachaDistribution>> GetGachaDistributionAsync(GachaDistributionType distributionType, CancellationToken token = default)
public async ValueTask<Response<GachaDistribution>> GetGachaDistributionAsync(GachaDistributionType distributionType, CancellationToken token = default)
{
Response<GachaDistribution>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<GachaDistribution>>(HutaoEndpoints.GachaLogStatisticsDistribution(distributionType), options, logger, token)

View File

@@ -21,7 +21,7 @@ internal sealed class ReliquarySetsConverter : JsonConverter<ReliquarySets>
List<ReliquarySet> sets = new();
foreach (StringSegment segment in new StringTokenizer(source, Separator.ToArray()))
{
if (segment.HasValue)
if (segment is { HasValue: true, Length: >0 })
{
sets.Add(new(segment.Value));
}

Some files were not shown because too many files have changed in this diff Show More