Compare commits

..

2 Commits

Author SHA1 Message Date
DismissedLight
5126337138 code style [skip ci] 2023-01-12 19:42:45 +08:00
DismissedLight
4d634d3264 improve concurrent execution 2023-01-12 19:38:06 +08:00
72 changed files with 801 additions and 525 deletions

View File

@@ -1,15 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
/// <summary>
/// 指示此类支持取消任务
/// </summary>
public interface ISupportCancellation
{
/// <summary>
/// 用于通知事件取消的取消令牌
/// </summary>
CancellationToken CancellationToken { get; set; }
}

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.Control;
@@ -33,9 +34,9 @@ public class ScopedPage : Page
/// </summary>
/// <typeparam name="TViewModel">视图模型类型</typeparam>
public void InitializeWith<TViewModel>()
where TViewModel : class, ISupportCancellation
where TViewModel : class, IViewModel
{
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
DataContext = viewModel;
}
@@ -59,11 +60,22 @@ public class ScopedPage : Page
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
viewLoadingCancellationTokenSource.Cancel();
using (viewLoadingCancellationTokenSource)
{
// Cancel tasks executed by the view model
viewLoadingCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext;
// Try dispose scope when page is not presented
serviceScope.Dispose();
viewLoadingCancellationTokenSource.Dispose();
using (SemaphoreSlim locker = viewModel.DisposeLock)
{
// Wait to ensure viewmodel operation is completed
locker.Wait();
viewModel.IsViewDisposed = true;
// Dispose the scope
serviceScope.Dispose();
}
}
}
/// <inheritdoc/>

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Control;
/// </summary>
/// <typeparam name="TFrom">源类型</typeparam>
/// <typeparam name="TTo">目标类型</typeparam>
public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
public abstract class ValueConverter<TFrom, TTo> : IValueConverter
{
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language)
@@ -23,7 +23,7 @@ public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
catch (Exception ex)
{
Ioc.Default
.GetRequiredService<ILogger<ValueConverterBase<TFrom, TTo>>>()
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
.LogError(ex, "值转换器异常");
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Linq.Expressions;
namespace Snap.Hutao.Core.ExpressionService;
/// <summary>
/// 枚举帮助类
/// </summary>
public static class EnumExtension
{
/// <summary>
/// 判断枚举是否有对应的Flag
/// </summary>
/// <typeparam name="T">枚举类型</typeparam>
/// <param name="enum">待检查的枚举</param>
/// <param name="value">值</param>
/// <returns>是否有对应的Flag</returns>
public static bool HasOption<T>(this T @enum, T value)
where T : struct, Enum
{
return ExpressionCache<T>.Entry(@enum, value);
}
private static class ExpressionCache<T>
{
public static readonly Func<T, T, bool> Entry = Get();
private static Func<T, T, bool> Get()
{
ParameterExpression paramSource = Expression.Parameter(typeof(T));
ParameterExpression paramValue = Expression.Parameter(typeof(T));
BinaryExpression logicalAnd = Expression.AndAssign(paramSource, paramValue);
BinaryExpression equal = Expression.Equal(logicalAnd, paramValue);
// 生成一个源类型入,目标类型出的 lamdba
return Expression.Lambda<Func<T, T, bool>>(equal, paramSource, paramValue).Compile();
}
}
}

View File

@@ -17,6 +17,11 @@ namespace Snap.Hutao.Core.IO.Bits;
[SuppressMessage("", "SA1600")]
internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{
/// <summary>
/// 任务名称前缀
/// </summary>
public const string JobNamePrefix = "SnapHutaoBitsJob";
private const uint BitsEngineNoProgressTimeout = 120;
private const int MaxResumeAttempts = 10;
@@ -43,12 +48,14 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath)
{
ILogger<BitsJob> service = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
string text = $"BitsDownloadJob - {uri}";
string text = $"{JobNamePrefix} - {uri}";
IBackgroundCopyJob ppJob;
try
{
backgroundCopyManager.CreateJob(text, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out ppJob);
ppJob.SetNotifyFlags(11u);
// BG_NOTIFY_JOB_TRANSFERRED & BG_NOTIFY_JOB_ERROR & BG_NOTIFY_JOB_MODIFICATION
ppJob.SetNotifyFlags(0b1011);
ppJob.SetNoProgressTimeout(BitsEngineNoProgressTimeout);
ppJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND);
ppJob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, null, null);

View File

@@ -2,6 +2,9 @@
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
namespace Snap.Hutao.Core.IO.Bits;
@@ -40,6 +43,41 @@ internal class BitsManager
return new(result, tempFile);
}
/// <summary>
/// 取消所有先前创建的任务
/// </summary>
public void CancelAllJobs()
{
IBackgroundCopyManager value;
try
{
value = lazyBackgroundCopyManager.Value;
}
catch (Exception ex)
{
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
return;
}
value.EnumJobs(0, out IEnumBackgroundCopyJobs pJobs);
pJobs.GetCount(out uint count);
List<IBackgroundCopyJob> jobsToCancel = new();
for (int i = 0; i < count; i++)
{
uint actualFetched = 0;
pJobs.Next(1, out IBackgroundCopyJob pJob, ref actualFetched);
pJob.GetDisplayName(out PWSTR name);
if (name.AsSpan().StartsWith(BitsJob.JobNamePrefix))
{
jobsToCancel.Add(pJob);
}
}
jobsToCancel.ForEach(job => job.Cancel());
}
private bool DownloadCore(Uri uri, string tempFile, Action<ProgressUpdateStatus> progress, CancellationToken token)
{
IBackgroundCopyManager value;
@@ -48,29 +86,37 @@ internal class BitsManager
{
value = lazyBackgroundCopyManager.Value;
}
catch (System.Exception ex)
catch (Exception ex)
{
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
return false;
}
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
try
{
try
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
{
bitsJob.WaitForCompletion(progress, token);
}
catch (System.Exception ex)
{
logger?.LogWarning(ex, "BITS download failed:");
return false;
}
try
{
bitsJob.WaitForCompletion(progress, token);
}
catch (Exception ex)
{
logger?.LogWarning(ex, "BITS download failed:");
return false;
}
if (bitsJob.ErrorCode != 0)
{
return false;
if (bitsJob.ErrorCode != 0)
{
return false;
}
}
}
catch (COMException)
{
// BITS job creation failed
return false;
}
return true;
}

View File

@@ -20,10 +20,10 @@ public static class DispatcherQueueExtension
using (ManualResetEventSlim blockEvent = new())
{
dispatcherQueue.TryEnqueue(() =>
{
action();
blockEvent.Set();
});
{
action();
blockEvent.Set();
});
blockEvent.Wait();
}

View File

@@ -43,9 +43,6 @@ public readonly struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueue
/// <inheritdoc/>
public void OnCompleted(Action continuation)
{
dispatherQueue.TryEnqueue(() =>
{
continuation();
});
dispatherQueue.TryEnqueue(() => continuation());
}
}

View File

@@ -12,10 +12,19 @@ public static class SemaphoreSlimExtensions
/// 异步进入信号量
/// </summary>
/// <param name="semaphoreSlim">信号量</param>
/// <param name="token">取消令牌</param>
/// <returns>可释放的对象,用于释放信号量</returns>
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim)
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim, CancellationToken token = default)
{
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
await semaphoreSlim.WaitAsync(token).ConfigureAwait(false);
}
catch (ObjectDisposedException ex)
{
throw new OperationCanceledException("信号量已经被释放,操作取消", ex);
}
return new SemaphoreSlimReleaser(semaphoreSlim);
}

View File

@@ -31,4 +31,14 @@ internal static class ThreadHelper
{
return new(Program.DispatcherQueue!);
}
/// <summary>
/// 在主线程上同步等待执行操作
/// </summary>
/// <param name="action">操作</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InvokeOnMainThread(Action action)
{
Program.DispatcherQueue!.Invoke(action);
}
}

View File

@@ -135,6 +135,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
logger.LogInformation(EventIds.WindowState, "Postion: [{pos}], Size: [{size}]", pos, size);
// appWindow.Show(true);
// appWindow.Show can't bring window to top.
window.Activate();
systemBackdrop = new(window);
@@ -190,7 +191,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
}
else
{
double scale = Persistence.GetScaleForWindow(handle);
double scale = Persistence.GetScaleForWindowHandle(handle);
// 48 is the navigation button leftInset
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), titleBar.ActualSize).Scale(scale);

View File

@@ -8,6 +8,7 @@ using Snap.Hutao.Win32;
using System.Runtime.InteropServices;
using Windows.Graphics;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing;
@@ -22,28 +23,24 @@ internal static class Persistence
/// </summary>
/// <param name="appWindow">应用窗体</param>
/// <param name="persistSize">持久化尺寸</param>
/// <param name="size">初始尺寸</param>
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
/// <param name="initialSize">初始尺寸</param>
public static unsafe void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 initialSize)
{
// Set first launch size.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd);
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
if (persistSize)
{
RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (persistedSize.Width * persistedSize.Height > 848 * 524)
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (persistedRect.Size() >= initialSize.Size())
{
rect = persistedSize;
rect = persistedRect;
}
}
unsafe
{
TransformToCenterScreen(&rect);
}
TransformToCenterScreen(&rect);
appWindow.MoveAndResize(rect);
}
@@ -53,7 +50,15 @@ internal static class Persistence
/// <param name="appWindow">应用窗体</param>
public static void Save(AppWindow appWindow)
{
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
GetWindowPlacement(hwnd, ref windowPlacement);
// prevent save value when we are maximized.
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
{
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
}
}
/// <summary>
@@ -61,7 +66,7 @@ internal static class Persistence
/// </summary>
/// <param name="hwnd">窗体句柄</param>
/// <returns>缩放比</returns>
public static double GetScaleForWindow(HWND hwnd)
public static double GetScaleForWindowHandle(HWND hwnd)
{
uint dpi = GetDpiForWindow(hwnd);
return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero);
@@ -69,7 +74,7 @@ internal static class Persistence
private static SizeInt32 TransformSizeForWindow(SizeInt32 size, HWND hwnd)
{
double scale = GetScaleForWindow(hwnd);
double scale = GetScaleForWindowHandle(hwnd);
return new((int)(size.Width * scale), (int)(size.Height * scale));
}

View File

@@ -87,7 +87,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
{
case WM_GETMINMAXINFO:
{
double scalingFactor = Persistence.GetScaleForWindow(hwnd);
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
break;
}

View File

@@ -51,7 +51,7 @@ public class Avatar : ICalculableSource<ICalculableAvatar>
/// <summary>
/// 武器
/// </summary>
public Weapon Weapon { get; set; } = default!;
public Weapon? Weapon { get; set; } = default!;
/// <summary>
/// 圣遗物列表

View File

@@ -174,6 +174,10 @@ public class User : ObservableObject
Cookie ltokenCookie = Cookie.Parse($"ltuid={Entity.Aid};ltoken={ltokenResponse.Data.Ltoken}");
Entity.Ltoken = ltokenCookie;
}
else
{
return false;
}
}
Response<ActionTicketWrapper> actionTicketResponse = await scope.ServiceProvider
@@ -194,6 +198,14 @@ public class User : ObservableObject
{
UserGameRoles = userGameRolesResponse.Data.List;
}
else
{
return false;
}
}
else
{
return false;
}
// 自动填充 CookieToken
@@ -209,6 +221,10 @@ public class User : ObservableObject
Cookie cookieTokenCookie = Cookie.Parse($"account_id={Entity.Aid};cookie_token={cookieTokenResponse.Data.CookieToken}");
Entity.CookieToken = cookieTokenCookie;
}
else
{
return false;
}
}
}

View File

@@ -3,15 +3,16 @@
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Configuration;
using System.Diagnostics;
namespace Snap.Hutao.Model.Entity.Database;
/// <summary>
/// 应用程序数据库上下文
/// </summary>
[DebuggerDisplay("Id = {ContextId}")]
public sealed class AppDbContext : DbContext
{
private readonly Guid contextId;
private readonly ILogger<AppDbContext>? logger;
/// <summary>
@@ -31,9 +32,8 @@ public sealed class AppDbContext : DbContext
public AppDbContext(DbContextOptions<AppDbContext> options, ILogger<AppDbContext> logger)
: this(options)
{
contextId = Guid.NewGuid();
this.logger = logger;
logger.LogInformation("AppDbContext[{id}] created.", contextId);
logger.LogInformation("AppDbContext[{id}] created.", ContextId);
}
/// <summary>
@@ -135,7 +135,7 @@ public sealed class AppDbContext : DbContext
public override void Dispose()
{
base.Dispose();
logger?.LogInformation("AppDbContext[{id}] disposed.", contextId);
logger?.LogInformation("AppDbContext[{id}] disposed.", ContextId);
}
/// <inheritdoc/>

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色头像转换器
/// </summary>
internal class AchievementIconConverter : ValueConverterBase<string, Uri>
internal class AchievementIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色卡片转换器
/// </summary>
internal class AvatarCardConverter : ValueConverterBase<string, Uri>
internal class AvatarCardConverter : ValueConverter<string, Uri>
{
private const string CostumeCard = "UI_AvatarIcon_Costume_Card.png";
private static readonly Uri UIAvatarIconCostumeCard = new(Web.HutaoEndpoints.StaticFile("AvatarCard", CostumeCard));

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色头像转换器
/// </summary>
internal class AvatarIconConverter : ValueConverterBase<string, Uri>
internal class AvatarIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色名片转换器
/// </summary>
internal class AvatarNameCardPicConverter : ValueConverterBase<Avatar.Avatar?, Uri>
internal class AvatarNameCardPicConverter : ValueConverter<Avatar.Avatar?, Uri>
{
/// <summary>
/// 从角色转换到名片

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色侧面头像转换器
/// </summary>
internal class AvatarSideIconConverter : ValueConverterBase<string, Uri>
internal class AvatarSideIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 描述参数解析器
/// </summary>
internal sealed partial class DescParamDescriptor : ValueConverterBase<DescParam, IList<LevelParam<string, ParameterInfo>>>
internal sealed partial class DescParamDescriptor : ValueConverter<DescParam, IList<LevelParam<string, ParameterInfo>>>
{
/// <summary>
/// 获取特定等级的解释

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 元素名称图标转换器
/// </summary>
internal class ElementNameIconConverter : ValueConverterBase<string, Uri>
internal class ElementNameIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 将中文元素名称转换为图标链接

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 表情图片转换器
/// </summary>
internal class EmotionIconConverter : ValueConverterBase<string, Uri>
internal class EmotionIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 武器图片转换器
/// </summary>
internal class EquipIconConverter : ValueConverterBase<string, Uri>
internal class EquipIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 立绘图标转换器
/// </summary>
internal class GachaAvatarIconConverter : ValueConverterBase<string, Uri>
internal class GachaAvatarIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 立绘转换器
/// </summary>
internal class GachaAvatarImgConverter : ValueConverterBase<string, Uri>
internal class GachaAvatarImgConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 武器祈愿图片转换器
/// </summary>
internal class GachaEquipIconConverter : ValueConverterBase<string, Uri>
internal class GachaEquipIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 物品图片转换器
/// </summary>
internal class ItemIconConverter : ValueConverterBase<string, Uri>
internal class ItemIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 基础属性翻译器
/// </summary>
internal class PropertyInfoDescriptor : ValueConverterBase<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
internal class PropertyInfoDescriptor : ValueConverter<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
{
/// <summary>
/// 格式化对

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 品质颜色转换器
/// </summary>
internal class QualityColorConverter : ValueConverterBase<ItemQuality, Color>
internal class QualityColorConverter : ValueConverter<ItemQuality, Color>
{
/// <inheritdoc/>
public override Color Convert(ItemQuality from)

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 物品等级转换器
/// </summary>
internal class QualityConverter : ValueConverterBase<ItemQuality, Uri>
internal class QualityConverter : ValueConverter<ItemQuality, Uri>
{
/// <inheritdoc/>
public override Uri Convert(ItemQuality from)

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 武器图片转换器
/// </summary>
internal class RelicIconConverter : ValueConverterBase<string, Uri>
internal class RelicIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 技能图标转换器
/// </summary>
internal class SkillIconConverter : ValueConverterBase<string, Uri>
internal class SkillIconConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 元素名称图标转换器
/// </summary>
internal class WeaponTypeIconConverter : ValueConverterBase<WeaponType, Uri>
internal class WeaponTypeIconConverter : ValueConverter<WeaponType, Uri>
{
/// <summary>
/// 将武器类型转换为图标链接

View File

@@ -28,6 +28,7 @@ CoWaitForMultipleObjects
// USER32
FindWindowEx
GetDpiForWindow
GetWindowPlacement
// COM BITS
BackgroundCopyManager

View File

@@ -12,7 +12,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.3.7.0" />
Version="1.3.9.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.InterChange.Achievement;
using System.Collections.ObjectModel;
@@ -52,21 +53,23 @@ internal class AchievementService : IAchievementService
}
/// <inheritdoc/>
public ObservableCollection<EntityArchive> GetArchiveCollection()
public async Task<ObservableCollection<EntityArchive>> GetArchiveCollectionAsync()
{
return archiveCollection ??= new(appDbContext.AchievementArchives.AsNoTracking().ToList());
await ThreadHelper.SwitchToMainThreadAsync();
return archiveCollection ??= appDbContext.AchievementArchives.AsNoTracking().ToObservableCollection();
}
/// <inheritdoc/>
public async Task RemoveArchiveAsync(EntityArchive archive)
{
// Sync cache
// Keep this on main thread.
await ThreadHelper.SwitchToMainThreadAsync();
archiveCollection!.Remove(archive);
// Sync database
await ThreadHelper.SwitchToBackgroundAsync();
// Cascade deleted the achievements.
await appDbContext.AchievementArchives
.Where(a => a.InnerId == archive.InnerId)
.ExecuteDeleteAsync()

View File

@@ -35,10 +35,10 @@ internal interface IAchievementService
List<BindingAchievement> GetAchievements(EntityArchive archive, IList<MetadataAchievement> metadata);
/// <summary>
/// 获取用于绑定的成就存档集合
/// 异步获取用于绑定的成就存档集合
/// </summary>
/// <returns>成就存档集合</returns>
ObservableCollection<EntityArchive> GetArchiveCollection();
Task<ObservableCollection<EntityArchive>> GetArchiveCollectionAsync();
/// <summary>
/// 异步导入UIAF数据

View File

@@ -0,0 +1,227 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.AvatarInfo.Composer;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using Snap.Hutao.Web.Response;
using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar;
using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
using RecordCharacter = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar.Character;
using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 角色信息数据库操作
/// </summary>
public class AvatarInfoDbOperation
{
private readonly AppDbContext appDbContext;
/// <summary>
/// 构造一个新的角色信息数据库操作
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
public AvatarInfoDbOperation(AppDbContext appDbContext)
{
this.appDbContext = appDbContext;
}
/// <summary>
/// 更新数据库角色信息
/// </summary>
/// <param name="uid">uid</param>
/// <param name="webInfos">Enka信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public List<EnkaAvatarInfo> UpdateDbAvatarInfos(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
{
token.ThrowIfCancellationRequested();
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.ToList();
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
foreach (EnkaAvatarInfo webInfo in webInfos)
{
if (AvatarIds.IsPlayer(webInfo.AvatarId))
{
continue;
}
token.ThrowIfCancellationRequested();
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
if (entity == null)
{
entity = ModelAvatarInfo.Create(uid, webInfo);
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = webInfo;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
token.ThrowIfCancellationRequested();
return GetDbAvatarInfos(uid);
}
/// <summary>
/// 米游社我的角色方式 更新数据库角色信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
{
token.ThrowIfCancellationRequested();
string uid = userAndUid.Uid.Value;
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.ToList();
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
Response<RecordPlayerInfo> playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (playerInfoResponse.IsOk())
{
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
if (charactersResponse.IsOk())
{
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
foreach (RecordCharacter character in characters)
{
if (AvatarIds.IsPlayer(character.Id))
{
continue;
}
token.ThrowIfCancellationRequested();
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id);
if (entity == null)
{
EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id };
avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false);
entity = ModelAvatarInfo.Create(uid, avatarInfo);
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false);
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
}
}
return GetDbAvatarInfos(uid);
}
/// <summary>
/// 米游社养成计算方式 更新数据库角色信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
{
token.ThrowIfCancellationRequested();
string uid = userAndUid.Uid.Value;
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.ToList();
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
CalculateClient calculateClient = Ioc.Default.GetRequiredService<CalculateClient>();
List<CalculateAvatar> avatars = await calculateClient.GetAvatarsAsync(userAndUid, token).ConfigureAwait(false);
CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService<CalculateAvatarDetailAvatarInfoComposer>();
foreach (CalculateAvatar avatar in avatars)
{
if (AvatarIds.IsPlayer(avatar.Id))
{
continue;
}
token.ThrowIfCancellationRequested();
Response<AvatarDetail> detailAvatarResponse = await calculateClient.GetAvatarDetailAsync(userAndUid, avatar, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (!detailAvatarResponse.IsOk())
{
continue;
}
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
AvatarDetail detailAvatar = detailAvatarResponse.Data;
if (entity == null)
{
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatar.Id };
avatarInfo = await composer.ComposeAsync(avatarInfo, detailAvatar).ConfigureAwait(false);
entity = ModelAvatarInfo.Create(uid, avatarInfo);
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = await composer.ComposeAsync(entity.Info, detailAvatar).ConfigureAwait(false);
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
return GetDbAvatarInfos(uid);
}
/// <summary>
/// 获取数据库角色信息
/// </summary>
/// <param name="uid">Uid</param>
/// <returns>角色列表</returns>
public List<EnkaAvatarInfo> GetDbAvatarInfos(string uid)
{
return appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.Select(i => i.Info)
.ToList();
}
private void EnsureItemsAvatarIdDistinct(ref List<ModelAvatarInfo> dbInfos, string uid)
{
int distinctCount = dbInfos.Select(info => info.Info.AvatarId).ToHashSet().Count;
// Avatars are actually less than the list told us.
if (distinctCount < dbInfos.Count)
{
appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.ExecuteDelete();
dbInfos = new();
}
}
}

View File

@@ -1,28 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.AvatarInfo.Composer;
using Snap.Hutao.Service.AvatarInfo.Factory;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Enka;
using Snap.Hutao.Web.Enka.Model;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using Snap.Hutao.Web.Response;
using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar;
using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using EnkaPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo;
using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
using RecordCharacter = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar.Character;
using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
namespace Snap.Hutao.Service.AvatarInfo;
@@ -37,6 +27,8 @@ internal class AvatarInfoService : IAvatarInfoService
private readonly IMetadataService metadataService;
private readonly ILogger<AvatarInfoService> logger;
private readonly AvatarInfoDbOperation avatarInfoDbOperation;
/// <summary>
/// 构造一个新的角色信息服务
/// </summary>
@@ -54,6 +46,8 @@ internal class AvatarInfoService : IAvatarInfoService
this.metadataService = metadataService;
this.summaryFactory = summaryFactory;
this.logger = logger;
avatarInfoDbOperation = new(appDbContext);
}
/// <inheritdoc/>
@@ -74,23 +68,20 @@ internal class AvatarInfoService : IAvatarInfoService
return new(RefreshResult.APIUnavailable, null);
}
if (resp.IsValid)
{
IList<EnkaAvatarInfo> list = UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token);
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
return new(RefreshResult.Ok, summary);
}
else
if (!resp.IsValid)
{
return new(RefreshResult.ShowcaseNotOpen, null);
}
List<EnkaAvatarInfo> list = avatarInfoDbOperation.UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token);
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
return new(RefreshResult.Ok, summary);
}
case RefreshOption.RequestFromHoyolabGameRecord:
{
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
IList<EnkaAvatarInfo> list = await UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
List<EnkaAvatarInfo> list = await avatarInfoDbOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
return new(RefreshResult.Ok, summary);
}
@@ -98,7 +89,7 @@ internal class AvatarInfoService : IAvatarInfoService
case RefreshOption.RequestFromHoyolabCalculate:
{
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
IList<EnkaAvatarInfo> list = await UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
List<EnkaAvatarInfo> list = await avatarInfoDbOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
return new(RefreshResult.Ok, summary);
}
@@ -106,7 +97,8 @@ internal class AvatarInfoService : IAvatarInfoService
default:
{
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(userAndUid.Uid.Value), token).ConfigureAwait(false);
List<EnkaAvatarInfo> list = avatarInfoDbOperation.GetDbAvatarInfos(userAndUid.Uid.Value);
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
return new(RefreshResult.Ok, summary.Avatars.Count == 0 ? null : summary);
}
@@ -134,153 +126,4 @@ internal class AvatarInfoService : IAvatarInfoService
return summary;
}
private List<EnkaAvatarInfo> UpdateDbAvatarInfos(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
{
token.ThrowIfCancellationRequested();
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.ToList();
foreach (EnkaAvatarInfo webInfo in webInfos)
{
if (AvatarIds.IsPlayer(webInfo.AvatarId))
{
continue;
}
token.ThrowIfCancellationRequested();
// TODO: ensure the operation executes atomically
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
if (entity == null)
{
entity = ModelAvatarInfo.Create(uid, webInfo);
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = webInfo;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
token.ThrowIfCancellationRequested();
return GetDbAvatarInfos(uid);
}
private async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
{
string uid = userAndUid.Uid.Value;
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.ToList();
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
Response<RecordPlayerInfo> playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid)
.ConfigureAwait(false);
// TODO: We should not refresh if response is not correct here.
if (playerInfoResponse.IsOk())
{
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data)
.ConfigureAwait(false);
if (charactersResponse.IsOk())
{
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
foreach (RecordCharacter character in characters)
{
if (AvatarIds.IsPlayer(character.Id))
{
continue;
}
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id);
if (entity == null)
{
EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id };
avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false);
entity = ModelAvatarInfo.Create(uid, avatarInfo);
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false);
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
}
}
return GetDbAvatarInfos(uid);
}
private async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
{
string uid = userAndUid.Uid.Value;
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.ToList();
CalculateClient calculateClient = Ioc.Default.GetRequiredService<CalculateClient>();
List<CalculateAvatar> avatars = await calculateClient.GetAvatarsAsync(userAndUid).ConfigureAwait(false);
CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService<CalculateAvatarDetailAvatarInfoComposer>();
foreach (CalculateAvatar avatar in avatars)
{
if (AvatarIds.IsPlayer(avatar.Id))
{
continue;
}
Response<AvatarDetail> detailAvatarResponse = await calculateClient.GetAvatarDetailAsync(userAndUid, avatar).ConfigureAwait(false);
if (!detailAvatarResponse.IsOk())
{
continue;
}
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
AvatarDetail detailAvatar = detailAvatarResponse.Data;
if (entity == null)
{
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatar.Id };
avatarInfo = await composer.ComposeAsync(avatarInfo, detailAvatar).ConfigureAwait(false);
entity = ModelAvatarInfo.Create(uid, avatarInfo);
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = await composer.ComposeAsync(entity.Info, detailAvatar).ConfigureAwait(false);
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
return GetDbAvatarInfos(uid);
}
private List<EnkaAvatarInfo> GetDbAvatarInfos(string uid)
{
try
{
return appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.Select(i => i.Info)
.ToList();
}
catch (ObjectDisposedException)
{
// appDbContext can be disposed unexpectedly
return new();
}
}
}

View File

@@ -40,7 +40,7 @@ internal class SummaryAvatarFactory
/// <returns>角色</returns>
public PropertyAvatar CreateAvatar()
{
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList);
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
return new()
@@ -65,11 +65,12 @@ internal class SummaryAvatarFactory
};
}
private ReliquaryAndWeapon ProcessEquip(IList<Equip> equipments)
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
{
List<PropertyReliquary> reliquaryList = new();
PropertyWeapon? weapon = null;
// equipments can be null
foreach (Equip equip in equipments)
{
switch (equip.Flat.ItemType)
@@ -134,9 +135,9 @@ internal class SummaryAvatarFactory
private struct ReliquaryAndWeapon
{
public List<PropertyReliquary> Reliquaries;
public PropertyWeapon Weapon;
public PropertyWeapon? Weapon;
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon weapon)
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon? weapon)
{
Reliquaries = reliquaries;
Weapon = weapon;

View File

@@ -45,7 +45,10 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
/// <inheritdoc/>
public void Receive(UserRemovedMessage message)
{
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
ThreadHelper.InvokeOnMainThread(() =>
{
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
});
}
/// <inheritdoc/>

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
@@ -28,7 +27,7 @@ namespace Snap.Hutao.Service.GachaLog;
/// 祈愿记录服务
/// </summary>
[Injection(InjectAs.Scoped, typeof(IGachaLogService))]
internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
internal class GachaLogService : IGachaLogService
{
/// <summary>
/// 祈愿记录查询的类型
@@ -95,9 +94,6 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
set => dbCurrent.Current = value;
}
/// <inheritdoc/>
public bool IsInitialized { get; set; }
/// <inheritdoc/>
public Task<UIGF> ExportToUIGFAsync(GachaArchive archive)
{
@@ -117,30 +113,29 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
}
/// <inheritdoc/>
public ObservableCollection<GachaArchive> GetArchiveCollection()
public async Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync()
{
return archiveCollection ??= new(appDbContext.GachaArchives.AsNoTracking().ToList());
await ThreadHelper.SwitchToMainThreadAsync();
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
/// <inheritdoc/>
public async ValueTask<bool> InitializeAsync()
public async ValueTask<bool> InitializeAsync(CancellationToken token)
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false);
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false);
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
IsInitialized = true;
return true;
}
else
{
IsInitialized = false;
return false;
}
return IsInitialized;
}
/// <inheritdoc/>
@@ -321,7 +316,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
archive = appDbContext.GachaArchives.Single(a => a.Uid == uid);
GachaArchive temp = archive;
Program.DispatcherQueue!.Invoke(() => archiveCollection!.Add(temp));
ThreadHelper.InvokeOnMainThread(() => archiveCollection!.Add(temp));
}
}
}

View File

@@ -26,10 +26,10 @@ internal interface IGachaLogService
Task<UIGF> ExportToUIGFAsync(GachaArchive archive);
/// <summary>
/// 获取可用于绑定的存档集合
/// 异步获取可用于绑定的存档集合
/// </summary>
/// <returns>存档集合</returns>
ObservableCollection<GachaArchive> GetArchiveCollection();
Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync();
/// <summary>
/// 获取祈愿日志Url提供器
@@ -58,7 +58,7 @@ internal interface IGachaLogService
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>是否初始化成功</returns>
ValueTask<bool> InitializeAsync();
ValueTask<bool> InitializeAsync(CancellationToken token);
/// <summary>
/// 刷新祈愿记录

View File

@@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
@@ -213,14 +214,15 @@ internal class GameService : IGameService, IDisposable
}
/// <inheritdoc/>
public ObservableCollection<GameAccount> GetGameAccountCollection()
public async Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
if (gameAccounts == null)
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
gameAccounts = new(appDbContext.GameAccounts.AsNoTracking().ToList());
gameAccounts = appDbContext.GameAccounts.AsNoTracking().ToObservableCollection();
}
}

View File

@@ -27,10 +27,10 @@ internal interface IGameService
ValueTask DetectGameAccountAsync();
/// <summary>
/// 获取游戏内账号集合
/// 异步获取游戏内账号集合
/// </summary>
/// <returns>游戏内账号集合</returns>
ObservableCollection<GameAccount> GetGameAccountCollection();
Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync();
/// <summary>
/// 异步获取游戏路径

View File

@@ -23,9 +23,9 @@ internal class HutaoCache : IHutaoCache
private Dictionary<AvatarId, Avatar>? idAvatarExtendedMap;
private bool isDatabaseViewModelInitialized;
private bool isWikiAvatarViewModelInitiaized;
private bool isWikiWeaponViewModelInitiaized;
private TaskCompletionSource<bool>? databaseViewModelTaskSource;
private TaskCompletionSource<bool>? wikiAvatarViewModelTaskSource;
private TaskCompletionSource<bool>? wikiWeaponViewModelTaskSource;
/// <summary>
/// 构造一个新的胡桃 API 缓存
@@ -62,11 +62,12 @@ internal class HutaoCache : IHutaoCache
/// <inheritdoc/>
public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
{
if (isDatabaseViewModelInitialized)
if (databaseViewModelTaskSource != null)
{
return true;
return await databaseViewModelTaskSource.Task.ConfigureAwait(false);
}
databaseViewModelTaskSource = new();
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
@@ -81,20 +82,23 @@ internal class HutaoCache : IHutaoCache
await Task.WhenAll(tasks).ConfigureAwait(false);
return isDatabaseViewModelInitialized = true;
databaseViewModelTaskSource.TrySetResult(true);
return true;
}
databaseViewModelTaskSource.TrySetResult(false);
return false;
}
/// <inheritdoc/>
public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync()
{
if (isWikiAvatarViewModelInitiaized)
if (wikiAvatarViewModelTaskSource != null)
{
return true;
return await wikiAvatarViewModelTaskSource.Task.ConfigureAwait(false);
}
wikiAvatarViewModelTaskSource = new();
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
@@ -116,21 +120,23 @@ internal class HutaoCache : IHutaoCache
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
}).ToList();
isWikiAvatarViewModelInitiaized = true;
wikiAvatarViewModelTaskSource.TrySetResult(true);
return true;
}
wikiAvatarViewModelTaskSource.TrySetResult(false);
return false;
}
/// <inheritdoc/>
public async ValueTask<bool> InitializeForWikiWeaponViewModelAsync()
{
if (isWikiWeaponViewModelInitiaized)
if (wikiWeaponViewModelTaskSource != null)
{
return true;
return await wikiWeaponViewModelTaskSource.Task.ConfigureAwait(false);
}
wikiWeaponViewModelTaskSource = new();
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
@@ -148,10 +154,11 @@ internal class HutaoCache : IHutaoCache
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
}).ToList();
isWikiWeaponViewModelInitiaized = true;
wikiWeaponViewModelTaskSource.TrySetResult(true);
return true;
}
wikiWeaponViewModelTaskSource.TrySetResult(false);
return false;
}

View File

@@ -104,12 +104,22 @@ internal class HutaoService : IHutaoService
Response<T> webResponse = await taskFunc(default).ConfigureAwait(false);
T web = webResponse.IsOk() ? webResponse.Data : new();
appDbContext.ObjectCache.AddAndSave(new()
try
{
Key = key,
ExpireTime = DateTimeOffset.Now.AddHours(4),
Value = JsonSerializer.Serialize(web, options),
});
appDbContext.ObjectCache.AddAndSave(new()
{
Key = key,
// we hold the cache for 4 hours, then just expire it.
ExpireTime = DateTimeOffset.Now.AddHours(4),
Value = JsonSerializer.Serialize(web, options),
});
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException)
{
// DbUpdateException: An error occurred while saving the entity changes.
// TODO: Not ignore it.
}
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
}

View File

@@ -138,7 +138,7 @@ internal class UserService : IUserService
{
List<Model.Binding.User.UserAndUid> userAndUids = new();
ObservableCollection<BindingUser> observableUsers = await GetUserCollectionAsync().ConfigureAwait(false);
foreach (BindingUser user in observableUsers.ToList())
foreach (BindingUser user in observableUsers)
{
foreach (UserGameRole role in user.UserGameRoles)
{
@@ -146,7 +146,7 @@ internal class UserService : IUserService
}
}
roleCollection = new(userAndUids);
roleCollection = userAndUids.ToObservableCollection();
}
return roleCollection;

View File

@@ -32,7 +32,7 @@
<RowDefinition/>
</Grid.RowDefinitions>
<CommandBar Background="{StaticResource CardBackgroundFillColorDefaultBrush}" DefaultLabelPosition="Right">
<CommandBar Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" DefaultLabelPosition="Right">
<AppBarButton
Command="{Binding RefreshCommand}"
Icon="{shcm:FontIcon Glyph=&#xE72C;}"
@@ -52,37 +52,27 @@
Style="{StaticResource BaseTextBlockStyle}"
Text="添加角色以定时刷新"/>
<ScrollViewer MaxHeight="320" Padding="16,0">
<ItemsControl ItemsSource="{Binding UserAndRoles}">
<ItemsControl ItemsSource="{Binding UserAndUids}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Padding="0,0,0,16">
<StackPanel>
<TextBlock Text="{Binding Role.Nickname}"/>
<TextBlock
Margin="0,2,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Role.Description}"/>
</StackPanel>
<TextBlock VerticalAlignment="Center" Text="{Binding Uid}"/>
<Button
Margin="16,0,0,0"
Padding="12"
Padding="6"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding DataContext.TrackRoleCommand, Source={StaticResource ViewModelBindingProxy}}"
CommandParameter="{Binding}"
Content="&#xE710;"
FontFamily="{StaticResource SymbolThemeFontFamily}"
Style="{StaticResource ButtonRevealStyle}"
ToolTipService.ToolTip="添加"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</Flyout>
</AppBarButton.Flyout>

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.Abstraction;
/// <summary>
/// 视图模型接口
/// </summary>
public interface IViewModel
{
/// <summary>
/// 用于通知页面卸载的取消令牌
/// </summary>
CancellationToken CancellationToken { get; set; }
/// <summary>
/// 释放操作锁
/// </summary>
SemaphoreSlim DisposeLock { get; set; }
/// <summary>
/// 对应的视图是否已经释放
/// </summary>
bool IsViewDisposed { get; set; }
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
namespace Snap.Hutao.ViewModel.Abstraction;
/// <summary>
/// 视图模型抽象类
/// </summary>
public abstract class ViewModel : ObservableObject, IViewModel
{
private bool isInitialized;
/// <summary>
/// 是否初始化完成
/// </summary>
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <inheritdoc/>
public SemaphoreSlim DisposeLock { get; set; } = new(1);
/// <inheritdoc/>
public bool IsViewDisposed { get; set; }
/// <summary>
/// 当页面被释放后抛出异常
/// </summary>
/// <exception cref="OperationCanceledException">操作被用户取消</exception>
protected void ThrowIfViewDisposed()
{
if (IsViewDisposed)
{
throw new OperationCanceledException("页面资源已经被释放,操作取消");
}
}
}

View File

@@ -1,12 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.UI;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.DataTransfer;
@@ -30,10 +28,7 @@ namespace Snap.Hutao.ViewModel;
/// </summary>
[Injection(InjectAs.Scoped)]
[SuppressMessage("", "SA1124")]
internal class AchievementViewModel
: ObservableObject,
ISupportCancellation,
INavigationRecipient
internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipient
{
private static readonly SortDescription IncompletedItemsFirstSortDescription = new(nameof(Model.Binding.Achievement.Achievement.IsChecked), SortDirection.Ascending);
private static readonly SortDescription CompletionTimeSortDescription = new(nameof(Model.Binding.Achievement.Achievement.Time), SortDirection.Descending);
@@ -53,7 +48,6 @@ internal class AchievementViewModel
private Model.Entity.AchievementArchive? selectedArchive;
private bool isIncompletedItemsFirst = true;
private string searchText = string.Empty;
private bool isInitialized;
private string? finishDescription;
/// <summary>
@@ -92,14 +86,6 @@ internal class AchievementViewModel
SaveAchievementCommand = new RelayCommand<Model.Binding.Achievement.Achievement>(SaveAchievement);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 是否初始化完成
/// </summary>
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
/// <summary>
/// 成就存档集合
/// </summary>
@@ -251,23 +237,38 @@ internal class AchievementViewModel
{
try
{
List<Model.Metadata.Achievement.AchievementGoal> goals = await metadataService.GetAchievementGoalsAsync(CancellationToken).ConfigureAwait(false);
List<Model.Binding.Achievement.AchievementGoal> sortedGoals;
ObservableCollection<Model.Entity.AchievementArchive> archives;
ThrowIfViewDisposed();
using (await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(false))
{
ThrowIfViewDisposed();
List<Model.Metadata.Achievement.AchievementGoal> goals = await metadataService.GetAchievementGoalsAsync(CancellationToken).ConfigureAwait(false);
sortedGoals = goals
.OrderBy(goal => goal.Order)
.Select(goal => new Model.Binding.Achievement.AchievementGoal(goal))
.ToList();
archives = await achievementService.GetArchiveCollectionAsync().ConfigureAwait(false);
}
await ThreadHelper.SwitchToMainThreadAsync();
AchievementGoals = goals.OrderBy(goal => goal.Order).Select(goal => new Model.Binding.Achievement.AchievementGoal(goal)).ToList();
Archives = achievementService.GetArchiveCollection();
AchievementGoals = sortedGoals;
Archives = archives;
SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
IsInitialized = true;
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
// Indicate initialization not succeed
// User canceled the loading operation,
// Indicate initialization not succeed.
openUICompletionSource.TrySetResult(false);
}
}
openUICompletionSource.TrySetResult(metaInitialized);
IsInitialized = metaInitialized;
}
#region

View File

@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Control;
using Snap.Hutao.Core;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.Abstraction;
@@ -17,7 +15,7 @@ namespace Snap.Hutao.ViewModel;
/// 公告视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
internal class AnnouncementViewModel : Abstraction.ViewModel
{
private readonly IAnnouncementService announcementService;
@@ -39,9 +37,6 @@ internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
OpenAnnouncementUICommand = new RelayCommand<string>(OpenAnnouncementUI);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 公告
/// </summary>

View File

@@ -1,10 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Control;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Extension;
@@ -19,6 +19,7 @@ using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI;
@@ -35,11 +36,12 @@ namespace Snap.Hutao.ViewModel;
/// TODO: support page unload as cancellation
/// </summary>
[Injection(InjectAs.Scoped)]
internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
internal class AvatarPropertyViewModel : Abstraction.ViewModel
{
private readonly IUserService userService;
private readonly IAvatarInfoService avatarInfoService;
private readonly IInfoBarService infoBarService;
private readonly IContentDialogFactory contentDialogFactory;
private Summary? summary;
private Avatar? selectedAvatar;
@@ -48,17 +50,20 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
/// </summary>
/// <param name="userService">用户服务</param>
/// <param name="avatarInfoService">角色信息服务</param>
/// <param name="contentDialogFactory">对话框工厂</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
/// <param name="infoBarService">信息条服务</param>
public AvatarPropertyViewModel(
IUserService userService,
IAvatarInfoService avatarInfoService,
IContentDialogFactory contentDialogFactory,
IAsyncRelayCommandFactory asyncRelayCommandFactory,
IInfoBarService infoBarService)
{
this.userService = userService;
this.avatarInfoService = avatarInfoService;
this.infoBarService = infoBarService;
this.contentDialogFactory = contentDialogFactory;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
RefreshFromEnkaApiCommand = asyncRelayCommandFactory.Create(RefreshByEnkaApiAsync);
@@ -68,9 +73,6 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
CultivateCommand = asyncRelayCommandFactory.Create<Avatar>(CultivateAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 简述对象
/// </summary>
@@ -171,8 +173,21 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
{
try
{
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(userAndUid, option, token).ConfigureAwait(false);
ValueResult<RefreshResult, Summary?> summaryResult;
ThrowIfViewDisposed();
using (await DisposeLock.EnterAsync(token).ConfigureAwait(false))
{
ThrowIfViewDisposed();
ContentDialog dialog = await contentDialogFactory.CreateForIndeterminateProgressAsync("获取数据中").ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
await using (await dialog.BlockAsync().ConfigureAwait(false))
{
summaryResult = await avatarInfoService.GetSummaryAsync(userAndUid, option, token).ConfigureAwait(false);
}
}
(RefreshResult result, Summary? summary) = summaryResult;
if (result == RefreshResult.Ok)
{
await ThreadHelper.SwitchToMainThreadAsync();
@@ -206,6 +221,12 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
if (userService.Current != null)
{
if (avatar.Weapon == null)
{
infoBarService.Warning("当前角色无法计算,请同步信息后再试");
return;
}
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
(bool isOk, CalcAvatarPromotionDelta delta) = await new CultivatePromotionDeltaDialog(avatar.ToCalculable(), avatar.Weapon.ToCalculable())
@@ -268,14 +289,31 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
Bgra8 tint = Bgra8.FromColor(tintColor);
softwareBitmap.NormalBlend(tint);
bool clipboardOpened = false;
using (InMemoryRandomAccessStream memory = new())
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, memory);
encoder.SetSoftwareBitmap(softwareBitmap);
await encoder.FlushAsync();
Clipboard.SetBitmapStream(memory);
try
{
Clipboard.SetBitmapStream(memory);
clipboardOpened = true;
}
catch (COMException)
{
// CLIPBRD_E_CANT_OPEN
}
}
infoBarService.Success("已导出到剪贴板");
if (clipboardOpened)
{
infoBarService.Success("已导出到剪贴板");
}
else
{
infoBarService.Warning("打开剪贴板失败");
}
}
}

View File

@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Cultivation;
using Snap.Hutao.Model.Entity;
@@ -20,14 +18,13 @@ namespace Snap.Hutao.ViewModel;
/// 养成视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class CultivationViewModel : ObservableObject, ISupportCancellation
internal class CultivationViewModel : Abstraction.ViewModel
{
private readonly ICultivationService cultivationService;
private readonly IInfoBarService infoBarService;
private readonly IMetadataService metadataService;
private readonly ILogger<CultivationViewModel> logger;
private bool isInitialized;
private ObservableCollection<CultivateProject>? projects;
private CultivateProject? selectedProject;
private List<Model.Binding.Inventory.InventoryItem>? inventoryItems;
@@ -63,14 +60,6 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation
NavigateToPageCommand = new RelayCommand<string>(NavigateToPage);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 是否初始化完成
/// </summary>
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
/// <summary>
/// 项目
/// </summary>

View File

@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Control;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Factory.Abstraction;
@@ -23,7 +21,7 @@ namespace Snap.Hutao.ViewModel;
/// 实时便笺视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
internal class DailyNoteViewModel : Abstraction.ViewModel
{
private readonly IUserService userService;
private readonly IDailyNoteService dailyNoteService;
@@ -73,9 +71,6 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
DailyNoteVerificationCommand = asyncRelayCommandFactory.Create(VerifyDailyNoteVerificationAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 刷新时间
/// </summary>
@@ -136,7 +131,7 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
/// <summary>
/// 用户与角色集合
/// </summary>
public ObservableCollection<UserAndUid>? UserAndRoles { get => userAndUids; set => userAndUids = value; }
public ObservableCollection<UserAndUid>? UserAndUids { get => userAndUids; set => userAndUids = value; }
/// <summary>
/// 实时便笺集合
@@ -175,7 +170,7 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
UserAndRoles = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
UserAndUids = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
refreshSecondsEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteRefreshSeconds, "480");
selectedRefreshTime = refreshTimes.Single(t => t.Value == refreshSecondsEntry.GetInt32());

View File

@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Factory.Abstraction;
@@ -22,7 +20,7 @@ namespace Snap.Hutao.ViewModel;
/// 祈愿记录视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class GachaLogViewModel : ObservableObject, ISupportCancellation
internal class GachaLogViewModel : Abstraction.ViewModel
{
private readonly IGachaLogService gachaLogService;
private readonly IInfoBarService infoBarService;
@@ -35,7 +33,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
private GachaStatistics? statistics;
private bool isAggressiveRefresh;
private HistoryWish? selectedHistoryWish;
private bool isInitialized;
/// <summary>
/// 构造一个新的祈愿记录视图模型
@@ -69,9 +66,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 存档集合
/// </summary>
@@ -112,11 +106,6 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
/// </summary>
public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); }
/// <summary>
/// 初始化是否完成
/// </summary>
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
/// <summary>
/// 页面加载命令
/// </summary>
@@ -154,13 +143,27 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
if (await gachaLogService.InitializeAsync().ConfigureAwait(true))
try
{
Archives = gachaLogService.GetArchiveCollection();
SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(true))
{
ObservableCollection<GachaArchive> archives;
await ThreadHelper.SwitchToMainThreadAsync();
IsInitialized = true;
ThrowIfViewDisposed();
using (await DisposeLock.EnterAsync().ConfigureAwait(false))
{
ThrowIfViewDisposed();
archives = await gachaLogService.GetArchiveCollectionAsync().ConfigureAwait(false);
}
await ThreadHelper.SwitchToMainThreadAsync();
Archives = archives;
SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true);
IsInitialized = true;
}
}
catch (OperationCanceledException)
{
}
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Service.Hutao;
@@ -14,7 +12,7 @@ namespace Snap.Hutao.ViewModel;
/// 胡桃数据库视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
internal class HutaoDatabaseViewModel : Abstraction.ViewModel
{
private readonly IHutaoCache hutaoCache;
@@ -28,7 +26,6 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
/// 构造一个新的胡桃数据库视图模型
/// </summary>
/// <param name="hutaoCache">胡桃服务缓存</param>
/// <param name="metadataService">元数据服务</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public HutaoDatabaseViewModel(IHutaoCache hutaoCache, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
@@ -37,9 +34,6 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 角色使用率
/// </summary>

View File

@@ -1,11 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Control;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Factory.Abstraction;
@@ -26,7 +24,7 @@ namespace Snap.Hutao.ViewModel;
/// 启动游戏视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
internal class LaunchGameViewModel : Abstraction.ViewModel
{
/// <summary>
/// 启动游戏目标 Uid
@@ -44,8 +42,7 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
{
new LaunchScheme(name: "官方服 | 天空岛", channel: "1", subChannel: "1", launcherId: "18"),
new LaunchScheme(name: "渠道服 | 世界树", channel: "14", subChannel: "0", launcherId: "17"),
// new LaunchScheme(name: "国际服 | 暂不支持", channel: "1", subChannel: "0"),
new LaunchScheme(name: "国际服 | 部分支持", channel: "1", subChannel: "0", launcherId: "unknown"),
};
private LaunchScheme? selectedScheme;
@@ -84,9 +81,6 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
AttachGameAccountCommand = new RelayCommand<GameAccount>(AttachGameAccountToCurrentUserGameRole);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 已知的服务器方案
/// </summary>
@@ -217,22 +211,32 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
bool gameExists = File.Exists(gameService.GetGamePathSkipLocator());
if (gameExists)
if (File.Exists(gameService.GetGamePathSkipLocator()))
{
MultiChannel multi = gameService.GetMultiChannel();
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
GameAccounts = gameService.GetGameAccountCollection();
// Sync uid
if (memoryCache.TryGetValue(DesiredUid, out object? value) && value is string uid)
try
{
SelectedGameAccount = GameAccounts.SingleOrDefault(g => g.AttachUid == uid);
}
ThrowIfViewDisposed();
using (await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(true))
{
ThrowIfViewDisposed();
// Sync from Settings
RetiveSetting();
MultiChannel multi = gameService.GetMultiChannel();
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
GameAccounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(true);
// Sync uid
if (memoryCache.TryGetValue(DesiredUid, out object? value) && value is string uid)
{
SelectedGameAccount = GameAccounts.SingleOrDefault(g => g.AttachUid == uid);
}
// Sync from Settings
RetiveSetting();
}
}
catch (OperationCanceledException)
{
}
}
else
{

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.Database;
@@ -23,7 +22,7 @@ namespace Snap.Hutao.ViewModel;
/// 设置视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class SettingViewModel : ObservableObject
internal class SettingViewModel : Abstraction.ViewModel
{
private readonly AppDbContext appDbContext;
private readonly IGameService gameService;

View File

@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Message;
using Snap.Hutao.Model.Binding.SpiralAbyss;
@@ -25,7 +23,7 @@ namespace Snap.Hutao.ViewModel;
/// 深渊记录视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellation, IRecipient<UserChangedMessage>
internal class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage>
{
private readonly ISpiralAbyssRecordService spiralAbyssRecordService;
private readonly IMetadataService metadataService;
@@ -62,9 +60,6 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
messenger.Register(this);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 深渊记录
/// </summary>
@@ -126,33 +121,61 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
{
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idAvatarMap = AvatarIds.ExtendAvatars(idAvatarMap);
if (userService.Current?.SelectedUserGameRole != null)
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
await UpdateSpiralAbyssCollectionAsync(UserAndUid.FromUser(userService.Current)).ConfigureAwait(false);
await UpdateSpiralAbyssCollectionAsync(userAndUid).ConfigureAwait(false);
}
else
{
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("请先选中角色与账号");
}
}
}
private async Task UpdateSpiralAbyssCollectionAsync(UserAndUid userAndUid)
{
ObservableCollection<SpiralAbyssEntry> temp = await spiralAbyssRecordService
.GetSpiralAbyssCollectionAsync(userAndUid)
.ConfigureAwait(false);
ObservableCollection<SpiralAbyssEntry>? temp = null;
try
{
ThrowIfViewDisposed();
using (await DisposeLock.EnterAsync().ConfigureAwait(false))
{
ThrowIfViewDisposed();
temp = await spiralAbyssRecordService
.GetSpiralAbyssCollectionAsync(userAndUid)
.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
await ThreadHelper.SwitchToMainThreadAsync();
SpiralAbyssEntries = temp;
SelectedEntry = SpiralAbyssEntries.FirstOrDefault();
SelectedEntry = SpiralAbyssEntries?.FirstOrDefault();
}
private async Task RefreshAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
if (userService.Current?.SelectedUserGameRole != null)
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
await spiralAbyssRecordService
.RefreshSpiralAbyssAsync(UserAndUid.FromUser(userService.Current))
.ConfigureAwait(false);
try
{
ThrowIfViewDisposed();
using (await DisposeLock.EnterAsync().ConfigureAwait(false))
{
ThrowIfViewDisposed();
await spiralAbyssRecordService
.RefreshSpiralAbyssAsync(userAndUid)
.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
await ThreadHelper.SwitchToMainThreadAsync();
SelectedEntry = SpiralAbyssEntries?.FirstOrDefault();

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Control;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Bits;
using Snap.Hutao.Extension;
@@ -19,7 +17,7 @@ namespace Snap.Hutao.ViewModel;
/// 测试视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class TestViewModel : ObservableObject, ISupportCancellation
internal class TestViewModel : Abstraction.ViewModel
{
/// <summary>
/// 构造一个新的测试视图模型
@@ -33,9 +31,6 @@ internal class TestViewModel : ObservableObject, ISupportCancellation
DownloadStaticFileCommand = asyncRelayCommandFactory.Create(DownloadStaticFileAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 打开游戏社区记录对话框命令
/// </summary>

View File

@@ -156,7 +156,14 @@ internal class UserViewModel : ObservableObject
private void LoginMihoyoUser()
{
Ioc.Default.GetRequiredService<INavigationService>().Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
if (Core.WebView2Helper.IsSupported)
{
Ioc.Default.GetRequiredService<INavigationService>().Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
}
else
{
infoBarService.Warning("尚未安装 WebView2 Runtime");
}
}
private async Task RemoveUserAsync(User? user)

View File

@@ -77,6 +77,8 @@ internal class WelcomeViewModel : ObservableObject
DownloadSummaries = new(downloadSummaries);
// Cancel all previous created jobs
serviceProvider.GetRequiredService<BitsManager>().CancelAllJobs();
await Task.WhenAll(downloadSummaries.Select(d => d.DownloadAndExtractAsync())).ConfigureAwait(true);
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
@@ -173,7 +175,7 @@ internal class WelcomeViewModel : ObservableObject
private void UpdateProgressStatus(ProgressUpdateStatus status)
{
Description = $"{Converters.ToFileSizeString(status.BytesRead)}/{Converters.ToFileSizeString(status.TotalBytes)}";
ProgressValue = (double)status.BytesRead / status.TotalBytes;
ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesRead / status.TotalBytes;
}
private void ExtractFiles(string file)

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.UI;
using Microsoft.Extensions.Primitives;
@@ -33,7 +32,7 @@ namespace Snap.Hutao.ViewModel;
/// 角色资料视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class WikiAvatarViewModel : ObservableObject
internal class WikiAvatarViewModel : Abstraction.ViewModel
{
private readonly IMetadataService metadataService;
private readonly IHutaoCache hutaoCache;

View File

@@ -1,11 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.UI;
using Microsoft.Extensions.Primitives;
using Snap.Hutao.Control;
using Snap.Hutao.Extension;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Cultivation;
@@ -31,7 +29,7 @@ namespace Snap.Hutao.ViewModel;
/// 武器资料视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
internal class WikiWeaponViewModel : Abstraction.ViewModel
{
private readonly List<WeaponId> skippedWeapons = new()
{
@@ -62,9 +60,6 @@ internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
FilterCommand = new RelayCommand<string>(ApplyFilter);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 角色列表
/// </summary>

View File

@@ -352,25 +352,7 @@ public class MiHoYoJSInterface
JsParam param = JsonSerializer.Deserialize<JsParam>(message)!;
logger.LogInformation("[OnMessage]\nMethod : {method}\nPayload : {payload}\nCallback: {callback}", param.Method, param.Payload, param.Callback);
IJsResult? result = param.Method switch
{
"closePage" => ClosePage(param),
"configure_share" => ConfigureShare(param),
"eventTrack" => null,
"getActionTicket" => await GetActionTicketAsync(param).ConfigureAwait(false),
"getCookieInfo" => GetCookieInfo(param),
"getCookieToken" => await GetCookieTokenAsync(param).ConfigureAwait(false),
"getDS" => GetDynamicSecrectV1(param),
"getDS2" => GetDynamicSecrectV2(param),
"getHTTPRequestHeaders" => GetHttpRequestHeader(param),
"getStatusBarHeight" => GetStatusBarHeight(param),
"getUserInfo" => GetUserInfo(param),
"hideLoading" => null,
"login" => null,
"pushPage" => PushPage(param),
"showLoading" => null,
_ => logger.LogWarning<IJsResult>("Unhandled Message Type: {method}", param.Method),
};
IJsResult? result = await TryGetJsResultFromJsParamAsync(param).ConfigureAwait(false);
if (result != null && param.Callback != null)
{
@@ -378,6 +360,37 @@ public class MiHoYoJSInterface
}
}
private async Task<IJsResult?> TryGetJsResultFromJsParamAsync(JsParam param)
{
try
{
return param.Method switch
{
"closePage" => ClosePage(param),
"configure_share" => ConfigureShare(param),
"eventTrack" => null,
"getActionTicket" => await GetActionTicketAsync(param).ConfigureAwait(false),
"getCookieInfo" => GetCookieInfo(param),
"getCookieToken" => await GetCookieTokenAsync(param).ConfigureAwait(false),
"getDS" => GetDynamicSecrectV1(param),
"getDS2" => GetDynamicSecrectV2(param),
"getHTTPRequestHeaders" => GetHttpRequestHeader(param),
"getStatusBarHeight" => GetStatusBarHeight(param),
"getUserInfo" => GetUserInfo(param),
"hideLoading" => null,
"login" => null,
"pushPage" => PushPage(param),
"showLoading" => null,
_ => logger.LogWarning<IJsResult>("Unhandled Message Type: {method}", param.Method),
};
}
catch (ObjectDisposedException)
{
// The dialog is already closed.
return null;
}
}
private void OnDOMContentLoaded(CoreWebView2 coreWebView2, CoreWebView2DOMContentLoadedEventArgs args)
{
coreWebView2.ExecuteScriptAsync(HideScrollBarScript).AsTask().SafeForget(logger);

View File

@@ -55,6 +55,7 @@ internal class GameRecordClient
// We hava a verification procedure to handle
if (resp?.ReturnCode == (int)KnownReturnCode.CODE1034)
{
resp.Message = "请求失败,请前往「米游社-我的角色-实时便笺」页面查看";
CardVerifier cardVerifier = Ioc.Default.GetRequiredService<CardVerifier>();
if (await cardVerifier.TryGetXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is string challenge)
@@ -70,7 +71,7 @@ internal class GameRecordClient
}
}
return Response.Response.DefaultIfNull(resp, "请求失败,请前往「米游社-我的角色-实时便笺」页面查看");
return Response.Response.DefaultIfNull(resp);
}
/// <summary>

View File

@@ -20,4 +20,24 @@ internal static class StructExtension
{
return new((int)(rectInt32.X * scale), (int)(rectInt32.Y * scale), (int)(rectInt32.Width * scale), (int)(rectInt32.Height * scale));
}
/// <summary>
/// 尺寸
/// </summary>
/// <param name="rectInt32">源</param>
/// <returns>结果</returns>
public static int Size(this RectInt32 rectInt32)
{
return rectInt32.Width * rectInt32.Height;
}
/// <summary>
/// 尺寸
/// </summary>
/// <param name="sizeInt32">源</param>
/// <returns>结果</returns>
public static int Size(this SizeInt32 sizeInt32)
{
return sizeInt32.Width * sizeInt32.Height;
}
}

View File

@@ -4,6 +4,7 @@
using System.Numerics;
using Windows.Graphics;
using Windows.Win32.System.Diagnostics.ToolHelp;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao.Win32;
@@ -21,6 +22,15 @@ internal static class StructMarshal
return new() { dwSize = (uint)sizeof(MODULEENTRY32) };
}
/// <summary>
/// 构造一个新的 <see cref="Windows.Win32.UI.WindowsAndMessaging.WINDOWPLACEMENT"/>
/// </summary>
/// <returns>新的实例</returns>
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
{
return new() { length = (uint)sizeof(WINDOWPLACEMENT) };
}
/// <summary>
/// 从 0,0 点构造一个新的<see cref="Windows.Graphics.RectInt32"/>
/// </summary>