diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index e63edcab..32a2a17e 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -71,6 +71,7 @@ public partial class App : Application if (firstInstance.IsCurrent) { + // manually invoke the Activation.Activate(firstInstance, activatedEventArgs); firstInstance.Activated += Activation.Activate; diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs index ac571ba4..5febbd59 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs @@ -39,7 +39,7 @@ public class CachedImage : ImageEx // check token state to determine whether the operation should be canceled. Must.ThrowOnCanceled(token, "Image source has changed."); - // BitmapImage initialize with a uri will increase image quality. + // BitmapImage initialize with a uri will increase image quality and loading speed. return new BitmapImage(new(file.Path)); } catch (COMException ex) when (ex.Is(COMError.WINCODEC_ERR_COMPONENTNOTFOUND)) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs index 4e874d87..c3b75a0f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs @@ -9,6 +9,7 @@ using Microsoft.UI.Xaml.Documents; using Microsoft.UI.Xaml.Media; using Snap.Hutao.Core; using System.Linq; +using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Windows.UI; @@ -22,6 +23,8 @@ public class DescriptionTextBlock : ContentControl private static readonly DependencyProperty DescriptionProperty = Property.Depend(nameof(Description), string.Empty, OnDescriptionChanged); + private static readonly Regex ColorRegex = new(@"]+)>([^<]+)", RegexOptions.Compiled); + /// /// 构造一个新的呈现描述文本的文本块 /// @@ -46,17 +49,17 @@ public class DescriptionTextBlock : ContentControl text.Inlines.Clear(); string[] lines = ((string)e.NewValue).Split('\n'); + foreach (string line in lines) { - MatchCollection matches = Regex.Matches(line, @"]+)>([^<]+)"); string left, right = line; - foreach (Match match in matches) + foreach (Match match in ColorRegex.Matches(line)) { - string matched = match.Groups[0].Value; - int matchPosition = right.IndexOf(matched); + string fullMatch = match.Groups[0].Value; + int matchPosition = right.IndexOf(fullMatch); left = right[..matchPosition]; - right = right[(matchPosition + matched.Length)..]; + right = right[(matchPosition + fullMatch.Length)..]; if (!string.IsNullOrWhiteSpace(left)) { @@ -66,7 +69,7 @@ public class DescriptionTextBlock : ContentControl string hexColor = match.Groups[1].Value; string content = match.Groups[2].Value; - text.Inlines.Add(new Run { Text = content, Foreground = GetSolidColorBrush(hexColor[..7]) }); + text.Inlines.Add(new Run { Text = content, Foreground = new SolidColorBrush(new HexColor(hexColor[1..])) }); } if (!string.IsNullOrWhiteSpace(right)) @@ -91,12 +94,33 @@ public class DescriptionTextBlock : ContentControl } } - private static SolidColorBrush GetSolidColorBrush(string hex) + [StructLayout(LayoutKind.Explicit)] + private struct HexColor { - hex = hex.Replace("#", string.Empty); - byte r = (byte)Convert.ToUInt32(hex.Substring(0, 2), 16); - byte g = (byte)Convert.ToUInt32(hex.Substring(2, 2), 16); - byte b = (byte)Convert.ToUInt32(hex.Substring(4, 2), 16); - return new SolidColorBrush(Color.FromArgb(255, r, g, b)); + [FieldOffset(3)] + public byte R; + [FieldOffset(2)] + public byte G; + [FieldOffset(1)] + public byte B; + [FieldOffset(0)] + public byte A; + + [FieldOffset(0)] + private readonly uint data; + + public HexColor(string hex) + { + R = 0; + G = 0; + B = 0; + A = 0; + data = Convert.ToUInt32(hex, 16); + } + + public static implicit operator Color(HexColor hexColor) + { + return Color.FromArgb(hexColor.A, hexColor.R, hexColor.G, hexColor.B); + } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/CacheBase.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/CacheBase.cs index 2d4490f6..3d99fe1c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/CacheBase.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/CacheBase.cs @@ -246,6 +246,7 @@ public abstract class CacheBase { try { + logger.LogInformation(EventIds.CacheRemoveFile, "Removing file {file}", file); await file.DeleteAsync().AsTask().ConfigureAwait(false); } catch diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs index 6f309d5f..af60d300 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs @@ -11,10 +11,14 @@ namespace Snap.Hutao.Core; /// internal static class CoreEnvironment { + // Used DS1 History + // 2.34.1 9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7 + // 2.35.2 N50pqm7FSy2AkFz2B3TqtuZMJ5TOl3Ep + /// /// 动态密钥1的盐 /// - public const string DynamicSecret1Salt = "9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7"; + public const string DynamicSecret1Salt = "N50pqm7FSy2AkFz2B3TqtuZMJ5TOl3Ep"; /// /// 动态密钥2的盐 @@ -25,7 +29,12 @@ internal static class CoreEnvironment /// /// 米游社请求UA /// - public const string HoyolabUA = $"miHoYoBBS/2.34.1"; + public const string HoyolabUA = $"miHoYoBBS/{HoyolabXrpcVersion}"; + + /// + /// 米游社 Rpc 版本 + /// + public const string HoyolabXrpcVersion = "2.35.2"; /// /// 标准UA diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs index f4164c8f..2fb08c12 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -36,7 +36,7 @@ internal static partial class IocHttpClientConfiguration { client.Timeout = Timeout.InfiniteTimeSpan; client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA); - client.DefaultRequestHeaders.Add("x-rpc-app_version", "2.34.1"); + client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion); client.DefaultRequestHeaders.Add("x-rpc-client_type", "5"); client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId); } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs index f9d93129..0d476c94 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs @@ -10,7 +10,6 @@ namespace Snap.Hutao.Core.Json.Converter; /// /// Json字典转换器 /// -/// 键的类型 public class StringEnumKeyDictionaryConverter : JsonConverterFactory { /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index abbbc6a3..f0214284 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -13,8 +13,7 @@ namespace Snap.Hutao.Core.LifeCycle; /// internal static class Activation { - private static volatile bool isActivating = false; - private static object activationLock = new(); + private static readonly SemaphoreSlim ActivateSemaphore = new(1); /// /// 响应激活事件 @@ -33,28 +32,13 @@ internal static class Activation /// 任务 private static async Task HandleActivationAsync(AppActivationArguments args) { - if (isActivating) + if (ActivateSemaphore.CurrentCount > 0) { - lock (activationLock) + using (await ActivateSemaphore.EnterAsync().ConfigureAwait(false)) { - if (isActivating) - { - return; - } + await HandleActivationCoreAsync(args).ConfigureAwait(false); } } - - lock (activationLock) - { - isActivating = true; - } - - await HandleActivationCoreAsync(args).ConfigureAwait(false); - - lock (activationLock) - { - isActivating = false; - } } private static async Task HandleActivationCoreAsync(AppActivationArguments args) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs index d0167cd9..cceeef1b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs @@ -54,18 +54,19 @@ internal sealed partial class DatebaseLogger : ILogger return; } + LogEntry entry = new() + { + Category = name, + LogLevel = logLevel, + EventId = eventId.Id, + Message = message, + Exception = exception?.ToString(), + }; + // DbContext is not a thread safe class, so we have to lock the wirte procedure lock (logDbContextLock) { - logDbContext.Logs.Add(new LogEntry - { - Category = name, - LogLevel = logLevel, - EventId = eventId.Id, - Message = message, - Exception = exception?.ToString(), - }); - + logDbContext.Logs.Add(entry); logDbContext.SaveChanges(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLoggerProvider.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLoggerProvider.cs index 00df85e9..50601e68 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLoggerProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLoggerProvider.cs @@ -10,14 +10,14 @@ using System.Linq; namespace Snap.Hutao.Core.Logging; /// -/// The provider for the . +/// The provider for the . /// [ProviderAlias("Database")] public sealed class DatebaseLoggerProvider : ILoggerProvider { private static readonly object LogDbContextLock = new(); - // the provider is created per logger, we don't want to create to much + // the provider is created per logger, we don't want to create too much private static volatile LogDbContext? logDbContext; private static LogDbContext LogDbContext diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs index 143d413e..635f6026 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs @@ -6,9 +6,10 @@ namespace Snap.Hutao.Core.Logging; /// /// 事件Id定义 /// +[SuppressMessage("", "SA1124")] internal static class EventIds { - // 异常 + #region 异常 /// /// 未经处理的异常 @@ -44,8 +45,9 @@ internal static class EventIds /// Xaml绑定错误 /// public static readonly EventId UnobservedTaskException = 100006; + #endregion - // 服务 + #region 服务 /// /// 导航历史 @@ -73,11 +75,17 @@ internal static class EventIds public static readonly EventId FileCaching = 100120; /// - /// 文件缓存 + /// 删除缓存文件 + /// + public static readonly EventId CacheRemoveFile = 100121; + + /// + /// 成就 /// public static readonly EventId Achievement = 100130; + #endregion - // 杂项 + #region 杂项 /// /// 杂项Log @@ -93,4 +101,5 @@ internal static class EventIds /// 子类控制 /// public static readonly EventId SubClassing = 200002; + #endregion } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/LocalSetting.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/LocalSetting.cs index 86ff7f43..91555939 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/LocalSetting.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/LocalSetting.cs @@ -28,10 +28,12 @@ internal static class LocalSetting /// 键 /// 默认值 /// 获取的值 - public static T? Get(string key, T? defaultValue = default) + [return:MaybeNull] + public static T Get(string key, [AllowNull] T defaultValue = default) { if (Container.Values.TryGetValue(key, out object? value)) { + // unbox the value return value is null ? defaultValue : (T)value; } else @@ -48,9 +50,8 @@ internal static class LocalSetting /// 键 /// 值 /// 设置的值 - public static T Set(string key, T value) + public static object? Set(string key, T value) { - Container.Values[key] = value; - return value; + return Container.Values[key] = value; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/IAwaitable.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/IAwaitable.cs new file mode 100644 index 00000000..959aaa72 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/IAwaitable.cs @@ -0,0 +1,35 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Threading.Abstraction; + +/// +/// 表示一个可等待对象,如果一个方法返回此类型的实例,则此方法可以使用 异步等待。 +/// +/// 用于给 await 确定返回时机的 IAwaiter 的实例。 +public interface IAwaitable + where TAwaiter : IAwaiter +{ + /// + /// 获取一个可用于 await 关键字异步等待的异步等待对象。 + /// 此方法会被编译器自动调用。 + /// + /// 等待器 + TAwaiter GetAwaiter(); +} + +/// +/// 表示一个包含返回值的可等待对象,如果一个方法返回此类型的实例,则此方法可以使用 异步等待返回值。 +/// +/// 用于给 await 确定返回时机的 的实例。 +/// 异步返回的返回值类型。 +public interface IAwaitable + where TAwaiter : IAwaiter +{ + /// + /// 获取一个可用于 await 关键字异步等待的异步等待对象。 + /// 此方法会被编译器自动调用。 + /// + /// 等待器 + TAwaiter GetAwaiter(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/IAwaiter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/IAwaiter.cs new file mode 100644 index 00000000..427821bf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/IAwaiter.cs @@ -0,0 +1,42 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Core.Threading.Abstraction; + +/// +/// 用于给 await 确定异步返回的时机。 +/// +public interface IAwaiter : INotifyCompletion +{ + /// + /// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常);此状态会被编译器自动调用。 + /// 在实现中,为了达到各种效果,可以灵活应用其值:可以始终为 true,或者始终为 false。 + /// + bool IsCompleted { get; } + + /// + /// 此方法会被编译器在 await 结束时自动调用以获取返回状态(包括异常)。 + /// + void GetResult(); +} + +/// +/// 用于给 await 确定异步返回的时机,并获取到返回值。 +/// +/// 异步返回的返回值类型。 +public interface IAwaiter : INotifyCompletion +{ + /// + /// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常);此状态会被编译器自动调用。 + /// 在实现中,为了达到各种效果,可以灵活应用其值:可以始终为 true,或者始终为 false。 + /// + bool IsCompleted { get; } + + /// + /// 获取此异步等待操作的返回值,此方法会被编译器在 await 结束时自动调用以获取返回值(包括异常)。 + /// + /// 异步操作的返回值。 + TResult GetResult(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/ICriticalAwaiter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/ICriticalAwaiter.cs new file mode 100644 index 00000000..4d0b04f2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Abstraction/ICriticalAwaiter.cs @@ -0,0 +1,23 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Core.Threading.Abstraction; + +/// +/// 当执行关键代码(此代码中的错误可能给应用程序中的其他状态造成负面影响)时, +/// 用于给 await 确定异步返回的时机。 +/// +public interface ICriticalAwaiter : IAwaiter, ICriticalNotifyCompletion +{ +} + +/// +/// 当执行关键代码(此代码中的错误可能给应用程序中的其他状态造成负面影响)时, +/// 用于给 await 确定异步返回的时机,并获取到返回值。 +/// +/// 异步返回的返回值类型。 +public interface ICriticalAwaiter : IAwaiter, ICriticalNotifyCompletion +{ +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs index 7ef0a398..1bb621d1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs @@ -21,13 +21,11 @@ internal class ConcurrentCancellationTokenSource /// 取消令牌 public CancellationToken Register(TItem item) { - if (waitingItems.TryRemove(item, out CancellationTokenSource? prevSource)) + if (waitingItems.TryRemove(item, out CancellationTokenSource? previousSource)) { - prevSource.Cancel(); + previousSource.Cancel(); } - CancellationTokenSource current = waitingItems.GetOrAdd(item, new CancellationTokenSource()); - - return current.Token; + return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs new file mode 100644 index 00000000..84d483a9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs @@ -0,0 +1,49 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Dispatching; +using Snap.Hutao.Core.Threading.Abstraction; + +namespace Snap.Hutao.Core.Threading; + +/// +/// 调度器队列切换操作 +/// +public struct DispatherQueueSwitchOperation : IAwaitable, IAwaiter +{ + private readonly DispatcherQueue dispatherQueue; + + /// + /// 构造一个新的同步上下文等待器 + /// + /// 同步上下文 + public DispatherQueueSwitchOperation(DispatcherQueue dispatherQueue) + { + this.dispatherQueue = dispatherQueue; + } + + /// + /// 是否完成 + /// + public bool IsCompleted => dispatherQueue.HasThreadAccess; + + /// + public void OnCompleted(Action continuation) + { + dispatherQueue.TryEnqueue(() => { continuation(); }); + } + + /// + public void GetResult() + { + } + + /// + /// 获取等待器 + /// + /// 等待器 + public DispatherQueueSwitchOperation GetAwaiter() + { + return this; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/Results.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Results.cs deleted file mode 100644 index b6c56d02..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/Results.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Core.Threading; - -/// -/// 构造复杂的结果 -/// -public static class Results -{ - /// - /// 根据条件构造结果 - /// - /// 结果的类型 - /// 条件 - /// 条件符合时的值 - /// 条件不符合时的值 - /// 结果 - public static Result Condition(bool condition, T trueValue, T falseValue) - where T : notnull - { - return new(condition, condition ? trueValue : falseValue); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SynchronizationContextAwaitable.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SynchronizationContextAwaitable.cs deleted file mode 100644 index d131549e..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SynchronizationContextAwaitable.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Core.Threading; - -/// -/// 同步上下文等待体 -/// -public struct SynchronizationContextAwaitable -{ - private readonly SynchronizationContext context; - - /// - /// 构造一个新的同步上下文等待体 - /// - /// 同步上下文 - public SynchronizationContextAwaitable(SynchronizationContext context) - { - this.context = context; - } - - /// - /// 获取等待器 - /// - /// 等待器 - public SynchronizationContextAwaiter GetAwaiter() - { - return new SynchronizationContextAwaiter(context); - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SynchronizationContextAwaiter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SynchronizationContextAwaiter.cs deleted file mode 100644 index e735fbcd..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SynchronizationContextAwaiter.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.Runtime.CompilerServices; - -namespace Snap.Hutao.Core.Threading; - -/// -/// 同步上下文等待器 -/// -public struct SynchronizationContextAwaiter : INotifyCompletion -{ - private static readonly SendOrPostCallback PostCallback = state => ((Action)state!)(); - private readonly SynchronizationContext context; - - /// - /// 构造一个新的同步上下文等待器 - /// - /// 同步上下文 - public SynchronizationContextAwaiter(SynchronizationContext context) - { - this.context = context; - } - - /// - /// 是否完成 - /// - public bool IsCompleted => context == SynchronizationContext.Current; - - /// - /// 完成操作 - /// - /// 后续操作 - [SuppressMessage("", "VSTHRD001")] - public void OnCompleted(Action continuation) => context.Post(PostCallback, continuation); - - /// - /// 获取执行结果 - /// - public void GetResult() - { - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/CompositionExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/CompositionExtensions.cs index 52a35d2b..6f64d469 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/CompositionExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/CompositionExtensions.cs @@ -60,7 +60,9 @@ internal static class CompositionExtensions /// 合成器 /// 源 /// 合成效果画刷 - public static CompositionEffectBrush CompositeGrayScaleEffectBrush(this Compositor compositor, CompositionBrush source) + public static CompositionEffectBrush CompositeGrayScaleEffectBrush( + this Compositor compositor, + CompositionBrush source) { GrayscaleEffect effect = new() { diff --git a/src/Snap.Hutao/Snap.Hutao/Program.cs b/src/Snap.Hutao/Snap.Hutao/Program.cs index 32e63b58..60c7c8a6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Program.cs +++ b/src/Snap.Hutao/Snap.Hutao/Program.cs @@ -15,23 +15,14 @@ namespace Snap.Hutao; public static class Program { private static volatile DispatcherQueue? dispatcherQueue; - private static volatile SynchronizationContext? context; - - /// - /// 主线程调度器队列 - /// - public static DispatcherQueue UIDispatcherQueue - { - get => Must.NotNull(dispatcherQueue!); - } /// /// 异步切换到主线程 /// /// 等待体 - public static SynchronizationContextAwaitable SwitchToMainThreadAsync() + public static DispatherQueueSwitchOperation SwitchToMainThreadAsync() { - return new SynchronizationContextAwaitable(context!); + return new(dispatcherQueue!); } [DllImport("Microsoft.ui.xaml.dll")] @@ -47,7 +38,7 @@ public static class Program Application.Start(p => { dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - context = new DispatcherQueueSynchronizationContext(dispatcherQueue); + DispatcherQueueSynchronizationContext context = new(dispatcherQueue); SynchronizationContext.SetSynchronizationContext(context); _ = new App(); }); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Sign/ISignService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Sign/ISignService.cs new file mode 100644 index 00000000..2f636486 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Sign/ISignService.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Sign; + +/// +/// 签到服务 +/// +public interface ISignService +{ + /// + /// 异步全部签到 + /// + /// 取消令牌 + /// 任务 + Task SignForAllAsync(CancellationToken token); +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Sign/SignResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Sign/SignResult.cs new file mode 100644 index 00000000..621f6fb6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Sign/SignResult.cs @@ -0,0 +1,37 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Sign; + +/// +/// 签到操作结果 +/// +public struct SignResult +{ + /// + /// 构造一个新的签到操作结果 + /// + /// 总次数 + /// 重试次数 + public SignResult(int totalCount, int retryCount, TimeSpan time) + { + TotalCount = totalCount; + RetryCount = retryCount; + Time = time; + } + + /// + /// 总次数 + /// + public int TotalCount { get; set; } + + /// + /// 重试次数 + /// + public int RetryCount { get; set; } + + /// + /// 用时 + /// + public TimeSpan Time { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Sign/SignService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Sign/SignService.cs new file mode 100644 index 00000000..3b77c584 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Sign/SignService.cs @@ -0,0 +1,117 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Diagnostics; +using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Web.Hoyolab.Takumi.Binding; +using Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; +using Snap.Hutao.Web.Response; +using System.Collections.Generic; + +namespace Snap.Hutao.Service.Sign; + +/// +/// 签到服务 +/// +[Injection(InjectAs.Transient, typeof(ISignService))] +internal class SignService : ISignService +{ + private readonly IUserService userService; + private readonly IInfoBarService infoBarService; + private readonly SignClient signClient; + + /// + /// 构造一个新的签到服务 + /// + /// 用户服务 + /// 信息条服务 + /// 签到客户端 + public SignService(IUserService userService, IInfoBarService infoBarService, SignClient signClient) + { + this.userService = userService; + this.infoBarService = infoBarService; + this.signClient = signClient; + } + + /// + public async Task SignForAllAsync(CancellationToken token) + { + IEnumerable? users = await userService + .GetUserCollectionAsync() + .ConfigureAwait(false); + Queue userRolesQueue = GetSignQueue(users); + + int totalCount = 0; + int retryCount = 0; + ValueStopwatch stopwatch = ValueStopwatch.StartNew(); + + while (userRolesQueue.TryDequeue(out UserRole current)) + { + totalCount++; + Response? resp = await signClient + .SignAsync(current, token) + .ConfigureAwait(false); + + Must.NotNull(resp!); + + if (resp.Data != null) + { + Must.Argument(resp.ReturnCode == 0, "返回代码应为 0"); + + // Geetest applied + if (resp.Data.Success != 0) + { + userRolesQueue.Enqueue(current); + retryCount++; + } + else + { + infoBarService.Information($"[{current.Role}] 签到成功"); + } + } + else + { + switch ((KnownReturnCode)resp.ReturnCode) + { + case KnownReturnCode.OK: + infoBarService.Information($"[{current.Role}] 签到成功"); + break; + case KnownReturnCode.NotLoggedIn: + case KnownReturnCode.AlreadySignedIn: + infoBarService.Information($"[{current.Role}] {resp.Message}"); + break; + case KnownReturnCode.InvalidRequest: + infoBarService.Information("米游社SALT过期,请更新胡桃"); + break; + default: + throw Must.NeverHappen(); + } + } + + if (userRolesQueue.Count > 0) + { + int seconds = Random.Shared.Next(5, 15); + await Task + .Delay(TimeSpan.FromSeconds(seconds), token) + .ConfigureAwait(false); + } + } + + return new(totalCount, retryCount, stopwatch.GetElapsedTime()); + } + + private static Queue GetSignQueue(IEnumerable users) + { + Queue queue = new(); + + foreach (Model.Binding.User user in users) + { + foreach (UserGameRole role in user.UserGameRoles) + { + queue.Enqueue(new(user, role)); + } + } + + return queue; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs index 9b7daeeb..c17d50c5 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs @@ -4,6 +4,7 @@ using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Core; using Snap.Hutao.Core.Logging; +using Snap.Hutao.Extension; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Navigation; using Snap.Hutao.View.Page; @@ -42,11 +43,13 @@ public sealed partial class MainView : UserControl private void OnUISettingsColorValuesChanged(UISettings sender, object args) { - Program.UIDispatcherQueue.TryEnqueue(UpdateTheme); + UpdateThemeAsync().SafeForget(); } - private void UpdateTheme() + private async Task UpdateThemeAsync() { + await Program.SwitchToMainThreadAsync(); + if (!ThemeHelper.Equals(App.Current.RequestedTheme, RequestedTheme)) { ILogger logger = Ioc.Default.GetRequiredService>(); diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml index 4a965821..aff07f7a 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml @@ -42,26 +42,23 @@ ItemsSource="{Binding Archives,Mode=OneWay}" SelectedItem="{Binding SelectedArchive,Mode=TwoWay}"/> - - - - - - - - + + + + + @@ -28,19 +26,16 @@ internal class ExperimentalFeaturesViewModel : ObservableObject /// /// 异步命令工厂 /// 数据文件夹 - /// 用户服务 - /// 签到客户端 - /// 信息栏服务 + /// 签到客户端 + /// 信息条服务 public ExperimentalFeaturesViewModel( IAsyncRelayCommandFactory asyncRelayCommandFactory, HutaoLocation hutaoLocation, - IUserService userService, - SignClient signClient, + ISignService signService, IInfoBarService infoBarService) { this.hutaoLocation = hutaoLocation; - this.userService = userService; - this.signClient = signClient; + this.signService = signService; this.infoBarService = infoBarService; OpenCacheFolderCommand = asyncRelayCommandFactory.Create(OpenCacheFolderAsync); @@ -73,20 +68,15 @@ internal class ExperimentalFeaturesViewModel : ObservableObject return Launcher.LaunchFolderPathAsync(hutaoLocation.GetPath()).AsTask(); } - private async Task SignAllUserGameRolesAsync() + private async Task SignAllUserGameRolesAsync(CancellationToken token) { - foreach (Model.Binding.User user in await userService.GetUserCollectionAsync()) - { - foreach (UserGameRole role in user.UserGameRoles) - { - Response? result = await signClient.SignAsync(user, role); - if (result != null) - { - infoBarService.Information(result.Message); - } + SignResult result = await signService.SignForAllAsync(token); - await Task.Delay(TimeSpan.FromSeconds(15)); - } - } + StringBuilder stringBuilder = new StringBuilder() + .Append($"签到完成 - 用时: {result.Time.TotalSeconds:F2} 秒\r\n") + .Append($"请求: {result.TotalCount} 次\r\n") + .Append($"补签: {result.RetryCount} 次"); + + infoBarService.Information(stringBuilder.ToString()); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignClient.cs index d7cc6e25..9e030d3b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignClient.cs @@ -83,6 +83,17 @@ internal class SignClient return resp?.Data; } + /// + /// 补签 + /// + /// 用户角色 + /// 取消令牌 + /// 签到结果 + public Task?> ReSignAsync(UserRole userRole, CancellationToken token = default) + { + return ReSignAsync(userRole.User, userRole.Role, token); + } + /// /// 补签 /// @@ -106,13 +117,24 @@ internal class SignClient return resp; } + /// + /// 签到 + /// + /// 用户角色 + /// 取消令牌 + /// 签到结果 + public Task?> SignAsync(UserRole userRole, CancellationToken token = default) + { + return SignAsync(userRole.User, userRole.Role, token); + } + /// /// 签到 /// /// 用户 /// 角色 /// 取消令牌 - /// 签到消息 + /// 签到结果 public async Task?> SignAsync(User user, UserGameRole role, CancellationToken token = default) { HttpResponseMessage response = await httpClient @@ -126,4 +148,4 @@ internal class SignClient return resp; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/UserRole.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/UserRole.cs new file mode 100644 index 00000000..31787898 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/UserRole.cs @@ -0,0 +1,34 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding; +using Snap.Hutao.Web.Hoyolab.Takumi.Binding; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; + +/// +/// 将用户与角色捆绑在一起 +/// +public struct UserRole +{ + /// + /// 构造一个新的用户角色 + /// + /// 用户 + /// 角色 + public UserRole(User user, UserGameRole role) + { + User = user; + Role = role; + } + + /// + /// 用户 + /// + public User User { get; set; } + + /// + /// 角色 + /// + public UserGameRole Role { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index b18fb6ce..cff37291 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Response; /// /// 已知的返回代码 /// -public enum KnownReturnCode +public enum KnownReturnCode : int { /// /// 无效请求 @@ -18,6 +18,11 @@ public enum KnownReturnCode /// AlreadySignedIn = -5003, + /// + /// 尚未登录 + /// + NotLoggedIn = -100, + /// /// 验证密钥过期 ///