mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
refactor viewmodel
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -39,6 +39,6 @@ internal sealed partial class ManualGameLocator : IGameLocator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new(false, null!);
|
return new(false, default!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ internal sealed partial class BottomTextSmallControl : UserControl
|
|||||||
{
|
{
|
||||||
public BottomTextSmallControl()
|
public BottomTextSmallControl()
|
||||||
{
|
{
|
||||||
this.InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
/// 精炼名称
|
/// 精炼名称
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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";
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user