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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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, + }; + } +}