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