announcement page

This commit is contained in:
DismissedLight
2022-05-04 10:41:56 +08:00
parent bfbb54e5f3
commit cdfd7e2830
59 changed files with 2754 additions and 54 deletions

View File

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

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

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

View 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);
}
}
}

View File

@@ -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>
/// 注入方法

View File

@@ -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;

View 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);
}
}

View 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));
}
}

View File

@@ -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>

View 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));
}
}

View 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);
}
}

View File

@@ -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>

View 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;
}
}
}

View 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;
}
}
}
}

View File

@@ -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;

View File

@@ -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);
}

View 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);
}
}
}
};
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}

View 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);
}

View 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("&lt;t.*?&gt;(.*?)&lt;/t&gt;", RegexOptions.Multiline);
Regex timeTagInnerRegex = new("(?<=&lt;t.*?&gt;)(.*?)(?=&lt;/t&gt;)");
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();
}
}

View 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;
});
}
}

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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="&#xE7C4;"/>
</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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View 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="&#xE8A7;"
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>

View 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();
}
}

View File

@@ -13,6 +13,6 @@ public sealed partial class SettingPage : Microsoft.UI.Xaml.Controls.Page
/// </summary>
public SettingPage()
{
this.InitializeComponent();
InitializeComponent();
}
}

View 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 运行时。");
}
}
}

View 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&region=cn_gf01&level=55&uid=100000000";
}

View File

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

View File

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

View File

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

View File

@@ -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();
}
}

View File

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

View File

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

View 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);
}
}

View 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");
}
}

View 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; }
}
}

View 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,
}

View 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; }
}

View 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}";
}
}

View 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,
};
}
}