diff --git a/src/Snap.Hutao/Snap.Hutao/Control/ISupportCancellation.cs b/src/Snap.Hutao/Snap.Hutao/Control/ISupportCancellation.cs
deleted file mode 100644
index d2777e65..00000000
--- a/src/Snap.Hutao/Snap.Hutao/Control/ISupportCancellation.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) DGP Studio. All rights reserved.
-// Licensed under the MIT license.
-
-namespace Snap.Hutao.Control;
-
-///
-/// 指示此类支持取消任务
-///
-public interface ISupportCancellation
-{
- ///
- /// 用于通知事件取消的取消令牌
- ///
- CancellationToken CancellationToken { get; set; }
-}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs b/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs
index b95ab694..68466f95 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/ScopedPage.cs
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation;
+using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.Control;
@@ -33,9 +34,9 @@ public class ScopedPage : Page
///
/// 视图模型类型
public void InitializeWith()
- where TViewModel : class, ISupportCancellation
+ where TViewModel : class, IViewModel
{
- ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService();
+ IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService();
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
DataContext = viewModel;
}
@@ -59,11 +60,22 @@ public class ScopedPage : Page
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
- viewLoadingCancellationTokenSource.Cancel();
+ using (viewLoadingCancellationTokenSource)
+ {
+ // Cancel tasks executed by the view model
+ viewLoadingCancellationTokenSource.Cancel();
+ IViewModel viewModel = (IViewModel)DataContext;
- // Try dispose scope when page is not presented
- serviceScope.Dispose();
- viewLoadingCancellationTokenSource.Dispose();
+ using (SemaphoreSlim locker = viewModel.DisposeLock)
+ {
+ // Wait to ensure viewmodel operation is completed
+ locker.Wait();
+ viewModel.IsViewDisposed = true;
+
+ // Dispose the scope
+ serviceScope.Dispose();
+ }
+ }
}
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/ValueConverterBase.cs b/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs
similarity index 90%
rename from src/Snap.Hutao/Snap.Hutao/Control/ValueConverterBase.cs
rename to src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs
index c57675cc..17166826 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/ValueConverterBase.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Control;
///
/// 源类型
/// 目标类型
-public abstract class ValueConverterBase : IValueConverter
+public abstract class ValueConverter : IValueConverter
{
///
public object? Convert(object value, Type targetType, object parameter, string language)
@@ -23,7 +23,7 @@ public abstract class ValueConverterBase : IValueConverter
catch (Exception ex)
{
Ioc.Default
- .GetRequiredService>>()
+ .GetRequiredService>>()
.LogError(ex, "值转换器异常");
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExpressionService/EnumExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExpressionService/EnumExtension.cs
deleted file mode 100644
index 3e3d5f5f..00000000
--- a/src/Snap.Hutao/Snap.Hutao/Core/ExpressionService/EnumExtension.cs
+++ /dev/null
@@ -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;
-
-///
-/// 枚举帮助类
-///
-public static class EnumExtension
-{
- ///
- /// 判断枚举是否有对应的Flag
- ///
- /// 枚举类型
- /// 待检查的枚举
- /// 值
- /// 是否有对应的Flag
- public static bool HasOption(this T @enum, T value)
- where T : struct, Enum
- {
- return ExpressionCache.Entry(@enum, value);
- }
-
- private static class ExpressionCache
- {
- public static readonly Func Entry = Get();
-
- private static Func 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>(equal, paramSource, paramValue).Compile();
- }
- }
-}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsJob.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsJob.cs
index bcaf7447..47d6db7f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsJob.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsJob.cs
@@ -17,6 +17,11 @@ namespace Snap.Hutao.Core.IO.Bits;
[SuppressMessage("", "SA1600")]
internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{
+ ///
+ /// 任务名称前缀
+ ///
+ public const string JobNamePrefix = "SnapHutaoBitsJob";
+
private const uint BitsEngineNoProgressTimeout = 120;
private const int MaxResumeAttempts = 10;
@@ -43,12 +48,14 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath)
{
ILogger service = serviceProvider.GetRequiredService>();
- string text = $"BitsDownloadJob - {uri}";
+ string text = $"{JobNamePrefix} - {uri}";
IBackgroundCopyJob ppJob;
try
{
backgroundCopyManager.CreateJob(text, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out ppJob);
- ppJob.SetNotifyFlags(11u);
+
+ // BG_NOTIFY_JOB_TRANSFERRED & BG_NOTIFY_JOB_ERROR & BG_NOTIFY_JOB_MODIFICATION
+ ppJob.SetNotifyFlags(0b1011);
ppJob.SetNoProgressTimeout(BitsEngineNoProgressTimeout);
ppJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND);
ppJob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, null, null);
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
index d95816ab..09f74588 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
@@ -2,6 +2,9 @@
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
+using System.Runtime.InteropServices;
+using Windows.Win32;
+using Windows.Win32.Foundation;
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
namespace Snap.Hutao.Core.IO.Bits;
@@ -40,6 +43,41 @@ internal class BitsManager
return new(result, tempFile);
}
+ ///
+ /// 取消所有先前创建的任务
+ ///
+ 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 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 progress, CancellationToken token)
{
IBackgroundCopyManager value;
@@ -48,29 +86,37 @@ internal class BitsManager
{
value = lazyBackgroundCopyManager.Value;
}
- catch (System.Exception ex)
+ catch (Exception ex)
{
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
return false;
}
- using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
+ try
{
- try
+ using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
{
- bitsJob.WaitForCompletion(progress, token);
- }
- catch (System.Exception ex)
- {
- logger?.LogWarning(ex, "BITS download failed:");
- return false;
- }
+ try
+ {
+ bitsJob.WaitForCompletion(progress, token);
+ }
+ catch (Exception ex)
+ {
+ logger?.LogWarning(ex, "BITS download failed:");
+ return false;
+ }
- if (bitsJob.ErrorCode != 0)
- {
- return false;
+ if (bitsJob.ErrorCode != 0)
+ {
+ return false;
+ }
}
}
+ catch (COMException)
+ {
+ // BITS job creation failed
+ return false;
+ }
return true;
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatcherQueueExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatcherQueueExtension.cs
index 92d34011..8210bdc2 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatcherQueueExtension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatcherQueueExtension.cs
@@ -20,10 +20,10 @@ public static class DispatcherQueueExtension
using (ManualResetEventSlim blockEvent = new())
{
dispatcherQueue.TryEnqueue(() =>
- {
- action();
- blockEvent.Set();
- });
+ {
+ action();
+ blockEvent.Set();
+ });
blockEvent.Wait();
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs
index 7fdc04ed..cdb43cbb 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs
@@ -43,9 +43,6 @@ public readonly struct DispatherQueueSwitchOperation : IAwaitable
public void OnCompleted(Action continuation)
{
- dispatherQueue.TryEnqueue(() =>
- {
- continuation();
- });
+ dispatherQueue.TryEnqueue(() => continuation());
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SemaphoreSlimExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SemaphoreSlimExtensions.cs
index 85853739..9a7a17b4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SemaphoreSlimExtensions.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SemaphoreSlimExtensions.cs
@@ -12,10 +12,19 @@ public static class SemaphoreSlimExtensions
/// 异步进入信号量
///
/// 信号量
+ /// 取消令牌
/// 可释放的对象,用于释放信号量
- public static async Task EnterAsync(this SemaphoreSlim semaphoreSlim)
+ public static async Task 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);
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs
index 83ab2999..f549a814 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs
@@ -31,4 +31,14 @@ internal static class ThreadHelper
{
return new(Program.DispatcherQueue!);
}
+
+ ///
+ /// 在主线程上同步等待执行操作
+ ///
+ /// 操作
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void InvokeOnMainThread(Action action)
+ {
+ Program.DispatcherQueue!.Invoke(action);
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs
index 597cc831..dc0053f0 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs
@@ -135,6 +135,7 @@ internal sealed class ExtendedWindow : IRecipient : IRecipient
/// 应用窗体
/// 持久化尺寸
- /// 初始尺寸
- 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.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
- SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
+ SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd);
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
if (persistSize)
{
- RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
- if (persistedSize.Width * persistedSize.Height > 848 * 524)
+ RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
+ if (persistedRect.Size() >= initialSize.Size())
{
- rect = persistedSize;
+ rect = persistedRect;
}
}
- unsafe
- {
- TransformToCenterScreen(&rect);
- }
-
+ TransformToCenterScreen(&rect);
appWindow.MoveAndResize(rect);
}
@@ -53,7 +50,15 @@ internal static class Persistence
/// 应用窗体
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());
+ }
}
///
@@ -61,7 +66,7 @@ internal static class Persistence
///
/// 窗体句柄
/// 缩放比
- public static double GetScaleForWindow(HWND hwnd)
+ public static double GetScaleForWindowHandle(HWND hwnd)
{
uint dpi = GetDpiForWindow(hwnd);
return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero);
@@ -69,7 +74,7 @@ internal static class Persistence
private static SizeInt32 TransformSizeForWindow(SizeInt32 size, HWND hwnd)
{
- double scale = GetScaleForWindow(hwnd);
+ double scale = GetScaleForWindowHandle(hwnd);
return new((int)(size.Width * scale), (int)(size.Height * scale));
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs
index a53185d7..2295cadc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs
@@ -87,7 +87,7 @@ internal class WindowSubclassManager : IDisposable
{
case WM_GETMINMAXINFO:
{
- double scalingFactor = Persistence.GetScaleForWindow(hwnd);
+ double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
break;
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs
index e4518053..5d325886 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs
@@ -51,7 +51,7 @@ public class Avatar : ICalculableSource
///
/// 武器
///
- public Weapon Weapon { get; set; } = default!;
+ public Weapon? Weapon { get; set; } = default!;
///
/// 圣遗物列表
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs
index fe041d44..51de30b9 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs
@@ -174,6 +174,10 @@ public class User : ObservableObject
Cookie ltokenCookie = Cookie.Parse($"ltuid={Entity.Aid};ltoken={ltokenResponse.Data.Ltoken}");
Entity.Ltoken = ltokenCookie;
}
+ else
+ {
+ return false;
+ }
}
Response actionTicketResponse = await scope.ServiceProvider
@@ -194,6 +198,14 @@ public class User : ObservableObject
{
UserGameRoles = userGameRolesResponse.Data.List;
}
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ return false;
}
// 自动填充 CookieToken
@@ -209,6 +221,10 @@ public class User : ObservableObject
Cookie cookieTokenCookie = Cookie.Parse($"account_id={Entity.Aid};cookie_token={cookieTokenResponse.Data.CookieToken}");
Entity.CookieToken = cookieTokenCookie;
}
+ else
+ {
+ return false;
+ }
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs
index c99c1309..67450531 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs
@@ -3,15 +3,16 @@
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Configuration;
+using System.Diagnostics;
namespace Snap.Hutao.Model.Entity.Database;
///
/// 应用程序数据库上下文
///
+[DebuggerDisplay("Id = {ContextId}")]
public sealed class AppDbContext : DbContext
{
- private readonly Guid contextId;
private readonly ILogger? logger;
///
@@ -31,9 +32,8 @@ public sealed class AppDbContext : DbContext
public AppDbContext(DbContextOptions options, ILogger logger)
: this(options)
{
- contextId = Guid.NewGuid();
this.logger = logger;
- logger.LogInformation("AppDbContext[{id}] created.", contextId);
+ logger.LogInformation("AppDbContext[{id}] created.", ContextId);
}
///
@@ -135,7 +135,7 @@ public sealed class AppDbContext : DbContext
public override void Dispose()
{
base.Dispose();
- logger?.LogInformation("AppDbContext[{id}] disposed.", contextId);
+ logger?.LogInformation("AppDbContext[{id}] disposed.", ContextId);
}
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AchievementIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AchievementIconConverter.cs
index cfe13d8a..de08dd3b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AchievementIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AchievementIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 角色头像转换器
///
-internal class AchievementIconConverter : ValueConverterBase
+internal class AchievementIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarCardConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarCardConverter.cs
index 398cc893..9c3d7df2 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarCardConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarCardConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 角色卡片转换器
///
-internal class AvatarCardConverter : ValueConverterBase
+internal class AvatarCardConverter : ValueConverter
{
private const string CostumeCard = "UI_AvatarIcon_Costume_Card.png";
private static readonly Uri UIAvatarIconCostumeCard = new(Web.HutaoEndpoints.StaticFile("AvatarCard", CostumeCard));
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarIconConverter.cs
index 1940d121..fb72175c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 角色头像转换器
///
-internal class AvatarIconConverter : ValueConverterBase
+internal class AvatarIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs
index f18fea57..110316dd 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 角色名片转换器
///
-internal class AvatarNameCardPicConverter : ValueConverterBase
+internal class AvatarNameCardPicConverter : ValueConverter
{
///
/// 从角色转换到名片
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarSideIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarSideIconConverter.cs
index 2c8d8447..8ac95abb 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarSideIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarSideIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 角色侧面头像转换器
///
-internal class AvatarSideIconConverter : ValueConverterBase
+internal class AvatarSideIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs
index 0ceab90d..7c80cf19 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 描述参数解析器
///
-internal sealed partial class DescParamDescriptor : ValueConverterBase>>
+internal sealed partial class DescParamDescriptor : ValueConverter>>
{
///
/// 获取特定等级的解释
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ElementNameIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ElementNameIconConverter.cs
index 758e174b..b9bb62e6 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ElementNameIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ElementNameIconConverter.cs
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 元素名称图标转换器
///
-internal class ElementNameIconConverter : ValueConverterBase
+internal class ElementNameIconConverter : ValueConverter
{
///
/// 将中文元素名称转换为图标链接
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EmotionIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EmotionIconConverter.cs
index 0c14473f..59a11760 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EmotionIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EmotionIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 表情图片转换器
///
-internal class EmotionIconConverter : ValueConverterBase
+internal class EmotionIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs
index 15bb711a..44345543 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 武器图片转换器
///
-internal class EquipIconConverter : ValueConverterBase
+internal class EquipIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaAvatarIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaAvatarIconConverter.cs
index eec7b9f4..9e342028 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaAvatarIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaAvatarIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 立绘图标转换器
///
-internal class GachaAvatarIconConverter : ValueConverterBase
+internal class GachaAvatarIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaAvatarImgConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaAvatarImgConverter.cs
index 4aec126d..bc52fc3b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaAvatarImgConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaAvatarImgConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 立绘转换器
///
-internal class GachaAvatarImgConverter : ValueConverterBase
+internal class GachaAvatarImgConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaEquipIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaEquipIconConverter.cs
index c9be600a..5ee3b637 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaEquipIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/GachaEquipIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 武器祈愿图片转换器
///
-internal class GachaEquipIconConverter : ValueConverterBase
+internal class GachaEquipIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ItemIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ItemIconConverter.cs
index 2c08b1d8..4a8c5736 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ItemIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/ItemIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 物品图片转换器
///
-internal class ItemIconConverter : ValueConverterBase
+internal class ItemIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs
index 9af3879c..d9507d5e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 基础属性翻译器
///
-internal class PropertyInfoDescriptor : ValueConverterBase>?>
+internal class PropertyInfoDescriptor : ValueConverter>?>
{
///
/// 格式化对
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs
index ff014fbe..790ec22b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 品质颜色转换器
///
-internal class QualityColorConverter : ValueConverterBase
+internal class QualityColorConverter : ValueConverter
{
///
public override Color Convert(ItemQuality from)
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityConverter.cs
index 20d629b2..51785d18 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityConverter.cs
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 物品等级转换器
///
-internal class QualityConverter : ValueConverterBase
+internal class QualityConverter : ValueConverter
{
///
public override Uri Convert(ItemQuality from)
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs
index ece404ec..48cd0256 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 武器图片转换器
///
-internal class RelicIconConverter : ValueConverterBase
+internal class RelicIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs
index 6b57ffbe..95435dac 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 技能图标转换器
///
-internal class SkillIconConverter : ValueConverterBase
+internal class SkillIconConverter : ValueConverter
{
///
/// 名称转Uri
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs
index 3574e50d..eb0a7806 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
///
/// 元素名称图标转换器
///
-internal class WeaponTypeIconConverter : ValueConverterBase
+internal class WeaponTypeIconConverter : ValueConverter
{
///
/// 将武器类型转换为图标链接
diff --git a/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt b/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt
index dc003ed5..a79362b4 100644
--- a/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt
+++ b/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt
@@ -28,6 +28,7 @@ CoWaitForMultipleObjects
// USER32
FindWindowEx
GetDpiForWindow
+GetWindowPlacement
// COM BITS
BackgroundCopyManager
diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
index 322bb221..4c8c4dc7 100644
--- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
+++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
@@ -12,7 +12,7 @@
+ Version="1.3.9.0" />
胡桃
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs
index 8b1919f0..f492e7b1 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs
@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
+using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.InterChange.Achievement;
using System.Collections.ObjectModel;
@@ -52,21 +53,23 @@ internal class AchievementService : IAchievementService
}
///
- public ObservableCollection GetArchiveCollection()
+ public async Task> GetArchiveCollectionAsync()
{
- return archiveCollection ??= new(appDbContext.AchievementArchives.AsNoTracking().ToList());
+ await ThreadHelper.SwitchToMainThreadAsync();
+ return archiveCollection ??= appDbContext.AchievementArchives.AsNoTracking().ToObservableCollection();
}
///
public async Task RemoveArchiveAsync(EntityArchive archive)
{
// Sync cache
- // Keep this on main thread.
await ThreadHelper.SwitchToMainThreadAsync();
archiveCollection!.Remove(archive);
// Sync database
await ThreadHelper.SwitchToBackgroundAsync();
+
+ // Cascade deleted the achievements.
await appDbContext.AchievementArchives
.Where(a => a.InnerId == archive.InnerId)
.ExecuteDeleteAsync()
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs
index 45da7456..d6216eeb 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs
@@ -35,10 +35,10 @@ internal interface IAchievementService
List GetAchievements(EntityArchive archive, IList metadata);
///
- /// 获取用于绑定的成就存档集合
+ /// 异步获取用于绑定的成就存档集合
///
/// 成就存档集合
- ObservableCollection GetArchiveCollection();
+ Task> GetArchiveCollectionAsync();
///
/// 异步导入UIAF数据
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs
new file mode 100644
index 00000000..8397e6bc
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs
@@ -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;
+
+///
+/// 角色信息数据库操作
+///
+public class AvatarInfoDbOperation
+{
+ private readonly AppDbContext appDbContext;
+
+ ///
+ /// 构造一个新的角色信息数据库操作
+ ///
+ /// 数据库上下文
+ public AvatarInfoDbOperation(AppDbContext appDbContext)
+ {
+ this.appDbContext = appDbContext;
+ }
+
+ ///
+ /// 更新数据库角色信息
+ ///
+ /// uid
+ /// Enka信息
+ /// 取消令牌
+ /// 角色列表
+ public List UpdateDbAvatarInfos(string uid, IEnumerable webInfos, CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+ List 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);
+ }
+
+ ///
+ /// 米游社我的角色方式 更新数据库角色信息
+ ///
+ /// 用户与角色
+ /// 取消令牌
+ /// 角色列表
+ public async Task> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+ string uid = userAndUid.Uid.Value;
+ List dbInfos = appDbContext.AvatarInfos
+ .Where(i => i.Uid == uid)
+ .ToList();
+ EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
+
+ GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService();
+ Response playerInfoResponse = await gameRecordClient
+ .GetPlayerInfoAsync(userAndUid, token)
+ .ConfigureAwait(false);
+
+ token.ThrowIfCancellationRequested();
+
+ if (playerInfoResponse.IsOk())
+ {
+ Response charactersResponse = await gameRecordClient
+ .GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
+ .ConfigureAwait(false);
+
+ if (charactersResponse.IsOk())
+ {
+ List characters = charactersResponse.Data.Avatars;
+
+ GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService();
+
+ 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);
+ }
+
+ ///
+ /// 米游社养成计算方式 更新数据库角色信息
+ ///
+ /// 用户与角色
+ /// 取消令牌
+ /// 角色列表
+ public async Task> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+ string uid = userAndUid.Uid.Value;
+ List dbInfos = appDbContext.AvatarInfos
+ .Where(i => i.Uid == uid)
+ .ToList();
+ EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
+
+ CalculateClient calculateClient = Ioc.Default.GetRequiredService();
+ List avatars = await calculateClient.GetAvatarsAsync(userAndUid, token).ConfigureAwait(false);
+
+ CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService();
+
+ foreach (CalculateAvatar avatar in avatars)
+ {
+ if (AvatarIds.IsPlayer(avatar.Id))
+ {
+ continue;
+ }
+
+ token.ThrowIfCancellationRequested();
+
+ Response 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);
+ }
+
+ ///
+ /// 获取数据库角色信息
+ ///
+ /// Uid
+ /// 角色列表
+ public List GetDbAvatarInfos(string uid)
+ {
+ return appDbContext.AvatarInfos
+ .Where(i => i.Uid == uid)
+ .Select(i => i.Info)
+ .ToList();
+ }
+
+ private void EnsureItemsAvatarIdDistinct(ref List 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
index 151c0d32..641a73e4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
@@ -37,6 +37,8 @@ internal class AvatarInfoService : IAvatarInfoService
private readonly IMetadataService metadataService;
private readonly ILogger logger;
+ private readonly AvatarInfoDbOperation avatarInfoDbOperation;
+
///
/// 构造一个新的角色信息服务
///
@@ -54,6 +56,8 @@ internal class AvatarInfoService : IAvatarInfoService
this.metadataService = metadataService;
this.summaryFactory = summaryFactory;
this.logger = logger;
+
+ avatarInfoDbOperation = new(appDbContext);
}
///
@@ -74,23 +78,20 @@ internal class AvatarInfoService : IAvatarInfoService
return new(RefreshResult.APIUnavailable, null);
}
- if (resp.IsValid)
- {
- IList list = UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token);
- Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
- token.ThrowIfCancellationRequested();
- return new(RefreshResult.Ok, summary);
- }
- else
+ if (!resp.IsValid)
{
return new(RefreshResult.ShowcaseNotOpen, null);
}
+
+ List list = avatarInfoDbOperation.UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token);
+ Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
+ return new(RefreshResult.Ok, summary);
}
case RefreshOption.RequestFromHoyolabGameRecord:
{
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
- IList list = await UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
+ List list = await avatarInfoDbOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
return new(RefreshResult.Ok, summary);
}
@@ -98,7 +99,7 @@ internal class AvatarInfoService : IAvatarInfoService
case RefreshOption.RequestFromHoyolabCalculate:
{
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
- IList list = await UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
+ List list = await avatarInfoDbOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
return new(RefreshResult.Ok, summary);
}
@@ -106,7 +107,8 @@ internal class AvatarInfoService : IAvatarInfoService
default:
{
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
- Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(userAndUid.Uid.Value), token).ConfigureAwait(false);
+ List list = avatarInfoDbOperation.GetDbAvatarInfos(userAndUid.Uid.Value);
+ Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
return new(RefreshResult.Ok, summary.Avatars.Count == 0 ? null : summary);
}
@@ -134,153 +136,4 @@ internal class AvatarInfoService : IAvatarInfoService
return summary;
}
-
- private List UpdateDbAvatarInfos(string uid, IEnumerable webInfos, CancellationToken token)
- {
- token.ThrowIfCancellationRequested();
- List 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> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
- {
- string uid = userAndUid.Uid.Value;
- List dbInfos = appDbContext.AvatarInfos
- .Where(i => i.Uid == uid)
- .ToList();
-
- GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService();
- Response playerInfoResponse = await gameRecordClient
- .GetPlayerInfoAsync(userAndUid)
- .ConfigureAwait(false);
-
- // TODO: We should not refresh if response is not correct here.
- if (playerInfoResponse.IsOk())
- {
- Response charactersResponse = await gameRecordClient
- .GetCharactersAsync(userAndUid, playerInfoResponse.Data)
- .ConfigureAwait(false);
-
- if (charactersResponse.IsOk())
- {
- List characters = charactersResponse.Data.Avatars;
-
- GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService();
-
- 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> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
- {
- string uid = userAndUid.Uid.Value;
- List dbInfos = appDbContext.AvatarInfos
- .Where(i => i.Uid == uid)
- .ToList();
-
- CalculateClient calculateClient = Ioc.Default.GetRequiredService();
- List avatars = await calculateClient.GetAvatarsAsync(userAndUid).ConfigureAwait(false);
-
- CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService();
-
- foreach (CalculateAvatar avatar in avatars)
- {
- if (AvatarIds.IsPlayer(avatar.Id))
- {
- continue;
- }
-
- Response 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 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();
- }
- }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs
index a01f343a..540ae603 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs
@@ -40,7 +40,7 @@ internal class SummaryAvatarFactory
/// 角色
public PropertyAvatar CreateAvatar()
{
- ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList);
+ ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
return new()
@@ -65,11 +65,12 @@ internal class SummaryAvatarFactory
};
}
- private ReliquaryAndWeapon ProcessEquip(IList equipments)
+ private ReliquaryAndWeapon ProcessEquip(List equipments)
{
List reliquaryList = new();
PropertyWeapon? weapon = null;
+ // equipments can be null
foreach (Equip equip in equipments)
{
switch (equip.Flat.ItemType)
@@ -134,9 +135,9 @@ internal class SummaryAvatarFactory
private struct ReliquaryAndWeapon
{
public List Reliquaries;
- public PropertyWeapon Weapon;
+ public PropertyWeapon? Weapon;
- public ReliquaryAndWeapon(List reliquaries, PropertyWeapon weapon)
+ public ReliquaryAndWeapon(List reliquaries, PropertyWeapon? weapon)
{
Reliquaries = reliquaries;
Weapon = weapon;
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
index 186a6990..639707cb 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
@@ -45,7 +45,10 @@ internal class DailyNoteService : IDailyNoteService, IRecipient
public void Receive(UserRemovedMessage message)
{
- entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
+ ThreadHelper.InvokeOnMainThread(() =>
+ {
+ entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
+ });
}
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
index 4194dc6c..98a16abf 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
@@ -28,7 +28,7 @@ namespace Snap.Hutao.Service.GachaLog;
/// 祈愿记录服务
///
[Injection(InjectAs.Scoped, typeof(IGachaLogService))]
-internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
+internal class GachaLogService : IGachaLogService
{
///
/// 祈愿记录查询的类型
@@ -95,9 +95,6 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
set => dbCurrent.Current = value;
}
- ///
- public bool IsInitialized { get; set; }
-
///
public Task ExportToUIGFAsync(GachaArchive archive)
{
@@ -117,30 +114,29 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
}
///
- public ObservableCollection GetArchiveCollection()
+ public async Task> GetArchiveCollectionAsync()
{
- return archiveCollection ??= new(appDbContext.GachaArchives.AsNoTracking().ToList());
+ await ThreadHelper.SwitchToMainThreadAsync();
+ return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
///
- public async ValueTask InitializeAsync()
+ public async ValueTask InitializeAsync(CancellationToken token)
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
- nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false);
- nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false);
+ nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
+ nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
- idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
- idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
+ idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
+ idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
- IsInitialized = true;
+ return true;
}
else
{
- IsInitialized = false;
+ return false;
}
-
- return IsInitialized;
}
///
@@ -321,7 +317,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
archive = appDbContext.GachaArchives.Single(a => a.Uid == uid);
GachaArchive temp = archive;
- Program.DispatcherQueue!.Invoke(() => archiveCollection!.Add(temp));
+ ThreadHelper.InvokeOnMainThread(() => archiveCollection!.Add(temp));
}
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs
index 8cf2027b..f09f5e14 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs
@@ -26,10 +26,10 @@ internal interface IGachaLogService
Task ExportToUIGFAsync(GachaArchive archive);
///
- /// 获取可用于绑定的存档集合
+ /// 异步获取可用于绑定的存档集合
///
/// 存档集合
- ObservableCollection GetArchiveCollection();
+ Task> GetArchiveCollectionAsync();
///
/// 获取祈愿日志Url提供器
@@ -58,7 +58,7 @@ internal interface IGachaLogService
///
/// 取消令牌
/// 是否初始化成功
- ValueTask InitializeAsync();
+ ValueTask InitializeAsync(CancellationToken token);
///
/// 刷新祈愿记录
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs
index b040a071..1b402513 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs
@@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.IO.Ini;
+using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
@@ -213,14 +214,15 @@ internal class GameService : IGameService, IDisposable
}
///
- public ObservableCollection GetGameAccountCollection()
+ public async Task> GetGameAccountCollectionAsync()
{
+ await ThreadHelper.SwitchToMainThreadAsync();
if (gameAccounts == null)
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService();
- gameAccounts = new(appDbContext.GameAccounts.AsNoTracking().ToList());
+ gameAccounts = appDbContext.GameAccounts.AsNoTracking().ToObservableCollection();
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs
index 456f99b5..b703f0a2 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs
@@ -27,10 +27,10 @@ internal interface IGameService
ValueTask DetectGameAccountAsync();
///
- /// 获取游戏内账号集合
+ /// 异步获取游戏内账号集合
///
/// 游戏内账号集合
- ObservableCollection GetGameAccountCollection();
+ Task> GetGameAccountCollectionAsync();
///
/// 异步获取游戏路径
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs
index 6db76d97..b851c3fc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs
@@ -23,9 +23,9 @@ internal class HutaoCache : IHutaoCache
private Dictionary? idAvatarExtendedMap;
- private bool isDatabaseViewModelInitialized;
- private bool isWikiAvatarViewModelInitiaized;
- private bool isWikiWeaponViewModelInitiaized;
+ private TaskCompletionSource? databaseViewModelTaskSource;
+ private TaskCompletionSource? wikiAvatarViewModelTaskSource;
+ private TaskCompletionSource? wikiWeaponViewModelTaskSource;
///
/// 构造一个新的胡桃 API 缓存
@@ -62,11 +62,12 @@ internal class HutaoCache : IHutaoCache
///
public async ValueTask InitializeForDatabaseViewModelAsync()
{
- if (isDatabaseViewModelInitialized)
+ if (databaseViewModelTaskSource != null)
{
- return true;
+ return await databaseViewModelTaskSource.Task.ConfigureAwait(false);
}
+ databaseViewModelTaskSource = new();
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
@@ -81,20 +82,23 @@ internal class HutaoCache : IHutaoCache
await Task.WhenAll(tasks).ConfigureAwait(false);
- return isDatabaseViewModelInitialized = true;
+ databaseViewModelTaskSource.TrySetResult(true);
+ return true;
}
+ databaseViewModelTaskSource.TrySetResult(false);
return false;
}
///
public async ValueTask InitializeForWikiAvatarViewModelAsync()
{
- if (isWikiAvatarViewModelInitiaized)
+ if (wikiAvatarViewModelTaskSource != null)
{
- return true;
+ return await wikiAvatarViewModelTaskSource.Task.ConfigureAwait(false);
}
+ wikiAvatarViewModelTaskSource = new();
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
@@ -116,21 +120,23 @@ internal class HutaoCache : IHutaoCache
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
}).ToList();
- isWikiAvatarViewModelInitiaized = true;
+ wikiAvatarViewModelTaskSource.TrySetResult(true);
return true;
}
+ wikiAvatarViewModelTaskSource.TrySetResult(false);
return false;
}
///
public async ValueTask InitializeForWikiWeaponViewModelAsync()
{
- if (isWikiWeaponViewModelInitiaized)
+ if (wikiWeaponViewModelTaskSource != null)
{
- return true;
+ return await wikiWeaponViewModelTaskSource.Task.ConfigureAwait(false);
}
+ wikiWeaponViewModelTaskSource = new();
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
@@ -148,10 +154,11 @@ internal class HutaoCache : IHutaoCache
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
}).ToList();
- isWikiWeaponViewModelInitiaized = true;
+ wikiWeaponViewModelTaskSource.TrySetResult(true);
return true;
}
+ wikiWeaponViewModelTaskSource.TrySetResult(false);
return false;
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs
index 2b7650da..1d84713d 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoService.cs
@@ -104,12 +104,22 @@ internal class HutaoService : IHutaoService
Response webResponse = await taskFunc(default).ConfigureAwait(false);
T web = webResponse.IsOk() ? webResponse.Data : new();
- appDbContext.ObjectCache.AddAndSave(new()
+ try
{
- Key = key,
- ExpireTime = DateTimeOffset.Now.AddHours(4),
- Value = JsonSerializer.Serialize(web, options),
- });
+ appDbContext.ObjectCache.AddAndSave(new()
+ {
+ Key = key,
+
+ // we hold the cache for 4 hours, then just expire it.
+ ExpireTime = DateTimeOffset.Now.AddHours(4),
+ Value = JsonSerializer.Serialize(web, options),
+ });
+ }
+ catch (Microsoft.EntityFrameworkCore.DbUpdateException)
+ {
+ // DbUpdateException: An error occurred while saving the entity changes.
+ // TODO: Not ignore it.
+ }
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
index 58ed9f05..731b23f4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
@@ -138,7 +138,7 @@ internal class UserService : IUserService
{
List userAndUids = new();
ObservableCollection observableUsers = await GetUserCollectionAsync().ConfigureAwait(false);
- foreach (BindingUser user in observableUsers.ToList())
+ foreach (BindingUser user in observableUsers)
{
foreach (UserGameRole role in user.UserGameRoles)
{
@@ -146,7 +146,7 @@ internal class UserService : IUserService
}
}
- roleCollection = new(userAndUids);
+ roleCollection = userAndUids.ToObservableCollection();
}
return roleCollection;
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/DailyNotePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/DailyNotePage.xaml
index eabc59d0..83af5872 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/DailyNotePage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/DailyNotePage.xaml
@@ -32,7 +32,7 @@
-
+
-
+
-
-
-
-
+
-
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/IViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/IViewModel.cs
new file mode 100644
index 00000000..b75e2ce1
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/IViewModel.cs
@@ -0,0 +1,25 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.ViewModel.Abstraction;
+
+///
+/// 视图模型接口
+///
+public interface IViewModel
+{
+ ///
+ /// 用于通知页面卸载的取消令牌
+ ///
+ CancellationToken CancellationToken { get; set; }
+
+ ///
+ /// 释放操作锁
+ ///
+ SemaphoreSlim DisposeLock { get; set; }
+
+ ///
+ /// 对应的视图是否已经释放
+ ///
+ bool IsViewDisposed { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModel.cs
new file mode 100644
index 00000000..34c0a0b6
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Abstraction/ViewModel.cs
@@ -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;
+
+///
+/// 视图模型抽象类
+///
+public abstract class ViewModel : ObservableObject, IViewModel
+{
+ private bool isInitialized;
+
+ ///
+ /// 是否初始化完成
+ ///
+ public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
+
+ ///
+ public CancellationToken CancellationToken { get; set; }
+
+ ///
+ public SemaphoreSlim DisposeLock { get; set; } = new(1);
+
+ ///
+ public bool IsViewDisposed { get; set; }
+
+ ///
+ /// 当页面被释放后抛出异常
+ ///
+ /// 操作被用户取消
+ protected void ThrowIfViewDisposed()
+ {
+ if (IsViewDisposed)
+ {
+ throw new OperationCanceledException("页面资源已经被释放,操作取消");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs
index 6fd5aab2..7d806d93 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs
@@ -1,12 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.UI;
using Microsoft.UI.Xaml.Controls;
-using Snap.Hutao.Control;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.DataTransfer;
@@ -30,10 +28,7 @@ namespace Snap.Hutao.ViewModel;
///
[Injection(InjectAs.Scoped)]
[SuppressMessage("", "SA1124")]
-internal class AchievementViewModel
- : ObservableObject,
- ISupportCancellation,
- INavigationRecipient
+internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipient
{
private static readonly SortDescription IncompletedItemsFirstSortDescription = new(nameof(Model.Binding.Achievement.Achievement.IsChecked), SortDirection.Ascending);
private static readonly SortDescription CompletionTimeSortDescription = new(nameof(Model.Binding.Achievement.Achievement.Time), SortDirection.Descending);
@@ -53,7 +48,6 @@ internal class AchievementViewModel
private Model.Entity.AchievementArchive? selectedArchive;
private bool isIncompletedItemsFirst = true;
private string searchText = string.Empty;
- private bool isInitialized;
private string? finishDescription;
///
@@ -92,14 +86,6 @@ internal class AchievementViewModel
SaveAchievementCommand = new RelayCommand(SaveAchievement);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
- ///
- /// 是否初始化完成
- ///
- public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
-
///
/// 成就存档集合
///
@@ -251,23 +237,38 @@ internal class AchievementViewModel
{
try
{
- List goals = await metadataService.GetAchievementGoalsAsync(CancellationToken).ConfigureAwait(false);
+ List sortedGoals;
+ ObservableCollection archives;
+
+ ThrowIfViewDisposed();
+ using (await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(false))
+ {
+ ThrowIfViewDisposed();
+
+ List goals = await metadataService.GetAchievementGoalsAsync(CancellationToken).ConfigureAwait(false);
+ sortedGoals = goals
+ .OrderBy(goal => goal.Order)
+ .Select(goal => new Model.Binding.Achievement.AchievementGoal(goal))
+ .ToList();
+ archives = await achievementService.GetArchiveCollectionAsync().ConfigureAwait(false);
+ }
await ThreadHelper.SwitchToMainThreadAsync();
- AchievementGoals = goals.OrderBy(goal => goal.Order).Select(goal => new Model.Binding.Achievement.AchievementGoal(goal)).ToList();
-
- Archives = achievementService.GetArchiveCollection();
+ AchievementGoals = sortedGoals;
+ Archives = archives;
SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
+
+ IsInitialized = true;
}
- catch (TaskCanceledException)
+ catch (OperationCanceledException)
{
- // Indicate initialization not succeed
+ // User canceled the loading operation,
+ // Indicate initialization not succeed.
openUICompletionSource.TrySetResult(false);
}
}
openUICompletionSource.TrySetResult(metaInitialized);
- IsInitialized = metaInitialized;
}
#region 存档操作
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs
index 7b8a462a..d1946821 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs
@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using Snap.Hutao.Control;
using Snap.Hutao.Core;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.Abstraction;
@@ -17,7 +15,7 @@ namespace Snap.Hutao.ViewModel;
/// 公告视图模型
///
[Injection(InjectAs.Scoped)]
-internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
+internal class AnnouncementViewModel : Abstraction.ViewModel
{
private readonly IAnnouncementService announcementService;
@@ -39,9 +37,6 @@ internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
OpenAnnouncementUICommand = new RelayCommand(OpenAnnouncementUI);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 公告
///
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs
index e5454c27..9dd926a5 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs
@@ -1,10 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
-using Snap.Hutao.Control;
+using Snap.Hutao.Control.Extension;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Extension;
@@ -19,6 +19,7 @@ using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
+using System.Runtime.InteropServices;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI;
@@ -35,11 +36,12 @@ namespace Snap.Hutao.ViewModel;
/// TODO: support page unload as cancellation
///
[Injection(InjectAs.Scoped)]
-internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
+internal class AvatarPropertyViewModel : Abstraction.ViewModel
{
private readonly IUserService userService;
private readonly IAvatarInfoService avatarInfoService;
private readonly IInfoBarService infoBarService;
+ private readonly IContentDialogFactory contentDialogFactory;
private Summary? summary;
private Avatar? selectedAvatar;
@@ -48,17 +50,20 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
///
/// 用户服务
/// 角色信息服务
+ /// 对话框工厂
/// 异步命令工厂
/// 信息条服务
public AvatarPropertyViewModel(
IUserService userService,
IAvatarInfoService avatarInfoService,
+ IContentDialogFactory contentDialogFactory,
IAsyncRelayCommandFactory asyncRelayCommandFactory,
IInfoBarService infoBarService)
{
this.userService = userService;
this.avatarInfoService = avatarInfoService;
this.infoBarService = infoBarService;
+ this.contentDialogFactory = contentDialogFactory;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
RefreshFromEnkaApiCommand = asyncRelayCommandFactory.Create(RefreshByEnkaApiAsync);
@@ -68,9 +73,6 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
CultivateCommand = asyncRelayCommandFactory.Create(CultivateAsync);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 简述对象
///
@@ -171,8 +173,21 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
{
try
{
- (RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(userAndUid, option, token).ConfigureAwait(false);
+ ValueResult summaryResult;
+ ThrowIfViewDisposed();
+ using (await DisposeLock.EnterAsync(token).ConfigureAwait(false))
+ {
+ ThrowIfViewDisposed();
+ ContentDialog dialog = await contentDialogFactory.CreateForIndeterminateProgressAsync("获取数据中").ConfigureAwait(false);
+ await ThreadHelper.SwitchToMainThreadAsync();
+ await using (await dialog.BlockAsync().ConfigureAwait(false))
+ {
+ summaryResult = await avatarInfoService.GetSummaryAsync(userAndUid, option, token).ConfigureAwait(false);
+ }
+ }
+
+ (RefreshResult result, Summary? summary) = summaryResult;
if (result == RefreshResult.Ok)
{
await ThreadHelper.SwitchToMainThreadAsync();
@@ -206,6 +221,12 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
if (userService.Current != null)
{
+ if (avatar.Weapon == null)
+ {
+ infoBarService.Warning("当前角色无法计算,请同步信息后再试");
+ return;
+ }
+
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
(bool isOk, CalcAvatarPromotionDelta delta) = await new CultivatePromotionDeltaDialog(avatar.ToCalculable(), avatar.Weapon.ToCalculable())
@@ -268,14 +289,31 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
Bgra8 tint = Bgra8.FromColor(tintColor);
softwareBitmap.NormalBlend(tint);
+ bool clipboardOpened = false;
using (InMemoryRandomAccessStream memory = new())
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, memory);
encoder.SetSoftwareBitmap(softwareBitmap);
await encoder.FlushAsync();
- Clipboard.SetBitmapStream(memory);
+
+ try
+ {
+ Clipboard.SetBitmapStream(memory);
+ clipboardOpened = true;
+ }
+ catch (COMException)
+ {
+ // CLIPBRD_E_CANT_OPEN
+ }
}
- infoBarService.Success("已导出到剪贴板");
+ if (clipboardOpened)
+ {
+ infoBarService.Success("已导出到剪贴板");
+ }
+ else
+ {
+ infoBarService.Warning("打开剪贴板失败");
+ }
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/CultivationViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/CultivationViewModel.cs
index cd3c3707..25381245 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/CultivationViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/CultivationViewModel.cs
@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Cultivation;
using Snap.Hutao.Model.Entity;
@@ -20,14 +18,13 @@ namespace Snap.Hutao.ViewModel;
/// 养成视图模型
///
[Injection(InjectAs.Scoped)]
-internal class CultivationViewModel : ObservableObject, ISupportCancellation
+internal class CultivationViewModel : Abstraction.ViewModel
{
private readonly ICultivationService cultivationService;
private readonly IInfoBarService infoBarService;
private readonly IMetadataService metadataService;
private readonly ILogger logger;
- private bool isInitialized;
private ObservableCollection? projects;
private CultivateProject? selectedProject;
private List? inventoryItems;
@@ -63,14 +60,6 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation
NavigateToPageCommand = new RelayCommand(NavigateToPage);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
- ///
- /// 是否初始化完成
- ///
- public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
-
///
/// 项目
///
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
index 0d006ac8..76703968 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using Snap.Hutao.Control;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Factory.Abstraction;
@@ -23,7 +21,7 @@ namespace Snap.Hutao.ViewModel;
/// 实时便笺视图模型
///
[Injection(InjectAs.Scoped)]
-internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
+internal class DailyNoteViewModel : Abstraction.ViewModel
{
private readonly IUserService userService;
private readonly IDailyNoteService dailyNoteService;
@@ -73,9 +71,6 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
DailyNoteVerificationCommand = asyncRelayCommandFactory.Create(VerifyDailyNoteVerificationAsync);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 刷新时间
///
@@ -136,7 +131,7 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
///
/// 用户与角色集合
///
- public ObservableCollection? UserAndRoles { get => userAndUids; set => userAndUids = value; }
+ public ObservableCollection? UserAndUids { get => userAndUids; set => userAndUids = value; }
///
/// 实时便笺集合
@@ -175,7 +170,7 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
- UserAndRoles = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
+ UserAndUids = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
refreshSecondsEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteRefreshSeconds, "480");
selectedRefreshTime = refreshTimes.Single(t => t.Value == refreshSecondsEntry.GetInt32());
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
index 870a31a4..a1db507c 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
-using Snap.Hutao.Control;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Factory.Abstraction;
@@ -22,7 +20,7 @@ namespace Snap.Hutao.ViewModel;
/// 祈愿记录视图模型
///
[Injection(InjectAs.Scoped)]
-internal class GachaLogViewModel : ObservableObject, ISupportCancellation
+internal class GachaLogViewModel : Abstraction.ViewModel
{
private readonly IGachaLogService gachaLogService;
private readonly IInfoBarService infoBarService;
@@ -35,7 +33,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
private GachaStatistics? statistics;
private bool isAggressiveRefresh;
private HistoryWish? selectedHistoryWish;
- private bool isInitialized;
///
/// 构造一个新的祈愿记录视图模型
@@ -69,9 +66,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 存档集合
///
@@ -112,11 +106,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
///
public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); }
- ///
- /// 初始化是否完成
- ///
- public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
-
///
/// 页面加载命令
///
@@ -154,13 +143,27 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
- if (await gachaLogService.InitializeAsync().ConfigureAwait(true))
+ try
{
- Archives = gachaLogService.GetArchiveCollection();
- SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
+ if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(true))
+ {
+ ObservableCollection archives;
- await ThreadHelper.SwitchToMainThreadAsync();
- IsInitialized = true;
+ ThrowIfViewDisposed();
+ using (await DisposeLock.EnterAsync().ConfigureAwait(false))
+ {
+ ThrowIfViewDisposed();
+ archives = await gachaLogService.GetArchiveCollectionAsync().ConfigureAwait(false);
+ }
+
+ await ThreadHelper.SwitchToMainThreadAsync();
+ Archives = archives;
+ SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
+ IsInitialized = true;
+ }
+ }
+ catch (OperationCanceledException)
+ {
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
index 285324b0..3414b98b 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
-using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Service.Hutao;
@@ -14,7 +12,7 @@ namespace Snap.Hutao.ViewModel;
/// 胡桃数据库视图模型
///
[Injection(InjectAs.Scoped)]
-internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
+internal class HutaoDatabaseViewModel : Abstraction.ViewModel
{
private readonly IHutaoCache hutaoCache;
@@ -28,7 +26,6 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
/// 构造一个新的胡桃数据库视图模型
///
/// 胡桃服务缓存
- /// 元数据服务
/// 异步命令工厂
public HutaoDatabaseViewModel(IHutaoCache hutaoCache, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
@@ -37,9 +34,6 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 角色使用率
///
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
index b4f4186f..e0d7860e 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
@@ -1,11 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
-using Snap.Hutao.Control;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Factory.Abstraction;
@@ -26,7 +24,7 @@ namespace Snap.Hutao.ViewModel;
/// 启动游戏视图模型
///
[Injection(InjectAs.Scoped)]
-internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
+internal class LaunchGameViewModel : Abstraction.ViewModel
{
///
/// 启动游戏目标 Uid
@@ -44,8 +42,7 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
{
new LaunchScheme(name: "官方服 | 天空岛", channel: "1", subChannel: "1", launcherId: "18"),
new LaunchScheme(name: "渠道服 | 世界树", channel: "14", subChannel: "0", launcherId: "17"),
-
- // new LaunchScheme(name: "国际服 | 暂不支持", channel: "1", subChannel: "0"),
+ new LaunchScheme(name: "国际服 | 部分支持", channel: "1", subChannel: "0", launcherId: "unknown"),
};
private LaunchScheme? selectedScheme;
@@ -84,9 +81,6 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
AttachGameAccountCommand = new RelayCommand(AttachGameAccountToCurrentUserGameRole);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 已知的服务器方案
///
@@ -217,22 +211,32 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
- bool gameExists = File.Exists(gameService.GetGamePathSkipLocator());
-
- if (gameExists)
+ if (File.Exists(gameService.GetGamePathSkipLocator()))
{
- MultiChannel multi = gameService.GetMultiChannel();
- SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
- GameAccounts = gameService.GetGameAccountCollection();
-
- // Sync uid
- if (memoryCache.TryGetValue(DesiredUid, out object? value) && value is string uid)
+ try
{
- SelectedGameAccount = GameAccounts.SingleOrDefault(g => g.AttachUid == uid);
- }
+ ThrowIfViewDisposed();
+ using (await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(true))
+ {
+ ThrowIfViewDisposed();
- // Sync from Settings
- RetiveSetting();
+ MultiChannel multi = gameService.GetMultiChannel();
+ SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
+ GameAccounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(true);
+
+ // Sync uid
+ if (memoryCache.TryGetValue(DesiredUid, out object? value) && value is string uid)
+ {
+ SelectedGameAccount = GameAccounts.SingleOrDefault(g => g.AttachUid == uid);
+ }
+
+ // Sync from Settings
+ RetiveSetting();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
}
else
{
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs
index 7e638c9f..4a244547 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs
@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.Database;
@@ -23,7 +22,7 @@ namespace Snap.Hutao.ViewModel;
/// 设置视图模型
///
[Injection(InjectAs.Scoped)]
-internal class SettingViewModel : ObservableObject
+internal class SettingViewModel : Abstraction.ViewModel
{
private readonly AppDbContext appDbContext;
private readonly IGameService gameService;
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyssRecordViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyssRecordViewModel.cs
index 04c863a1..ecfacbc1 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyssRecordViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyssRecordViewModel.cs
@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
-using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Message;
using Snap.Hutao.Model.Binding.SpiralAbyss;
@@ -15,6 +14,7 @@ using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.SpiralAbyss;
using Snap.Hutao.Service.User;
+using Snap.Hutao.ViewModel.Abstraction;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Model.Post;
using System.Collections.ObjectModel;
@@ -25,7 +25,7 @@ namespace Snap.Hutao.ViewModel;
/// 深渊记录视图模型
///
[Injection(InjectAs.Scoped)]
-internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellation, IRecipient
+internal class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient
{
private readonly ISpiralAbyssRecordService spiralAbyssRecordService;
private readonly IMetadataService metadataService;
@@ -62,9 +62,6 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
messenger.Register(this);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 深渊记录
///
@@ -126,33 +123,61 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
{
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idAvatarMap = AvatarIds.ExtendAvatars(idAvatarMap);
- if (userService.Current?.SelectedUserGameRole != null)
+
+ if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
- await UpdateSpiralAbyssCollectionAsync(UserAndUid.FromUser(userService.Current)).ConfigureAwait(false);
+ await UpdateSpiralAbyssCollectionAsync(userAndUid).ConfigureAwait(false);
+ }
+ else
+ {
+ Ioc.Default.GetRequiredService().Warning("请先选中角色与账号");
}
}
}
private async Task UpdateSpiralAbyssCollectionAsync(UserAndUid userAndUid)
{
- ObservableCollection temp = await spiralAbyssRecordService
- .GetSpiralAbyssCollectionAsync(userAndUid)
- .ConfigureAwait(false);
+ ObservableCollection? temp = null;
+ try
+ {
+ ThrowIfViewDisposed();
+ using (await DisposeLock.EnterAsync().ConfigureAwait(false))
+ {
+ ThrowIfViewDisposed();
+ temp = await spiralAbyssRecordService
+ .GetSpiralAbyssCollectionAsync(userAndUid)
+ .ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
await ThreadHelper.SwitchToMainThreadAsync();
SpiralAbyssEntries = temp;
- SelectedEntry = SpiralAbyssEntries.FirstOrDefault();
+ SelectedEntry = SpiralAbyssEntries?.FirstOrDefault();
}
private async Task RefreshAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
- if (userService.Current?.SelectedUserGameRole != null)
+ if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
- await spiralAbyssRecordService
- .RefreshSpiralAbyssAsync(UserAndUid.FromUser(userService.Current))
- .ConfigureAwait(false);
+ try
+ {
+ ThrowIfViewDisposed();
+ using (await DisposeLock.EnterAsync().ConfigureAwait(false))
+ {
+ ThrowIfViewDisposed();
+ await spiralAbyssRecordService
+ .RefreshSpiralAbyssAsync(userAndUid)
+ .ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
await ThreadHelper.SwitchToMainThreadAsync();
SelectedEntry = SpiralAbyssEntries?.FirstOrDefault();
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
index 0b1bc9bc..a8790ebe 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using CommunityToolkit.Mvvm.ComponentModel;
-using Snap.Hutao.Control;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Bits;
using Snap.Hutao.Extension;
@@ -19,7 +17,7 @@ namespace Snap.Hutao.ViewModel;
/// 测试视图模型
///
[Injection(InjectAs.Scoped)]
-internal class TestViewModel : ObservableObject, ISupportCancellation
+internal class TestViewModel : Abstraction.ViewModel
{
///
/// 构造一个新的测试视图模型
@@ -33,9 +31,6 @@ internal class TestViewModel : ObservableObject, ISupportCancellation
DownloadStaticFileCommand = asyncRelayCommandFactory.Create(DownloadStaticFileAsync);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 打开游戏社区记录对话框命令
///
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs
index 680660f4..7d2268c6 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs
@@ -156,7 +156,14 @@ internal class UserViewModel : ObservableObject
private void LoginMihoyoUser()
{
- Ioc.Default.GetRequiredService().Navigate(INavigationAwaiter.Default);
+ if (Core.WebView2Helper.IsSupported)
+ {
+ Ioc.Default.GetRequiredService().Navigate(INavigationAwaiter.Default);
+ }
+ else
+ {
+ infoBarService.Warning("尚未安装 WebView2 Runtime");
+ }
}
private async Task RemoveUserAsync(User? user)
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
index 54c9d47d..df00071c 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
@@ -77,6 +77,8 @@ internal class WelcomeViewModel : ObservableObject
DownloadSummaries = new(downloadSummaries);
+ // Cancel all previous created jobs
+ serviceProvider.GetRequiredService().CancelAllJobs();
await Task.WhenAll(downloadSummaries.Select(d => d.DownloadAndExtractAsync())).ConfigureAwait(true);
serviceProvider.GetRequiredService().Send(new Message.WelcomeStateCompleteMessage());
@@ -173,7 +175,7 @@ internal class WelcomeViewModel : ObservableObject
private void UpdateProgressStatus(ProgressUpdateStatus status)
{
Description = $"{Converters.ToFileSizeString(status.BytesRead)}/{Converters.ToFileSizeString(status.TotalBytes)}";
- ProgressValue = (double)status.BytesRead / status.TotalBytes;
+ ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesRead / status.TotalBytes;
}
private void ExtractFiles(string file)
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiAvatarViewModel.cs
index faf0c324..7c8178ce 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiAvatarViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiAvatarViewModel.cs
@@ -33,7 +33,7 @@ namespace Snap.Hutao.ViewModel;
/// 角色资料视图模型
///
[Injection(InjectAs.Scoped)]
-internal class WikiAvatarViewModel : ObservableObject
+internal class WikiAvatarViewModel : Abstraction.ViewModel
{
private readonly IMetadataService metadataService;
private readonly IHutaoCache hutaoCache;
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiWeaponViewModel.cs
index b413542d..12b7d439 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiWeaponViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiWeaponViewModel.cs
@@ -5,7 +5,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.UI;
using Microsoft.Extensions.Primitives;
-using Snap.Hutao.Control;
using Snap.Hutao.Extension;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Cultivation;
@@ -19,6 +18,7 @@ using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
+using Snap.Hutao.ViewModel.Abstraction;
using Snap.Hutao.Web.Response;
using System.Collections.Immutable;
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
@@ -31,7 +31,7 @@ namespace Snap.Hutao.ViewModel;
/// 武器资料视图模型
///
[Injection(InjectAs.Scoped)]
-internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
+internal class WikiWeaponViewModel : Abstraction.ViewModel
{
private readonly List skippedWeapons = new()
{
@@ -62,9 +62,6 @@ internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
FilterCommand = new RelayCommand(ApplyFilter);
}
- ///
- public CancellationToken CancellationToken { get; set; }
-
///
/// 角色列表
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
index 085e7acf..c230620f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
@@ -352,25 +352,7 @@ public class MiHoYoJSInterface
JsParam param = JsonSerializer.Deserialize(message)!;
logger.LogInformation("[OnMessage]\nMethod : {method}\nPayload : {payload}\nCallback: {callback}", param.Method, param.Payload, param.Callback);
- IJsResult? result = param.Method switch
- {
- "closePage" => ClosePage(param),
- "configure_share" => ConfigureShare(param),
- "eventTrack" => null,
- "getActionTicket" => await GetActionTicketAsync(param).ConfigureAwait(false),
- "getCookieInfo" => GetCookieInfo(param),
- "getCookieToken" => await GetCookieTokenAsync(param).ConfigureAwait(false),
- "getDS" => GetDynamicSecrectV1(param),
- "getDS2" => GetDynamicSecrectV2(param),
- "getHTTPRequestHeaders" => GetHttpRequestHeader(param),
- "getStatusBarHeight" => GetStatusBarHeight(param),
- "getUserInfo" => GetUserInfo(param),
- "hideLoading" => null,
- "login" => null,
- "pushPage" => PushPage(param),
- "showLoading" => null,
- _ => logger.LogWarning("Unhandled Message Type: {method}", param.Method),
- };
+ IJsResult? result = await TryGetJsResultFromJsParamAsync(param).ConfigureAwait(false);
if (result != null && param.Callback != null)
{
@@ -378,6 +360,37 @@ public class MiHoYoJSInterface
}
}
+ private async Task 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("Unhandled Message Type: {method}", param.Method),
+ };
+ }
+ catch (ObjectDisposedException)
+ {
+ // The dialog is already closed.
+ return null;
+ }
+ }
+
private void OnDOMContentLoaded(CoreWebView2 coreWebView2, CoreWebView2DOMContentLoadedEventArgs args)
{
coreWebView2.ExecuteScriptAsync(HideScrollBarScript).AsTask().SafeForget(logger);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs
index 713157bd..13393c5b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs
@@ -55,6 +55,7 @@ internal class GameRecordClient
// We hava a verification procedure to handle
if (resp?.ReturnCode == (int)KnownReturnCode.CODE1034)
{
+ resp.Message = "请求失败,请前往「米游社-我的角色-实时便笺」页面查看";
CardVerifier cardVerifier = Ioc.Default.GetRequiredService();
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);
}
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs b/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs
index cb6138a4..1fa785da 100644
--- a/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs
@@ -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));
}
+
+ ///
+ /// 尺寸
+ ///
+ /// 源
+ /// 结果
+ public static int Size(this RectInt32 rectInt32)
+ {
+ return rectInt32.Width * rectInt32.Height;
+ }
+
+ ///
+ /// 尺寸
+ ///
+ /// 源
+ /// 结果
+ public static int Size(this SizeInt32 sizeInt32)
+ {
+ return sizeInt32.Width * sizeInt32.Height;
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Win32/StructMarshal.cs b/src/Snap.Hutao/Snap.Hutao/Win32/StructMarshal.cs
index bbf6332e..601545a3 100644
--- a/src/Snap.Hutao/Snap.Hutao/Win32/StructMarshal.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Win32/StructMarshal.cs
@@ -4,6 +4,7 @@
using System.Numerics;
using Windows.Graphics;
using Windows.Win32.System.Diagnostics.ToolHelp;
+using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao.Win32;
@@ -21,6 +22,15 @@ internal static class StructMarshal
return new() { dwSize = (uint)sizeof(MODULEENTRY32) };
}
+ ///
+ /// 构造一个新的
+ ///
+ /// 新的实例
+ public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
+ {
+ return new() { length = (uint)sizeof(WINDOWPLACEMENT) };
+ }
+
///
/// 从 0,0 点构造一个新的
///