From cdfd7e28302701a528411c70a2ffd7e4b8af4bc9 Mon Sep 17 00:00:00 2001
From: DismissedLight <1686188646@qq.com>
Date: Wed, 4 May 2022 10:41:56 +0800
Subject: [PATCH] announcement page
---
src/SettingsUI/Controls/Setting/Setting.cs | 10 +-
.../Controls/SettingsGroup/SettingsGroup.cs | 8 +-
src/SettingsUI/SettingsUI.csproj | 3 -
src/Snap.Hutao/Snap.Hutao/App.xaml | 14 +-
src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 40 ++-
.../Control/Behavior/AutoHeightBehavior.cs | 56 +++++
.../Control/Cancellable/CancellablePage.cs | 33 +++
.../Cancellable/ISupportCancellation.cs | 15 ++
src/Snap.Hutao/Snap.Hutao/Core/Browser.cs | 44 ++++
.../Core/DependencyInjection/InjectAs.cs | 5 +-
.../ServiceCollectionExtensions.cs | 2 -
src/Snap.Hutao/Snap.Hutao/Core/HttpJson.cs | 39 +++
src/Snap.Hutao/Snap.Hutao/Core/Json.cs | 133 ++++++++++
.../Snap.Hutao/Core/Logging/EventIds.cs | 2 -
src/Snap.Hutao/Snap.Hutao/Core/Observable.cs | 43 ++++
.../Snap.Hutao/Core/ProcessHelper.cs | 55 +++++
src/Snap.Hutao/Snap.Hutao/Core/Property.cs | 4 +-
.../Snap.Hutao/Core/Threading/Watcher.cs | 80 ++++++
.../Snap.Hutao/Core/WebView2Helper.cs | 46 ++++
.../Extension/ReflectionExtension.cs | 5 +-
.../Abstraction/IAsyncRelayCommandFactory.cs | 76 ++++++
.../Factory/AsyncRelayCommandFactory.cs | 102 ++++++++
src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs | 6 +-
src/Snap.Hutao/Snap.Hutao/MainWindow.xaml | 4 +-
.../Snap.Hutao/Package.appxmanifest | 2 +-
.../Abstraction/IAnnouncementService.cs | 21 ++
.../Service/Abstraction/IInfoBarService.cs | 100 ++++++++
.../Snap.Hutao/Service/AnnouncementService.cs | 101 ++++++++
.../Snap.Hutao/Service/InfoBarService.cs | 121 +++++++++
.../Snap.Hutao/Service/NavigationService.cs | 5 +-
src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 25 +-
.../View/Converter/BoolToObjectConverter.cs | 86 +++++++
.../Converter/BoolToVisibilityConverter.cs | 21 ++
.../BoolToVisibilityRevertConverter.cs | 21 ++
.../View/Converter/ConvertHelper.cs | 41 ++++
.../Converter/PercentageToHeightConverter.cs | 49 ++++
.../Converter/PercentageToWidthConverter.cs | 49 ++++
src/Snap.Hutao/Snap.Hutao/View/MainView.xaml | 64 ++++-
.../Snap.Hutao/View/MainView.xaml.cs | 6 +-
.../View/Page/AnnouncementContentPage.xaml | 10 +
.../View/Page/AnnouncementContentPage.xaml.cs | 56 +++++
.../View/Page/AnnouncementPage.xaml | 229 ++++++++++++++++++
.../View/Page/AnnouncementPage.xaml.cs | 22 ++
.../Snap.Hutao/View/Page/SettingPage.xaml.cs | 2 +-
.../ViewModel/AnnouncementViewModel.cs | 109 +++++++++
.../Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs | 23 ++
.../Hk4e/Common/Announcement/Announcement.cs | 175 +++++++++++++
.../Announcement/AnnouncementContent.cs | 49 ++++
.../Announcement/AnnouncementListWrapper.cs | 25 ++
.../Announcement/AnnouncementProvider.cs | 59 +++++
.../Common/Announcement/AnnouncementType.cs | 30 +++
.../Announcement/AnnouncementWrapper.cs | 50 ++++
.../Snap.Hutao/Web/Request/AuthRequester.cs | 36 +++
.../Snap.Hutao/Web/Request/RequestOptions.cs | 43 ++++
.../Snap.Hutao/Web/Request/Requester.cs | 207 ++++++++++++++++
.../Web/Response/KnownReturnCode.cs | 40 +++
.../Snap.Hutao/Web/Response/ListWrapper.cs | 19 ++
.../Snap.Hutao/Web/Response/Response.cs | 54 +++++
.../Web/Response/Response{TData}.cs | 33 +++
59 files changed, 2754 insertions(+), 54 deletions(-)
create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Behavior/AutoHeightBehavior.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Cancellable/CancellablePage.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Cancellable/ISupportCancellation.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Browser.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/HttpJson.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Json.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Observable.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IAsyncRelayCommandFactory.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IAnnouncementService.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToObjectConverter.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToVisibilityConverter.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToVisibilityRevertConverter.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Converter/ConvertHelper.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Converter/PercentageToHeightConverter.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Converter/PercentageToWidthConverter.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml
create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/Announcement.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementContent.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementListWrapper.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementProvider.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementType.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementWrapper.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Request/AuthRequester.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Request/RequestOptions.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Request/Requester.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Response/ListWrapper.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs
create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs
diff --git a/src/SettingsUI/Controls/Setting/Setting.cs b/src/SettingsUI/Controls/Setting/Setting.cs
index 7191d6d7..8aeac268 100644
--- a/src/SettingsUI/Controls/Setting/Setting.cs
+++ b/src/SettingsUI/Controls/Setting/Setting.cs
@@ -17,9 +17,9 @@ public class Setting : ContentControl
{
private const string PartIconPresenter = "IconPresenter";
private const string PartDescriptionPresenter = "DescriptionPresenter";
- private ContentPresenter _iconPresenter;
- private ContentPresenter _descriptionPresenter;
- private Setting _setting;
+ private ContentPresenter? _iconPresenter;
+ private ContentPresenter? _descriptionPresenter;
+ private Setting? _setting;
public Setting()
{
@@ -146,11 +146,11 @@ public class Setting : ContentControl
if (_setting.Description == null)
{
- _setting._descriptionPresenter.Visibility = Visibility.Collapsed;
+ _setting._descriptionPresenter!.Visibility = Visibility.Collapsed;
}
else
{
- _setting._descriptionPresenter.Visibility = Visibility.Visible;
+ _setting._descriptionPresenter!.Visibility = Visibility.Visible;
}
}
}
diff --git a/src/SettingsUI/Controls/SettingsGroup/SettingsGroup.cs b/src/SettingsUI/Controls/SettingsGroup/SettingsGroup.cs
index 5456447c..2bb07e3f 100644
--- a/src/SettingsUI/Controls/SettingsGroup/SettingsGroup.cs
+++ b/src/SettingsUI/Controls/SettingsGroup/SettingsGroup.cs
@@ -18,8 +18,8 @@ namespace SettingsUI.Controls;
public partial class SettingsGroup : ItemsControl
{
private const string PartDescriptionPresenter = "DescriptionPresenter";
- private ContentPresenter _descriptionPresenter;
- private SettingsGroup _settingsGroup;
+ private ContentPresenter? _descriptionPresenter;
+ private SettingsGroup? _settingsGroup;
public SettingsGroup()
{
@@ -86,11 +86,11 @@ public partial class SettingsGroup : ItemsControl
if (_settingsGroup.Description == null)
{
- _settingsGroup._descriptionPresenter.Visibility = Visibility.Collapsed;
+ _settingsGroup._descriptionPresenter!.Visibility = Visibility.Collapsed;
}
else
{
- _settingsGroup._descriptionPresenter.Visibility = Visibility.Visible;
+ _settingsGroup._descriptionPresenter!.Visibility = Visibility.Visible;
}
}
diff --git a/src/SettingsUI/SettingsUI.csproj b/src/SettingsUI/SettingsUI.csproj
index 784ae36f..891362fd 100644
--- a/src/SettingsUI/SettingsUI.csproj
+++ b/src/SettingsUI/SettingsUI.csproj
@@ -11,9 +11,6 @@
-
-
-
diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml b/src/Snap.Hutao/Snap.Hutao/App.xaml
index b296064c..3baaeaee 100644
--- a/src/Snap.Hutao/Snap.Hutao/App.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/App.xaml
@@ -1,19 +1,27 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
-
+
-
+
+
+ 8
+ 4
+ 4,4,0,0
+ 0,4,4,0
+ 0,0,4,4
+ 2,2,2,2
diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
index 337d0933..746c88a0 100644
--- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
@@ -2,8 +2,9 @@
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
+using Snap.Hutao.Core;
+using Snap.Hutao.Web.Request;
namespace Snap.Hutao;
@@ -23,12 +24,7 @@ public partial class App : Application
// load app resource
InitializeComponent();
- // prepare DI
- Ioc.Default.ConfigureServices(new ServiceCollection()
- .AddLogging(builder => builder.AddDebug())
- .AddHttpClient()
- .AddInjections(typeof(App))
- .BuildServiceProvider());
+ InitializeDependencyInjection();
}
///
@@ -41,4 +37,32 @@ public partial class App : Application
mainWindow = Ioc.Default.GetRequiredService();
mainWindow.Activate();
}
-}
+
+ private static void InitializeDependencyInjection()
+ {
+ // prepare DI
+ IServiceProvider services = new ServiceCollection()
+ .AddLogging(builder => builder.AddDebug())
+
+ // http json
+ .AddHttpClient()
+ .ConfigureHttpClient(client =>
+ {
+ client.Timeout = Timeout.InfiniteTimeSpan;
+ client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Snap Hutao");
+ })
+ .Services
+
+ // requester & auth reuqester
+ .AddHttpClient(nameof(Requester))
+ .AddTypedClient()
+ .ConfigureHttpClient(client => client.Timeout = Timeout.InfiniteTimeSpan)
+ .Services
+
+ // inject app wide services
+ .AddInjections(typeof(App))
+ .BuildServiceProvider();
+
+ Ioc.Default.ConfigureServices(services);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/AutoHeightBehavior.cs b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/AutoHeightBehavior.cs
new file mode 100644
index 00000000..dbc5f750
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/AutoHeightBehavior.cs
@@ -0,0 +1,56 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml;
+using Microsoft.Xaml.Interactivity;
+using Snap.Hutao.Core;
+
+namespace Snap.Hutao.Control.Behavior;
+
+///
+/// 按给定比例自动调整高度的行为
+///
+internal class AutoHeightBehavior : Behavior
+{
+ private static readonly DependencyProperty TargetWidthProperty = Property.Depend(nameof(TargetWidth), 1080D);
+ private static readonly DependencyProperty TargetHeightProperty = Property.Depend(nameof(TargetHeight), 390D);
+
+ ///
+ /// 目标宽度
+ ///
+ public double TargetWidth
+ {
+ get => (double)GetValue(TargetWidthProperty);
+
+ set => SetValue(TargetWidthProperty, value);
+ }
+
+ ///
+ /// 目标高度
+ ///
+ public double TargetHeight
+ {
+ get => (double)GetValue(TargetHeightProperty);
+
+ set => SetValue(TargetHeightProperty, value);
+ }
+
+ ///
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+ AssociatedObject.SizeChanged += OnSizeChanged;
+ }
+
+ ///
+ protected override void OnDetaching()
+ {
+ base.OnDetaching();
+ AssociatedObject.SizeChanged -= OnSizeChanged;
+ }
+
+ private void OnSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ AssociatedObject.Height = (double)((FrameworkElement)sender).ActualWidth * (TargetHeight / TargetWidth);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Cancellable/CancellablePage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Cancellable/CancellablePage.cs
new file mode 100644
index 00000000..362b72b7
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Cancellable/CancellablePage.cs
@@ -0,0 +1,33 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Navigation;
+
+namespace Snap.Hutao.Control.Cancellable;
+
+///
+/// 表示支持取消加载的异步页面
+/// 在被导航到其他页面前触发取消异步通知
+///
+public class CancellablePage : Page
+{
+ private readonly CancellationTokenSource viewLoadingConcellationTokenSource = new();
+
+ ///
+ /// 初始化
+ ///
+ /// 视图模型
+ public void Initialize(ISupportCancellation viewModel)
+ {
+ viewModel.CancellationToken = viewLoadingConcellationTokenSource.Token;
+ DataContext = viewModel;
+ }
+
+ ///
+ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
+ {
+ viewLoadingConcellationTokenSource.Cancel();
+ base.OnNavigatingFrom(e);
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Cancellable/ISupportCancellation.cs b/src/Snap.Hutao/Snap.Hutao/Control/Cancellable/ISupportCancellation.cs
new file mode 100644
index 00000000..62f8de0f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Cancellable/ISupportCancellation.cs
@@ -0,0 +1,15 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Cancellable;
+
+///
+/// 指示此类支持取消任务
+///
+public interface ISupportCancellation
+{
+ ///
+ /// 用于通知取消的取消回执
+ ///
+ CancellationToken CancellationToken { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Browser.cs b/src/Snap.Hutao/Snap.Hutao/Core/Browser.cs
new file mode 100644
index 00000000..2d5fd681
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Browser.cs
@@ -0,0 +1,44 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Core;
+
+///
+/// 封装了打开浏览器的方法
+///
+public static class Browser
+{
+ ///
+ /// 打开浏览器
+ ///
+ /// 链接
+ /// 失败时执行的回调
+ public static void Open(string url, Action? failAction = null)
+ {
+ try
+ {
+ ProcessHelper.Start(url);
+ }
+ catch (Exception ex)
+ {
+ failAction?.Invoke(ex);
+ }
+ }
+
+ ///
+ /// 打开浏览器
+ ///
+ /// 获取链接回调
+ /// 失败时执行的回调
+ public static void Open(Func urlFunc, Action? failAction = null)
+ {
+ try
+ {
+ ProcessHelper.Start(urlFunc.Invoke());
+ }
+ catch (Exception ex)
+ {
+ failAction?.Invoke(ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/InjectAs.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/InjectAs.cs
index 0a2bf0f8..a9b0adcd 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/InjectAs.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/InjectAs.cs
@@ -1,4 +1,7 @@
-namespace Snap.Hutao.Core.DependencyInjection;
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Core.DependencyInjection;
///
/// 注入方法
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceCollectionExtensions.cs
index ca982e17..75b11955 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceCollectionExtensions.cs
@@ -2,8 +2,6 @@
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
-using Snap.Hutao.Core.DependencyInjection.Annotation;
-using Snap.Hutao.Core.Validation;
using Snap.Hutao.Extension;
namespace Snap.Hutao.Core.DependencyInjection;
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/HttpJson.cs b/src/Snap.Hutao/Snap.Hutao/Core/HttpJson.cs
new file mode 100644
index 00000000..b0df8fb2
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/HttpJson.cs
@@ -0,0 +1,39 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Net.Http;
+
+namespace Snap.Hutao.Core;
+
+///
+/// Http Json 处理
+///
+public class HttpJson
+{
+ private readonly Json json;
+ private readonly HttpClient httpClient;
+
+ ///
+ /// 初始化一个新的 Http Json 处理 实例
+ ///
+ /// Json 处理器
+ /// http 客户端
+ public HttpJson(Json json, HttpClient httpClient)
+ {
+ this.json = json;
+ this.httpClient = httpClient;
+ }
+
+ ///
+ /// 从网站上下载json并转换为对象
+ ///
+ /// 对象的类型
+ /// 链接
+ /// 取消令牌
+ /// Json字符串中的反序列化对象, 如果反序列化失败会抛出异常
+ public async Task FromWebsiteAsync(string url, CancellationToken cancellationToken = default)
+ {
+ string response = await httpClient.GetStringAsync(url, cancellationToken);
+ return json.ToObject(response);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json.cs
new file mode 100644
index 00000000..eb470f2a
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Json.cs
@@ -0,0 +1,133 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+using System.IO;
+
+namespace Snap.Hutao.Core;
+
+///
+/// Json操作
+///
+[Injection(InjectAs.Transient)]
+public class Json
+{
+ private readonly ILogger logger;
+
+ private readonly JsonSerializerSettings jsonSerializerSettings = new()
+ {
+ DateFormatString = "yyyy'-'MM'-'dd' 'HH':'mm':'ss.FFFFFFFK",
+ Formatting = Formatting.Indented,
+ };
+
+ ///
+ /// 初始化一个新的 Json操作 实例
+ ///
+ /// 日志器
+ public Json(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ ///
+ /// 将JSON反序列化为指定的.NET类型
+ ///
+ /// 要反序列化的对象的类型
+ /// 要反序列化的JSON
+ /// Json字符串中的反序列化对象, 如果反序列化失败会抛出异常
+ public T? ToObject(string value)
+ {
+ try
+ {
+ return JsonConvert.DeserializeObject(value);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError("反序列化Json时遇到问题:{ex}", ex);
+ }
+
+ return default;
+ }
+
+ ///
+ /// 将JSON反序列化为指定的.NET类型
+ /// 若为null则返回一个新建的实例
+ ///
+ /// 指定的类型
+ /// 字符串
+ /// Json字符串中的反序列化对象, 如果反序列化失败会抛出异常
+ public T ToObjectOrNew(string value)
+ where T : new()
+ {
+ return ToObject(value) ?? new T();
+ }
+
+ ///
+ /// 将指定的对象序列化为JSON字符串
+ ///
+ /// 要序列化的对象
+ /// 对象的JSON字符串表示形式
+ public string Stringify(object? value)
+ {
+ return JsonConvert.SerializeObject(value, jsonSerializerSettings);
+ }
+
+ ///
+ /// 使用 , 和 从文件中读取后转化为实体类
+ ///
+ /// 要反序列化的对象的类型
+ /// 存放JSON数据的文件路径
+ /// JSON字符串中的反序列化对象, 如果反序列化失败则抛出异常,若文件不存在则返回
+ public T? FromFile(string fileName)
+ {
+ if (File.Exists(fileName))
+ {
+ // FileShare.Read is important to read some file
+ using (StreamReader sr = new(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)))
+ {
+ return ToObject(sr.ReadToEnd());
+ }
+ }
+ else
+ {
+ return default;
+ }
+ }
+
+ ///
+ /// 使用 , 和 从文件中读取后转化为实体类
+ /// 若为null则返回一个新建的实例
+ ///
+ /// 要反序列化的对象的类型
+ /// 存放JSON数据的文件路径
+ /// JSON字符串中的反序列化对象
+ public T FromFileOrNew(string fileName)
+ where T : new()
+ {
+ return FromFile(fileName) ?? new T();
+ }
+
+ ///
+ /// 从文件中读取后转化为实体类
+ ///
+ /// 要反序列化的对象的类型
+ /// 存放JSON数据的文件
+ /// JSON字符串中的反序列化对象
+ public T? FromFile(FileInfo file)
+ {
+ using (StreamReader sr = file.OpenText())
+ {
+ return ToObject(sr.ReadToEnd());
+ }
+ }
+
+ ///
+ /// 将对象保存到文件
+ ///
+ /// 文件名称
+ /// 对象
+ public void ToFile(string fileName, object? value)
+ {
+ File.WriteAllText(fileName, Stringify(value));
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs
index 253b5b74..3dcb9f33 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs
@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using Microsoft.Extensions.Logging;
-
namespace Snap.Hutao.Core.Logging;
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Observable.cs b/src/Snap.Hutao/Snap.Hutao/Core/Observable.cs
new file mode 100644
index 00000000..9dc24c49
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Observable.cs
@@ -0,0 +1,43 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Snap.Hutao.Core;
+
+///
+/// 简单的实现了 接口
+///
+public class Observable : INotifyPropertyChanged
+{
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ ///
+ /// 设置字段的值
+ ///
+ /// 字段类型
+ /// 现有值
+ /// 新的值
+ /// 属性名称
+ protected void Set([NotNullIfNotNull("value")] ref T storage, T value, [CallerMemberName] string propertyName = default!)
+ {
+ if (Equals(storage, value))
+ {
+ return;
+ }
+
+ storage = value;
+ OnPropertyChanged(propertyName);
+ }
+
+ ///
+ /// 触发
+ ///
+ /// 属性名称
+ protected void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs
new file mode 100644
index 00000000..47db1914
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs
@@ -0,0 +1,55 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Diagnostics;
+
+namespace Snap.Hutao.Core;
+
+///
+/// 进程帮助类
+///
+public static class ProcessHelper
+{
+ ///
+ /// 启动进程
+ ///
+ /// 路径
+ /// 使用shell
+ /// 进程
+ public static Process? Start(string path, bool useShellExecute = true)
+ {
+ ProcessStartInfo processInfo = new(path)
+ {
+ UseShellExecute = useShellExecute,
+ };
+ return Process.Start(processInfo);
+ }
+
+ ///
+ /// 启动进程
+ ///
+ /// 路径
+ /// 命令行参数
+ /// 使用shell
+ /// 进程
+ public static Process? Start(string path, string arguments, bool useShellExecute = true)
+ {
+ ProcessStartInfo processInfo = new(path)
+ {
+ UseShellExecute = useShellExecute,
+ Arguments = arguments,
+ };
+ return Process.Start(processInfo);
+ }
+
+ ///
+ /// 启动进程
+ ///
+ /// 路径
+ /// 使用shell
+ /// 进程
+ public static Process? Start(Uri uri, bool useShellExecute = true)
+ {
+ return Start(uri.AbsolutePath, useShellExecute);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Property.cs b/src/Snap.Hutao/Snap.Hutao/Core/Property.cs
index 882e0f67..c7070b2c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Property.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Property.cs
@@ -19,7 +19,7 @@ internal static class Property
/// 注册的依赖属性
public static DependencyProperty Depend(string name)
{
- return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new PropertyMetadata(default(TProperty)));
+ return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new PropertyMetadata(null));
}
///
@@ -55,7 +55,7 @@ internal static class Property
/// 注册的附加属性
public static DependencyProperty Attach(string name)
{
- return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new PropertyMetadata(default(TProperty)));
+ return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new PropertyMetadata(null));
}
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs
new file mode 100644
index 00000000..39ee0715
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs
@@ -0,0 +1,80 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft;
+
+namespace Snap.Hutao.Core.Threading;
+
+///
+/// 提供简单易用的状态提示信息
+/// 用于任务的状态跟踪
+/// 同时继承了
+///
+public class Watcher : Observable
+{
+ private readonly bool isReusable;
+ private bool hasUsed;
+ private bool isWorking;
+ private bool isCompleted;
+
+ ///
+ /// 构造一个新的工作监视器
+ ///
+ /// 是否可以重用
+ public Watcher(bool isReusable = true)
+ {
+ this.isReusable = isReusable;
+ }
+
+ ///
+ /// 是否正在工作
+ ///
+ public bool IsWorking
+ {
+ get => isWorking;
+
+ private set => Set(ref isWorking, value);
+ }
+
+ ///
+ /// 工作是否完成
+ ///
+ public bool IsCompleted
+ {
+ get => isCompleted;
+
+ private set => Set(ref isCompleted, value);
+ }
+
+ ///
+ /// 对某个操作进行监视,
+ /// 无法防止代码重入
+ ///
+ /// 一个可释放的对象,用于在操作完成时自动提示监视器工作已经完成
+ /// 重用了一个不可重用的监视器
+ public IDisposable Watch()
+ {
+ Verify.Operation(isReusable || !hasUsed, $"此 {nameof(Watcher)} 不允许多次使用");
+
+ hasUsed = true;
+ IsWorking = true;
+
+ return new WorkDisposable(this);
+ }
+
+ private struct WorkDisposable : IDisposable
+ {
+ private readonly Watcher work;
+
+ public WorkDisposable(Watcher work)
+ {
+ this.work = work;
+ }
+
+ public void Dispose()
+ {
+ work.IsWorking = false;
+ work.IsCompleted = true;
+ }
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs b/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs
new file mode 100644
index 00000000..83c948d3
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs
@@ -0,0 +1,46 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.Web.WebView2.Core;
+
+namespace Snap.Hutao.Core;
+
+///
+/// 检测 WebView2运行时 是否存在
+/// 不再使用注册表检查方式
+///
+internal class WebView2Helper
+{
+ private static bool hasEverDetected = false;
+ private static bool isSupported = false;
+
+ ///
+ /// 检测 WebView2 是否存在
+ ///
+ public static bool IsSupported
+ {
+ get
+ {
+ if (hasEverDetected)
+ {
+ return isSupported;
+ }
+ else
+ {
+ hasEverDetected = true;
+ isSupported = true;
+ try
+ {
+ string version = CoreWebView2Environment.GetAvailableBrowserVersionString();
+ }
+ catch (Exception ex)
+ {
+ Ioc.Default.GetRequiredService>().LogError(ex, "WebView2 运行时未安装");
+ isSupported = false;
+ }
+
+ return isSupported;
+ }
+ }
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/ReflectionExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/ReflectionExtension.cs
index 2c9bf79c..4b28296c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Extension/ReflectionExtension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Extension/ReflectionExtension.cs
@@ -1,4 +1,7 @@
-using System.Reflection;
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Reflection;
namespace Snap.Hutao.Extension;
diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IAsyncRelayCommandFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IAsyncRelayCommandFactory.cs
new file mode 100644
index 00000000..02f2469e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IAsyncRelayCommandFactory.cs
@@ -0,0 +1,76 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using CommunityToolkit.Mvvm.Input;
+
+namespace Snap.Hutao.Factory.Abstraction;
+
+///
+/// Factory for creating with additional processing.
+///
+public interface IAsyncRelayCommandFactory
+{
+ ///
+ /// Create a reference to AsyncRelayCommand.
+ ///
+ /// The cancelable execution logic.
+ /// AsyncRelayCommand.
+ AsyncRelayCommand Create(Func cancelableExecute);
+
+ ///
+ /// Create a reference to AsyncRelayCommand.
+ ///
+ /// The cancelable execution logic.
+ /// The execution status logic.
+ /// AsyncRelayCommand.
+ AsyncRelayCommand Create(Func cancelableExecute, Func canExecute);
+
+ ///
+ /// Create a reference to AsyncRelayCommand.
+ ///
+ /// The execution logic.
+ /// AsyncRelayCommand.
+ AsyncRelayCommand Create(Func execute);
+
+ ///
+ /// Create a reference to AsyncRelayCommand.
+ ///
+ /// The execution logic.
+ /// The execution status logic.
+ /// AsyncRelayCommand.
+ AsyncRelayCommand Create(Func execute, Func canExecute);
+
+ ///
+ /// Create a reference to AsyncRelayCommand.
+ ///
+ /// The type of the command parameter.
+ /// The cancelable execution logic.
+ /// AsyncRelayCommand.
+ AsyncRelayCommand Create(Func cancelableExecute);
+
+ ///
+ /// Create a reference to AsyncRelayCommand.
+ ///
+ /// The type of the command parameter.
+ /// The cancelable execution logic.
+ /// The execution status logic.
+ /// AsyncRelayCommand.
+ AsyncRelayCommand Create(Func cancelableExecute, Predicate canExecute);
+
+ ///
+ /// Create a reference to AsyncRelayCommand.
+ ///
+ /// The type of the command parameter.
+ /// The execution logic.
+ /// AsyncRelayCommand.
+ AsyncRelayCommand Create(Func execute);
+
+ ///
+ /// Create a reference to AsyncRelayCommand.
+ ///
+ /// The type of the command parameter.
+ /// The execution logic.
+ /// The execution status logic.
+ /// AsyncRelayCommand.
+ AsyncRelayCommand Create(Func execute, Predicate canExecute);
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs
new file mode 100644
index 00000000..90b1472d
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs
@@ -0,0 +1,102 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.AppCenter.Crashes;
+using Snap.Hutao.Factory.Abstraction;
+
+namespace Snap.Hutao.Factory;
+
+///
+[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
+internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
+{
+ private readonly ILogger logger;
+
+ ///
+ /// 构造一个新的异步命令工厂
+ ///
+ /// 日志器
+ public AsyncRelayCommandFactory(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ ///
+ public AsyncRelayCommand Create(Func execute)
+ {
+ return Register(new AsyncRelayCommand(execute));
+ }
+
+ ///
+ public AsyncRelayCommand Create(Func cancelableExecute)
+ {
+ return Register(new AsyncRelayCommand(cancelableExecute));
+ }
+
+ ///
+ public AsyncRelayCommand Create(Func execute, Predicate canExecute)
+ {
+ return Register(new AsyncRelayCommand(execute, canExecute));
+ }
+
+ ///
+ public AsyncRelayCommand Create(Func cancelableExecute, Predicate canExecute)
+ {
+ return Register(new AsyncRelayCommand(cancelableExecute, canExecute));
+ }
+
+ ///
+ public AsyncRelayCommand Create(Func execute)
+ {
+ return Register(new AsyncRelayCommand(execute));
+ }
+
+ ///
+ public AsyncRelayCommand Create(Func cancelableExecute)
+ {
+ return Register(new AsyncRelayCommand(cancelableExecute));
+ }
+
+ ///
+ public AsyncRelayCommand Create(Func execute, Func canExecute)
+ {
+ return Register(new AsyncRelayCommand(execute, canExecute));
+ }
+
+ ///
+ public AsyncRelayCommand Create(Func cancelableExecute, Func canExecute)
+ {
+ return Register(new AsyncRelayCommand(cancelableExecute, canExecute));
+ }
+
+ private AsyncRelayCommand Register(AsyncRelayCommand command)
+ {
+ ReportException(command);
+ return command;
+ }
+
+ private AsyncRelayCommand Register(AsyncRelayCommand command)
+ {
+ ReportException(command);
+ return command;
+ }
+
+ private void ReportException(IAsyncRelayCommand command)
+ {
+ command.PropertyChanged += (sender, args) =>
+ {
+ if (sender is IAsyncRelayCommand asyncRelayCommand)
+ {
+ if (args.PropertyName == nameof(AsyncRelayCommand.ExecutionTask))
+ {
+ if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
+ {
+ logger.LogError(exception, "异步命令发生了错误");
+ Crashes.TrackError(exception);
+ }
+ }
+ }
+ };
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs b/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs
index 36fdfdfc..27a15f43 100644
--- a/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs
+++ b/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs
@@ -2,11 +2,11 @@
// Licensed under the MIT license.
global using CommunityToolkit.Mvvm.DependencyInjection;
-global using Microsoft;
+global using Microsoft.Extensions.Logging;
global using Snap.Hutao.Core.DependencyInjection;
global using Snap.Hutao.Core.DependencyInjection.Annotation;
global using Snap.Hutao.Core.Validation;
global using System;
global using System.Diagnostics.CodeAnalysis;
-global using System.Windows;
-global using System.Windows.Input;
+global using System.Threading;
+global using System.Threading.Tasks;
diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml
index b4c7bcbd..ed9a2b68 100644
--- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml
@@ -2,7 +2,6 @@
x:Class="Snap.Hutao.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:local="using:Snap.Hutao"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:view="using:Snap.Hutao.View"
@@ -18,7 +17,6 @@
x:Name="TitleBarGrid"
Background="Transparent"/>
-
+
diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
index 101f94d7..b81d279c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
+++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
@@ -9,7 +9,7 @@
+ Version="1.0.2.0" />
胡桃
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IAnnouncementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IAnnouncementService.cs
new file mode 100644
index 00000000..e836cae3
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IAnnouncementService.cs
@@ -0,0 +1,21 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+using System.Windows.Input;
+
+namespace Snap.Hutao.Service.Abstraction;
+
+///
+/// 公告服务
+///
+public interface IAnnouncementService
+{
+ ///
+ /// 异步获取游戏公告与活动
+ ///
+ /// 打开公告时触发的命令
+ /// 取消令牌
+ /// 公告包装器
+ Task GetAnnouncementsAsync(ICommand openAnnouncementUICommand, CancellationToken cancellationToken = default);
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs
new file mode 100644
index 00000000..5878871c
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs
@@ -0,0 +1,100 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml.Controls;
+
+namespace Snap.Hutao.Service.Abstraction;
+
+///
+/// 消息条服务
+///
+public interface IInfoBarService
+{
+ ///
+ /// 显示错误消息
+ ///
+ /// 消息
+ /// 关闭延迟
+ void Error(string message, int delay = 0);
+
+ ///
+ /// 显示错误消息
+ ///
+ /// 标题
+ /// 消息
+ /// 关闭延迟
+ void Error(string title, string message, int delay = 0);
+
+ ///
+ /// 显示错误消息
+ ///
+ /// 异常
+ /// 关闭延迟
+ void Error(Exception ex, int delay = 0);
+
+ ///
+ /// 显示错误消息
+ ///
+ /// 异常
+ /// 标题
+ /// 关闭延迟
+ void Error(Exception ex, string title, int delay = 0);
+
+ ///
+ /// 显示提示信息
+ ///
+ /// 消息
+ /// 关闭延迟
+ void Information(string message, int delay = 3000);
+
+ ///
+ /// 显示提示信息
+ ///
+ /// 标题
+ /// 消息
+ /// 关闭延迟
+ void Information(string title, string message, int delay = 3000);
+
+ ///
+ /// 使用指定的 初始化服务
+ ///
+ /// 信息条的目标容器
+ void Initialize(StackPanel container);
+
+ ///
+ /// 显示特定的信息条
+ ///
+ /// 信息条
+ /// 关闭延迟
+ void Show(InfoBar infoBar, int delay = 0);
+
+ ///
+ /// 显示成功信息
+ ///
+ /// 消息
+ /// 关闭延迟
+ void Success(string message, int delay = 3000);
+
+ ///
+ /// 显示成功信息
+ ///
+ /// 标题
+ /// 消息
+ /// 关闭延迟
+ void Success(string title, string message, int delay = 3000);
+
+ ///
+ /// 显示警告信息
+ ///
+ /// 消息
+ /// 关闭延迟
+ void Warning(string message, int delay = 0);
+
+ ///
+ /// 显示警告信息
+ ///
+ /// 标题
+ /// 消息
+ /// 关闭延迟
+ void Warning(string title, string message, int delay = 0);
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs
new file mode 100644
index 00000000..5654cb8e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs
@@ -0,0 +1,101 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Service.Abstraction;
+using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Windows.Input;
+
+namespace Snap.Hutao.Service;
+
+///
+[Injection(InjectAs.Transient, typeof(IAnnouncementService))]
+internal class AnnouncementService : IAnnouncementService
+{
+ private readonly AnnouncementProvider announcementProvider;
+
+ ///
+ /// 构造一个新的公告服务
+ ///
+ /// 公告提供器
+ public AnnouncementService(AnnouncementProvider announcementProvider)
+ {
+ this.announcementProvider = announcementProvider;
+ }
+
+ ///
+ public async Task GetAnnouncementsAsync(ICommand openAnnouncementUICommand, CancellationToken cancellationToken = default)
+ {
+ AnnouncementWrapper? wrapper = await announcementProvider.GetAnnouncementWrapperAsync(cancellationToken);
+ List contents = await announcementProvider.GetAnnouncementContentsAsync(cancellationToken);
+
+ Dictionary contentMap = contents
+ .ToDictionary(id => id.AnnId, content => content.Content);
+
+ if (wrapper?.List is List announcementListWrappers)
+ {
+ // 将活动公告置于上方
+ announcementListWrappers.Reverse();
+
+ // 将公告内容联入公告列表
+ JoinAnnouncements(openAnnouncementUICommand, contentMap, announcementListWrappers);
+
+ // we only cares about activities
+ if (announcementListWrappers[0].List is List activities)
+ {
+ AdjustActivitiesTime(ref activities);
+ }
+
+ return wrapper;
+ }
+
+ return new();
+ }
+
+ private void JoinAnnouncements(ICommand openAnnouncementUICommand, Dictionary contentMap, List announcementListWrappers)
+ {
+ // 匹配特殊的时间格式: (.*?)
+ Regex timeTagRegrex = new("<t.*?>(.*?)</t>", RegexOptions.Multiline);
+ Regex timeTagInnerRegex = new("(?<=<t.*?>)(.*?)(?=</t>)");
+
+ announcementListWrappers.ForEach(listWrapper =>
+ {
+ listWrapper.List?.ForEach(item =>
+ {
+ // fix key issue
+ if (contentMap.TryGetValue(item.AnnId, out string? rawContent))
+ {
+ // remove tag
+ rawContent = timeTagRegrex.Replace(rawContent!, x => timeTagInnerRegex.Match(x.Value).Value);
+ }
+
+ item.Content = rawContent;
+ item.OpenAnnouncementUICommand = openAnnouncementUICommand;
+ });
+ });
+ }
+
+ private void AdjustActivitiesTime(ref List activities)
+ {
+ // Match yyyy/MM/dd HH:mm:ss time format
+ Regex dateTimeRegex = new(@"(\d+\/\d+\/\d+\s\d+:\d+:\d+)", RegexOptions.IgnoreCase | RegexOptions.Multiline);
+ activities.ForEach(item =>
+ {
+ Match matched = dateTimeRegex.Match(item.Content ?? string.Empty);
+ if (matched.Success && DateTime.TryParse(matched.Value, out DateTime time))
+ {
+ if (time > item.StartTime && time < item.EndTime)
+ {
+ item.StartTime = time;
+ }
+ }
+ });
+
+ activities = activities
+ .OrderBy(i => i.StartTime)
+ .ThenBy(i => i.EndTime)
+ .ToList();
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs
new file mode 100644
index 00000000..001793c7
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs
@@ -0,0 +1,121 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.VisualStudio.Threading;
+using Snap.Hutao.Service.Abstraction;
+
+namespace Snap.Hutao.Service;
+
+///
+[Injection(InjectAs.Singleton, typeof(IInfoBarService))]
+internal class InfoBarService : IInfoBarService
+{
+ private StackPanel? infoBarStack;
+
+ ///
+ public void Initialize(StackPanel container)
+ {
+ infoBarStack = container;
+ }
+
+ ///
+ public void Information(string message, int delay = 3000)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Informational, null, message, delay);
+ }
+
+ ///
+ public void Information(string title, string message, int delay = 3000)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Informational, title, message, delay);
+ }
+
+ ///
+ public void Success(string message, int delay = 3000)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Success, null, message, delay);
+ }
+
+ ///
+ public void Success(string title, string message, int delay = 3000)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Success, title, message, delay);
+ }
+
+ ///
+ public void Warning(string message, int delay = 0)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Warning, null, message, delay);
+ }
+
+ ///
+ public void Warning(string title, string message, int delay = 0)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Warning, title, message, delay);
+ }
+
+ ///
+ public void Error(string message, int delay = 0)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Error, null, message, delay);
+ }
+
+ ///
+ public void Error(string title, string message, int delay = 0)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Error, title, message, delay);
+ }
+
+ ///
+ public void Error(Exception ex, int delay = 0)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, ex.Message, delay);
+ }
+
+ ///
+ public void Error(Exception ex, string title, int delay = 0)
+ {
+ PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay);
+ }
+
+ ///
+ public void Show(InfoBar infoBar, int delay = 0)
+ {
+ Must.NotNull(infoBarStack!).DispatcherQueue.TryEnqueue(ShowInfoBarOnUIThreadAsync(infoBarStack, infoBar, delay).Forget);
+ }
+
+ private async Task ShowInfoBarOnUIThreadAsync(StackPanel stack, InfoBar infoBar, int delay)
+ {
+ infoBar.Closed += OnInfoBarClosed;
+ stack.Children.Add(infoBar);
+
+ if (delay > 0)
+ {
+ await Task.Delay(delay);
+ infoBar.IsOpen = false;
+ }
+ }
+
+ private void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay)
+ {
+ InfoBar infoBar = new()
+ {
+ Severity = severity,
+ Title = title,
+ Message = message,
+ IsOpen = true,
+ };
+
+ Show(infoBar, delay);
+ }
+
+ private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args)
+ {
+ Must.NotNull(infoBarStack!).DispatcherQueue.TryEnqueue(() =>
+ {
+ infoBarStack.Children.Remove(sender);
+ sender.Closed -= OnInfoBarClosed;
+ });
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/NavigationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/NavigationService.cs
index a8b503f6..7e0aff4a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/NavigationService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/NavigationService.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.AppCenter.Analytics;
-using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
@@ -16,7 +15,7 @@ namespace Snap.Hutao.Service;
///
/// 导航服务
///
-[Injection(InjectAs.Transient, typeof(INavigationService))]
+[Injection(InjectAs.Singleton, typeof(INavigationService))]
internal class NavigationService : INavigationService
{
private readonly ILogger logger;
@@ -120,7 +119,7 @@ internal class NavigationService : INavigationService
}
// 首次导航失败时使属性持续保存为false
- HasEverNavigated |= result;
+ HasEverNavigated = HasEverNavigated || result;
return result;
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
index 55c4694d..1ef2f3aa 100644
--- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
+++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
@@ -14,6 +14,7 @@
true
zh-CN
zh-CN
+
False
True
F8C2255969BEA4A681CED102771BF807856AEC02
@@ -27,6 +28,8 @@
+
+
@@ -47,15 +50,23 @@
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -73,13 +84,21 @@
-
-
+
+
+ MSBuild:Compile
+
+
+
+
+ MSBuild:Compile
+
+
MSBuild:Compile
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToObjectConverter.cs b/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToObjectConverter.cs
new file mode 100644
index 00000000..3307a7f9
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToObjectConverter.cs
@@ -0,0 +1,86 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Data;
+
+namespace Snap.Hutao.View.Converter;
+
+///
+/// This class converts a boolean value into an other object.
+/// Can be used to convert true/false to visibility, a couple of colors, couple of images, etc.
+///
+public class BoolToObjectConverter : DependencyObject, IValueConverter
+{
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly DependencyProperty TrueValueProperty =
+ DependencyProperty.Register(nameof(TrueValue), typeof(object), typeof(BoolToObjectConverter), new PropertyMetadata(null));
+
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly DependencyProperty FalseValueProperty =
+ DependencyProperty.Register(nameof(FalseValue), typeof(object), typeof(BoolToObjectConverter), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets the value to be returned when the boolean is true
+ ///
+ public object TrueValue
+ {
+ get => GetValue(TrueValueProperty);
+ set => SetValue(TrueValueProperty, value);
+ }
+
+ ///
+ /// Gets or sets the value to be returned when the boolean is false
+ ///
+ public object FalseValue
+ {
+ get => GetValue(FalseValueProperty);
+ set => SetValue(FalseValueProperty, value);
+ }
+
+ ///
+ /// Convert a boolean value to an other object.
+ ///
+ /// The source data being passed to the target.
+ /// The type of the target property, as a type reference.
+ /// An optional parameter to be used to invert the converter logic.
+ /// The language of the conversion.
+ /// The value to be passed to the target dependency property.
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ bool boolValue = value is bool valid && valid;
+
+ // Negate if needed
+ if (ConvertHelper.TryParseBool(parameter))
+ {
+ boolValue = !boolValue;
+ }
+
+ return ConvertHelper.Convert(boolValue ? TrueValue : FalseValue, targetType);
+ }
+
+ ///
+ /// Convert back the value to a boolean
+ ///
+ /// If the parameter is a reference type, must match its reference to return true.
+ /// The target data being passed to the source.
+ /// The type of the target property, as a type reference (System.Type for Microsoft .NET, a TypeName helper struct for Visual C++ component extensions (C++/CX)).
+ /// An optional parameter to be used to invert the converter logic.
+ /// The language of the conversion.
+ /// The value to be passed to the source object.
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ bool result = Equals(value, ConvertHelper.Convert(TrueValue, value.GetType()));
+
+ if (ConvertHelper.TryParseBool(parameter))
+ {
+ result = !result;
+ }
+
+ return result;
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToVisibilityConverter.cs b/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToVisibilityConverter.cs
new file mode 100644
index 00000000..3884dfc1
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToVisibilityConverter.cs
@@ -0,0 +1,21 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml;
+
+namespace Snap.Hutao.View.Converter;
+
+///
+/// This class converts a boolean value into a Visibility enumeration.
+///
+public class BoolToVisibilityConverter : BoolToObjectConverter
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BoolToVisibilityConverter()
+ {
+ TrueValue = Visibility.Visible;
+ FalseValue = Visibility.Collapsed;
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToVisibilityRevertConverter.cs b/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToVisibilityRevertConverter.cs
new file mode 100644
index 00000000..42cf1fbb
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Converter/BoolToVisibilityRevertConverter.cs
@@ -0,0 +1,21 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml;
+
+namespace Snap.Hutao.View.Converter;
+
+///
+/// This class converts a boolean value into a Visibility enumeration.
+///
+public class BoolToVisibilityRevertConverter : BoolToObjectConverter
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BoolToVisibilityRevertConverter()
+ {
+ TrueValue = Visibility.Collapsed;
+ FalseValue = Visibility.Visible;
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Converter/ConvertHelper.cs b/src/Snap.Hutao/Snap.Hutao/View/Converter/ConvertHelper.cs
new file mode 100644
index 00000000..48d55bdd
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Converter/ConvertHelper.cs
@@ -0,0 +1,41 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml.Markup;
+
+namespace Snap.Hutao.View.Converter;
+
+///
+/// Static class used to provide internal tools
+///
+internal static class ConvertHelper
+{
+ ///
+ /// Helper method to safely cast an object to a boolean
+ ///
+ /// Parameter to cast to a boolean
+ /// Bool value or false if cast failed
+ internal static bool TryParseBool(object parameter)
+ {
+ bool parsed = false;
+ if (parameter != null)
+ {
+ _ = bool.TryParse(parameter.ToString(), out parsed);
+ }
+
+ return parsed;
+ }
+
+ ///
+ /// Helper method to convert a value from a source type to a target type.
+ ///
+ /// The value to convert
+ /// The target type
+ /// The converted value
+ internal static object Convert(object value, Type targetType)
+ {
+ return targetType.IsInstanceOfType(value)
+ ? value
+ : XamlBindingHelper.ConvertValue(targetType, value);
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Converter/PercentageToHeightConverter.cs b/src/Snap.Hutao/Snap.Hutao/View/Converter/PercentageToHeightConverter.cs
new file mode 100644
index 00000000..1263b05e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Converter/PercentageToHeightConverter.cs
@@ -0,0 +1,49 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Data;
+using Snap.Hutao.Core;
+
+namespace Snap.Hutao.View.Converter;
+
+///
+/// 百分比转换高度
+///
+public sealed class PercentageToHeightConverter : DependencyObject, IValueConverter
+{
+ private static readonly DependencyProperty TargetWidthProperty = Property.Depend(nameof(TargetWidth), 1080D);
+ private static readonly DependencyProperty TargetHeightProperty = Property.Depend(nameof(TargetHeight), 390D);
+
+ ///
+ /// 目标宽度
+ ///
+ public double TargetWidth
+ {
+ get => (double)GetValue(TargetWidthProperty);
+
+ set => SetValue(TargetWidthProperty, value);
+ }
+
+ ///
+ /// 目标高度
+ ///
+ public double TargetHeight
+ {
+ get => (double)GetValue(TargetHeightProperty);
+
+ set => SetValue(TargetHeightProperty, value);
+ }
+
+ ///
+ public object Convert(object value, Type targetType, object parameter, string culture)
+ {
+ return (double)value * (TargetHeight / TargetWidth);
+ }
+
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, string culture)
+ {
+ throw Must.NeverHappen();
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Converter/PercentageToWidthConverter.cs b/src/Snap.Hutao/Snap.Hutao/View/Converter/PercentageToWidthConverter.cs
new file mode 100644
index 00000000..fff71f66
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Converter/PercentageToWidthConverter.cs
@@ -0,0 +1,49 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Data;
+using Snap.Hutao.Core;
+
+namespace Snap.Hutao.View.Converter;
+
+///
+/// 百分比转宽度
+///
+public sealed class PercentageToWidthConverter : DependencyObject, IValueConverter
+{
+ private static readonly DependencyProperty TargetWidthProperty = Property.Depend(nameof(TargetWidth), 1080D);
+ private static readonly DependencyProperty TargetHeightProperty = Property.Depend(nameof(TargetHeight), 390D);
+
+ ///
+ /// 目标宽度
+ ///
+ public double TargetWidth
+ {
+ get => (double)GetValue(TargetWidthProperty);
+
+ set => SetValue(TargetWidthProperty, value);
+ }
+
+ ///
+ /// 目标高度
+ ///
+ public double TargetHeight
+ {
+ get => (double)GetValue(TargetHeightProperty);
+
+ set => SetValue(TargetHeightProperty, value);
+ }
+
+ ///
+ public object Convert(object value, Type targetType, object parameter, string culture)
+ {
+ return (double)value * (TargetWidth / TargetHeight);
+ }
+
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, string culture)
+ {
+ throw Must.NeverHappen();
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
index 536c480b..5672b9fb 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml
@@ -2,25 +2,73 @@
x:Class="Snap.Hutao.View.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:local="using:Snap.Hutao.View"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:helper="using:Snap.Hutao.View.Helper"
+ xmlns:page="using:Snap.Hutao.View.Page"
mc:Ignorable="d">
-
+ ExpandedModeThresholdWidth="720"
+ IsBackEnabled="{x:Bind ContentFrame.CanGoBack}">
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs
index 61f4c805..9857fdab 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs
@@ -12,15 +12,19 @@ namespace Snap.Hutao.View;
public sealed partial class MainView : UserControl
{
private readonly INavigationService navigationService;
+ private readonly IInfoBarService infoBarService;
///
/// 构造一个新的主视图
///
public MainView()
{
- this.InitializeComponent();
+ InitializeComponent();
navigationService = Ioc.Default.GetRequiredService();
navigationService.Initialize(NavView, ContentFrame);
+
+ infoBarService = Ioc.Default.GetRequiredService();
+ infoBarService.Initialize(InfoBarStack);
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml
new file mode 100644
index 00000000..3684333f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml.cs
new file mode 100644
index 00000000..dcb95ca8
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml.cs
@@ -0,0 +1,56 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml.Navigation;
+using Microsoft.VisualStudio.Threading;
+using Snap.Hutao.Core;
+
+namespace Snap.Hutao.View.Page;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class AnnouncementContentPage : Microsoft.UI.Xaml.Controls.Page
+{
+ // support click open browser.
+ private const string MihoyoSDKDefinition =
+ @"window.miHoYoGameJSSDK = {
+openInBrowser: function(url){ window.chrome.webview.postMessage(url); },
+openInWebview: function(url){ location.href = url }}";
+
+ private string? targetContent;
+
+ ///
+ /// 构造一个新的公告窗体
+ ///
+ /// 要展示的内容
+ public AnnouncementContentPage()
+ {
+ InitializeComponent();
+ }
+
+ ///
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ base.OnNavigatedTo(e);
+ targetContent = e.Parameter as string;
+ LoadAnnouncementAsync().Forget();
+ }
+
+ private async Task LoadAnnouncementAsync()
+ {
+ try
+ {
+ await WebView.EnsureCoreWebView2Async();
+
+ await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(MihoyoSDKDefinition);
+ WebView.CoreWebView2.WebMessageReceived += (_, e) => Browser.Open(e.TryGetWebMessageAsString);
+ }
+ catch
+ {
+ return;
+ }
+
+ WebView.NavigateToString(targetContent);
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml
new file mode 100644
index 00000000..21c91799
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Visible
+
+
+
+
+
+
+
+
+
+ Collapsed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml.cs
new file mode 100644
index 00000000..dfc07c1d
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml.cs
@@ -0,0 +1,22 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Control.Cancellable;
+using Snap.Hutao.ViewModel;
+
+namespace Snap.Hutao.View.Page;
+
+///
+/// 公告页面
+///
+public sealed partial class AnnouncementPage : CancellablePage
+{
+ ///
+ /// 构造一个新的公告页面
+ ///
+ public AnnouncementPage()
+ {
+ Initialize(Ioc.Default.GetRequiredService());
+ InitializeComponent();
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml.cs
index 1f7c3e24..f6b94e74 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml.cs
@@ -13,6 +13,6 @@ public sealed partial class SettingPage : Microsoft.UI.Xaml.Controls.Page
///
public SettingPage()
{
- this.InitializeComponent();
+ InitializeComponent();
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs
new file mode 100644
index 00000000..fd4f2fe5
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs
@@ -0,0 +1,109 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Snap.Hutao.Control.Cancellable;
+using Snap.Hutao.Core;
+using Snap.Hutao.Core.Threading;
+using Snap.Hutao.Factory.Abstraction;
+using Snap.Hutao.Service.Abstraction;
+using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+using System.Windows.Input;
+
+namespace Snap.Hutao.ViewModel;
+
+///
+/// 公告视图模型
+///
+[Injection(InjectAs.Transient)]
+internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
+{
+ private readonly IAnnouncementService announcementService;
+ private readonly INavigationService navigationService;
+ private readonly IInfoBarService infoBarService;
+ private readonly ILogger logger;
+
+ private AnnouncementWrapper? announcement;
+
+ ///
+ /// 构造一个公告视图模型
+ ///
+ /// 公告服务
+ /// 导航服务
+ /// 异步命令工厂
+ /// 信息条服务
+ /// 日志器
+ public AnnouncementViewModel(
+ IAnnouncementService announcementService,
+ INavigationService navigationService,
+ IAsyncRelayCommandFactory asyncRelayCommandFactory,
+ IInfoBarService infoBarService,
+ ILogger logger)
+ {
+ this.announcementService = announcementService;
+ this.navigationService = navigationService;
+ this.infoBarService = infoBarService;
+ this.logger = logger;
+
+ OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
+ OpenAnnouncementUICommand = new RelayCommand(OpenAnnouncementUI);
+ }
+
+ ///
+ public CancellationToken CancellationToken { get; set; }
+
+ ///
+ /// 公告
+ ///
+ public AnnouncementWrapper? Announcement
+ {
+ get => announcement;
+
+ set => SetProperty(ref announcement, value);
+ }
+
+ ///
+ /// 打开界面监视器
+ ///
+ public Watcher OpeningUI { get; } = new(false);
+
+ ///
+ /// 打开界面触发的命令
+ ///
+ public ICommand OpenUICommand { get; }
+
+ ///
+ /// 打开公告UI触发的命令
+ ///
+ public ICommand OpenAnnouncementUICommand { get; }
+
+ private async Task OpenUIAsync()
+ {
+ using (OpeningUI.Watch())
+ {
+ try
+ {
+ Announcement = await announcementService.GetAnnouncementsAsync(OpenAnnouncementUICommand, CancellationToken);
+ }
+ catch (TaskCanceledException)
+ {
+ logger.LogInformation("Open UI cancelled");
+ }
+ }
+ }
+
+ private void OpenAnnouncementUI(string? content)
+ {
+ logger.LogInformation("Open Announcement Command Triggered");
+
+ if (WebView2Helper.IsSupported)
+ {
+ navigationService.Navigate(data: content);
+ }
+ else
+ {
+ infoBarService.Warning("尚未安装 WebView2 运行时。");
+ }
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs
new file mode 100644
index 00000000..5cca4efa
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs
@@ -0,0 +1,23 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Web.Hoyolab;
+
+///
+/// 主机Url集合
+///
+internal static class ApiEndpoints
+{
+ ///
+ /// 公告列表
+ ///
+ public static readonly string AnnList = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnList?{AnnouncementQuery}";
+
+ ///
+ /// 公告内容
+ ///
+ public static readonly string AnnContent = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery}";
+
+ private const string Hk4eApi = "https://hk4e-api.mihoyo.com";
+ private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000";
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/Announcement.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/Announcement.cs
new file mode 100644
index 00000000..89ee03c7
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/Announcement.cs
@@ -0,0 +1,175 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+using System.Windows.Input;
+
+namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+
+///
+/// 公告
+///
+public class Announcement : AnnouncementContent
+{
+ private double timePercent;
+
+ ///
+ /// 类型标签
+ ///
+ [JsonProperty("type_label")]
+ public string? TypeLabel { get; set; }
+
+ ///
+ /// 标签文本
+ ///
+ [JsonProperty("tag_label")]
+ public string? TagLabel { get; set; }
+
+ ///
+ /// 标签图标
+ ///
+ [JsonProperty("tag_icon")]
+ public string? TagIcon { get; set; }
+
+ ///
+ /// 登录提醒
+ ///
+ [JsonProperty("login_alert")]
+ public int LoginAlert { get; set; }
+
+ ///
+ /// 开始时间
+ ///
+ [JsonProperty("start_time")]
+ public DateTime StartTime { get; set; }
+
+ ///
+ /// 结束时间
+ ///
+ [JsonProperty("end_time")]
+ public DateTime EndTime { get; set; }
+
+ ///
+ /// 启动展示窗口的命令
+ ///
+ public ICommand? OpenAnnouncementUICommand { get; set; }
+
+ ///
+ /// 是否应展示时间
+ ///
+ public bool ShouldShowTimeDescription
+ {
+ get => Type == 1;
+ }
+
+ ///
+ /// 时间
+ ///
+ public string TimeDescription
+ {
+ get
+ {
+ DateTime now = DateTime.UtcNow + TimeSpan.FromHours(8);
+
+ // 尚未开始
+ if (StartTime > now)
+ {
+ TimeSpan span = StartTime - now;
+ if (span.TotalDays <= 1)
+ {
+ return $"{(int)span.TotalHours} 小时后开始";
+ }
+
+ return $"{(int)span.TotalDays} 天后开始";
+ }
+ else
+ {
+ TimeSpan span = EndTime - now;
+ if (span.TotalDays <= 1)
+ {
+ return $"{(int)span.TotalHours} 小时后结束";
+ }
+
+ return $"{(int)span.TotalDays} 天后结束";
+ }
+ }
+ }
+
+ ///
+ /// 是否应显示时间百分比
+ ///
+ public bool ShouldShowTimePercent
+ {
+ get => ShouldShowTimeDescription && (TimePercent > 0 && TimePercent < 1);
+ }
+
+ ///
+ /// 时间百分比
+ ///
+ public double TimePercent
+ {
+ get
+ {
+ if (timePercent == 0)
+ {
+ // UTC+8
+ DateTime currentTime = DateTime.UtcNow.AddHours(8);
+ TimeSpan current = currentTime - StartTime;
+ TimeSpan total = EndTime - StartTime;
+ timePercent = current / total;
+ }
+
+ return timePercent;
+ }
+ }
+
+ ///
+ /// 格式化的起止时间
+ ///
+ public string TimeFormatted
+ {
+ get => $"{StartTime:yyyy.MM.dd HH:mm} - {EndTime:yyyy.MM.dd HH:mm}";
+ }
+
+ ///
+ /// 类型
+ ///
+ [JsonProperty("type")]
+ public int Type { get; set; }
+
+ ///
+ /// 提醒
+ ///
+ [JsonProperty("remind")]
+ public int Remind { get; set; }
+
+ ///
+ /// 通知
+ ///
+ [JsonProperty("alert")]
+ public int Alert { get; set; }
+
+ ///
+ /// 标签开始时间
+ ///
+ [JsonProperty("tag_start_time")]
+ public string? TagStartTime { get; set; }
+
+ ///
+ /// 标签结束时间
+ ///
+ [JsonProperty("tag_end_time")]
+ public string? TagEndTime { get; set; }
+
+ ///
+ /// 提醒版本
+ ///
+ [JsonProperty("remind_ver")]
+ public int RemindVer { get; set; }
+
+ ///
+ /// 是否含有内容
+ ///
+ [JsonProperty("has_content")]
+ public bool HasContent { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementContent.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementContent.cs
new file mode 100644
index 00000000..3bce5e46
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementContent.cs
@@ -0,0 +1,49 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+
+namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+
+///
+/// 公告内容
+///
+public class AnnouncementContent
+{
+ ///
+ /// 公告Id
+ ///
+ [JsonProperty("ann_id")]
+ public int AnnId { get; set; }
+
+ ///
+ /// 公告标题
+ ///
+ [JsonProperty("title")]
+ public string? Title { get; set; }
+
+ ///
+ /// 副标题
+ ///
+ [JsonProperty("subtitle")]
+ public string? Subtitle { get; set; }
+
+ ///
+ /// 横幅Url
+ ///
+ [JsonProperty("banner")]
+ public string? Banner { get; set; }
+
+ ///
+ /// 内容字符串
+ /// 可能包含了一些html格式
+ ///
+ [JsonProperty("content")]
+ public string? Content { get; set; }
+
+ ///
+ /// 语言
+ ///
+ [JsonProperty("lang")]
+ public string? Lang { get; set; }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementListWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementListWrapper.cs
new file mode 100644
index 00000000..22c150a5
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementListWrapper.cs
@@ -0,0 +1,25 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+using Snap.Hutao.Web.Response;
+
+namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+
+///
+/// 公告列表
+///
+public class AnnouncementListWrapper : ListWrapper
+{
+ ///
+ /// 类型Id
+ ///
+ [JsonProperty("type_id")]
+ public int TypeId { get; set; }
+
+ ///
+ /// 类型标签
+ ///
+ [JsonProperty("type_label")]
+ public string? TypeLabel { get; set; }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementProvider.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementProvider.cs
new file mode 100644
index 00000000..603a31ff
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementProvider.cs
@@ -0,0 +1,59 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Web.Request;
+using Snap.Hutao.Web.Response;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+
+///
+/// 公告提供器
+///
+[Injection(InjectAs.Transient)]
+internal class AnnouncementProvider
+{
+ private readonly Requester requester;
+
+ ///
+ /// 构造一个新的公告提供器
+ ///
+ /// 请求器
+ /// GZip 请求器
+ public AnnouncementProvider(Requester requester)
+ {
+ this.requester = requester;
+ }
+
+ ///
+ /// 异步获取公告列表
+ ///
+ /// 取消令牌
+ /// 公告列表
+ public async Task GetAnnouncementWrapperAsync(CancellationToken cancellationToken = default)
+ {
+ Response? resp = await requester
+ .Reset()
+ .GetAsync(ApiEndpoints.AnnList, cancellationToken)
+ .ConfigureAwait(false);
+ return resp?.Data;
+ }
+
+ ///
+ /// 异步获取公告内容列表
+ ///
+ /// 取消令牌
+ /// 公告内容列表
+ public async Task> GetAnnouncementContentsAsync(CancellationToken cancellationToken = default)
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+
+ Response>? resp = await requester
+ .Reset()
+ .AddHeader("Accept", RequestOptions.Json)
+ .GetAsync>(ApiEndpoints.AnnContent, cancellationToken)
+ .ConfigureAwait(false);
+ return resp?.Data?.List ?? new();
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementType.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementType.cs
new file mode 100644
index 00000000..d624d918
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementType.cs
@@ -0,0 +1,30 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+
+namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+
+///
+/// 公告类型
+///
+public class AnnouncementType
+{
+ ///
+ /// Id
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+
+ ///
+ /// 名称
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// 国际化名称
+ ///
+ [JsonProperty("mi18n_name")]
+ public string? MI18NName { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementWrapper.cs
new file mode 100644
index 00000000..f20085eb
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementWrapper.cs
@@ -0,0 +1,50 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+using Snap.Hutao.Web.Response;
+using System.Collections.Generic;
+
+namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
+
+///
+/// 公告包装器
+///
+public class AnnouncementWrapper : ListWrapper
+{
+ ///
+ /// 总数
+ ///
+ [JsonProperty("total")]
+ public int Total { get; set; }
+
+ ///
+ /// 类型列表
+ ///
+ [JsonProperty("type_list")]
+ public List? TypeList { get; set; }
+
+ ///
+ /// 提醒
+ ///
+ [JsonProperty("alert")]
+ public bool Alert { get; set; }
+
+ ///
+ /// 提醒Id
+ ///
+ [JsonProperty("alert_id")]
+ public int AlertId { get; set; }
+
+ ///
+ /// 时区
+ ///
+ [JsonProperty("timezone")]
+ public int TimeZone { get; set; }
+
+ ///
+ /// 时间戳
+ ///
+ [JsonProperty("t")]
+ public long TimeStamp { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/AuthRequester.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/AuthRequester.cs
new file mode 100644
index 00000000..1d420f79
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/AuthRequester.cs
@@ -0,0 +1,36 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Core;
+using System.Net.Http;
+
+namespace Snap.Hutao.Web.Request;
+
+///
+/// 验证 Token 请求器
+///
+public class AuthRequester : Requester
+{
+ ///
+ /// 构造一个新的 对象
+ ///
+ /// Http 客户端
+ /// Json 处理器
+ /// 消息器
+ public AuthRequester(HttpClient httpClient, Json json, ILogger logger)
+ : base(httpClient, json, logger)
+ {
+ }
+
+ ///
+ /// 验证令牌
+ ///
+ public string? AuthToken { get; set; }
+
+ ///
+ protected override void PrepareHttpClient()
+ {
+ base.PrepareHttpClient();
+ HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", AuthToken);
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/RequestOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/RequestOptions.cs
new file mode 100644
index 00000000..c4a43a32
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/RequestOptions.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+
+namespace Snap.Hutao.Web.Request
+{
+ ///
+ /// 请求选项
+ /// 用于添加到请求头中
+ ///
+ public class RequestOptions : Dictionary
+ {
+ ///
+ /// 不再使用
+ ///
+ [Obsolete("不再使用")]
+ public const string CommonUA2101 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.10.1";
+
+ ///
+ /// 不再使用
+ ///
+ [Obsolete("不再使用")]
+ public const string CommonUA2111 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.11.1";
+
+ ///
+ /// 支持更新的DS2算法
+ ///
+ public const string CommonUA2161 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.16.1";
+
+ ///
+ /// 应用程序/Json
+ ///
+ public const string Json = "application/json";
+
+ ///
+ /// 指示请求由米游社发起
+ ///
+ public const string Hyperion = "com.mihoyo.hyperion";
+
+ ///
+ /// 设备Id
+ ///
+ public static readonly string DeviceId = Guid.NewGuid().ToString("D");
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Requester.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Requester.cs
new file mode 100644
index 00000000..3b4bd4fb
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Requester.cs
@@ -0,0 +1,207 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Core;
+using Snap.Hutao.Web.Response;
+using System.Net.Http;
+using System.Text;
+
+namespace Snap.Hutao.Web.Request;
+
+///
+/// 请求器
+///
+[Injection(InjectAs.Transient)]
+public class Requester
+{
+ private readonly HttpClient httpClient;
+ private readonly Json json;
+ private readonly ILogger logger;
+
+ ///
+ /// 构造一个新的 对象
+ ///
+ /// Http 客户端
+ /// Json 处理器
+ /// 消息器
+ public Requester(HttpClient httpClient, Json json, ILogger logger)
+ {
+ this.httpClient = httpClient;
+ this.json = json;
+ this.logger = logger;
+ }
+
+ ///
+ /// 请求头
+ ///
+ public RequestOptions Headers { get; set; } = new RequestOptions();
+
+ ///
+ /// 内部使用的
+ ///
+ protected HttpClient HttpClient { get => httpClient; }
+
+ ///
+ /// GET 操作
+ ///
+ /// 返回的类类型
+ /// 地址
+ /// 取消令牌
+ /// 响应
+ public async Task?> GetAsync(string? url, CancellationToken cancellationToken = default)
+ {
+ logger.LogInformation("GET {urlbase}", url?.Split('?')[0]);
+ return url is null
+ ? null
+ : await RequestAsync(
+ client => new RequestInfo(url, () => client.GetAsync(url, cancellationToken)),
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// GET 操作
+ ///
+ /// 返回的类类型
+ /// 地址
+ /// 编码
+ /// 取消令牌
+ /// 响应
+ public async Task?> GetAsync(string? url, Encoding encoding, CancellationToken cancellationToken = default)
+ {
+ logger.LogInformation("GET {urlbase}", url?.Split('?')[0]);
+ return url is null
+ ? null
+ : await RequestAsync(
+ client => new RequestInfo(url, () => client.GetAsync(url, cancellationToken), encoding),
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// POST 操作
+ ///
+ /// 返回的类类型
+ /// 地址
+ /// 要发送的.NET(匿名)对象
+ /// 取消令牌
+ /// 响应
+ public async Task?> PostAsync(string? url, object data, CancellationToken cancellationToken = default)
+ {
+ string dataString = json.Stringify(data);
+ logger.LogInformation("POST {urlbase} with\n{dataString}", url?.Split('?')[0], dataString);
+ return url is null
+ ? null
+ : await RequestAsync(
+ client => new RequestInfo(url, () => client.PostAsync(url, new StringContent(dataString), cancellationToken)),
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// POST 操作,Content-Type
+ ///
+ /// 返回的类类型
+ /// 地址
+ /// 要发送的.NET(匿名)对象
+ /// 内容类型
+ /// 取消令牌
+ /// 响应
+ public async Task?> PostAsync(string? url, object data, string contentType, CancellationToken cancellationToken = default)
+ {
+ string dataString = json.Stringify(data);
+ logger.LogInformation("POST {urlbase} with\n{dataString}", url?.Split('?')[0], dataString);
+ return url is null
+ ? null
+ : await RequestAsync(
+ client => new RequestInfo(url, () => client.PostAsync(url, new StringContent(dataString, Encoding.UTF8, contentType), cancellationToken)),
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// 重置状态
+ /// 清空请求头
+ ///
+ /// 链式调用需要的实例
+ public Requester Reset()
+ {
+ Headers.Clear();
+ return this;
+ }
+
+ ///
+ /// 添加请求头
+ ///
+ /// 键
+ /// 值
+ /// 链式调用需要的实例
+ public Requester AddHeader(string key, string value)
+ {
+ Headers.Add(key, value);
+ return this;
+ }
+
+ ///
+ /// 在请求前准备
+ ///
+ protected virtual void PrepareHttpClient()
+ {
+ HttpClient.DefaultRequestHeaders.Clear();
+
+ foreach ((string name, string value) in Headers)
+ {
+ HttpClient.DefaultRequestHeaders.Add(name, value);
+ }
+ }
+
+ private async Task?> RequestAsync(Func requestFunc, CancellationToken cancellationToken = default)
+ {
+ PrepareHttpClient();
+ RequestInfo? info = requestFunc(HttpClient);
+
+ try
+ {
+ HttpResponseMessage response = await info.RequestAsyncFunc.Invoke()
+ .ConfigureAwait(false);
+
+ string contentString = await response.Content.ReadAsStringAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ if (info.Encoding is not null)
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(contentString);
+ info.Encoding.GetString(bytes);
+ }
+
+ logger.LogInformation("Response String :{contentString}", contentString);
+
+ return json.ToObject>(contentString);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "请求时遇到问题");
+ return Response.CreateFail($"{ex.Message}");
+ }
+ finally
+ {
+ logger.LogInformation("Request Completed");
+ }
+ }
+
+ private record RequestInfo
+ {
+ public RequestInfo(string url, Func> httpResponseMessage, Encoding? encoding = null)
+ {
+ Url = url;
+ RequestAsyncFunc = httpResponseMessage;
+ Encoding = encoding;
+ }
+
+ public string Url { get; set; }
+
+ public Func> RequestAsyncFunc { get; set; }
+
+ public Encoding? Encoding { 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
new file mode 100644
index 00000000..0f289b12
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs
@@ -0,0 +1,40 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Web.Response;
+
+///
+/// 已知的返回代码
+///
+public enum KnownReturnCode
+{
+ ///
+ /// 内部错误
+ ///
+ InternalFailure = int.MinValue,
+
+ ///
+ /// 已经签到过了
+ ///
+ AlreadySignedIn = -5003,
+
+ ///
+ /// 验证密钥过期
+ ///
+ AuthKeyTimeOut = -101,
+
+ ///
+ /// Ok
+ ///
+ OK = 0,
+
+ ///
+ /// 未定义
+ ///
+ NotDefined = 7,
+
+ ///
+ /// 数据未公开
+ ///
+ DataIsNotPublicForTheUser = 10102,
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/ListWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/ListWrapper.cs
new file mode 100644
index 00000000..5aeedd84
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/ListWrapper.cs
@@ -0,0 +1,19 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+using System.Collections.Generic;
+
+namespace Snap.Hutao.Web.Response;
+
+///
+/// 列表对象包装器
+///
+/// 列表的元素类型
+public class ListWrapper
+{
+ ///
+ /// 列表
+ ///
+ [JsonProperty("list")] public List? List { get; set; }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs
new file mode 100644
index 00000000..c9d472df
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs
@@ -0,0 +1,54 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+
+namespace Snap.Hutao.Web.Response;
+
+///
+/// 提供 的非泛型基类
+///
+public class Response
+{
+ ///
+ /// 返回代码
+ ///
+ [JsonProperty("retcode")]
+ public int ReturnCode { get; set; }
+
+ ///
+ /// 消息
+ ///
+ [JsonProperty("message")]
+ public string? Message { get; set; }
+
+ ///
+ /// 响应是否正常
+ ///
+ /// 响应
+ /// 是否Ok
+ public static bool IsOk(Response? response)
+ {
+ return response is not null && response.ReturnCode == 0;
+ }
+
+ ///
+ /// 构造一个失败的响应
+ ///
+ /// 消息
+ /// 响应
+ public static Response CreateFail(string message)
+ {
+ return new Response()
+ {
+ ReturnCode = (int)KnownReturnCode.InternalFailure,
+ Message = message,
+ };
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"状态:{ReturnCode} | 信息:{Message}";
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs
new file mode 100644
index 00000000..28f7492e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs
@@ -0,0 +1,33 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Newtonsoft.Json;
+
+namespace Snap.Hutao.Web.Response;
+
+///
+/// Mihoyo 标准API响应
+///
+/// 数据类型
+public class Response : Response
+{
+ ///
+ /// 数据
+ ///
+ [JsonProperty("data")]
+ public TData? Data { get; set; }
+
+ ///
+ /// 构造一个失败的响应
+ ///
+ /// 消息
+ /// 响应
+ public static new Response CreateFail(string message)
+ {
+ return new Response()
+ {
+ ReturnCode = (int)KnownReturnCode.InternalFailure,
+ Message = message,
+ };
+ }
+}