mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
announcement page
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.0.3" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22000.197" />
|
||||
<PackageReference Include="nucs.JsonSettings" Version="2.0.0-alpha7" />
|
||||
<PackageReference Include="nucs.JsonSettings.Autosave" Version="2.0.0-alpha7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
<Application
|
||||
x:Class="Snap.Hutao.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<muxc:XamlControlsResources/>
|
||||
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/SettingsUI.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Other app resources here -->
|
||||
<!--Modify Window title bar color-->
|
||||
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush" />
|
||||
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush" />
|
||||
<StaticResource x:Key="WindowCaptionForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
|
||||
<StaticResource x:Key="WindowCaptionForegroundDisabled" ResourceKey="SystemControlForegroundBaseHighBrush" />
|
||||
|
||||
<CornerRadius x:Key="WindowCornerRadius">8</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusTop">4,4,0,0</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusRight">0,4,4,0</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusBottom">0,0,4,4</CornerRadius>
|
||||
<CornerRadius x:Key="SmallCompatCornerRadius">2,2,2,2</CornerRadius>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,4 +37,32 @@ public partial class App : Application
|
||||
mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
mainWindow.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitializeDependencyInjection()
|
||||
{
|
||||
// prepare DI
|
||||
IServiceProvider services = new ServiceCollection()
|
||||
.AddLogging(builder => builder.AddDebug())
|
||||
|
||||
// http json
|
||||
.AddHttpClient<HttpJson>()
|
||||
.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<Requester>(nameof(Requester))
|
||||
.AddTypedClient<AuthRequester>()
|
||||
.ConfigureHttpClient(client => client.Timeout = Timeout.InfiniteTimeSpan)
|
||||
.Services
|
||||
|
||||
// inject app wide services
|
||||
.AddInjections(typeof(App))
|
||||
.BuildServiceProvider();
|
||||
|
||||
Ioc.Default.ConfigureServices(services);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
internal class AutoHeightBehavior : Behavior<FrameworkElement>
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AutoHeightBehavior>.Depend(nameof(TargetWidth), 1080D);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AutoHeightBehavior>.Depend(nameof(TargetHeight), 390D);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
AssociatedObject.SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
base.OnDetaching();
|
||||
AssociatedObject.SizeChanged -= OnSizeChanged;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
AssociatedObject.Height = (double)((FrameworkElement)sender).ActualWidth * (TargetHeight / TargetWidth);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 表示支持取消加载的异步页面
|
||||
/// 在被导航到其他页面前触发取消异步通知
|
||||
/// </summary>
|
||||
public class CancellablePage : Page
|
||||
{
|
||||
private readonly CancellationTokenSource viewLoadingConcellationTokenSource = new();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
/// <param name="viewModel">视图模型</param>
|
||||
public void Initialize(ISupportCancellation viewModel)
|
||||
{
|
||||
viewModel.CancellationToken = viewLoadingConcellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
viewLoadingConcellationTokenSource.Cancel();
|
||||
base.OnNavigatingFrom(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Cancellable;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此类支持取消任务
|
||||
/// </summary>
|
||||
public interface ISupportCancellation
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于通知取消的取消回执
|
||||
/// </summary>
|
||||
CancellationToken CancellationToken { get; set; }
|
||||
}
|
||||
44
src/Snap.Hutao/Snap.Hutao/Core/Browser.cs
Normal file
44
src/Snap.Hutao/Snap.Hutao/Core/Browser.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 封装了打开浏览器的方法
|
||||
/// </summary>
|
||||
public static class Browser
|
||||
{
|
||||
/// <summary>
|
||||
/// 打开浏览器
|
||||
/// </summary>
|
||||
/// <param name="url">链接</param>
|
||||
/// <param name="failAction">失败时执行的回调</param>
|
||||
public static void Open(string url, Action<Exception>? failAction = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessHelper.Start(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failAction?.Invoke(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开浏览器
|
||||
/// </summary>
|
||||
/// <param name="urlFunc">获取链接回调</param>
|
||||
/// <param name="failAction">失败时执行的回调</param>
|
||||
public static void Open(Func<string> urlFunc, Action<Exception>? failAction = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessHelper.Start(urlFunc.Invoke());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failAction?.Invoke(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 注入方法
|
||||
|
||||
@@ -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;
|
||||
|
||||
39
src/Snap.Hutao/Snap.Hutao/Core/HttpJson.cs
Normal file
39
src/Snap.Hutao/Snap.Hutao/Core/HttpJson.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Http Json 处理
|
||||
/// </summary>
|
||||
public class HttpJson
|
||||
{
|
||||
private readonly Json json;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一个新的 Http Json 处理 实例
|
||||
/// </summary>
|
||||
/// <param name="json">Json 处理器</param>
|
||||
/// <param name="httpClient">http 客户端</param>
|
||||
public HttpJson(Json json, HttpClient httpClient)
|
||||
{
|
||||
this.json = json;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从网站上下载json并转换为对象
|
||||
/// </summary>
|
||||
/// <typeparam name="T">对象的类型</typeparam>
|
||||
/// <param name="url">链接</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns>
|
||||
public async Task<T?> FromWebsiteAsync<T>(string url, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string response = await httpClient.GetStringAsync(url, cancellationToken);
|
||||
return json.ToObject<T>(response);
|
||||
}
|
||||
}
|
||||
133
src/Snap.Hutao/Snap.Hutao/Core/Json.cs
Normal file
133
src/Snap.Hutao/Snap.Hutao/Core/Json.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Json操作
|
||||
/// </summary>
|
||||
[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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一个新的 Json操作 实例
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
public Json(ILogger<Json> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将JSON反序列化为指定的.NET类型
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
|
||||
/// <param name="value">要反序列化的JSON</param>
|
||||
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns>
|
||||
public T? ToObject<T>(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError("反序列化Json时遇到问题:{ex}", ex);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将JSON反序列化为指定的.NET类型
|
||||
/// 若为null则返回一个新建的实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">指定的类型</typeparam>
|
||||
/// <param name="value">字符串</param>
|
||||
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns>
|
||||
public T ToObjectOrNew<T>(string value)
|
||||
where T : new()
|
||||
{
|
||||
return ToObject<T>(value) ?? new T();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将指定的对象序列化为JSON字符串
|
||||
/// </summary>
|
||||
/// <param name="value">要序列化的对象</param>
|
||||
/// <returns>对象的JSON字符串表示形式</returns>
|
||||
public string Stringify(object? value)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value, jsonSerializerSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="FileMode.Open"/>, <see cref="FileAccess.Read"/> 和 <see cref="FileShare.Read"/> 从文件中读取后转化为实体类
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
|
||||
/// <param name="fileName">存放JSON数据的文件路径</param>
|
||||
/// <returns>JSON字符串中的反序列化对象, 如果反序列化失败则抛出异常,若文件不存在则返回 <see langword="null"/></returns>
|
||||
public T? FromFile<T>(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<T>(sr.ReadToEnd());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="FileMode.Open"/>, <see cref="FileAccess.Read"/> 和 <see cref="FileShare.Read"/> 从文件中读取后转化为实体类
|
||||
/// 若为null则返回一个新建的实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
|
||||
/// <param name="fileName">存放JSON数据的文件路径</param>
|
||||
/// <returns>JSON字符串中的反序列化对象</returns>
|
||||
public T FromFileOrNew<T>(string fileName)
|
||||
where T : new()
|
||||
{
|
||||
return FromFile<T>(fileName) ?? new T();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件中读取后转化为实体类
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
|
||||
/// <param name="file">存放JSON数据的文件</param>
|
||||
/// <returns>JSON字符串中的反序列化对象</returns>
|
||||
public T? FromFile<T>(FileInfo file)
|
||||
{
|
||||
using (StreamReader sr = file.OpenText())
|
||||
{
|
||||
return ToObject<T>(sr.ReadToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象保存到文件
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名称</param>
|
||||
/// <param name="value">对象</param>
|
||||
public void ToFile(string fileName, object? value)
|
||||
{
|
||||
File.WriteAllText(fileName, Stringify(value));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
|
||||
43
src/Snap.Hutao/Snap.Hutao/Core/Observable.cs
Normal file
43
src/Snap.Hutao/Snap.Hutao/Core/Observable.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 简单的实现了 <see cref="INotifyPropertyChanged"/> 接口
|
||||
/// </summary>
|
||||
public class Observable : INotifyPropertyChanged
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 设置字段的值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">字段类型</typeparam>
|
||||
/// <param name="storage">现有值</param>
|
||||
/// <param name="value">新的值</param>
|
||||
/// <param name="propertyName">属性名称</param>
|
||||
protected void Set<T>([NotNullIfNotNull("value")] ref T storage, T value, [CallerMemberName] string propertyName = default!)
|
||||
{
|
||||
if (Equals(storage, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
storage = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发 <see cref="PropertyChanged"/>
|
||||
/// </summary>
|
||||
/// <param name="propertyName">属性名称</param>
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
55
src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs
Normal file
55
src/Snap.Hutao/Snap.Hutao/Core/ProcessHelper.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 进程帮助类
|
||||
/// </summary>
|
||||
public static class ProcessHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动进程
|
||||
/// </summary>
|
||||
/// <param name="path">路径</param>
|
||||
/// <param name="useShellExecute">使用shell</param>
|
||||
/// <returns>进程</returns>
|
||||
public static Process? Start(string path, bool useShellExecute = true)
|
||||
{
|
||||
ProcessStartInfo processInfo = new(path)
|
||||
{
|
||||
UseShellExecute = useShellExecute,
|
||||
};
|
||||
return Process.Start(processInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动进程
|
||||
/// </summary>
|
||||
/// <param name="path">路径</param>
|
||||
/// <param name="arguments">命令行参数</param>
|
||||
/// <param name="useShellExecute">使用shell</param>
|
||||
/// <returns>进程</returns>
|
||||
public static Process? Start(string path, string arguments, bool useShellExecute = true)
|
||||
{
|
||||
ProcessStartInfo processInfo = new(path)
|
||||
{
|
||||
UseShellExecute = useShellExecute,
|
||||
Arguments = arguments,
|
||||
};
|
||||
return Process.Start(processInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动进程
|
||||
/// </summary>
|
||||
/// <param name="uri">路径</param>
|
||||
/// <param name="useShellExecute">使用shell</param>
|
||||
/// <returns>进程</returns>
|
||||
public static Process? Start(Uri uri, bool useShellExecute = true)
|
||||
{
|
||||
return Start(uri.AbsolutePath, useShellExecute);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ internal static class Property<TOwner>
|
||||
/// <returns>注册的依赖属性</returns>
|
||||
public static DependencyProperty Depend<TProperty>(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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,7 +55,7 @@ internal static class Property<TOwner>
|
||||
/// <returns>注册的附加属性</returns>
|
||||
public static DependencyProperty Attach<TProperty>(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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
80
src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs
Normal file
80
src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 提供简单易用的状态提示信息
|
||||
/// 用于任务的状态跟踪
|
||||
/// 同时继承了 <see cref="Observable"/>
|
||||
/// </summary>
|
||||
public class Watcher : Observable
|
||||
{
|
||||
private readonly bool isReusable;
|
||||
private bool hasUsed;
|
||||
private bool isWorking;
|
||||
private bool isCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的工作监视器
|
||||
/// </summary>
|
||||
/// <param name="isReusable">是否可以重用</param>
|
||||
public Watcher(bool isReusable = true)
|
||||
{
|
||||
this.isReusable = isReusable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否正在工作
|
||||
/// </summary>
|
||||
public bool IsWorking
|
||||
{
|
||||
get => isWorking;
|
||||
|
||||
private set => Set(ref isWorking, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工作是否完成
|
||||
/// </summary>
|
||||
public bool IsCompleted
|
||||
{
|
||||
get => isCompleted;
|
||||
|
||||
private set => Set(ref isCompleted, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对某个操作进行监视,
|
||||
/// 无法防止代码重入
|
||||
/// </summary>
|
||||
/// <returns>一个可释放的对象,用于在操作完成时自动提示监视器工作已经完成</returns>
|
||||
/// <exception cref="InvalidOperationException">重用了一个不可重用的监视器</exception>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs
Normal file
46
src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 检测 WebView2运行时 是否存在
|
||||
/// 不再使用注册表检查方式
|
||||
/// </summary>
|
||||
internal class WebView2Helper
|
||||
{
|
||||
private static bool hasEverDetected = false;
|
||||
private static bool isSupported = false;
|
||||
|
||||
/// <summary>
|
||||
/// 检测 WebView2 是否存在
|
||||
/// </summary>
|
||||
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<ILogger<WebView2Helper>>().LogError(ex, "WebView2 运行时未安装");
|
||||
isSupported = false;
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="AsyncRelayCommand"/> with additional processing.
|
||||
/// </summary>
|
||||
public interface IAsyncRelayCommandFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand Create(Func<Task> execute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute);
|
||||
}
|
||||
102
src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs
Normal file
102
src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs
Normal file
@@ -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;
|
||||
|
||||
/// <inheritdoc cref="IAsyncRelayCommandFactory"/>
|
||||
[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
|
||||
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的异步命令工厂
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand<T>(execute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand<T>(cancelableExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand<T>(execute, canExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand<T>(cancelableExecute, canExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand Create(Func<Task> execute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand(execute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand(cancelableExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand(execute, canExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand(cancelableExecute, canExecute));
|
||||
}
|
||||
|
||||
private AsyncRelayCommand Register(AsyncRelayCommand command)
|
||||
{
|
||||
ReportException(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
private AsyncRelayCommand<T> Register<T>(AsyncRelayCommand<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
<view:MainView
|
||||
Grid.Row="1"/>
|
||||
<view:MainView Grid.Row="1"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.0.0.0" />
|
||||
Version="1.0.2.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告服务
|
||||
/// </summary>
|
||||
public interface IAnnouncementService
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步获取游戏公告与活动
|
||||
/// </summary>
|
||||
/// <param name="openAnnouncementUICommand">打开公告时触发的命令</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>公告包装器</returns>
|
||||
Task<AnnouncementWrapper> GetAnnouncementsAsync(ICommand openAnnouncementUICommand, CancellationToken cancellationToken = default);
|
||||
}
|
||||
100
src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs
Normal file
100
src/Snap.Hutao/Snap.Hutao/Service/Abstraction/IInfoBarService.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 消息条服务
|
||||
/// </summary>
|
||||
public interface IInfoBarService
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示错误消息
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Error(string message, int delay = 0);
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Error(string title, string message, int delay = 0);
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息
|
||||
/// </summary>
|
||||
/// <param name="ex">异常</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Error(Exception ex, int delay = 0);
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息
|
||||
/// </summary>
|
||||
/// <param name="ex">异常</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Error(Exception ex, string title, int delay = 0);
|
||||
|
||||
/// <summary>
|
||||
/// 显示提示信息
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Information(string message, int delay = 3000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示提示信息
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Information(string title, string message, int delay = 3000);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的 <see cref="StackPanel"/> 初始化服务
|
||||
/// </summary>
|
||||
/// <param name="container">信息条的目标容器</param>
|
||||
void Initialize(StackPanel container);
|
||||
|
||||
/// <summary>
|
||||
/// 显示特定的信息条
|
||||
/// </summary>
|
||||
/// <param name="infoBar">信息条</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Show(InfoBar infoBar, int delay = 0);
|
||||
|
||||
/// <summary>
|
||||
/// 显示成功信息
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Success(string message, int delay = 3000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示成功信息
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Success(string title, string message, int delay = 3000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示警告信息
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Warning(string message, int delay = 0);
|
||||
|
||||
/// <summary>
|
||||
/// 显示警告信息
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Warning(string title, string message, int delay = 0);
|
||||
}
|
||||
101
src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs
Normal file
101
src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs
Normal file
@@ -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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Injection(InjectAs.Transient, typeof(IAnnouncementService))]
|
||||
internal class AnnouncementService : IAnnouncementService
|
||||
{
|
||||
private readonly AnnouncementProvider announcementProvider;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的公告服务
|
||||
/// </summary>
|
||||
/// <param name="announcementProvider">公告提供器</param>
|
||||
public AnnouncementService(AnnouncementProvider announcementProvider)
|
||||
{
|
||||
this.announcementProvider = announcementProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<AnnouncementWrapper> GetAnnouncementsAsync(ICommand openAnnouncementUICommand, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AnnouncementWrapper? wrapper = await announcementProvider.GetAnnouncementWrapperAsync(cancellationToken);
|
||||
List<AnnouncementContent> contents = await announcementProvider.GetAnnouncementContentsAsync(cancellationToken);
|
||||
|
||||
Dictionary<int, string?> contentMap = contents
|
||||
.ToDictionary(id => id.AnnId, content => content.Content);
|
||||
|
||||
if (wrapper?.List is List<AnnouncementListWrapper> announcementListWrappers)
|
||||
{
|
||||
// 将活动公告置于上方
|
||||
announcementListWrappers.Reverse();
|
||||
|
||||
// 将公告内容联入公告列表
|
||||
JoinAnnouncements(openAnnouncementUICommand, contentMap, announcementListWrappers);
|
||||
|
||||
// we only cares about activities
|
||||
if (announcementListWrappers[0].List is List<Announcement> activities)
|
||||
{
|
||||
AdjustActivitiesTime(ref activities);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
private void JoinAnnouncements(ICommand openAnnouncementUICommand, Dictionary<int, string?> contentMap, List<AnnouncementListWrapper> announcementListWrappers)
|
||||
{
|
||||
// 匹配特殊的时间格式: <t>(.*?)</t>
|
||||
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 <t/> tag
|
||||
rawContent = timeTagRegrex.Replace(rawContent!, x => timeTagInnerRegex.Match(x.Value).Value);
|
||||
}
|
||||
|
||||
item.Content = rawContent;
|
||||
item.OpenAnnouncementUICommand = openAnnouncementUICommand;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void AdjustActivitiesTime(ref List<Announcement> 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();
|
||||
}
|
||||
}
|
||||
121
src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs
Normal file
121
src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs
Normal file
@@ -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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Injection(InjectAs.Singleton, typeof(IInfoBarService))]
|
||||
internal class InfoBarService : IInfoBarService
|
||||
{
|
||||
private StackPanel? infoBarStack;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(StackPanel container)
|
||||
{
|
||||
infoBarStack = container;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Information(string message, int delay = 3000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Informational, null, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Information(string title, string message, int delay = 3000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Informational, title, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Success(string message, int delay = 3000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Success, null, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Success(string title, string message, int delay = 3000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Success, title, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Warning(string message, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Warning, null, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Warning(string title, string message, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Warning, title, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(string message, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Error, null, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(string title, string message, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Error, title, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(Exception ex, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, ex.Message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(Exception ex, string title, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 导航服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(INavigationService))]
|
||||
[Injection(InjectAs.Singleton, typeof(INavigationService))]
|
||||
internal class NavigationService : INavigationService
|
||||
{
|
||||
private readonly ILogger<INavigationService> logger;
|
||||
@@ -120,7 +119,7 @@ internal class NavigationService : INavigationService
|
||||
}
|
||||
|
||||
// 首次导航失败时使属性持续保存为false
|
||||
HasEverNavigated |= result;
|
||||
HasEverNavigated = HasEverNavigated || result;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<NeutralLanguage>zh-CN</NeutralLanguage>
|
||||
<DefaultLanguage>zh-CN</DefaultLanguage>
|
||||
|
||||
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
|
||||
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
||||
<PackageCertificateThumbprint>F8C2255969BEA4A681CED102771BF807856AEC02</PackageCertificateThumbprint>
|
||||
@@ -27,6 +28,8 @@
|
||||
<ItemGroup>
|
||||
<None Remove="stylecop.json" />
|
||||
<None Remove="View\MainView.xaml" />
|
||||
<None Remove="View\Page\AnnouncementContentPage.xaml" />
|
||||
<None Remove="View\Page\AnnouncementPage.xaml" />
|
||||
<None Remove="View\Page\SettingPage.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -47,15 +50,23 @@
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="7.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI" Version="7.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Animations" Version="7.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
|
||||
<PackageReference Include="Microsoft.AppCenter.Analytics" Version="4.5.1" />
|
||||
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.5.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.1.46" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.1.46">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.46" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.0.3" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22000.197" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.8" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -73,13 +84,21 @@
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Control\" />
|
||||
<Folder Include="ViewModel\" />
|
||||
<Folder Include="Model\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SettingsUI\SettingsUI.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\AnnouncementContentPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\AnnouncementPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\SettingPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class BoolToObjectConverter : DependencyObject, IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="TrueValue"/> property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty TrueValueProperty =
|
||||
DependencyProperty.Register(nameof(TrueValue), typeof(object), typeof(BoolToObjectConverter), new PropertyMetadata(null));
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="FalseValue"/> property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty FalseValueProperty =
|
||||
DependencyProperty.Register(nameof(FalseValue), typeof(object), typeof(BoolToObjectConverter), new PropertyMetadata(null));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value to be returned when the boolean is true
|
||||
/// </summary>
|
||||
public object TrueValue
|
||||
{
|
||||
get => GetValue(TrueValueProperty);
|
||||
set => SetValue(TrueValueProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value to be returned when the boolean is false
|
||||
/// </summary>
|
||||
public object FalseValue
|
||||
{
|
||||
get => GetValue(FalseValueProperty);
|
||||
set => SetValue(FalseValueProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a boolean value to an other object.
|
||||
/// </summary>
|
||||
/// <param name="value">The source data being passed to the target.</param>
|
||||
/// <param name="targetType">The type of the target property, as a type reference.</param>
|
||||
/// <param name="parameter">An optional parameter to be used to invert the converter logic.</param>
|
||||
/// <param name="language">The language of the conversion.</param>
|
||||
/// <returns>The value to be passed to the target dependency property.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert back the value to a boolean
|
||||
/// </summary>
|
||||
/// <remarks>If the <paramref name="value"/> parameter is a reference type, <see cref="TrueValue"/> must match its reference to return true.</remarks>
|
||||
/// <param name="value">The target data being passed to the source.</param>
|
||||
/// <param name="targetType">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)).</param>
|
||||
/// <param name="parameter">An optional parameter to be used to invert the converter logic.</param>
|
||||
/// <param name="language">The language of the conversion.</param>
|
||||
/// <returns>The value to be passed to the source object.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This class converts a boolean value into a Visibility enumeration.
|
||||
/// </summary>
|
||||
public class BoolToVisibilityConverter : BoolToObjectConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BoolToVisibilityConverter"/> class.
|
||||
/// </summary>
|
||||
public BoolToVisibilityConverter()
|
||||
{
|
||||
TrueValue = Visibility.Visible;
|
||||
FalseValue = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This class converts a boolean value into a Visibility enumeration.
|
||||
/// </summary>
|
||||
public class BoolToVisibilityRevertConverter : BoolToObjectConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BoolToVisibilityConverter"/> class.
|
||||
/// </summary>
|
||||
public BoolToVisibilityRevertConverter()
|
||||
{
|
||||
TrueValue = Visibility.Collapsed;
|
||||
FalseValue = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
41
src/Snap.Hutao/Snap.Hutao/View/Converter/ConvertHelper.cs
Normal file
41
src/Snap.Hutao/Snap.Hutao/View/Converter/ConvertHelper.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Static class used to provide internal tools
|
||||
/// </summary>
|
||||
internal static class ConvertHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper method to safely cast an object to a boolean
|
||||
/// </summary>
|
||||
/// <param name="parameter">Parameter to cast to a boolean</param>
|
||||
/// <returns>Bool value or false if cast failed</returns>
|
||||
internal static bool TryParseBool(object parameter)
|
||||
{
|
||||
bool parsed = false;
|
||||
if (parameter != null)
|
||||
{
|
||||
_ = bool.TryParse(parameter.ToString(), out parsed);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert a value from a source type to a target type.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to convert</param>
|
||||
/// <param name="targetType">The target type</param>
|
||||
/// <returns>The converted value</returns>
|
||||
internal static object Convert(object value, Type targetType)
|
||||
{
|
||||
return targetType.IsInstanceOfType(value)
|
||||
? value
|
||||
: XamlBindingHelper.ConvertValue(targetType, value);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 百分比转换高度
|
||||
/// </summary>
|
||||
public sealed class PercentageToHeightConverter : DependencyObject, IValueConverter
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<PercentageToHeightConverter>.Depend(nameof(TargetWidth), 1080D);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<PercentageToHeightConverter>.Depend(nameof(TargetHeight), 390D);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string culture)
|
||||
{
|
||||
return (double)value * (TargetHeight / TargetWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string culture)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 百分比转宽度
|
||||
/// </summary>
|
||||
public sealed class PercentageToWidthConverter : DependencyObject, IValueConverter
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<PercentageToWidthConverter>.Depend(nameof(TargetWidth), 1080D);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<PercentageToWidthConverter>.Depend(nameof(TargetHeight), 390D);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string culture)
|
||||
{
|
||||
return (double)value * (TargetWidth / TargetHeight);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string culture)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
<Grid>
|
||||
<NavigationView
|
||||
x:Name="NavView"
|
||||
OpenPaneLength="172"
|
||||
Margin="0,0,0,0"
|
||||
CompactModeThresholdWidth="360"
|
||||
ExpandedModeThresholdWidth="720">
|
||||
<Frame
|
||||
x:Name="ContentFrame">
|
||||
ExpandedModeThresholdWidth="720"
|
||||
IsBackEnabled="{x:Bind ContentFrame.CanGoBack}">
|
||||
<NavigationView.MenuItems>
|
||||
<NavigationViewItem
|
||||
Content="活动"
|
||||
helper:NavHelper.NavigateTo="page:AnnouncementPage">
|
||||
<NavigationViewItem.Icon>
|
||||
<FontIcon Glyph=""/>
|
||||
</NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
</NavigationView.MenuItems>
|
||||
<Frame x:Name="ContentFrame">
|
||||
<Frame.ContentTransitions>
|
||||
<NavigationThemeTransition>
|
||||
<DrillInNavigationTransitionInfo/>
|
||||
</NavigationThemeTransition>
|
||||
<TransitionCollection>
|
||||
<NavigationThemeTransition>
|
||||
<DrillInNavigationTransitionInfo/>
|
||||
</NavigationThemeTransition>
|
||||
</TransitionCollection>
|
||||
</Frame.ContentTransitions>
|
||||
</Frame>
|
||||
</NavigationView>
|
||||
|
||||
<StackPanel
|
||||
x:Name="InfoBarStack"
|
||||
Margin="32"
|
||||
MaxWidth="640"
|
||||
VerticalAlignment="Bottom">
|
||||
<StackPanel.Resources>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarErrorSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#442726"
|
||||
FallbackColor="#442726"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarWarningSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#433519"
|
||||
FallbackColor="#433519"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarSuccessSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#393D1B"
|
||||
FallbackColor="#393D1B"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarInformationalSeverityBackgroundBrush"
|
||||
TintOpacity="0.6"
|
||||
TintColor="#34424d"
|
||||
FallbackColor="#34424d"/>
|
||||
</StackPanel.Resources>
|
||||
<StackPanel.OpacityTransition>
|
||||
<ScalarTransition/>
|
||||
</StackPanel.OpacityTransition>
|
||||
<StackPanel.Transitions>
|
||||
<TransitionCollection>
|
||||
<AddDeleteThemeTransition/>
|
||||
</TransitionCollection>
|
||||
</StackPanel.Transitions>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -12,15 +12,19 @@ namespace Snap.Hutao.View;
|
||||
public sealed partial class MainView : UserControl
|
||||
{
|
||||
private readonly INavigationService navigationService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的主视图
|
||||
/// </summary>
|
||||
public MainView()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
InitializeComponent();
|
||||
|
||||
navigationService = Ioc.Default.GetRequiredService<INavigationService>();
|
||||
navigationService.Initialize(NavView, ContentFrame);
|
||||
|
||||
infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
infoBarService.Initialize(InfoBarStack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<Page
|
||||
x:Class="Snap.Hutao.View.Page.AnnouncementContentPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
<WebView2 x:Name="WebView"/>
|
||||
</Page>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的公告窗体
|
||||
/// </summary>
|
||||
/// <param name="content">要展示的内容</param>
|
||||
public AnnouncementContentPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
229
src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml
Normal file
229
src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml
Normal file
@@ -0,0 +1,229 @@
|
||||
<shcc:CancellablePage
|
||||
x:Class="Snap.Hutao.View.Page.AnnouncementPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:cwui="using:CommunityToolkit.WinUI.UI"
|
||||
xmlns:cwua="using:CommunityToolkit.WinUI.UI.Animations"
|
||||
xmlns:cwub="using:CommunityToolkit.WinUI.UI.Behaviors"
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:mxim="using:Microsoft.Xaml.Interactions.Media"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shcc="using:Snap.Hutao.Control.Cancellable"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Converter"
|
||||
mc:Ignorable="d"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<mxic:EventTriggerBehavior EventName="Loaded">
|
||||
<mxic:InvokeCommandAction Command="{Binding OpenUICommand}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
</mxi:Interaction.Behaviors>
|
||||
<shcc:CancellablePage.Resources>
|
||||
<shvc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
|
||||
</shcc:CancellablePage.Resources>
|
||||
<Grid>
|
||||
<ScrollViewer
|
||||
Padding="0,0,4,0"
|
||||
Visibility="{Binding OpeningUI.IsWorking,Converter={StaticResource BoolToVisibilityRevertConverter}}">
|
||||
<StackPanel>
|
||||
<ItemsControl
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding Announcement.List}"
|
||||
Padding="0"
|
||||
Margin="12,12,0,0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Text="{Binding TypeLabel}"
|
||||
Margin="0,0,0,12"
|
||||
Style="{StaticResource TitleTextBlockStyle}"/>
|
||||
<cwuc:AdaptiveGridView
|
||||
cwua:ItemsReorderAnimation.Duration="0:0:0.06"
|
||||
SelectionMode="None"
|
||||
DesiredWidth="320"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding List}"
|
||||
Margin="0,0,0,0">
|
||||
<cwuc:AdaptiveGridView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
|
||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||
</Style>
|
||||
</cwuc:AdaptiveGridView.ItemContainerStyle>
|
||||
<cwuc:AdaptiveGridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
CornerRadius="{StaticResource CompatCornerRadius}"
|
||||
Background="{StaticResource SystemControlPageBackgroundAltHighBrush}"
|
||||
cwui:UIElementExtensions.ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<!--Image Layer-->
|
||||
<Border
|
||||
cwui:UIElementExtensions.ClipToBounds="True">
|
||||
<Border
|
||||
VerticalAlignment="Top"
|
||||
cwui:VisualExtensions.NormalizedCenterPoint="0.5">
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:AutoHeightBehavior TargetWidth="1080" TargetHeight="390"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
<Border.Background>
|
||||
<ImageBrush
|
||||
ImageSource="{Binding Banner}"
|
||||
Stretch="UniformToFill"/>
|
||||
</Border.Background>
|
||||
<cwua:Explicit.Animations>
|
||||
<cwua:AnimationSet x:Name="ImageZoomInAnimation">
|
||||
<cwua:ScaleAnimation
|
||||
EasingMode="EaseOut"
|
||||
EasingType="Circle"
|
||||
To="1.1"
|
||||
Duration="0:0:0.5"/>
|
||||
</cwua:AnimationSet>
|
||||
<cwua:AnimationSet x:Name="ImageZoomOutAnimation">
|
||||
<cwua:ScaleAnimation
|
||||
EasingMode="EaseOut"
|
||||
EasingType="Circle"
|
||||
To="1"
|
||||
Duration="0:0:0.5"/>
|
||||
</cwua:AnimationSet>
|
||||
</cwua:Explicit.Animations>
|
||||
</Border>
|
||||
</Border>
|
||||
<!--Time Description-->
|
||||
<Grid Grid.Row="0">
|
||||
<Border
|
||||
Height="24"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Visibility="{Binding ShouldShowTimeDescription,Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#00000000"/>
|
||||
<GradientStop Offset="1" Color="#A0000000"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<ProgressBar
|
||||
Height="1"
|
||||
MinHeight="1"
|
||||
Value="{Binding TimePercent,Mode=OneWay}"
|
||||
CornerRadius="0"
|
||||
Maximum="1"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="Transparent"/>
|
||||
</Border>
|
||||
<Border
|
||||
Padding="8,4"
|
||||
Visibility="{Binding ShouldShowTimeDescription,Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<TextBlock
|
||||
Opacity="0.6"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding TimeDescription}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<!--General Description-->
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
CornerRadius="{StaticResource CompatCornerRadiusBottom}">
|
||||
<StackPanel Margin="4" VerticalAlignment="Bottom">
|
||||
<Grid Margin="4,6,0,0" HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding Subtitle}"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
TextWrapping="NoWrap"
|
||||
TextTrimming="WordEllipsis"/>
|
||||
|
||||
<Button
|
||||
x:Name="OpenAnnouncementButton"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
BorderBrush="{x:Null}"
|
||||
Visibility="Collapsed"
|
||||
Command="{Binding OpenAnnouncementUICommand}"
|
||||
CommandParameter="{Binding Content}"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding Title}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
TextWrapping="NoWrap"
|
||||
TextTrimming="WordEllipsis"
|
||||
Margin="4,6,0,0"
|
||||
Opacity="0.6"/>
|
||||
|
||||
<TextBlock
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
FontSize="10"
|
||||
Opacity="0.4"
|
||||
Margin="4,4,0,4"
|
||||
Text="{Binding TimeFormatted}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Border.Resources>
|
||||
<Storyboard x:Name="OpenAnnouncementButtonVisibleStoryboard">
|
||||
<ObjectAnimationUsingKeyFrames
|
||||
Storyboard.TargetName="OpenAnnouncementButton"
|
||||
Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<Visibility>Visible</Visibility>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
|
||||
<Storyboard x:Name="OpenAnnouncementButtonCollapsedStoryboard">
|
||||
<ObjectAnimationUsingKeyFrames
|
||||
Storyboard.TargetName="OpenAnnouncementButton"
|
||||
Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<Visibility>Collapsed</Visibility>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</Border.Resources>
|
||||
<mxi:Interaction.Behaviors>
|
||||
<mxic:EventTriggerBehavior EventName="PointerEntered">
|
||||
<cwub:StartAnimationAction Animation="{Binding ElementName=ImageZoomInAnimation}" />
|
||||
<mxim:ControlStoryboardAction Storyboard="{StaticResource OpenAnnouncementButtonVisibleStoryboard}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
<mxic:EventTriggerBehavior EventName="PointerExited">
|
||||
<cwub:StartAnimationAction Animation="{Binding ElementName=ImageZoomOutAnimation}" />
|
||||
<mxim:ControlStoryboardAction Storyboard="{StaticResource OpenAnnouncementButtonCollapsedStoryboard}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</cwuc:AdaptiveGridView.ItemTemplate>
|
||||
</cwuc:AdaptiveGridView>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</shcc:CancellablePage>
|
||||
22
src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml.cs
Normal file
22
src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告页面
|
||||
/// </summary>
|
||||
public sealed partial class AnnouncementPage : CancellablePage
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的公告页面
|
||||
/// </summary>
|
||||
public AnnouncementPage()
|
||||
{
|
||||
Initialize(Ioc.Default.GetRequiredService<AnnouncementViewModel>());
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,6 @@ public sealed partial class SettingPage : Microsoft.UI.Xaml.Controls.Page
|
||||
/// </summary>
|
||||
public SettingPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
||||
109
src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs
Normal file
109
src/Snap.Hutao/Snap.Hutao/ViewModel/AnnouncementViewModel.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
private readonly IAnnouncementService announcementService;
|
||||
private readonly INavigationService navigationService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly ILogger<AnnouncementViewModel> logger;
|
||||
|
||||
private AnnouncementWrapper? announcement;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个公告视图模型
|
||||
/// </summary>
|
||||
/// <param name="announcementService">公告服务</param>
|
||||
/// <param name="navigationService">导航服务</param>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
/// <param name="infoBarService">信息条服务</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public AnnouncementViewModel(
|
||||
IAnnouncementService announcementService,
|
||||
INavigationService navigationService,
|
||||
IAsyncRelayCommandFactory asyncRelayCommandFactory,
|
||||
IInfoBarService infoBarService,
|
||||
ILogger<AnnouncementViewModel> logger)
|
||||
{
|
||||
this.announcementService = announcementService;
|
||||
this.navigationService = navigationService;
|
||||
this.infoBarService = infoBarService;
|
||||
this.logger = logger;
|
||||
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
OpenAnnouncementUICommand = new RelayCommand<string>(OpenAnnouncementUI);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公告
|
||||
/// </summary>
|
||||
public AnnouncementWrapper? Announcement
|
||||
{
|
||||
get => announcement;
|
||||
|
||||
set => SetProperty(ref announcement, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开界面监视器
|
||||
/// </summary>
|
||||
public Watcher OpeningUI { get; } = new(false);
|
||||
|
||||
/// <summary>
|
||||
/// 打开界面触发的命令
|
||||
/// </summary>
|
||||
public ICommand OpenUICommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 打开公告UI触发的命令
|
||||
/// </summary>
|
||||
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<View.Page.AnnouncementContentPage>(data: content);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning("尚未安装 WebView2 运行时。");
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs
Normal file
23
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
/// <summary>
|
||||
/// 主机Url集合
|
||||
/// </summary>
|
||||
internal static class ApiEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// 公告列表
|
||||
/// </summary>
|
||||
public static readonly string AnnList = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnList?{AnnouncementQuery}";
|
||||
|
||||
/// <summary>
|
||||
/// 公告内容
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告
|
||||
/// </summary>
|
||||
public class Announcement : AnnouncementContent
|
||||
{
|
||||
private double timePercent;
|
||||
|
||||
/// <summary>
|
||||
/// 类型标签
|
||||
/// </summary>
|
||||
[JsonProperty("type_label")]
|
||||
public string? TypeLabel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签文本
|
||||
/// </summary>
|
||||
[JsonProperty("tag_label")]
|
||||
public string? TagLabel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签图标
|
||||
/// </summary>
|
||||
[JsonProperty("tag_icon")]
|
||||
public string? TagIcon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录提醒
|
||||
/// </summary>
|
||||
[JsonProperty("login_alert")]
|
||||
public int LoginAlert { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
[JsonProperty("start_time")]
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
[JsonProperty("end_time")]
|
||||
public DateTime EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动展示窗口的命令
|
||||
/// </summary>
|
||||
public ICommand? OpenAnnouncementUICommand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否应展示时间
|
||||
/// </summary>
|
||||
public bool ShouldShowTimeDescription
|
||||
{
|
||||
get => Type == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 时间
|
||||
/// </summary>
|
||||
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} 天后结束";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否应显示时间百分比
|
||||
/// </summary>
|
||||
public bool ShouldShowTimePercent
|
||||
{
|
||||
get => ShouldShowTimeDescription && (TimePercent > 0 && TimePercent < 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 时间百分比
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化的起止时间
|
||||
/// </summary>
|
||||
public string TimeFormatted
|
||||
{
|
||||
get => $"{StartTime:yyyy.MM.dd HH:mm} - {EndTime:yyyy.MM.dd HH:mm}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 类型
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提醒
|
||||
/// </summary>
|
||||
[JsonProperty("remind")]
|
||||
public int Remind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通知
|
||||
/// </summary>
|
||||
[JsonProperty("alert")]
|
||||
public int Alert { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签开始时间
|
||||
/// </summary>
|
||||
[JsonProperty("tag_start_time")]
|
||||
public string? TagStartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签结束时间
|
||||
/// </summary>
|
||||
[JsonProperty("tag_end_time")]
|
||||
public string? TagEndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提醒版本
|
||||
/// </summary>
|
||||
[JsonProperty("remind_ver")]
|
||||
public int RemindVer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否含有内容
|
||||
/// </summary>
|
||||
[JsonProperty("has_content")]
|
||||
public bool HasContent { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告内容
|
||||
/// </summary>
|
||||
public class AnnouncementContent
|
||||
{
|
||||
/// <summary>
|
||||
/// 公告Id
|
||||
/// </summary>
|
||||
[JsonProperty("ann_id")]
|
||||
public int AnnId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公告标题
|
||||
/// </summary>
|
||||
[JsonProperty("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 副标题
|
||||
/// </summary>
|
||||
[JsonProperty("subtitle")]
|
||||
public string? Subtitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 横幅Url
|
||||
/// </summary>
|
||||
[JsonProperty("banner")]
|
||||
public string? Banner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容字符串
|
||||
/// 可能包含了一些html格式
|
||||
/// </summary>
|
||||
[JsonProperty("content")]
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言
|
||||
/// </summary>
|
||||
[JsonProperty("lang")]
|
||||
public string? Lang { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告列表
|
||||
/// </summary>
|
||||
public class AnnouncementListWrapper : ListWrapper<Announcement>
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型Id
|
||||
/// </summary>
|
||||
[JsonProperty("type_id")]
|
||||
public int TypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型标签
|
||||
/// </summary>
|
||||
[JsonProperty("type_label")]
|
||||
public string? TypeLabel { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告提供器
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class AnnouncementProvider
|
||||
{
|
||||
private readonly Requester requester;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的公告提供器
|
||||
/// </summary>
|
||||
/// <param name="requester">请求器</param>
|
||||
/// <param name="gZipRequester">GZip 请求器</param>
|
||||
public AnnouncementProvider(Requester requester)
|
||||
{
|
||||
this.requester = requester;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取公告列表
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>公告列表</returns>
|
||||
public async Task<AnnouncementWrapper?> GetAnnouncementWrapperAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Response<AnnouncementWrapper>? resp = await requester
|
||||
.Reset()
|
||||
.GetAsync<AnnouncementWrapper>(ApiEndpoints.AnnList, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return resp?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取公告内容列表
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>公告内容列表</returns>
|
||||
public async Task<List<AnnouncementContent>> GetAnnouncementContentsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
Response<ListWrapper<AnnouncementContent>>? resp = await requester
|
||||
.Reset()
|
||||
.AddHeader("Accept", RequestOptions.Json)
|
||||
.GetAsync<ListWrapper<AnnouncementContent>>(ApiEndpoints.AnnContent, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return resp?.Data?.List ?? new();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告类型
|
||||
/// </summary>
|
||||
public class AnnouncementType
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 国际化名称
|
||||
/// </summary>
|
||||
[JsonProperty("mi18n_name")]
|
||||
public string? MI18NName { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 公告包装器
|
||||
/// </summary>
|
||||
public class AnnouncementWrapper : ListWrapper<AnnouncementListWrapper>
|
||||
{
|
||||
/// <summary>
|
||||
/// 总数
|
||||
/// </summary>
|
||||
[JsonProperty("total")]
|
||||
public int Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型列表
|
||||
/// </summary>
|
||||
[JsonProperty("type_list")]
|
||||
public List<AnnouncementType>? TypeList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提醒
|
||||
/// </summary>
|
||||
[JsonProperty("alert")]
|
||||
public bool Alert { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提醒Id
|
||||
/// </summary>
|
||||
[JsonProperty("alert_id")]
|
||||
public int AlertId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时区
|
||||
/// </summary>
|
||||
[JsonProperty("timezone")]
|
||||
public int TimeZone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间戳
|
||||
/// </summary>
|
||||
[JsonProperty("t")]
|
||||
public long TimeStamp { get; set; }
|
||||
}
|
||||
36
src/Snap.Hutao/Snap.Hutao/Web/Request/AuthRequester.cs
Normal file
36
src/Snap.Hutao/Snap.Hutao/Web/Request/AuthRequester.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 验证 Token 请求器
|
||||
/// </summary>
|
||||
public class AuthRequester : Requester
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="Requester"/> 对象
|
||||
/// </summary>
|
||||
/// <param name="httpClient">Http 客户端</param>
|
||||
/// <param name="json">Json 处理器</param>
|
||||
/// <param name="logger">消息器</param>
|
||||
public AuthRequester(HttpClient httpClient, Json json, ILogger<Requester> logger)
|
||||
: base(httpClient, json, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证令牌
|
||||
/// </summary>
|
||||
public string? AuthToken { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void PrepareHttpClient()
|
||||
{
|
||||
base.PrepareHttpClient();
|
||||
HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", AuthToken);
|
||||
}
|
||||
}
|
||||
43
src/Snap.Hutao/Snap.Hutao/Web/Request/RequestOptions.cs
Normal file
43
src/Snap.Hutao/Snap.Hutao/Web/Request/RequestOptions.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Snap.Hutao.Web.Request
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求选项
|
||||
/// 用于添加到请求头中
|
||||
/// </summary>
|
||||
public class RequestOptions : Dictionary<string, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// 不再使用
|
||||
/// </summary>
|
||||
[Obsolete("不再使用")]
|
||||
public const string CommonUA2101 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.10.1";
|
||||
|
||||
/// <summary>
|
||||
/// 不再使用
|
||||
/// </summary>
|
||||
[Obsolete("不再使用")]
|
||||
public const string CommonUA2111 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.11.1";
|
||||
|
||||
/// <summary>
|
||||
/// 支持更新的DS2算法
|
||||
/// </summary>
|
||||
public const string CommonUA2161 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.16.1";
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序/Json
|
||||
/// </summary>
|
||||
public const string Json = "application/json";
|
||||
|
||||
/// <summary>
|
||||
/// 指示请求由米游社发起
|
||||
/// </summary>
|
||||
public const string Hyperion = "com.mihoyo.hyperion";
|
||||
|
||||
/// <summary>
|
||||
/// 设备Id
|
||||
/// </summary>
|
||||
public static readonly string DeviceId = Guid.NewGuid().ToString("D");
|
||||
}
|
||||
}
|
||||
207
src/Snap.Hutao/Snap.Hutao/Web/Request/Requester.cs
Normal file
207
src/Snap.Hutao/Snap.Hutao/Web/Request/Requester.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 请求器
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
public class Requester
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly Json json;
|
||||
private readonly ILogger<Requester> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="Requester"/> 对象
|
||||
/// </summary>
|
||||
/// <param name="httpClient">Http 客户端</param>
|
||||
/// <param name="json">Json 处理器</param>
|
||||
/// <param name="logger">消息器</param>
|
||||
public Requester(HttpClient httpClient, Json json, ILogger<Requester> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.json = json;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求头
|
||||
/// </summary>
|
||||
public RequestOptions Headers { get; set; } = new RequestOptions();
|
||||
|
||||
/// <summary>
|
||||
/// 内部使用的 <see cref="HttpClient"/>
|
||||
/// </summary>
|
||||
protected HttpClient HttpClient { get => httpClient; }
|
||||
|
||||
/// <summary>
|
||||
/// GET 操作
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">返回的类类型</typeparam>
|
||||
/// <param name="url">地址</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>响应</returns>
|
||||
public async Task<Response<TResult>?> GetAsync<TResult>(string? url, CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger.LogInformation("GET {urlbase}", url?.Split('?')[0]);
|
||||
return url is null
|
||||
? null
|
||||
: await RequestAsync<TResult>(
|
||||
client => new RequestInfo(url, () => client.GetAsync(url, cancellationToken)),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET 操作
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">返回的类类型</typeparam>
|
||||
/// <param name="url">地址</param>
|
||||
/// <param name="encoding">编码</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>响应</returns>
|
||||
public async Task<Response<TResult>?> GetAsync<TResult>(string? url, Encoding encoding, CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger.LogInformation("GET {urlbase}", url?.Split('?')[0]);
|
||||
return url is null
|
||||
? null
|
||||
: await RequestAsync<TResult>(
|
||||
client => new RequestInfo(url, () => client.GetAsync(url, cancellationToken), encoding),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST 操作
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">返回的类类型</typeparam>
|
||||
/// <param name="url">地址</param>
|
||||
/// <param name="data">要发送的.NET(匿名)对象</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>响应</returns>
|
||||
public async Task<Response<TResult>?> PostAsync<TResult>(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<TResult>(
|
||||
client => new RequestInfo(url, () => client.PostAsync(url, new StringContent(dataString), cancellationToken)),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST 操作,Content-Type
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">返回的类类型</typeparam>
|
||||
/// <param name="url">地址</param>
|
||||
/// <param name="data">要发送的.NET(匿名)对象</param>
|
||||
/// <param name="contentType">内容类型</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>响应</returns>
|
||||
public async Task<Response<TResult>?> PostAsync<TResult>(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<TResult>(
|
||||
client => new RequestInfo(url, () => client.PostAsync(url, new StringContent(dataString, Encoding.UTF8, contentType), cancellationToken)),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置状态
|
||||
/// 清空请求头
|
||||
/// </summary>
|
||||
/// <returns>链式调用需要的实例</returns>
|
||||
public Requester Reset()
|
||||
{
|
||||
Headers.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加请求头
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>链式调用需要的实例</returns>
|
||||
public Requester AddHeader(string key, string value)
|
||||
{
|
||||
Headers.Add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在请求前准备 <see cref="System.Net.Http.HttpClient"/>
|
||||
/// </summary>
|
||||
protected virtual void PrepareHttpClient()
|
||||
{
|
||||
HttpClient.DefaultRequestHeaders.Clear();
|
||||
|
||||
foreach ((string name, string value) in Headers)
|
||||
{
|
||||
HttpClient.DefaultRequestHeaders.Add(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Response<TResult>?> RequestAsync<TResult>(Func<HttpClient, RequestInfo> 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<Response<TResult>>(contentString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "请求时遇到问题");
|
||||
return Response<TResult>.CreateFail($"{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
logger.LogInformation("Request Completed");
|
||||
}
|
||||
}
|
||||
|
||||
private record RequestInfo
|
||||
{
|
||||
public RequestInfo(string url, Func<Task<HttpResponseMessage>> httpResponseMessage, Encoding? encoding = null)
|
||||
{
|
||||
Url = url;
|
||||
RequestAsyncFunc = httpResponseMessage;
|
||||
Encoding = encoding;
|
||||
}
|
||||
|
||||
public string Url { get; set; }
|
||||
|
||||
public Func<Task<HttpResponseMessage>> RequestAsyncFunc { get; set; }
|
||||
|
||||
public Encoding? Encoding { get; set; }
|
||||
}
|
||||
}
|
||||
40
src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs
Normal file
40
src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Response;
|
||||
|
||||
/// <summary>
|
||||
/// 已知的返回代码
|
||||
/// </summary>
|
||||
public enum KnownReturnCode
|
||||
{
|
||||
/// <summary>
|
||||
/// 内部错误
|
||||
/// </summary>
|
||||
InternalFailure = int.MinValue,
|
||||
|
||||
/// <summary>
|
||||
/// 已经签到过了
|
||||
/// </summary>
|
||||
AlreadySignedIn = -5003,
|
||||
|
||||
/// <summary>
|
||||
/// 验证密钥过期
|
||||
/// </summary>
|
||||
AuthKeyTimeOut = -101,
|
||||
|
||||
/// <summary>
|
||||
/// Ok
|
||||
/// </summary>
|
||||
OK = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 未定义
|
||||
/// </summary>
|
||||
NotDefined = 7,
|
||||
|
||||
/// <summary>
|
||||
/// 数据未公开
|
||||
/// </summary>
|
||||
DataIsNotPublicForTheUser = 10102,
|
||||
}
|
||||
19
src/Snap.Hutao/Snap.Hutao/Web/Response/ListWrapper.cs
Normal file
19
src/Snap.Hutao/Snap.Hutao/Web/Response/ListWrapper.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 列表对象包装器
|
||||
/// </summary>
|
||||
/// <typeparam name="T">列表的元素类型</typeparam>
|
||||
public class ListWrapper<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表
|
||||
/// </summary>
|
||||
[JsonProperty("list")] public List<T>? List { get; set; }
|
||||
}
|
||||
54
src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs
Normal file
54
src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Snap.Hutao.Web.Response;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 <see cref="Response{T}"/> 的非泛型基类
|
||||
/// </summary>
|
||||
public class Response
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回代码
|
||||
/// </summary>
|
||||
[JsonProperty("retcode")]
|
||||
public int ReturnCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息
|
||||
/// </summary>
|
||||
[JsonProperty("message")]
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 响应是否正常
|
||||
/// </summary>
|
||||
/// <param name="response">响应</param>
|
||||
/// <returns>是否Ok</returns>
|
||||
public static bool IsOk(Response? response)
|
||||
{
|
||||
return response is not null && response.ReturnCode == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个失败的响应
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <returns>响应</returns>
|
||||
public static Response CreateFail(string message)
|
||||
{
|
||||
return new Response()
|
||||
{
|
||||
ReturnCode = (int)KnownReturnCode.InternalFailure,
|
||||
Message = message,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"状态:{ReturnCode} | 信息:{Message}";
|
||||
}
|
||||
}
|
||||
33
src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs
Normal file
33
src/Snap.Hutao/Snap.Hutao/Web/Response/Response{TData}.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Snap.Hutao.Web.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Mihoyo 标准API响应
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">数据类型</typeparam>
|
||||
public class Response<TData> : Response
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据
|
||||
/// </summary>
|
||||
[JsonProperty("data")]
|
||||
public TData? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个失败的响应
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <returns>响应</returns>
|
||||
public static new Response<TData> CreateFail(string message)
|
||||
{
|
||||
return new Response<TData>()
|
||||
{
|
||||
ReturnCode = (int)KnownReturnCode.InternalFailure,
|
||||
Message = message,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user