mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5126337138 | ||
|
|
4d634d3264 |
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此类支持取消任务
|
||||
/// </summary>
|
||||
public interface ISupportCancellation
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于通知事件取消的取消令牌
|
||||
/// </summary>
|
||||
CancellationToken CancellationToken { get; set; }
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.ViewModel.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
@@ -33,9 +34,9 @@ public class ScopedPage : Page
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">视图模型类型</typeparam>
|
||||
public void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, ISupportCancellation
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
@@ -59,11 +60,22 @@ public class ScopedPage : Page
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
base.OnNavigatingFrom(e);
|
||||
viewLoadingCancellationTokenSource.Cancel();
|
||||
using (viewLoadingCancellationTokenSource)
|
||||
{
|
||||
// Cancel tasks executed by the view model
|
||||
viewLoadingCancellationTokenSource.Cancel();
|
||||
IViewModel viewModel = (IViewModel)DataContext;
|
||||
|
||||
// Try dispose scope when page is not presented
|
||||
serviceScope.Dispose();
|
||||
viewLoadingCancellationTokenSource.Dispose();
|
||||
using (SemaphoreSlim locker = viewModel.DisposeLock)
|
||||
{
|
||||
// Wait to ensure viewmodel operation is completed
|
||||
locker.Wait();
|
||||
viewModel.IsViewDisposed = true;
|
||||
|
||||
// Dispose the scope
|
||||
serviceScope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Control;
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">源类型</typeparam>
|
||||
/// <typeparam name="TTo">目标类型</typeparam>
|
||||
public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
|
||||
public abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
@@ -23,7 +23,7 @@ public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverterBase<TFrom, TTo>>>()
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Snap.Hutao.Core.ExpressionService;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举帮助类
|
||||
/// </summary>
|
||||
public static class EnumExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断枚举是否有对应的Flag
|
||||
/// </summary>
|
||||
/// <typeparam name="T">枚举类型</typeparam>
|
||||
/// <param name="enum">待检查的枚举</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>是否有对应的Flag</returns>
|
||||
public static bool HasOption<T>(this T @enum, T value)
|
||||
where T : struct, Enum
|
||||
{
|
||||
return ExpressionCache<T>.Entry(@enum, value);
|
||||
}
|
||||
|
||||
private static class ExpressionCache<T>
|
||||
{
|
||||
public static readonly Func<T, T, bool> Entry = Get();
|
||||
|
||||
private static Func<T, T, bool> Get()
|
||||
{
|
||||
ParameterExpression paramSource = Expression.Parameter(typeof(T));
|
||||
ParameterExpression paramValue = Expression.Parameter(typeof(T));
|
||||
|
||||
BinaryExpression logicalAnd = Expression.AndAssign(paramSource, paramValue);
|
||||
BinaryExpression equal = Expression.Equal(logicalAnd, paramValue);
|
||||
|
||||
// 生成一个源类型入,目标类型出的 lamdba
|
||||
return Expression.Lambda<Func<T, T, bool>>(equal, paramSource, paramValue).Compile();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ namespace Snap.Hutao.Core.IO.Bits;
|
||||
[SuppressMessage("", "SA1600")]
|
||||
internal class BitsJob : DisposableObject, IBackgroundCopyCallback
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务名称前缀
|
||||
/// </summary>
|
||||
public const string JobNamePrefix = "SnapHutaoBitsJob";
|
||||
|
||||
private const uint BitsEngineNoProgressTimeout = 120;
|
||||
private const int MaxResumeAttempts = 10;
|
||||
|
||||
@@ -43,12 +48,14 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
|
||||
public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath)
|
||||
{
|
||||
ILogger<BitsJob> service = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
|
||||
string text = $"BitsDownloadJob - {uri}";
|
||||
string text = $"{JobNamePrefix} - {uri}";
|
||||
IBackgroundCopyJob ppJob;
|
||||
try
|
||||
{
|
||||
backgroundCopyManager.CreateJob(text, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out ppJob);
|
||||
ppJob.SetNotifyFlags(11u);
|
||||
|
||||
// BG_NOTIFY_JOB_TRANSFERRED & BG_NOTIFY_JOB_ERROR & BG_NOTIFY_JOB_MODIFICATION
|
||||
ppJob.SetNotifyFlags(0b1011);
|
||||
ppJob.SetNoProgressTimeout(BitsEngineNoProgressTimeout);
|
||||
ppJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND);
|
||||
ppJob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, null, null);
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Bits;
|
||||
@@ -40,6 +43,41 @@ internal class BitsManager
|
||||
return new(result, tempFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消所有先前创建的任务
|
||||
/// </summary>
|
||||
public void CancelAllJobs()
|
||||
{
|
||||
IBackgroundCopyManager value;
|
||||
try
|
||||
{
|
||||
value = lazyBackgroundCopyManager.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
value.EnumJobs(0, out IEnumBackgroundCopyJobs pJobs);
|
||||
pJobs.GetCount(out uint count);
|
||||
|
||||
List<IBackgroundCopyJob> jobsToCancel = new();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
uint actualFetched = 0;
|
||||
pJobs.Next(1, out IBackgroundCopyJob pJob, ref actualFetched);
|
||||
pJob.GetDisplayName(out PWSTR name);
|
||||
if (name.AsSpan().StartsWith(BitsJob.JobNamePrefix))
|
||||
{
|
||||
jobsToCancel.Add(pJob);
|
||||
}
|
||||
}
|
||||
|
||||
jobsToCancel.ForEach(job => job.Cancel());
|
||||
}
|
||||
|
||||
private bool DownloadCore(Uri uri, string tempFile, Action<ProgressUpdateStatus> progress, CancellationToken token)
|
||||
{
|
||||
IBackgroundCopyManager value;
|
||||
@@ -48,29 +86,37 @@ internal class BitsManager
|
||||
{
|
||||
value = lazyBackgroundCopyManager.Value;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
|
||||
try
|
||||
{
|
||||
try
|
||||
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
|
||||
{
|
||||
bitsJob.WaitForCompletion(progress, token);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "BITS download failed:");
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
bitsJob.WaitForCompletion(progress, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "BITS download failed:");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bitsJob.ErrorCode != 0)
|
||||
{
|
||||
return false;
|
||||
if (bitsJob.ErrorCode != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// BITS job creation failed
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ public static class DispatcherQueueExtension
|
||||
using (ManualResetEventSlim blockEvent = new())
|
||||
{
|
||||
dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
action();
|
||||
blockEvent.Set();
|
||||
});
|
||||
{
|
||||
action();
|
||||
blockEvent.Set();
|
||||
});
|
||||
|
||||
blockEvent.Wait();
|
||||
}
|
||||
|
||||
@@ -43,9 +43,6 @@ public readonly struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueue
|
||||
/// <inheritdoc/>
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
dispatherQueue.TryEnqueue(() =>
|
||||
{
|
||||
continuation();
|
||||
});
|
||||
dispatherQueue.TryEnqueue(() => continuation());
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,19 @@ public static class SemaphoreSlimExtensions
|
||||
/// 异步进入信号量
|
||||
/// </summary>
|
||||
/// <param name="semaphoreSlim">信号量</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>可释放的对象,用于释放信号量</returns>
|
||||
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim)
|
||||
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim, CancellationToken token = default)
|
||||
{
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
throw new OperationCanceledException("信号量已经被释放,操作取消", ex);
|
||||
}
|
||||
|
||||
return new SemaphoreSlimReleaser(semaphoreSlim);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,4 +31,14 @@ internal static class ThreadHelper
|
||||
{
|
||||
return new(Program.DispatcherQueue!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在主线程上同步等待执行操作
|
||||
/// </summary>
|
||||
/// <param name="action">操作</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeOnMainThread(Action action)
|
||||
{
|
||||
Program.DispatcherQueue!.Invoke(action);
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
logger.LogInformation(EventIds.WindowState, "Postion: [{pos}], Size: [{size}]", pos, size);
|
||||
|
||||
// appWindow.Show(true);
|
||||
// appWindow.Show can't bring window to top.
|
||||
window.Activate();
|
||||
|
||||
systemBackdrop = new(window);
|
||||
@@ -190,7 +191,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
}
|
||||
else
|
||||
{
|
||||
double scale = Persistence.GetScaleForWindow(handle);
|
||||
double scale = Persistence.GetScaleForWindowHandle(handle);
|
||||
|
||||
// 48 is the navigation button leftInset
|
||||
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), titleBar.ActualSize).Scale(scale);
|
||||
|
||||
@@ -8,6 +8,7 @@ using Snap.Hutao.Win32;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
@@ -22,28 +23,24 @@ internal static class Persistence
|
||||
/// </summary>
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
/// <param name="persistSize">持久化尺寸</param>
|
||||
/// <param name="size">初始尺寸</param>
|
||||
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
|
||||
/// <param name="initialSize">初始尺寸</param>
|
||||
public static unsafe void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 initialSize)
|
||||
{
|
||||
// Set first launch size.
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd);
|
||||
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
||||
|
||||
if (persistSize)
|
||||
{
|
||||
RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedSize.Width * persistedSize.Height > 848 * 524)
|
||||
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedRect.Size() >= initialSize.Size())
|
||||
{
|
||||
rect = persistedSize;
|
||||
rect = persistedRect;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
TransformToCenterScreen(&rect);
|
||||
}
|
||||
|
||||
TransformToCenterScreen(&rect);
|
||||
appWindow.MoveAndResize(rect);
|
||||
}
|
||||
|
||||
@@ -53,7 +50,15 @@ internal static class Persistence
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
public static void Save(AppWindow appWindow)
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
|
||||
GetWindowPlacement(hwnd, ref windowPlacement);
|
||||
|
||||
// prevent save value when we are maximized.
|
||||
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,7 +66,7 @@ internal static class Persistence
|
||||
/// </summary>
|
||||
/// <param name="hwnd">窗体句柄</param>
|
||||
/// <returns>缩放比</returns>
|
||||
public static double GetScaleForWindow(HWND hwnd)
|
||||
public static double GetScaleForWindowHandle(HWND hwnd)
|
||||
{
|
||||
uint dpi = GetDpiForWindow(hwnd);
|
||||
return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero);
|
||||
@@ -69,7 +74,7 @@ internal static class Persistence
|
||||
|
||||
private static SizeInt32 TransformSizeForWindow(SizeInt32 size, HWND hwnd)
|
||||
{
|
||||
double scale = GetScaleForWindow(hwnd);
|
||||
double scale = GetScaleForWindowHandle(hwnd);
|
||||
return new((int)(size.Width * scale), (int)(size.Height * scale));
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
|
||||
{
|
||||
case WM_GETMINMAXINFO:
|
||||
{
|
||||
double scalingFactor = Persistence.GetScaleForWindow(hwnd);
|
||||
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
|
||||
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class Avatar : ICalculableSource<ICalculableAvatar>
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public Weapon Weapon { get; set; } = default!;
|
||||
public Weapon? Weapon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物列表
|
||||
|
||||
@@ -174,6 +174,10 @@ public class User : ObservableObject
|
||||
Cookie ltokenCookie = Cookie.Parse($"ltuid={Entity.Aid};ltoken={ltokenResponse.Data.Ltoken}");
|
||||
Entity.Ltoken = ltokenCookie;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Response<ActionTicketWrapper> actionTicketResponse = await scope.ServiceProvider
|
||||
@@ -194,6 +198,14 @@ public class User : ObservableObject
|
||||
{
|
||||
UserGameRoles = userGameRolesResponse.Data.List;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 自动填充 CookieToken
|
||||
@@ -209,6 +221,10 @@ public class User : ObservableObject
|
||||
Cookie cookieTokenCookie = Cookie.Parse($"account_id={Entity.Aid};cookie_token={cookieTokenResponse.Data.CookieToken}");
|
||||
Entity.CookieToken = cookieTokenCookie;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Model.Entity.Configuration;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Model.Entity.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序数据库上下文
|
||||
/// </summary>
|
||||
[DebuggerDisplay("Id = {ContextId}")]
|
||||
public sealed class AppDbContext : DbContext
|
||||
{
|
||||
private readonly Guid contextId;
|
||||
private readonly ILogger<AppDbContext>? logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -31,9 +32,8 @@ public sealed class AppDbContext : DbContext
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options, ILogger<AppDbContext> logger)
|
||||
: this(options)
|
||||
{
|
||||
contextId = Guid.NewGuid();
|
||||
this.logger = logger;
|
||||
logger.LogInformation("AppDbContext[{id}] created.", contextId);
|
||||
logger.LogInformation("AppDbContext[{id}] created.", ContextId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,7 +135,7 @@ public sealed class AppDbContext : DbContext
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
logger?.LogInformation("AppDbContext[{id}] disposed.", contextId);
|
||||
logger?.LogInformation("AppDbContext[{id}] disposed.", ContextId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色头像转换器
|
||||
/// </summary>
|
||||
internal class AchievementIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class AchievementIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色卡片转换器
|
||||
/// </summary>
|
||||
internal class AvatarCardConverter : ValueConverterBase<string, Uri>
|
||||
internal class AvatarCardConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
private const string CostumeCard = "UI_AvatarIcon_Costume_Card.png";
|
||||
private static readonly Uri UIAvatarIconCostumeCard = new(Web.HutaoEndpoints.StaticFile("AvatarCard", CostumeCard));
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色头像转换器
|
||||
/// </summary>
|
||||
internal class AvatarIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class AvatarIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色名片转换器
|
||||
/// </summary>
|
||||
internal class AvatarNameCardPicConverter : ValueConverterBase<Avatar.Avatar?, Uri>
|
||||
internal class AvatarNameCardPicConverter : ValueConverter<Avatar.Avatar?, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 从角色转换到名片
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色侧面头像转换器
|
||||
/// </summary>
|
||||
internal class AvatarSideIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class AvatarSideIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 描述参数解析器
|
||||
/// </summary>
|
||||
internal sealed partial class DescParamDescriptor : ValueConverterBase<DescParam, IList<LevelParam<string, ParameterInfo>>>
|
||||
internal sealed partial class DescParamDescriptor : ValueConverter<DescParam, IList<LevelParam<string, ParameterInfo>>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取特定等级的解释
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 元素名称图标转换器
|
||||
/// </summary>
|
||||
internal class ElementNameIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class ElementNameIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 将中文元素名称转换为图标链接
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 表情图片转换器
|
||||
/// </summary>
|
||||
internal class EmotionIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class EmotionIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 武器图片转换器
|
||||
/// </summary>
|
||||
internal class EquipIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class EquipIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 立绘图标转换器
|
||||
/// </summary>
|
||||
internal class GachaAvatarIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class GachaAvatarIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 立绘转换器
|
||||
/// </summary>
|
||||
internal class GachaAvatarImgConverter : ValueConverterBase<string, Uri>
|
||||
internal class GachaAvatarImgConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 武器祈愿图片转换器
|
||||
/// </summary>
|
||||
internal class GachaEquipIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class GachaEquipIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 物品图片转换器
|
||||
/// </summary>
|
||||
internal class ItemIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class ItemIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 基础属性翻译器
|
||||
/// </summary>
|
||||
internal class PropertyInfoDescriptor : ValueConverterBase<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
|
||||
internal class PropertyInfoDescriptor : ValueConverter<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 格式化对
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 品质颜色转换器
|
||||
/// </summary>
|
||||
internal class QualityColorConverter : ValueConverterBase<ItemQuality, Color>
|
||||
internal class QualityColorConverter : ValueConverter<ItemQuality, Color>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Color Convert(ItemQuality from)
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 物品等级转换器
|
||||
/// </summary>
|
||||
internal class QualityConverter : ValueConverterBase<ItemQuality, Uri>
|
||||
internal class QualityConverter : ValueConverter<ItemQuality, Uri>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Uri Convert(ItemQuality from)
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 武器图片转换器
|
||||
/// </summary>
|
||||
internal class RelicIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class RelicIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 技能图标转换器
|
||||
/// </summary>
|
||||
internal class SkillIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class SkillIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 元素名称图标转换器
|
||||
/// </summary>
|
||||
internal class WeaponTypeIconConverter : ValueConverterBase<WeaponType, Uri>
|
||||
internal class WeaponTypeIconConverter : ValueConverter<WeaponType, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 将武器类型转换为图标链接
|
||||
|
||||
@@ -28,6 +28,7 @@ CoWaitForMultipleObjects
|
||||
// USER32
|
||||
FindWindowEx
|
||||
GetDpiForWindow
|
||||
GetWindowPlacement
|
||||
|
||||
// COM BITS
|
||||
BackgroundCopyManager
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.3.7.0" />
|
||||
Version="1.3.9.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -52,21 +53,23 @@ internal class AchievementService : IAchievementService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<EntityArchive> GetArchiveCollection()
|
||||
public async Task<ObservableCollection<EntityArchive>> GetArchiveCollectionAsync()
|
||||
{
|
||||
return archiveCollection ??= new(appDbContext.AchievementArchives.AsNoTracking().ToList());
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return archiveCollection ??= appDbContext.AchievementArchives.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RemoveArchiveAsync(EntityArchive archive)
|
||||
{
|
||||
// Sync cache
|
||||
// Keep this on main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
archiveCollection!.Remove(archive);
|
||||
|
||||
// Sync database
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
|
||||
// Cascade deleted the achievements.
|
||||
await appDbContext.AchievementArchives
|
||||
.Where(a => a.InnerId == archive.InnerId)
|
||||
.ExecuteDeleteAsync()
|
||||
|
||||
@@ -35,10 +35,10 @@ internal interface IAchievementService
|
||||
List<BindingAchievement> GetAchievements(EntityArchive archive, IList<MetadataAchievement> metadata);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于绑定的成就存档集合
|
||||
/// 异步获取用于绑定的成就存档集合
|
||||
/// </summary>
|
||||
/// <returns>成就存档集合</returns>
|
||||
ObservableCollection<EntityArchive> GetArchiveCollection();
|
||||
Task<ObservableCollection<EntityArchive>> GetArchiveCollectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步导入UIAF数据
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Binding.User;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.AvatarInfo.Composer;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar;
|
||||
using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
|
||||
using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
|
||||
using RecordCharacter = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar.Character;
|
||||
using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo;
|
||||
|
||||
/// <summary>
|
||||
/// 角色信息数据库操作
|
||||
/// </summary>
|
||||
public class AvatarInfoDbOperation
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色信息数据库操作
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
public AvatarInfoDbOperation(AppDbContext appDbContext)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新数据库角色信息
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="webInfos">Enka信息</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
public List<EnkaAvatarInfo> UpdateDbAvatarInfos(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
|
||||
|
||||
foreach (EnkaAvatarInfo webInfo in webInfos)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(webInfo.AvatarId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
|
||||
if (entity == null)
|
||||
{
|
||||
entity = ModelAvatarInfo.Create(uid, webInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = webInfo;
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 米游社我的角色方式 更新数据库角色信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
public async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
string uid = userAndUid.Uid.Value;
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
|
||||
|
||||
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
|
||||
Response<RecordPlayerInfo> playerInfoResponse = await gameRecordClient
|
||||
.GetPlayerInfoAsync(userAndUid, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (playerInfoResponse.IsOk())
|
||||
{
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse = await gameRecordClient
|
||||
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (charactersResponse.IsOk())
|
||||
{
|
||||
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
|
||||
|
||||
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
|
||||
|
||||
foreach (RecordCharacter character in characters)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(character.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id };
|
||||
avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false);
|
||||
entity = ModelAvatarInfo.Create(uid, avatarInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 米游社养成计算方式 更新数据库角色信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
public async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
string uid = userAndUid.Uid.Value;
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
|
||||
|
||||
CalculateClient calculateClient = Ioc.Default.GetRequiredService<CalculateClient>();
|
||||
List<CalculateAvatar> avatars = await calculateClient.GetAvatarsAsync(userAndUid, token).ConfigureAwait(false);
|
||||
|
||||
CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService<CalculateAvatarDetailAvatarInfoComposer>();
|
||||
|
||||
foreach (CalculateAvatar avatar in avatars)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(avatar.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
Response<AvatarDetail> detailAvatarResponse = await calculateClient.GetAvatarDetailAsync(userAndUid, avatar, token).ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (!detailAvatarResponse.IsOk())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
|
||||
AvatarDetail detailAvatar = detailAvatarResponse.Data;
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatar.Id };
|
||||
avatarInfo = await composer.ComposeAsync(avatarInfo, detailAvatar).ConfigureAwait(false);
|
||||
entity = ModelAvatarInfo.Create(uid, avatarInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = await composer.ComposeAsync(entity.Info, detailAvatar).ConfigureAwait(false);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据库角色信息
|
||||
/// </summary>
|
||||
/// <param name="uid">Uid</param>
|
||||
/// <returns>角色列表</returns>
|
||||
public List<EnkaAvatarInfo> GetDbAvatarInfos(string uid)
|
||||
{
|
||||
return appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.Select(i => i.Info)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void EnsureItemsAvatarIdDistinct(ref List<ModelAvatarInfo> dbInfos, string uid)
|
||||
{
|
||||
int distinctCount = dbInfos.Select(info => info.Info.AvatarId).ToHashSet().Count;
|
||||
|
||||
// Avatars are actually less than the list told us.
|
||||
if (distinctCount < dbInfos.Count)
|
||||
{
|
||||
appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ExecuteDelete();
|
||||
|
||||
dbInfos = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
using Snap.Hutao.Model.Binding.User;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.AvatarInfo.Composer;
|
||||
using Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Enka;
|
||||
using Snap.Hutao.Web.Enka.Model;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar;
|
||||
using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
|
||||
using EnkaPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo;
|
||||
using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
|
||||
using RecordCharacter = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar.Character;
|
||||
using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo;
|
||||
|
||||
@@ -37,6 +27,8 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly ILogger<AvatarInfoService> logger;
|
||||
|
||||
private readonly AvatarInfoDbOperation avatarInfoDbOperation;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色信息服务
|
||||
/// </summary>
|
||||
@@ -54,6 +46,8 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
this.metadataService = metadataService;
|
||||
this.summaryFactory = summaryFactory;
|
||||
this.logger = logger;
|
||||
|
||||
avatarInfoDbOperation = new(appDbContext);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -74,23 +68,20 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
return new(RefreshResult.APIUnavailable, null);
|
||||
}
|
||||
|
||||
if (resp.IsValid)
|
||||
{
|
||||
IList<EnkaAvatarInfo> list = UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token);
|
||||
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
else
|
||||
if (!resp.IsValid)
|
||||
{
|
||||
return new(RefreshResult.ShowcaseNotOpen, null);
|
||||
}
|
||||
|
||||
List<EnkaAvatarInfo> list = avatarInfoDbOperation.UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token);
|
||||
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
|
||||
case RefreshOption.RequestFromHoyolabGameRecord:
|
||||
{
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
|
||||
IList<EnkaAvatarInfo> list = await UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
|
||||
List<EnkaAvatarInfo> list = await avatarInfoDbOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
|
||||
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
@@ -98,7 +89,7 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
case RefreshOption.RequestFromHoyolabCalculate:
|
||||
{
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
|
||||
IList<EnkaAvatarInfo> list = await UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
|
||||
List<EnkaAvatarInfo> list = await avatarInfoDbOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
|
||||
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
@@ -106,7 +97,8 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
default:
|
||||
{
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
|
||||
Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(userAndUid.Uid.Value), token).ConfigureAwait(false);
|
||||
List<EnkaAvatarInfo> list = avatarInfoDbOperation.GetDbAvatarInfos(userAndUid.Uid.Value);
|
||||
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new(RefreshResult.Ok, summary.Avatars.Count == 0 ? null : summary);
|
||||
}
|
||||
@@ -134,153 +126,4 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
private List<EnkaAvatarInfo> UpdateDbAvatarInfos(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
foreach (EnkaAvatarInfo webInfo in webInfos)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(webInfo.AvatarId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// TODO: ensure the operation executes atomically
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
|
||||
if (entity == null)
|
||||
{
|
||||
entity = ModelAvatarInfo.Create(uid, webInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = webInfo;
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
private async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
|
||||
{
|
||||
string uid = userAndUid.Uid.Value;
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
|
||||
Response<RecordPlayerInfo> playerInfoResponse = await gameRecordClient
|
||||
.GetPlayerInfoAsync(userAndUid)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// TODO: We should not refresh if response is not correct here.
|
||||
if (playerInfoResponse.IsOk())
|
||||
{
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse = await gameRecordClient
|
||||
.GetCharactersAsync(userAndUid, playerInfoResponse.Data)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (charactersResponse.IsOk())
|
||||
{
|
||||
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
|
||||
|
||||
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
|
||||
|
||||
foreach (RecordCharacter character in characters)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(character.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id };
|
||||
avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false);
|
||||
entity = ModelAvatarInfo.Create(uid, avatarInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
private async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
|
||||
{
|
||||
string uid = userAndUid.Uid.Value;
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
CalculateClient calculateClient = Ioc.Default.GetRequiredService<CalculateClient>();
|
||||
List<CalculateAvatar> avatars = await calculateClient.GetAvatarsAsync(userAndUid).ConfigureAwait(false);
|
||||
|
||||
CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService<CalculateAvatarDetailAvatarInfoComposer>();
|
||||
|
||||
foreach (CalculateAvatar avatar in avatars)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(avatar.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Response<AvatarDetail> detailAvatarResponse = await calculateClient.GetAvatarDetailAsync(userAndUid, avatar).ConfigureAwait(false);
|
||||
|
||||
if (!detailAvatarResponse.IsOk())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
|
||||
AvatarDetail detailAvatar = detailAvatarResponse.Data;
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatar.Id };
|
||||
avatarInfo = await composer.ComposeAsync(avatarInfo, detailAvatar).ConfigureAwait(false);
|
||||
entity = ModelAvatarInfo.Create(uid, avatarInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = await composer.ComposeAsync(entity.Info, detailAvatar).ConfigureAwait(false);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
private List<EnkaAvatarInfo> GetDbAvatarInfos(string uid)
|
||||
{
|
||||
try
|
||||
{
|
||||
return appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.Select(i => i.Info)
|
||||
.ToList();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// appDbContext can be disposed unexpectedly
|
||||
return new();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ internal class SummaryAvatarFactory
|
||||
/// <returns>角色</returns>
|
||||
public PropertyAvatar CreateAvatar()
|
||||
{
|
||||
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList);
|
||||
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
|
||||
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
|
||||
|
||||
return new()
|
||||
@@ -65,11 +65,12 @@ internal class SummaryAvatarFactory
|
||||
};
|
||||
}
|
||||
|
||||
private ReliquaryAndWeapon ProcessEquip(IList<Equip> equipments)
|
||||
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
|
||||
{
|
||||
List<PropertyReliquary> reliquaryList = new();
|
||||
PropertyWeapon? weapon = null;
|
||||
|
||||
// equipments can be null
|
||||
foreach (Equip equip in equipments)
|
||||
{
|
||||
switch (equip.Flat.ItemType)
|
||||
@@ -134,9 +135,9 @@ internal class SummaryAvatarFactory
|
||||
private struct ReliquaryAndWeapon
|
||||
{
|
||||
public List<PropertyReliquary> Reliquaries;
|
||||
public PropertyWeapon Weapon;
|
||||
public PropertyWeapon? Weapon;
|
||||
|
||||
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon weapon)
|
||||
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon? weapon)
|
||||
{
|
||||
Reliquaries = reliquaries;
|
||||
Weapon = weapon;
|
||||
|
||||
@@ -45,7 +45,10 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
|
||||
/// <inheritdoc/>
|
||||
public void Receive(UserRemovedMessage message)
|
||||
{
|
||||
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
|
||||
ThreadHelper.InvokeOnMainThread(() =>
|
||||
{
|
||||
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
@@ -28,7 +27,7 @@ namespace Snap.Hutao.Service.GachaLog;
|
||||
/// 祈愿记录服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped, typeof(IGachaLogService))]
|
||||
internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
internal class GachaLogService : IGachaLogService
|
||||
{
|
||||
/// <summary>
|
||||
/// 祈愿记录查询的类型
|
||||
@@ -95,9 +94,6 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
set => dbCurrent.Current = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInitialized { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<UIGF> ExportToUIGFAsync(GachaArchive archive)
|
||||
{
|
||||
@@ -117,30 +113,29 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GachaArchive> GetArchiveCollection()
|
||||
public async Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync()
|
||||
{
|
||||
return archiveCollection ??= new(appDbContext.GachaArchives.AsNoTracking().ToList());
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeAsync()
|
||||
public async ValueTask<bool> InitializeAsync(CancellationToken token)
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false);
|
||||
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false);
|
||||
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
IsInitialized = true;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsInitialized = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsInitialized;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -321,7 +316,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
|
||||
archive = appDbContext.GachaArchives.Single(a => a.Uid == uid);
|
||||
GachaArchive temp = archive;
|
||||
Program.DispatcherQueue!.Invoke(() => archiveCollection!.Add(temp));
|
||||
ThreadHelper.InvokeOnMainThread(() => archiveCollection!.Add(temp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ internal interface IGachaLogService
|
||||
Task<UIGF> ExportToUIGFAsync(GachaArchive archive);
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用于绑定的存档集合
|
||||
/// 异步获取可用于绑定的存档集合
|
||||
/// </summary>
|
||||
/// <returns>存档集合</returns>
|
||||
ObservableCollection<GachaArchive> GetArchiveCollection();
|
||||
Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取祈愿日志Url提供器
|
||||
@@ -58,7 +58,7 @@ internal interface IGachaLogService
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否初始化成功</returns>
|
||||
ValueTask<bool> InitializeAsync();
|
||||
ValueTask<bool> InitializeAsync(CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新祈愿记录
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.IO.Ini;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
@@ -213,14 +214,15 @@ internal class GameService : IGameService, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GameAccount> GetGameAccountCollection()
|
||||
public async Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync()
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
if (gameAccounts == null)
|
||||
{
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
gameAccounts = new(appDbContext.GameAccounts.AsNoTracking().ToList());
|
||||
gameAccounts = appDbContext.GameAccounts.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ internal interface IGameService
|
||||
ValueTask DetectGameAccountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取游戏内账号集合
|
||||
/// 异步获取游戏内账号集合
|
||||
/// </summary>
|
||||
/// <returns>游戏内账号集合</returns>
|
||||
ObservableCollection<GameAccount> GetGameAccountCollection();
|
||||
Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取游戏路径
|
||||
|
||||
@@ -23,9 +23,9 @@ internal class HutaoCache : IHutaoCache
|
||||
|
||||
private Dictionary<AvatarId, Avatar>? idAvatarExtendedMap;
|
||||
|
||||
private bool isDatabaseViewModelInitialized;
|
||||
private bool isWikiAvatarViewModelInitiaized;
|
||||
private bool isWikiWeaponViewModelInitiaized;
|
||||
private TaskCompletionSource<bool>? databaseViewModelTaskSource;
|
||||
private TaskCompletionSource<bool>? wikiAvatarViewModelTaskSource;
|
||||
private TaskCompletionSource<bool>? wikiWeaponViewModelTaskSource;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃 API 缓存
|
||||
@@ -62,11 +62,12 @@ internal class HutaoCache : IHutaoCache
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
|
||||
{
|
||||
if (isDatabaseViewModelInitialized)
|
||||
if (databaseViewModelTaskSource != null)
|
||||
{
|
||||
return true;
|
||||
return await databaseViewModelTaskSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
databaseViewModelTaskSource = new();
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||
@@ -81,20 +82,23 @@ internal class HutaoCache : IHutaoCache
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
return isDatabaseViewModelInitialized = true;
|
||||
databaseViewModelTaskSource.TrySetResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
databaseViewModelTaskSource.TrySetResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync()
|
||||
{
|
||||
if (isWikiAvatarViewModelInitiaized)
|
||||
if (wikiAvatarViewModelTaskSource != null)
|
||||
{
|
||||
return true;
|
||||
return await wikiAvatarViewModelTaskSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
wikiAvatarViewModelTaskSource = new();
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||
@@ -116,21 +120,23 @@ internal class HutaoCache : IHutaoCache
|
||||
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
|
||||
}).ToList();
|
||||
|
||||
isWikiAvatarViewModelInitiaized = true;
|
||||
wikiAvatarViewModelTaskSource.TrySetResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
wikiAvatarViewModelTaskSource.TrySetResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForWikiWeaponViewModelAsync()
|
||||
{
|
||||
if (isWikiWeaponViewModelInitiaized)
|
||||
if (wikiWeaponViewModelTaskSource != null)
|
||||
{
|
||||
return true;
|
||||
return await wikiWeaponViewModelTaskSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
wikiWeaponViewModelTaskSource = new();
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||
@@ -148,10 +154,11 @@ internal class HutaoCache : IHutaoCache
|
||||
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
|
||||
}).ToList();
|
||||
|
||||
isWikiWeaponViewModelInitiaized = true;
|
||||
wikiWeaponViewModelTaskSource.TrySetResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
wikiWeaponViewModelTaskSource.TrySetResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,12 +104,22 @@ internal class HutaoService : IHutaoService
|
||||
Response<T> webResponse = await taskFunc(default).ConfigureAwait(false);
|
||||
T web = webResponse.IsOk() ? webResponse.Data : new();
|
||||
|
||||
appDbContext.ObjectCache.AddAndSave(new()
|
||||
try
|
||||
{
|
||||
Key = key,
|
||||
ExpireTime = DateTimeOffset.Now.AddHours(4),
|
||||
Value = JsonSerializer.Serialize(web, options),
|
||||
});
|
||||
appDbContext.ObjectCache.AddAndSave(new()
|
||||
{
|
||||
Key = key,
|
||||
|
||||
// we hold the cache for 4 hours, then just expire it.
|
||||
ExpireTime = DateTimeOffset.Now.AddHours(4),
|
||||
Value = JsonSerializer.Serialize(web, options),
|
||||
});
|
||||
}
|
||||
catch (Microsoft.EntityFrameworkCore.DbUpdateException)
|
||||
{
|
||||
// DbUpdateException: An error occurred while saving the entity changes.
|
||||
// TODO: Not ignore it.
|
||||
}
|
||||
|
||||
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ internal class UserService : IUserService
|
||||
{
|
||||
List<Model.Binding.User.UserAndUid> userAndUids = new();
|
||||
ObservableCollection<BindingUser> observableUsers = await GetUserCollectionAsync().ConfigureAwait(false);
|
||||
foreach (BindingUser user in observableUsers.ToList())
|
||||
foreach (BindingUser user in observableUsers)
|
||||
{
|
||||
foreach (UserGameRole role in user.UserGameRoles)
|
||||
{
|
||||
@@ -146,7 +146,7 @@ internal class UserService : IUserService
|
||||
}
|
||||
}
|
||||
|
||||
roleCollection = new(userAndUids);
|
||||
roleCollection = userAndUids.ToObservableCollection();
|
||||
}
|
||||
|
||||
return roleCollection;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<CommandBar Background="{StaticResource CardBackgroundFillColorDefaultBrush}" DefaultLabelPosition="Right">
|
||||
<CommandBar Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" DefaultLabelPosition="Right">
|
||||
<AppBarButton
|
||||
Command="{Binding RefreshCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
@@ -52,37 +52,27 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="添加角色以定时刷新"/>
|
||||
<ScrollViewer MaxHeight="320" Padding="16,0">
|
||||
<ItemsControl ItemsSource="{Binding UserAndRoles}">
|
||||
<ItemsControl ItemsSource="{Binding UserAndUids}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Padding="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Role.Nickname}"/>
|
||||
<TextBlock
|
||||
Margin="0,2,0,0"
|
||||
Opacity="0.6"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding Role.Description}"/>
|
||||
</StackPanel>
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding Uid}"/>
|
||||
<Button
|
||||
Margin="16,0,0,0"
|
||||
Padding="12"
|
||||
Padding="6"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
BorderBrush="{x:Null}"
|
||||
BorderThickness="0"
|
||||
Command="{Binding DataContext.TrackRoleCommand, Source={StaticResource ViewModelBindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
Style="{StaticResource ButtonRevealStyle}"
|
||||
ToolTipService.ToolTip="添加"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</AppBarButton.Flyout>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.ViewModel.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 视图模型接口
|
||||
/// </summary>
|
||||
public interface IViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于通知页面卸载的取消令牌
|
||||
/// </summary>
|
||||
CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 释放操作锁
|
||||
/// </summary>
|
||||
SemaphoreSlim DisposeLock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对应的视图是否已经释放
|
||||
/// </summary>
|
||||
bool IsViewDisposed { get; set; }
|
||||
}
|
||||
40
src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModel.cs
Normal file
40
src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModel.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Snap.Hutao.ViewModel.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 视图模型抽象类
|
||||
/// </summary>
|
||||
public abstract class ViewModel : ObservableObject, IViewModel
|
||||
{
|
||||
private bool isInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// 是否初始化完成
|
||||
/// </summary>
|
||||
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SemaphoreSlim DisposeLock { get; set; } = new(1);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsViewDisposed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当页面被释放后抛出异常
|
||||
/// </summary>
|
||||
/// <exception cref="OperationCanceledException">操作被用户取消</exception>
|
||||
protected void ThrowIfViewDisposed()
|
||||
{
|
||||
if (IsViewDisposed)
|
||||
{
|
||||
throw new OperationCanceledException("页面资源已经被释放,操作取消");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.DataTransfer;
|
||||
@@ -30,10 +28,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal class AchievementViewModel
|
||||
: ObservableObject,
|
||||
ISupportCancellation,
|
||||
INavigationRecipient
|
||||
internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipient
|
||||
{
|
||||
private static readonly SortDescription IncompletedItemsFirstSortDescription = new(nameof(Model.Binding.Achievement.Achievement.IsChecked), SortDirection.Ascending);
|
||||
private static readonly SortDescription CompletionTimeSortDescription = new(nameof(Model.Binding.Achievement.Achievement.Time), SortDirection.Descending);
|
||||
@@ -53,7 +48,6 @@ internal class AchievementViewModel
|
||||
private Model.Entity.AchievementArchive? selectedArchive;
|
||||
private bool isIncompletedItemsFirst = true;
|
||||
private string searchText = string.Empty;
|
||||
private bool isInitialized;
|
||||
private string? finishDescription;
|
||||
|
||||
/// <summary>
|
||||
@@ -92,14 +86,6 @@ internal class AchievementViewModel
|
||||
SaveAchievementCommand = new RelayCommand<Model.Binding.Achievement.Achievement>(SaveAchievement);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否初始化完成
|
||||
/// </summary>
|
||||
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 成就存档集合
|
||||
/// </summary>
|
||||
@@ -251,23 +237,38 @@ internal class AchievementViewModel
|
||||
{
|
||||
try
|
||||
{
|
||||
List<Model.Metadata.Achievement.AchievementGoal> goals = await metadataService.GetAchievementGoalsAsync(CancellationToken).ConfigureAwait(false);
|
||||
List<Model.Binding.Achievement.AchievementGoal> sortedGoals;
|
||||
ObservableCollection<Model.Entity.AchievementArchive> archives;
|
||||
|
||||
ThrowIfViewDisposed();
|
||||
using (await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
|
||||
List<Model.Metadata.Achievement.AchievementGoal> goals = await metadataService.GetAchievementGoalsAsync(CancellationToken).ConfigureAwait(false);
|
||||
sortedGoals = goals
|
||||
.OrderBy(goal => goal.Order)
|
||||
.Select(goal => new Model.Binding.Achievement.AchievementGoal(goal))
|
||||
.ToList();
|
||||
archives = await achievementService.GetArchiveCollectionAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
AchievementGoals = goals.OrderBy(goal => goal.Order).Select(goal => new Model.Binding.Achievement.AchievementGoal(goal)).ToList();
|
||||
|
||||
Archives = achievementService.GetArchiveCollection();
|
||||
AchievementGoals = sortedGoals;
|
||||
Archives = archives;
|
||||
SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Indicate initialization not succeed
|
||||
// User canceled the loading operation,
|
||||
// Indicate initialization not succeed.
|
||||
openUICompletionSource.TrySetResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
openUICompletionSource.TrySetResult(metaInitialized);
|
||||
IsInitialized = metaInitialized;
|
||||
}
|
||||
|
||||
#region 存档操作
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
@@ -17,7 +15,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 公告视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
|
||||
internal class AnnouncementViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly IAnnouncementService announcementService;
|
||||
|
||||
@@ -39,9 +37,6 @@ internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
|
||||
OpenAnnouncementUICommand = new RelayCommand<string>(OpenAnnouncementUI);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公告
|
||||
/// </summary>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Control.Media;
|
||||
using Snap.Hutao.Core.IO.DataTransfer;
|
||||
using Snap.Hutao.Extension;
|
||||
@@ -19,6 +19,7 @@ using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.UI;
|
||||
@@ -35,11 +36,12 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// TODO: support page unload as cancellation
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
internal class AvatarPropertyViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly IUserService userService;
|
||||
private readonly IAvatarInfoService avatarInfoService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private Summary? summary;
|
||||
private Avatar? selectedAvatar;
|
||||
|
||||
@@ -48,17 +50,20 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
/// </summary>
|
||||
/// <param name="userService">用户服务</param>
|
||||
/// <param name="avatarInfoService">角色信息服务</param>
|
||||
/// <param name="contentDialogFactory">对话框工厂</param>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
/// <param name="infoBarService">信息条服务</param>
|
||||
public AvatarPropertyViewModel(
|
||||
IUserService userService,
|
||||
IAvatarInfoService avatarInfoService,
|
||||
IContentDialogFactory contentDialogFactory,
|
||||
IAsyncRelayCommandFactory asyncRelayCommandFactory,
|
||||
IInfoBarService infoBarService)
|
||||
{
|
||||
this.userService = userService;
|
||||
this.avatarInfoService = avatarInfoService;
|
||||
this.infoBarService = infoBarService;
|
||||
this.contentDialogFactory = contentDialogFactory;
|
||||
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
RefreshFromEnkaApiCommand = asyncRelayCommandFactory.Create(RefreshByEnkaApiAsync);
|
||||
@@ -68,9 +73,6 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
CultivateCommand = asyncRelayCommandFactory.Create<Avatar>(CultivateAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 简述对象
|
||||
/// </summary>
|
||||
@@ -171,8 +173,21 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
try
|
||||
{
|
||||
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(userAndUid, option, token).ConfigureAwait(false);
|
||||
ValueResult<RefreshResult, Summary?> summaryResult;
|
||||
ThrowIfViewDisposed();
|
||||
using (await DisposeLock.EnterAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
ContentDialog dialog = await contentDialogFactory.CreateForIndeterminateProgressAsync("获取数据中").ConfigureAwait(false);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await using (await dialog.BlockAsync().ConfigureAwait(false))
|
||||
{
|
||||
summaryResult = await avatarInfoService.GetSummaryAsync(userAndUid, option, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
(RefreshResult result, Summary? summary) = summaryResult;
|
||||
if (result == RefreshResult.Ok)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
@@ -206,6 +221,12 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
if (userService.Current != null)
|
||||
{
|
||||
if (avatar.Weapon == null)
|
||||
{
|
||||
infoBarService.Warning("当前角色无法计算,请同步信息后再试");
|
||||
return;
|
||||
}
|
||||
|
||||
// ContentDialog must be created by main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
(bool isOk, CalcAvatarPromotionDelta delta) = await new CultivatePromotionDeltaDialog(avatar.ToCalculable(), avatar.Weapon.ToCalculable())
|
||||
@@ -268,14 +289,31 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
Bgra8 tint = Bgra8.FromColor(tintColor);
|
||||
softwareBitmap.NormalBlend(tint);
|
||||
|
||||
bool clipboardOpened = false;
|
||||
using (InMemoryRandomAccessStream memory = new())
|
||||
{
|
||||
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, memory);
|
||||
encoder.SetSoftwareBitmap(softwareBitmap);
|
||||
await encoder.FlushAsync();
|
||||
Clipboard.SetBitmapStream(memory);
|
||||
|
||||
try
|
||||
{
|
||||
Clipboard.SetBitmapStream(memory);
|
||||
clipboardOpened = true;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// CLIPBRD_E_CANT_OPEN
|
||||
}
|
||||
}
|
||||
|
||||
infoBarService.Success("已导出到剪贴板");
|
||||
if (clipboardOpened)
|
||||
{
|
||||
infoBarService.Success("已导出到剪贴板");
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning("打开剪贴板失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Binding.Cultivation;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
@@ -20,14 +18,13 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 养成视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class CultivationViewModel : ObservableObject, ISupportCancellation
|
||||
internal class CultivationViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly ICultivationService cultivationService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly ILogger<CultivationViewModel> logger;
|
||||
|
||||
private bool isInitialized;
|
||||
private ObservableCollection<CultivateProject>? projects;
|
||||
private CultivateProject? selectedProject;
|
||||
private List<Model.Binding.Inventory.InventoryItem>? inventoryItems;
|
||||
@@ -63,14 +60,6 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation
|
||||
NavigateToPageCommand = new RelayCommand<string>(NavigateToPage);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否初始化完成
|
||||
/// </summary>
|
||||
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 项目
|
||||
/// </summary>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
@@ -23,7 +21,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 实时便笺视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
|
||||
internal class DailyNoteViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly IUserService userService;
|
||||
private readonly IDailyNoteService dailyNoteService;
|
||||
@@ -73,9 +71,6 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
|
||||
DailyNoteVerificationCommand = asyncRelayCommandFactory.Create(VerifyDailyNoteVerificationAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 刷新时间
|
||||
/// </summary>
|
||||
@@ -136,7 +131,7 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
|
||||
/// <summary>
|
||||
/// 用户与角色集合
|
||||
/// </summary>
|
||||
public ObservableCollection<UserAndUid>? UserAndRoles { get => userAndUids; set => userAndUids = value; }
|
||||
public ObservableCollection<UserAndUid>? UserAndUids { get => userAndUids; set => userAndUids = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺集合
|
||||
@@ -175,7 +170,7 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
UserAndRoles = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
|
||||
UserAndUids = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
|
||||
|
||||
refreshSecondsEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteRefreshSeconds, "480");
|
||||
selectedRefreshTime = refreshTimes.Single(t => t.Value == refreshSecondsEntry.GetInt32());
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
@@ -22,7 +20,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 祈愿记录视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
internal class GachaLogViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly IGachaLogService gachaLogService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
@@ -35,7 +33,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
private GachaStatistics? statistics;
|
||||
private bool isAggressiveRefresh;
|
||||
private HistoryWish? selectedHistoryWish;
|
||||
private bool isInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的祈愿记录视图模型
|
||||
@@ -69,9 +66,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存档集合
|
||||
/// </summary>
|
||||
@@ -112,11 +106,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
/// </summary>
|
||||
public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化是否完成
|
||||
/// </summary>
|
||||
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 页面加载命令
|
||||
/// </summary>
|
||||
@@ -154,13 +143,27 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
if (await gachaLogService.InitializeAsync().ConfigureAwait(true))
|
||||
try
|
||||
{
|
||||
Archives = gachaLogService.GetArchiveCollection();
|
||||
SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
|
||||
if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(true))
|
||||
{
|
||||
ObservableCollection<GachaArchive> archives;
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
IsInitialized = true;
|
||||
ThrowIfViewDisposed();
|
||||
using (await DisposeLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
archives = await gachaLogService.GetArchiveCollectionAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Archives = archives;
|
||||
SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
|
||||
IsInitialized = true;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Binding.Hutao;
|
||||
using Snap.Hutao.Service.Hutao;
|
||||
@@ -14,7 +12,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 胡桃数据库视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
|
||||
internal class HutaoDatabaseViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly IHutaoCache hutaoCache;
|
||||
|
||||
@@ -28,7 +26,6 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
|
||||
/// 构造一个新的胡桃数据库视图模型
|
||||
/// </summary>
|
||||
/// <param name="hutaoCache">胡桃服务缓存</param>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
public HutaoDatabaseViewModel(IHutaoCache hutaoCache, IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
||||
{
|
||||
@@ -37,9 +34,6 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色使用率
|
||||
/// </summary>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
@@ -26,7 +24,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 启动游戏视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
internal class LaunchGameViewModel : Abstraction.ViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动游戏目标 Uid
|
||||
@@ -44,8 +42,7 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
new LaunchScheme(name: "官方服 | 天空岛", channel: "1", subChannel: "1", launcherId: "18"),
|
||||
new LaunchScheme(name: "渠道服 | 世界树", channel: "14", subChannel: "0", launcherId: "17"),
|
||||
|
||||
// new LaunchScheme(name: "国际服 | 暂不支持", channel: "1", subChannel: "0"),
|
||||
new LaunchScheme(name: "国际服 | 部分支持", channel: "1", subChannel: "0", launcherId: "unknown"),
|
||||
};
|
||||
|
||||
private LaunchScheme? selectedScheme;
|
||||
@@ -84,9 +81,6 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
AttachGameAccountCommand = new RelayCommand<GameAccount>(AttachGameAccountToCurrentUserGameRole);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已知的服务器方案
|
||||
/// </summary>
|
||||
@@ -217,22 +211,32 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
bool gameExists = File.Exists(gameService.GetGamePathSkipLocator());
|
||||
|
||||
if (gameExists)
|
||||
if (File.Exists(gameService.GetGamePathSkipLocator()))
|
||||
{
|
||||
MultiChannel multi = gameService.GetMultiChannel();
|
||||
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
|
||||
GameAccounts = gameService.GetGameAccountCollection();
|
||||
|
||||
// Sync uid
|
||||
if (memoryCache.TryGetValue(DesiredUid, out object? value) && value is string uid)
|
||||
try
|
||||
{
|
||||
SelectedGameAccount = GameAccounts.SingleOrDefault(g => g.AttachUid == uid);
|
||||
}
|
||||
ThrowIfViewDisposed();
|
||||
using (await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(true))
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
|
||||
// Sync from Settings
|
||||
RetiveSetting();
|
||||
MultiChannel multi = gameService.GetMultiChannel();
|
||||
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
|
||||
GameAccounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(true);
|
||||
|
||||
// Sync uid
|
||||
if (memoryCache.TryGetValue(DesiredUid, out object? value) && value is string uid)
|
||||
{
|
||||
SelectedGameAccount = GameAccounts.SingleOrDefault(g => g.AttachUid == uid);
|
||||
}
|
||||
|
||||
// Sync from Settings
|
||||
RetiveSetting();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Snap.Hutao.Core.Database;
|
||||
@@ -23,7 +22,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 设置视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class SettingViewModel : ObservableObject
|
||||
internal class SettingViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly IGameService gameService;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Message;
|
||||
using Snap.Hutao.Model.Binding.SpiralAbyss;
|
||||
@@ -25,7 +23,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 深渊记录视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellation, IRecipient<UserChangedMessage>
|
||||
internal class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage>
|
||||
{
|
||||
private readonly ISpiralAbyssRecordService spiralAbyssRecordService;
|
||||
private readonly IMetadataService metadataService;
|
||||
@@ -62,9 +60,6 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
|
||||
messenger.Register(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 深渊记录
|
||||
/// </summary>
|
||||
@@ -126,33 +121,61 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
|
||||
{
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
idAvatarMap = AvatarIds.ExtendAvatars(idAvatarMap);
|
||||
if (userService.Current?.SelectedUserGameRole != null)
|
||||
|
||||
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
|
||||
{
|
||||
await UpdateSpiralAbyssCollectionAsync(UserAndUid.FromUser(userService.Current)).ConfigureAwait(false);
|
||||
await UpdateSpiralAbyssCollectionAsync(userAndUid).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("请先选中角色与账号");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateSpiralAbyssCollectionAsync(UserAndUid userAndUid)
|
||||
{
|
||||
ObservableCollection<SpiralAbyssEntry> temp = await spiralAbyssRecordService
|
||||
.GetSpiralAbyssCollectionAsync(userAndUid)
|
||||
.ConfigureAwait(false);
|
||||
ObservableCollection<SpiralAbyssEntry>? temp = null;
|
||||
try
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
using (await DisposeLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
temp = await spiralAbyssRecordService
|
||||
.GetSpiralAbyssCollectionAsync(userAndUid)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SpiralAbyssEntries = temp;
|
||||
SelectedEntry = SpiralAbyssEntries.FirstOrDefault();
|
||||
SelectedEntry = SpiralAbyssEntries?.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (userService.Current?.SelectedUserGameRole != null)
|
||||
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
|
||||
{
|
||||
await spiralAbyssRecordService
|
||||
.RefreshSpiralAbyssAsync(UserAndUid.FromUser(userService.Current))
|
||||
.ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
using (await DisposeLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
await spiralAbyssRecordService
|
||||
.RefreshSpiralAbyssAsync(userAndUid)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedEntry = SpiralAbyssEntries?.FirstOrDefault();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Bits;
|
||||
using Snap.Hutao.Extension;
|
||||
@@ -19,7 +17,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 测试视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class TestViewModel : ObservableObject, ISupportCancellation
|
||||
internal class TestViewModel : Abstraction.ViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的测试视图模型
|
||||
@@ -33,9 +31,6 @@ internal class TestViewModel : ObservableObject, ISupportCancellation
|
||||
DownloadStaticFileCommand = asyncRelayCommandFactory.Create(DownloadStaticFileAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 打开游戏社区记录对话框命令
|
||||
/// </summary>
|
||||
|
||||
@@ -156,7 +156,14 @@ internal class UserViewModel : ObservableObject
|
||||
|
||||
private void LoginMihoyoUser()
|
||||
{
|
||||
Ioc.Default.GetRequiredService<INavigationService>().Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
|
||||
if (Core.WebView2Helper.IsSupported)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<INavigationService>().Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning("尚未安装 WebView2 Runtime");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveUserAsync(User? user)
|
||||
|
||||
@@ -77,6 +77,8 @@ internal class WelcomeViewModel : ObservableObject
|
||||
|
||||
DownloadSummaries = new(downloadSummaries);
|
||||
|
||||
// Cancel all previous created jobs
|
||||
serviceProvider.GetRequiredService<BitsManager>().CancelAllJobs();
|
||||
await Task.WhenAll(downloadSummaries.Select(d => d.DownloadAndExtractAsync())).ConfigureAwait(true);
|
||||
|
||||
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
|
||||
@@ -173,7 +175,7 @@ internal class WelcomeViewModel : ObservableObject
|
||||
private void UpdateProgressStatus(ProgressUpdateStatus status)
|
||||
{
|
||||
Description = $"{Converters.ToFileSizeString(status.BytesRead)}/{Converters.ToFileSizeString(status.TotalBytes)}";
|
||||
ProgressValue = (double)status.BytesRead / status.TotalBytes;
|
||||
ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesRead / status.TotalBytes;
|
||||
}
|
||||
|
||||
private void ExtractFiles(string file)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
@@ -33,7 +32,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 角色资料视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class WikiAvatarViewModel : ObservableObject
|
||||
internal class WikiAvatarViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly IHutaoCache hutaoCache;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Binding.Cultivation;
|
||||
@@ -31,7 +29,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 武器资料视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
|
||||
internal class WikiWeaponViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly List<WeaponId> skippedWeapons = new()
|
||||
{
|
||||
@@ -62,9 +60,6 @@ internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
|
||||
FilterCommand = new RelayCommand<string>(ApplyFilter);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色列表
|
||||
/// </summary>
|
||||
|
||||
@@ -352,25 +352,7 @@ public class MiHoYoJSInterface
|
||||
JsParam param = JsonSerializer.Deserialize<JsParam>(message)!;
|
||||
|
||||
logger.LogInformation("[OnMessage]\nMethod : {method}\nPayload : {payload}\nCallback: {callback}", param.Method, param.Payload, param.Callback);
|
||||
IJsResult? result = param.Method switch
|
||||
{
|
||||
"closePage" => ClosePage(param),
|
||||
"configure_share" => ConfigureShare(param),
|
||||
"eventTrack" => null,
|
||||
"getActionTicket" => await GetActionTicketAsync(param).ConfigureAwait(false),
|
||||
"getCookieInfo" => GetCookieInfo(param),
|
||||
"getCookieToken" => await GetCookieTokenAsync(param).ConfigureAwait(false),
|
||||
"getDS" => GetDynamicSecrectV1(param),
|
||||
"getDS2" => GetDynamicSecrectV2(param),
|
||||
"getHTTPRequestHeaders" => GetHttpRequestHeader(param),
|
||||
"getStatusBarHeight" => GetStatusBarHeight(param),
|
||||
"getUserInfo" => GetUserInfo(param),
|
||||
"hideLoading" => null,
|
||||
"login" => null,
|
||||
"pushPage" => PushPage(param),
|
||||
"showLoading" => null,
|
||||
_ => logger.LogWarning<IJsResult>("Unhandled Message Type: {method}", param.Method),
|
||||
};
|
||||
IJsResult? result = await TryGetJsResultFromJsParamAsync(param).ConfigureAwait(false);
|
||||
|
||||
if (result != null && param.Callback != null)
|
||||
{
|
||||
@@ -378,6 +360,37 @@ public class MiHoYoJSInterface
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IJsResult?> TryGetJsResultFromJsParamAsync(JsParam param)
|
||||
{
|
||||
try
|
||||
{
|
||||
return param.Method switch
|
||||
{
|
||||
"closePage" => ClosePage(param),
|
||||
"configure_share" => ConfigureShare(param),
|
||||
"eventTrack" => null,
|
||||
"getActionTicket" => await GetActionTicketAsync(param).ConfigureAwait(false),
|
||||
"getCookieInfo" => GetCookieInfo(param),
|
||||
"getCookieToken" => await GetCookieTokenAsync(param).ConfigureAwait(false),
|
||||
"getDS" => GetDynamicSecrectV1(param),
|
||||
"getDS2" => GetDynamicSecrectV2(param),
|
||||
"getHTTPRequestHeaders" => GetHttpRequestHeader(param),
|
||||
"getStatusBarHeight" => GetStatusBarHeight(param),
|
||||
"getUserInfo" => GetUserInfo(param),
|
||||
"hideLoading" => null,
|
||||
"login" => null,
|
||||
"pushPage" => PushPage(param),
|
||||
"showLoading" => null,
|
||||
_ => logger.LogWarning<IJsResult>("Unhandled Message Type: {method}", param.Method),
|
||||
};
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// The dialog is already closed.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDOMContentLoaded(CoreWebView2 coreWebView2, CoreWebView2DOMContentLoadedEventArgs args)
|
||||
{
|
||||
coreWebView2.ExecuteScriptAsync(HideScrollBarScript).AsTask().SafeForget(logger);
|
||||
|
||||
@@ -55,6 +55,7 @@ internal class GameRecordClient
|
||||
// We hava a verification procedure to handle
|
||||
if (resp?.ReturnCode == (int)KnownReturnCode.CODE1034)
|
||||
{
|
||||
resp.Message = "请求失败,请前往「米游社-我的角色-实时便笺」页面查看";
|
||||
CardVerifier cardVerifier = Ioc.Default.GetRequiredService<CardVerifier>();
|
||||
|
||||
if (await cardVerifier.TryGetXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is string challenge)
|
||||
@@ -70,7 +71,7 @@ internal class GameRecordClient
|
||||
}
|
||||
}
|
||||
|
||||
return Response.Response.DefaultIfNull(resp, "请求失败,请前往「米游社-我的角色-实时便笺」页面查看");
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,4 +20,24 @@ internal static class StructExtension
|
||||
{
|
||||
return new((int)(rectInt32.X * scale), (int)(rectInt32.Y * scale), (int)(rectInt32.Width * scale), (int)(rectInt32.Height * scale));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尺寸
|
||||
/// </summary>
|
||||
/// <param name="rectInt32">源</param>
|
||||
/// <returns>结果</returns>
|
||||
public static int Size(this RectInt32 rectInt32)
|
||||
{
|
||||
return rectInt32.Width * rectInt32.Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尺寸
|
||||
/// </summary>
|
||||
/// <param name="sizeInt32">源</param>
|
||||
/// <returns>结果</returns>
|
||||
public static int Size(this SizeInt32 sizeInt32)
|
||||
{
|
||||
return sizeInt32.Width * sizeInt32.Height;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Numerics;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.System.Diagnostics.ToolHelp;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao.Win32;
|
||||
|
||||
@@ -21,6 +22,15 @@ internal static class StructMarshal
|
||||
return new() { dwSize = (uint)sizeof(MODULEENTRY32) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="Windows.Win32.UI.WindowsAndMessaging.WINDOWPLACEMENT"/>
|
||||
/// </summary>
|
||||
/// <returns>新的实例</returns>
|
||||
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
|
||||
{
|
||||
return new() { length = (uint)sizeof(WINDOWPLACEMENT) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 0,0 点构造一个新的<see cref="Windows.Graphics.RectInt32"/>
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user