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 @@ - + - + - - - - + [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 点构造一个新的 ///