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(); Location location = syntax.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location); Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location);
context.ReportDiagnostic(diagnostic); context.ReportDiagnostic(diagnostic);

View File

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

View File

@@ -17,4 +17,11 @@ internal interface IMappingFrom<TSelf, T1, T2>
{ {
[Pure] [Pure]
static abstract TSelf From(T1 t1, T2 t2); 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 /// HoYoLAB web
/// </summary> /// </summary>
/// <param name="client">配置后的客户端</param> /// <param name="client">配置后的客户端</param>
[SuppressMessage("", "IDE0051")]
private static void XRpc4Configuration(HttpClient client) private static void XRpc4Configuration(HttpClient client)
{ {
client.Timeout = Timeout.InfiniteTimeSpan; client.Timeout = Timeout.InfiniteTimeSpan;

View File

@@ -34,4 +34,10 @@ internal interface IContentDialogFactory
/// <param name="title">标题</param> /// <param name="title">标题</param>
/// <returns>内容对话框</returns> /// <returns>内容对话框</returns>
ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title); 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 Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Factory.Abstraction;
using System.Security.Authentication;
namespace Snap.Hutao.Factory; namespace Snap.Hutao.Factory;
/// <inheritdoc cref="IContentDialogFactory"/> /// <inheritdoc cref="IContentDialogFactory"/>
[HighQuality] [HighQuality]
[Injection(InjectAs.Transient, typeof(IContentDialogFactory))] [ConstructorGenerated]
internal sealed class ContentDialogFactory : IContentDialogFactory [Injection(InjectAs.Singleton, typeof(IContentDialogFactory))]
internal sealed partial class ContentDialogFactory : IContentDialogFactory
{ {
private readonly MainWindow mainWindow; private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly MainWindow mainWindow;
/// <summary>
/// 构造一个新的内容对话框工厂
/// </summary>
/// <param name="taskContext">任务上下文</param>
/// <param name="mainWindow">主窗体</param>
public ContentDialogFactory(ITaskContext taskContext, MainWindow mainWindow)
{
this.taskContext = taskContext;
this.mainWindow = mainWindow;
}
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<ContentDialogResult> CreateForConfirmAsync(string title, string content) public async ValueTask<ContentDialogResult> CreateForConfirmAsync(string title, string content)
@@ -71,4 +63,17 @@ internal sealed class ContentDialogFactory : IContentDialogFactory
return dialog; 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格式的信息 /// UIGF格式的信息
/// </summary> /// </summary>
[HighQuality] [HighQuality]
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, IServiceProvider, string> internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, MetadataOptions, string>
{ {
/// <summary> /// <summary>
/// 用户Uid /// 用户Uid
@@ -58,17 +58,8 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, IServiceProvider, string
[JsonPropertyName("uigf_version")] [JsonPropertyName("uigf_version")]
public string UIGFVersion { get; set; } = default!; public string UIGFVersion { get; set; } = default!;
/// <summary> public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid)
/// 构造一个新的专用 UIGF 信息
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="uid">uid</param>
/// <returns>专用 UIGF 信息</returns>
public static UIGFInfo From(IServiceProvider serviceProvider, string uid)
{ {
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
MetadataOptions metadataOptions = serviceProvider.GetRequiredService<MetadataOptions>();
return new() return new()
{ {
Uid = uid, 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) public async ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take)
{ {
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())
@@ -66,7 +67,7 @@ internal sealed partial class AchievementDbService : IAchievementDbService
.AsNoTracking() .AsNoTracking()
.Where(a => a.ArchiveId == archiveId) .Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED) .Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.OrderByDescending(a => a.Time) .OrderByDescending(a => a.Time.ToString())
.Take(take) .Take(take)
.ToListAsync() .ToListAsync()
.ConfigureAwait(false); .ConfigureAwait(false);

View File

@@ -25,8 +25,8 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
private readonly IAvatarInfoDbService avatarInfoDbService; private readonly IAvatarInfoDbService avatarInfoDbService;
private readonly ILogger<AvatarInfoService> logger; private readonly ILogger<AvatarInfoService> logger;
private readonly IMetadataService metadataService; private readonly IMetadataService metadataService;
private readonly IServiceProvider serviceProvider;
private readonly ISummaryFactory summaryFactory; private readonly ISummaryFactory summaryFactory;
private readonly EnkaClient enkaClient;
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default) 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) private async ValueTask<EnkaResponse?> GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default)
{ {
EnkaClient enkaClient = serviceProvider.GetRequiredService<EnkaClient>();
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false) return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
?? await enkaClient.GetDataAsync(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.Binding;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
using Snap.Hutao.Web.Response; using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using Windows.Foundation.Metadata; using Windows.Foundation.Metadata;
namespace Snap.Hutao.Service.DailyNote; namespace Snap.Hutao.Service.DailyNote;
@@ -18,38 +19,26 @@ namespace Snap.Hutao.Service.DailyNote;
/// 实时便笺通知器 /// 实时便笺通知器
/// </summary> /// </summary>
[HighQuality] [HighQuality]
internal sealed class DailyNoteNotificationOperation [ConstructorGenerated]
[Injection(InjectAs.Singleton)]
internal sealed partial class DailyNoteNotificationOperation
{ {
private const string ToastHeaderIdArgument = "DAILYNOTE"; private const string ToastHeaderIdArgument = "DAILYNOTE";
private const string ToastAttributionUnknown = "Unknown"; private const string ToastAttributionUnknown = "Unknown";
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly IServiceProvider serviceProvider; private readonly IGameService gameService;
private readonly DailyNoteEntry entry; private readonly BindingClient bindingClient;
private readonly DailyNoteOptions options;
/// <summary> public async ValueTask SendAsync(DailyNoteEntry entry)
/// 构造一个新的实时便笺通知器
/// </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()
{ {
if (entry.DailyNote is null) if (entry.DailyNote is null)
{ {
return; return;
} }
List<NotifyInfo> notifyInfos = new(); List<DailyNoteNotifyInfo> notifyInfos = new();
CheckNotifySuppressed(entry, notifyInfos); CheckNotifySuppressed(entry, notifyInfos);
@@ -58,92 +47,95 @@ internal sealed class DailyNoteNotificationOperation
return; 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>(); List<UserGameRole> roles = rolesResponse.Data.List;
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? ToastAttributionUnknown;
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));
} }
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. // Image limitation.
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast?tabs=uwp#adding-images
// NotifySuppressed judge // 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) if (!entry.ResinNotifySuppressed)
{ {
notifyInfos.Add(new( notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierResin, SH.ServiceDailyNoteNotifierResin,
Web.Hoyolab.Images.UIItemIcon210, Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_210.png"),
$"{entry.DailyNote.CurrentResin}", $"{entry.DailyNote.CurrentResin}",
string.Format(SH.ServiceDailyNoteNotifierResinCurrent, entry.DailyNote.CurrentResin))); SH.ServiceDailyNoteNotifierResinCurrent.Format(entry.DailyNote.CurrentResin)));
entry.ResinNotifySuppressed = true; entry.ResinNotifySuppressed = true;
} }
} }
@@ -151,16 +143,20 @@ internal sealed class DailyNoteNotificationOperation
{ {
entry.ResinNotifySuppressed = false; entry.ResinNotifySuppressed = false;
} }
}
private static void CheckHomeCoinNotifySuppressed(DailyNoteEntry entry, List<DailyNoteNotifyInfo> notifyInfos)
{
ArgumentNullException.ThrowIfNull(entry.DailyNote);
if (entry.DailyNote.CurrentHomeCoin >= entry.HomeCoinNotifyThreshold) if (entry.DailyNote.CurrentHomeCoin >= entry.HomeCoinNotifyThreshold)
{ {
if (!entry.HomeCoinNotifySuppressed) if (!entry.HomeCoinNotifySuppressed)
{ {
notifyInfos.Add(new( notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierHomeCoin, SH.ServiceDailyNoteNotifierHomeCoin,
Web.Hoyolab.Images.UIItemIcon204, Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_204.png"),
$"{entry.DailyNote.CurrentHomeCoin}", $"{entry.DailyNote.CurrentHomeCoin}",
string.Format(SH.ServiceDailyNoteNotifierHomeCoinCurrent, entry.DailyNote.CurrentHomeCoin))); SH.ServiceDailyNoteNotifierHomeCoinCurrent.Format(entry.DailyNote.CurrentHomeCoin)));
entry.HomeCoinNotifySuppressed = true; entry.HomeCoinNotifySuppressed = true;
} }
} }
@@ -168,14 +164,17 @@ internal sealed class DailyNoteNotificationOperation
{ {
entry.HomeCoinNotifySuppressed = false; 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) if (!entry.DailyTaskNotifySuppressed)
{ {
notifyInfos.Add(new( notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierDailyTask, SH.ServiceDailyNoteNotifierDailyTask,
Web.Hoyolab.Images.UIMarkQuestEventsProce, Web.HutaoEndpoints.StaticFile("Bg", "UI_MarkQuest_Events_Proce.png"),
SH.ServiceDailyNoteNotifierDailyTaskHint, SH.ServiceDailyNoteNotifierDailyTaskHint,
entry.DailyNote.ExtraTaskRewardDescription)); entry.DailyNote.ExtraTaskRewardDescription));
entry.DailyTaskNotifySuppressed = true; entry.DailyTaskNotifySuppressed = true;
@@ -185,14 +184,17 @@ internal sealed class DailyNoteNotificationOperation
{ {
entry.DailyTaskNotifySuppressed = false; 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) if (!entry.TransformerNotifySuppressed)
{ {
notifyInfos.Add(new( notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierTransformer, SH.ServiceDailyNoteNotifierTransformer,
Web.Hoyolab.Images.UIItemIcon220021, Web.HutaoEndpoints.StaticFile("ItemIcon", "UI_ItemIcon_220021.png"),
SH.ServiceDailyNoteNotifierTransformerAdaptiveHint, SH.ServiceDailyNoteNotifierTransformerAdaptiveHint,
SH.ServiceDailyNoteNotifierTransformerHint)); SH.ServiceDailyNoteNotifierTransformerHint));
entry.TransformerNotifySuppressed = true; entry.TransformerNotifySuppressed = true;
@@ -202,14 +204,18 @@ internal sealed class DailyNoteNotificationOperation
{ {
entry.TransformerNotifySuppressed = false; 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.ExpeditionNotify && entry.DailyNote.Expeditions.All(e => e.Status == ExpeditionStatus.Finished))
{ {
if (!entry.ExpeditionNotifySuppressed) if (!entry.ExpeditionNotifySuppressed)
{ {
notifyInfos.Add(new( notifyInfos.Add(new(
SH.ServiceDailyNoteNotifierExpedition, SH.ServiceDailyNoteNotifierExpedition,
Web.Hoyolab.Images.UIIconInteeExplore1, Web.HutaoEndpoints.StaticFile("Bg", "UI_Icon_Intee_Explore_1.png"),
SH.ServiceDailyNoteNotifierExpeditionAdaptiveHint, SH.ServiceDailyNoteNotifierExpeditionAdaptiveHint,
SH.ServiceDailyNoteNotifierExpeditionHint)); SH.ServiceDailyNoteNotifierExpeditionHint));
entry.ExpeditionNotifySuppressed = true; entry.ExpeditionNotifySuppressed = true;
@@ -224,22 +230,6 @@ internal sealed class DailyNoteNotificationOperation
private bool ShouldSuppressPopup(DailyNoteOptions options) private bool ShouldSuppressPopup(DailyNoteOptions options)
{ {
// Prevent notify when we are in game && silent mode. // Prevent notify when we are in game && silent mode.
return options.IsSilentWhenPlayingGame && serviceProvider.GetRequiredService<IGameService>().IsGameRunning(); return options.IsSilentWhenPlayingGame && gameService.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;
}
} }
} }

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))] [Injection(InjectAs.Singleton, typeof(IDailyNoteService))]
internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessage> internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessage>
{ {
private readonly DailyNoteNotificationOperation dailyNoteNotificationOperation;
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly IDailyNoteDbService dailyNoteDbService; private readonly IDailyNoteDbService dailyNoteDbService;
private readonly IUserService userService; 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); entries?.SingleOrDefault(e => e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote);
// database // database
await new DailyNoteNotificationOperation(serviceProvider, entry).SendAsync().ConfigureAwait(false); await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false);
entry.DailyNote = dailyNote; entry.DailyNote = dailyNote;
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false); 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.Model.Metadata.Weapon;
using Snap.Hutao.Service.Metadata; using Snap.Hutao.Service.Metadata;
using Snap.Hutao.ViewModel.GachaLog; using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hutao;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Snap.Hutao.Service.GachaLog.Factory; namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -21,8 +22,8 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
[Injection(InjectAs.Scoped, typeof(IGachaStatisticsFactory))] [Injection(InjectAs.Scoped, typeof(IGachaStatisticsFactory))]
internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
{ {
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService; private readonly IMetadataService metadataService;
private readonly HomaGachaLogClient homaGachaLogClient;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly AppOptions options; private readonly AppOptions options;
@@ -33,32 +34,25 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
List<GachaEvent> gachaEvents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false); List<GachaEvent> gachaEvents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
List<HistoryWishBuilder> historyWishBuilders = gachaEvents.SelectList(gachaEvent => new HistoryWishBuilder(gachaEvent, context)); 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( private static GachaStatistics CreateCore(
IServiceProvider serviceProvider, ITaskContext taskContext,
HomaGachaLogClient gachaLogClient,
List<GachaItem> items, List<GachaItem> items,
List<HistoryWishBuilder> historyWishBuilders, List<HistoryWishBuilder> historyWishBuilders,
in GachaLogServiceMetadataContext context, in GachaLogServiceMetadataContext context,
bool isEmptyHistoryWishVisible) bool isEmptyHistoryWishVisible)
{ {
TypedWishSummaryBuilder standardWishBuilder = new( TypedWishSummaryBuilderContext standardContext = TypedWishSummaryBuilderContext.StandardWish(taskContext, gachaLogClient);
serviceProvider, TypedWishSummaryBuilder standardWishBuilder = new(standardContext);
SH.ServiceGachaLogFactoryPermanentWishName,
TypedWishSummaryBuilder.IsStandardWish, TypedWishSummaryBuilderContext avatarContext = TypedWishSummaryBuilderContext.AvatarEventWish(taskContext, gachaLogClient);
Web.Hutao.GachaLog.GachaDistributionType.Standard); TypedWishSummaryBuilder avatarWishBuilder = new(avatarContext);
TypedWishSummaryBuilder avatarWishBuilder = new(
serviceProvider, TypedWishSummaryBuilderContext weaponContext = TypedWishSummaryBuilderContext.WeaponEventWish(taskContext, gachaLogClient);
SH.ServiceGachaLogFactoryAvatarWishName, TypedWishSummaryBuilder weaponWishBuilder = new(weaponContext);
TypedWishSummaryBuilder.IsAvatarEventWish,
Web.Hutao.GachaLog.GachaDistributionType.AvatarEvent);
TypedWishSummaryBuilder weaponWishBuilder = new(
serviceProvider,
SH.ServiceGachaLogFactoryWeaponWishName,
TypedWishSummaryBuilder.IsWeaponEventWish,
Web.Hutao.GachaLog.GachaDistributionType.WeaponEvent,
80);
Dictionary<Avatar, int> orangeAvatarCounter = new(); Dictionary<Avatar, int> orangeAvatarCounter = new();
Dictionary<Avatar, int> purpleAvatarCounter = new(); Dictionary<Avatar, int> purpleAvatarCounter = new();

View File

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

View File

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

View File

@@ -29,20 +29,27 @@ internal readonly struct GachaItemSaveContext
/// <summary> /// <summary>
/// 数据集 /// 数据集
/// </summary> /// </summary>
public readonly DbSet<GachaItem> GachaItems; public readonly IGachaLogDbService GachaLogDbService;
/// <summary> public GachaItemSaveContext(List<GachaItem> itemsToAdd, bool isLazy, long endId, IGachaLogDbService gachaLogDbService)
/// 构造一个新的祈愿物品
/// </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)
{ {
ItemsToAdd = itemsToAdd; ItemsToAdd = itemsToAdd;
IsLazy = isLazy; IsLazy = isLazy;
EndId = endId; 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; namespace Snap.Hutao.Service.GachaLog;
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IGachaLogDbService))] [Injection(InjectAs.Singleton, typeof(IGachaLogDbService))]
internal sealed partial class GachaLogDbService : IGachaLogDbService internal sealed partial class GachaLogDbService : IGachaLogDbService
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
@@ -30,7 +30,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
} }
catch (SqliteException ex) catch (SqliteException ex)
{ {
string message = string.Format(SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage, ex.Message); string message = SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage.Format(ex.Message);
throw ThrowHelper.UserdataCorrupted(message, ex); throw ThrowHelper.UserdataCorrupted(message, ex);
} }
} }
@@ -202,4 +202,26 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
await appDbContext.GachaItems.AddRangeAndSaveAsync(items).ConfigureAwait(false); 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> /// </summary>
public GachaConfigType CurrentType; public GachaConfigType CurrentType;
private readonly IServiceProvider serviceProvider;
private readonly GachaLogServiceMetadataContext serviceContext; private readonly GachaLogServiceMetadataContext serviceContext;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
private readonly bool isLazy; private readonly bool isLazy;
/// <summary> public GachaLogFetchContext(IGachaLogDbService gachaLogDbService, ITaskContext taskContext, in GachaLogServiceMetadataContext serviceContext, bool isLazy)
/// 构造一个新的祈愿记录获取上下文
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="serviceContext">祈愿服务上下文</param>
/// <param name="isLazy">是否为懒惰模式</param>
public GachaLogFetchContext(IServiceProvider serviceProvider, in GachaLogServiceMetadataContext serviceContext, bool isLazy)
{ {
this.serviceProvider = serviceProvider; this.gachaLogDbService = gachaLogDbService;
this.taskContext = taskContext;
this.serviceContext = serviceContext; this.serviceContext = serviceContext;
this.isLazy = isLazy; this.isLazy = isLazy;
} }
@@ -99,7 +95,7 @@ internal struct GachaLogFetchContext
{ {
if (TargetArchive is null) 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); DbEndId ??= gachaLogDbService.GetNewestGachaItemIdByArchiveIdAndQueryType(TargetArchive.InnerId, CurrentType);
@@ -131,7 +127,8 @@ internal struct GachaLogFetchContext
/// <param name="item">物品</param> /// <param name="item">物品</param>
public void AddItem(GachaLogItem item) 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)); FetchStatus.Items.Add(serviceContext.GetItemByNameAndType(item.Name, item.ItemType));
QueryOptions.EndId = item.Id; QueryOptions.EndId = item.Id;
} }
@@ -141,16 +138,11 @@ internal struct GachaLogFetchContext
/// </summary> /// </summary>
public readonly void SaveItems() 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>(); GachaItemSaveContext saveContext = new(ItemsToAdd, isLazy, QueryOptions.EndId, gachaLogDbService);
saveContext.SaveItems(TargetArchive);
// 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);
}
} }
} }

View File

@@ -25,9 +25,9 @@ namespace Snap.Hutao.Service.GachaLog;
[Injection(InjectAs.Scoped, typeof(IGachaLogHutaoCloudService))] [Injection(InjectAs.Scoped, typeof(IGachaLogHutaoCloudService))]
internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudService internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudService
{ {
private readonly IMetadataService metadataService;
private readonly HomaGachaLogClient homaGachaLogClient; private readonly HomaGachaLogClient homaGachaLogClient;
private readonly IGachaLogDbService gachaLogDbService; private readonly IGachaLogDbService gachaLogDbService;
private readonly IServiceProvider serviceProvider;
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask<Response<List<GachaEntry>>> GetGachaEntriesAsync(CancellationToken token = default) 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); Response<GachaEventStatistics> response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false);
if (response.IsOk()) if (response.IsOk())
{ {
IMetadataService metadataService = serviceProvider.GetRequiredService<IMetadataService>();
if (await metadataService.InitializeAsync().ConfigureAwait(false)) if (await metadataService.InitializeAsync().ConfigureAwait(false))
{ {
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).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 IGachaStatisticsFactory gachaStatisticsFactory;
private readonly IUIGFExportService gachaLogExportService; private readonly IUIGFExportService gachaLogExportService;
private readonly IUIGFImportService gachaLogImportService; private readonly IUIGFImportService gachaLogImportService;
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService; private readonly IMetadataService metadataService;
private readonly ILogger<GachaLogService> logger; private readonly ILogger<GachaLogService> logger;
private readonly GachaInfoClient gachaInfoClient; 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) private async ValueTask<ValueResult<bool, GachaArchive?>> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
{ {
ArgumentNullException.ThrowIfNull(ArchiveCollection); ArgumentNullException.ThrowIfNull(ArchiveCollection);
GachaLogFetchContext fetchContext = new(serviceProvider, context, isLazy); GachaLogFetchContext fetchContext = new(gachaLogDbService, taskContext, context, isLazy);
foreach (GachaConfigType configType in GachaLog.QueryTypes) foreach (GachaConfigType configType in GachaLog.QueryTypes)
{ {
@@ -172,7 +171,7 @@ internal sealed partial class GachaLogService : IGachaLogService
.GetGachaLogPageAsync(fetchContext.QueryOptions, token) .GetGachaLogPageAsync(fetchContext.QueryOptions, token)
.ConfigureAwait(false); .ConfigureAwait(false);
if (response.TryGetData(out GachaLogPage? page, serviceProvider)) if (response.TryGetData(out GachaLogPage? page))
{ {
List<GachaLogItem> items = page.List; List<GachaLogItem> items = page.List;
fetchContext.ResetForProcessingPage(); fetchContext.ResetForProcessingPage();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,9 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
? GameConstants.GenshinImpactData ? GameConstants.GenshinImpactData
: GameConstants.YuanShenData; : 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(); Regex versionRegex = VersionRegex();
DirectoryInfo? lastestVersionCacheFolder = webCacheFolder DirectoryInfo? lastestVersionCacheFolder = webCacheFolder
.EnumerateDirectories() .EnumerateDirectories()
@@ -51,53 +53,45 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
{ {
(bool isOk, string path) = await gameService.GetGamePathAsync().ConfigureAwait(false); (bool isOk, string path) = await gameService.GetGamePathAsync().ConfigureAwait(false);
if (isOk && (!string.IsNullOrEmpty(path))) 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
{ {
return new(false, SH.ServiceGachaLogUrlProviderCachePathInvalid); 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) private static string? Match(MemoryStream stream, bool isOversea)

View File

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

View File

@@ -18,7 +18,6 @@ internal sealed partial class UIGFImportService : IUIGFImportService
{ {
private readonly ILogger<UIGFImportService> logger; private readonly ILogger<UIGFImportService> logger;
private readonly MetadataOptions metadataOptions; private readonly MetadataOptions metadataOptions;
private readonly IServiceProvider serviceProvider;
private readonly IGachaLogDbService gachaLogDbService; private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
@@ -29,11 +28,11 @@ internal sealed partial class UIGFImportService : IUIGFImportService
if (!metadataOptions.IsCurrentLocale(uigf.Info.Language)) 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); 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; Guid archiveId = archive.InnerId;
long trimId = gachaLogDbService.GetOldestGachaItemIdByArchiveId(archiveId); long trimId = gachaLogDbService.GetOldestGachaItemIdByArchiveId(archiveId);

View File

@@ -15,7 +15,7 @@ internal sealed class GameFileOperationException : Exception
/// <param name="message">消息</param> /// <param name="message">消息</param>
/// <param name="innerException">内部错误</param> /// <param name="innerException">内部错误</param>
public GameFileOperationException(string message, Exception innerException) 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 else
{ {
return new(false, null!); return new(false, default!);
} }
} }
@@ -112,9 +112,11 @@ internal sealed partial class GameService : IGameService
public bool SetChannelOptions(LaunchScheme scheme) public bool SetChannelOptions(LaunchScheme scheme)
{ {
string gamePath = appOptions.GamePath; 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 try
{ {
using (FileStream readStream = File.OpenRead(configPath)) using (FileStream readStream = File.OpenRead(configPath))
@@ -124,11 +126,11 @@ internal sealed partial class GameService : IGameService
} }
catch (FileNotFoundException ex) catch (FileNotFoundException ex)
{ {
ThrowHelper.GameFileOperation(string.Format(SH.ServiceGameSetMultiChannelConfigFileNotFound, configPath), ex); ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex);
} }
catch (DirectoryNotFoundException ex) catch (DirectoryNotFoundException ex)
{ {
ThrowHelper.GameFileOperation(string.Format(SH.ServiceGameSetMultiChannelConfigFileNotFound, configPath), ex); ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex);
} }
catch (UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
@@ -168,7 +170,8 @@ internal sealed partial class GameService : IGameService
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress) public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{ {
string gamePath = appOptions.GamePath; string gamePath = appOptions.GamePath;
string gameFolder = Path.GetDirectoryName(gamePath)!; string? gameFolder = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(gameFolder);
string gameFileName = Path.GetFileName(gamePath); string gameFileName = Path.GetFileName(gamePath);
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
@@ -241,39 +244,36 @@ internal sealed partial class GameService : IGameService
} }
string gamePath = appOptions.GamePath; string gamePath = appOptions.GamePath;
if (string.IsNullOrWhiteSpace(gamePath)) ArgumentNullException.ThrowIfNullOrEmpty(gamePath);
using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath))
{ {
// TODO: throw exception try
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)
{ {
ProcessInterop.DisableProtection(game, gamePath); bool isFirstInstance = Interlocked.Increment(ref runningGamesCounter) == 1;
}
if (isAdvancedOptionsAllowed && launchOptions.UnlockFps) game.Start();
{
await ProcessInterop.UnlockFpsAsync(serviceProvider, game, default).ConfigureAwait(false); 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/> /// <inheritdoc/>
@@ -373,7 +373,8 @@ internal sealed partial class GameService : IGameService
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
gameAccounts!.Remove(gameAccount); ArgumentNullException.ThrowIfNull(gameAccounts);
gameAccounts.Remove(gameAccount);
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
await gameDbService.DeleteGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false); 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;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
using System.Globalization;
using Windows.Graphics; using Windows.Graphics;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi; using Windows.Win32.Graphics.Gdi;
@@ -140,12 +141,12 @@ internal sealed class LaunchOptions : DbStoreOptions
[AllowNull] [AllowNull]
public NameValue<int> Monitor 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 set
{ {
if (value is not null) 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))] [Injection(InjectAs.Transient, typeof(IGameLocatorFactory))]
internal sealed partial class GameLocatorFactory : IGameLocatorFactory internal sealed partial class GameLocatorFactory : IGameLocatorFactory
{ {
[SuppressMessage("", "SH301")]
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public IGameLocator Create(GameLocationSource source) 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 else
{ {
string? path = Path.GetDirectoryName(result.Value); 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; string? escapedPath;
using (FileStream stream = File.OpenRead(configPath)) using (FileStream stream = File.OpenRead(configPath))
{ {
@@ -64,12 +65,12 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
} }
else else
{ {
return new(false, null!); return new(false, default!);
} }
} }
else 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' // Some one's folder might begin with 'u'
if (!hex4Result.Contains(@"\u")) if (!hex4Result.Contains(@"\u", StringComparison.Ordinal))
{ {
// fix path with \ // fix path with \
hex4Result = hex4Result.Replace(@"\", @"\\"); hex4Result = hex4Result.Replace(@"\", @"\\", StringComparison.Ordinal);
} }
return Regex.Unescape(hex4Result); return Regex.Unescape(hex4Result);

View File

@@ -116,7 +116,9 @@ internal sealed partial class PackageConverter
if (entry.Length != 0) if (entry.Length != 0)
{ {
string targetPath = Path.Combine(gameFolder, entry.FullName); 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); entry.ExtractToFile(targetPath, true);
} }
} }
@@ -180,7 +182,8 @@ internal sealed partial class PackageConverter
Regex dataFolderRegex = DataFolderRegex(); Regex dataFolderRegex = DataFolderRegex();
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } row && !string.IsNullOrEmpty(row)) 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}"); item.RelativePath = dataFolderRegex.Replace(item.RelativePath, "{0}");
results.Add(item.RelativePath, item); 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) 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); string cacheFile = context.GetServerCacheTargetFilePath(remoteName);
if (File.Exists(cacheFile)) if (File.Exists(cacheFile))
@@ -276,7 +279,7 @@ internal sealed partial class PackageConverter
{ {
// System.IO.IOException: The response ended prematurely. // System.IO.IOException: The response ended prematurely.
// System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream. // 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) if (backup)
{ {
string localFileName = string.Format(info.Local.RelativePath, context.FromDataFolder); string localFileName = info.Local.RelativePath.Format(context.FromDataFolder);
string localFilePath = context.GetGameFolderFilePath(localFileName); 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); File.Move(localFilePath, context.GetServerCacheBackupFilePath(localFileName), true);
} }
if (target) if (target)
{ {
string targetFileName = string.Format(info.Remote.RelativePath, context.ToDataFolder); string targetFileName = info.Remote.RelativePath.Format(context.ToDataFolder);
string targetFilePath = context.GetGameFolderFilePath(targetFileName); 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); File.Move(context.GetServerCacheTargetFilePath(targetFileName), targetFilePath, true);
} }
} }

View File

@@ -88,7 +88,8 @@ internal static class RegistryInterop
private static string GetPowerShellLocation() 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())) 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 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)), 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 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)), 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))] [Injection(InjectAs.Scoped, typeof(IHutaoService))]
internal sealed partial class HutaoService : 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 HomaSpiralAbyssClient homaClient;
private readonly IServiceProvider serviceProvider;
private readonly JsonSerializerOptions options; private readonly JsonSerializerOptions options;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
@@ -67,59 +68,30 @@ internal sealed partial class HutaoService : IHutaoService
return FromCacheOrWebAsync(nameof(TeamAppearance), homaClient.GetTeamCombinationsAsync); return FromCacheOrWebAsync(nameof(TeamAppearance), homaClient.GetTeamCombinationsAsync);
} }
private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, Func<CancellationToken, Task<Response<T>>> taskFunc) private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, Func<CancellationToken, ValueTask<Response<T>>> taskFunc)
where T : new() where T : class, new()
{ {
string key = $"{nameof(HutaoService)}.Cache.{typeName}"; string key = $"{nameof(HutaoService)}.Cache.{typeName}";
if (memoryCache.TryGetValue(key, out object? cache)) 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>(); return memoryCache.Set(key, value, cacheExpireTime);
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));
}
}
} }
Response<T> webResponse = await taskFunc(default).ConfigureAwait(false); Response<T> webResponse = await taskFunc(default).ConfigureAwait(false);
T? data = webResponse.Data; T? data = webResponse.Data;
try if (data is not null)
{ {
if (data is not null) await objectCacheDbService.AddObjectCacheAsync(key, cacheExpireTime, data).ConfigureAwait(false);
{
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.
} }
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) public void UpdateUserInfo(UserInfo userInfo)
{ {
IsLicensedDeveloper = userInfo.IsLicensedDeveloper; 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; 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> /// </summary>
internal partial class MetadataService internal partial class MetadataService
{ {
#pragma warning disable CA1823
private const string FileNameAchievement = "Achievement"; private const string FileNameAchievement = "Achievement";
private const string FileNameAchievementGoal = "AchievementGoal"; private const string FileNameAchievementGoal = "AchievementGoal";
private const string FileNameAvatar = "Avatar"; private const string FileNameAvatar = "Avatar";
@@ -30,4 +31,5 @@ internal partial class MetadataService
private const string FileNameWeapon = "Weapon"; private const string FileNameWeapon = "Weapon";
private const string FileNameWeaponCurve = "WeaponCurve"; private const string FileNameWeaponCurve = "WeaponCurve";
private const string FileNameWeaponPromote = "WeaponPromote"; private const string FileNameWeaponPromote = "WeaponPromote";
#pragma warning restore
} }

View File

@@ -132,7 +132,7 @@ internal sealed class NavigationService : INavigationService, INavigationInitial
case NavigationResult.AlreadyNavigatedTo: case NavigationResult.AlreadyNavigatedTo:
{ {
if (frame!.Content is ScopedPage scopedPage) if (frame is { Content: ScopedPage scopedPage })
{ {
await scopedPage.NotifyRecipientAsync((INavigationData)data).ConfigureAwait(false); await scopedPage.NotifyRecipientAsync((INavigationData)data).ConfigureAwait(false);
} }
@@ -158,11 +158,9 @@ internal sealed class NavigationService : INavigationService, INavigationInitial
{ {
taskContext.InvokeOnMainThread(() => taskContext.InvokeOnMainThread(() =>
{ {
bool canGoBack = frame?.CanGoBack ?? false; if (frame is { CanGoBack: true })
if (canGoBack)
{ {
frame!.GoBack(); frame.GoBack();
SyncSelectedNavigationViewItemWith(frame.Content.GetType()); SyncSelectedNavigationViewItemWith(frame.Content.GetType());
} }
}); });
@@ -234,6 +232,7 @@ internal sealed class NavigationService : INavigationService, INavigationInitial
private void OnPaneStateChanged(NavigationView sender, object args) 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; infoBar.Closed += infobarClosedEventHandler;
collection!.Add(infoBar); ArgumentNullException.ThrowIfNull(collection);
collection.Add(infoBar);
if (delay > 0) if (delay > 0)
{ {
@@ -137,7 +138,8 @@ internal sealed class InfoBarService : IInfoBarService
private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args) private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args)
{ {
taskContext.InvokeOnMainThread(() => collection!.Remove(sender)); ArgumentNullException.ThrowIfNull(collection);
taskContext.InvokeOnMainThread(() => collection.Remove(sender));
sender.Closed -= infobarClosedEventHandler; sender.Closed -= infobarClosedEventHandler;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
/// </summary> /// </summary>
/// <param name="serviceProvider">服务提供器</param> /// <param name="serviceProvider">服务提供器</param>
/// <param name="options">选项</param> /// <param name="options">选项</param>
[SuppressMessage("", "SH002")]
public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, CalculableOptions options) public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, CalculableOptions options)
{ {
InitializeComponent(); InitializeComponent();
@@ -40,37 +41,35 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
/// 异步获取提升差异 /// 异步获取提升差异
/// </summary> /// </summary>
/// <returns>提升差异</returns> /// <returns>提升差异</returns>
public async Task<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaAsync() public async ValueTask<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaAsync()
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync(); ContentDialogResult result = await ShowAsync();
if (result == ContentDialogResult.Primary) 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
{ {
return new(false, default!); 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> /// </summary>
/// <returns>是否导入</returns> /// <returns>是否导入</returns>
public async Task<bool> GetShouldImportAsync() public async ValueTask<bool> GetShouldImportAsync()
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
return await ShowAsync() == ContentDialogResult.Primary; return await ShowAsync() == ContentDialogResult.Primary;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
InitializeComponent(); 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>(); JsonSerializerOptions options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
ILogger<LoginHoyoverseUserPage> logger = serviceProvider.GetRequiredService<ILogger<LoginHoyoverseUserPage>>(); 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) .TryCatchGetFromJsonAsync<WebApiResponse<AccountInfoWrapper>>(ApiOsEndpoints.WebApiOsAccountLoginByCookie, options, logger, token)
.ConfigureAwait(false); .ConfigureAwait(false);
if (resp != null) if (resp is not null)
{ {
return resp.Data.AccountInfo.AccountId.ToString(); return $"{resp.Data.AccountInfo.AccountId}";
} }
return string.Empty; return string.Empty;

View File

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

View File

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

View File

@@ -38,10 +38,16 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
[Injection(InjectAs.Scoped)] [Injection(InjectAs.Scoped)]
internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage> 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 ITaskContext taskContext;
private readonly IUserService userService; private readonly IUserService userService;
private readonly IInfoBarService infoBarService; private readonly IInfoBarService infoBarService;
private Summary? summary; private Summary? summary;
private AvatarView? selectedAvatar; private AvatarView? selectedAvatar;
@@ -109,15 +115,13 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
ValueResult<RefreshResult, Summary?> summaryResult; ValueResult<RefreshResult, Summary?> summaryResult;
using (await EnterCriticalExecutionAsync().ConfigureAwait(false)) using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{ {
ContentDialog dialog = await serviceProvider ContentDialog dialog = await contentDialogFactory
.GetRequiredService<IContentDialogFactory>()
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyFetch) .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyFetch)
.ConfigureAwait(false); .ConfigureAwait(false);
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{ {
summaryResult = await serviceProvider summaryResult = await avatarInfoService
.GetRequiredService<IAvatarInfoService>()
.GetSummaryAsync(userAndUid, option, token) .GetSummaryAsync(userAndUid, option, token)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@@ -139,7 +143,8 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
break; break;
case RefreshResult.StatusCodeNotSucceed: case RefreshResult.StatusCodeNotSucceed:
infoBarService.Warning(summary!.Message); ArgumentNullException.ThrowIfNull(summary);
infoBarService.Warning(summary.Message);
break; break;
case RefreshResult.ShowcaseNotOpen: case RefreshResult.ShowcaseNotOpen:
@@ -167,49 +172,50 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
} }
// ContentDialog must be created by main thread. // ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); 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); (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (isOk) if (!isOk)
{ {
Response<CalculatorConsumption> consumptionResponse = await serviceProvider return;
.GetRequiredService<CalculatorClient>() }
.ComputeAsync(userService.Current.Entity, delta)
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); .ConfigureAwait(false);
if (consumptionResponse.IsOk()) if (avatarAndWeaponSaved)
{ {
ICultivationService cultivationService = serviceProvider.GetRequiredService<ICultivationService>(); infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
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);
}
} }
else
{
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
}
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
} }
} }
else else
@@ -231,7 +237,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
bool clipboardOpened = false; bool clipboardOpened = false;
using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight)) 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); softwareBitmap.NormalBlend(tint);
using (InMemoryRandomAccessStream memory = new()) using (InMemoryRandomAccessStream memory = new())
{ {
@@ -239,7 +245,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
encoder.SetSoftwareBitmap(softwareBitmap); encoder.SetSoftwareBitmap(softwareBitmap);
await encoder.FlushAsync(); 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>
/// 精炼属性 /// 精炼属性
/// </summary> /// </summary>
public string AffixLevel { get => string.Format(SH.ModelBindingAvatarPropertyWeaponAffixFormat, AffixLevelNumber); } public string AffixLevel { get => SH.ModelBindingAvatarPropertyWeaponAffixFormat.Format(AffixLevelNumber); }
/// <summary> /// <summary>
/// 精炼名称 /// 精炼名称

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service; using Snap.Hutao.Service;
using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Game;
@@ -33,10 +34,12 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
/// </summary> /// </summary>
public const string DesiredUid = nameof(DesiredUid); public const string DesiredUid = nameof(DesiredUid);
private readonly IServiceProvider serviceProvider; private readonly IContentDialogFactory contentDialogFactory;
private readonly INavigationService navigationService;
private readonly IInfoBarService infoBarService; private readonly IInfoBarService infoBarService;
private readonly LaunchOptions launchOptions; private readonly LaunchOptions launchOptions;
private readonly RuntimeOptions hutaoOptions; private readonly RuntimeOptions hutaoOptions;
private readonly ResourceClient resourceClient;
private readonly IUserService userService; private readonly IUserService userService;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly IGameService gameService; private readonly IGameService gameService;
@@ -102,8 +105,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
protected override async ValueTask<bool> InitializeUIAsync() protected override async ValueTask<bool> InitializeUIAsync()
{ {
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (File.Exists(AppOptions.GamePath)) if (File.Exists(AppOptions.GamePath))
{ {
try try
@@ -127,7 +128,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
} }
else else
{ {
infoBarService.Warning(string.Format(SH.ViewModelLaunchGameMultiChannelReadFail, options.ConfigFilePath)); infoBarService.Warning(SH.ViewModelLaunchGameMultiChannelReadFail.Format(options.ConfigFilePath));
} }
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection; ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
@@ -153,7 +154,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{ {
infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid); infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid);
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
await serviceProvider.GetRequiredService<INavigationService>() await navigationService
.NavigateAsync<View.Page.SettingPage>(INavigationAwaiter.Default, true) .NavigateAsync<View.Page.SettingPage>(INavigationAwaiter.Default, true)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@@ -164,8 +165,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private async ValueTask UpdateGameResourceAsync(LaunchScheme scheme) private async ValueTask UpdateGameResourceAsync(LaunchScheme scheme)
{ {
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await serviceProvider Web.Response.Response<GameResource> response = await resourceClient
.GetRequiredService<ResourceClient>()
.GetResourceAsync(scheme) .GetResourceAsync(scheme)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -179,8 +179,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
[Command("LaunchCommand", AllowConcurrentExecutions = true)] [Command("LaunchCommand", AllowConcurrentExecutions = true)]
private async Task LaunchAsync() private async Task LaunchAsync()
{ {
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (SelectedScheme is not null) if (SelectedScheme is not null)
{ {
try try
@@ -188,8 +186,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
if (gameService.SetChannelOptions(SelectedScheme)) if (gameService.SetChannelOptions(SelectedScheme))
{ {
// Channel changed, we need to change local file. // Channel changed, we need to change local file.
await taskContext.SwitchToMainThreadAsync(); LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
LaunchGamePackageConvertDialog dialog = serviceProvider.CreateInstance<LaunchGamePackageConvertDialog>();
Progress<Service.Game.Package.PackageReplaceStatus> progress = new(state => dialog.State = state.Clone()); Progress<Service.Game.Package.PackageReplaceStatus> progress = new(state => dialog.State = state.Clone());
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{ {
@@ -232,7 +229,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
} }
catch (UserdataCorruptedException ex) 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 IServiceProvider serviceProvider;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly IImageCache imageCache;
private readonly HttpClient httpClient; private readonly HttpClient httpClient;
private readonly string fileName; private readonly string fileName;
private readonly string fileUrl; private readonly string fileUrl;
@@ -41,6 +42,7 @@ internal sealed class DownloadSummary : ObservableObject
{ {
taskContext = serviceProvider.GetRequiredService<ITaskContext>(); taskContext = serviceProvider.GetRequiredService<ITaskContext>();
httpClient = serviceProvider.GetRequiredService<HttpClient>(); httpClient = serviceProvider.GetRequiredService<HttpClient>();
imageCache = serviceProvider.GetRequiredService<IImageCache>();
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>(); RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
@@ -72,7 +74,7 @@ internal sealed class DownloadSummary : ObservableObject
/// 异步下载并解压 /// 异步下载并解压
/// </summary> /// </summary>
/// <returns>任务</returns> /// <returns>任务</returns>
public async Task<bool> DownloadAndExtractAsync() public async ValueTask<bool> DownloadAndExtractAsync()
{ {
ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>(); ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>();
try try
@@ -116,13 +118,14 @@ internal sealed class DownloadSummary : ObservableObject
private void ExtractFiles(Stream stream) 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)) using (ZipArchive archive = new(stream))
{ {
foreach (ZipArchiveEntry entry in archive.Entries) foreach (ZipArchiveEntry entry in archive.Entries)
{ {
string destPath = imageCache.GetFileFromCategoryAndName(fileName, entry.FullName); string destPath = imageCacheFilePathOperation.GetFileFromCategoryAndName(fileName, entry.FullName);
entry.ExtractToFile(destPath, true); entry.ExtractToFile(destPath, true);
} }
} }

View File

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

View File

@@ -19,7 +19,7 @@ internal sealed class BattleView
/// <param name="idAvatarMap">Id角色映射</param> /// <param name="idAvatarMap">Id角色映射</param>
public BattleView(Battle battle, Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap) 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])); 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> /// <param name="idAvatarMap">Id角色映射</param>
public FloorView(Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.Floor floor, Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap) 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}"; SettleTime = $"{DateTimeOffset.FromUnixTimeSeconds(floor.SettleTime).ToLocalTime():yyyy.MM.dd HH:mm:ss}";
Star = floor.Star; Star = floor.Star;
Levels = floor.Levels.SelectList(l => new LevelView(l, idAvatarMap)); Levels = floor.Levels.SelectList(l => new LevelView(l, idAvatarMap));

View File

@@ -18,7 +18,7 @@ internal sealed class LevelView
/// <param name="idAvatarMap">Id角色映射</param> /// <param name="idAvatarMap">Id角色映射</param>
public LevelView(Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.Level level, Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap) 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; Star = level.Star;
Battles = level.Battles.SelectList(b => new BattleView(b, idAvatarMap)); 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> internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage>
{ {
private readonly ISpiralAbyssRecordService spiralAbyssRecordService; private readonly ISpiralAbyssRecordService spiralAbyssRecordService;
private readonly IServiceProvider serviceProvider; private readonly HomaSpiralAbyssClient spiralAbyssClient;
private readonly IMetadataService metadataService; private readonly IMetadataService metadataService;
private readonly IInfoBarService infoBarService; private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
@@ -148,15 +148,12 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
[Command("UploadSpiralAbyssRecordCommand")] [Command("UploadSpiralAbyssRecordCommand")]
private async Task UploadSpiralAbyssRecordAsync() private async Task UploadSpiralAbyssRecordAsync()
{ {
HomaSpiralAbyssClient homaClient = serviceProvider.GetRequiredService<HomaSpiralAbyssClient>();
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) 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) 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()) if (response.IsOk())
{ {

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,17 +47,17 @@ internal static class CoreWebView2Extension
{ {
CoreWebView2CookieManager cookieManager = webView.CookieManager; 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); 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); 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); cookieManager.AddMihoyoCookie(Cookie.STUID, sToken, isOversea).AddMihoyoCookie(Cookie.STOKEN, sToken, isOversea);
} }

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ internal sealed partial class EnkaClient
/// <param name="playerUid">玩家Uid</param> /// <param name="playerUid">玩家Uid</param>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>Enka API 响应</returns> /// <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); return TryGetEnkaResponseCoreAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), token);
} }
@@ -43,12 +43,12 @@ internal sealed partial class EnkaClient
/// <param name="playerUid">玩家Uid</param> /// <param name="playerUid">玩家Uid</param>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>Enka API 响应</returns> /// <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 try
{ {

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ internal sealed partial class AccountClient
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns> /// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)] [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 Response<GameAuthKey>? resp = await httpClient
.SetUser(user, CookieType.SToken) .SetUser(user, CookieType.SToken)

View File

@@ -16,5 +16,5 @@ internal interface IUserClient
/// <param name="user">用户</param> /// <param name="user">用户</param>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>详细信息</returns> /// <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> /// <param name="token">取消令牌</param>
/// <returns>详细信息</returns> /// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)] [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 Response<UserFullInfoWrapper>? resp = await httpClient
// .SetUser(user, CookieType.SToken) // .SetUser(user, CookieType.SToken)
.SetReferer(ApiEndpoints.BbsReferer) .SetReferer(ApiEndpoints.BbsReferer)
// .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true) // .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); .ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp); return Response.Response.DefaultIfNull(resp);

View File

@@ -28,11 +28,12 @@ internal sealed partial class UserClientOversea : IUserClient
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>详细信息</returns> /// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.None)] [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 Response<UserFullInfoWrapper>? resp = await httpClient
.SetUser(user, CookieType.LToken) .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); .ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp); 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="distributionType">分布类型</param>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>祈愿分布</returns> /// <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 Response<GachaDistribution>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<GachaDistribution>>(HutaoEndpoints.GachaLogStatisticsDistribution(distributionType), options, logger, token) .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(); List<ReliquarySet> sets = new();
foreach (StringSegment segment in new StringTokenizer(source, Separator.ToArray())) foreach (StringSegment segment in new StringTokenizer(source, Separator.ToArray()))
{ {
if (segment.HasValue) if (segment is { HasValue: true, Length: >0 })
{ {
sets.Add(new(segment.Value)); sets.Add(new(segment.Value));
} }

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