add database intergrations

This commit is contained in:
DismissedLight
2022-05-05 22:53:07 +08:00
parent 7b50f28e7e
commit c0d80084b7
44 changed files with 939 additions and 379 deletions

View File

@@ -3,8 +3,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
using Snap.Hutao.Web.Request;
namespace Snap.Hutao;
@@ -40,29 +38,13 @@ public partial class App : Application
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
.AddHttpClients()
.AddDefaultJsonSerializerOptions()
.AddInjections(typeof(App))
.BuildServiceProvider();
Ioc.Default.ConfigureServices(services);
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
namespace Snap.Hutao.Context.Database;
/// <summary>
/// 应用程序数据库上下文
/// </summary>
internal class AppDbContext : DbContext
{
/// <summary>
/// 构造一个新的应用程序数据库上下文
/// </summary>
/// <param name="options">选项</param>
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
}

View File

@@ -0,0 +1,153 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
using System.IO;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 文件系统上下文
/// </summary>
/// <typeparam name="TLocation">路径位置类型</typeparam>
internal abstract class FileSystemContext
{
private readonly IFileSystemLocation location;
/// <summary>
/// 初始化文件系统上下文
/// </summary>
/// <param name="location">指定的文件系统位置</param>
public FileSystemContext(IFileSystemLocation location)
{
this.location = location;
}
/// <summary>
/// 检查根目录
/// </summary>
/// <returns>是否创建了路径</returns>
public bool EnsureDirectory()
{
string folder = location.GetPath();
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
return true;
}
return false;
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="file">文件名称</param>
/// <returns>是否存在</returns>
public bool FileExists(string file)
{
return File.Exists(Locate(file));
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="folder">文件夹名称</param>
/// <param name="file">文件名称</param>
/// <returns>是否存在</returns>
public bool FileExists(string folder, string file)
{
return File.Exists(Locate(folder, file));
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="folder">文件夹名称</param>
/// <returns>是否存在</returns>
public bool FolderExists(string folder)
{
return Directory.Exists(Locate(folder));
}
/// <summary>
/// 定位根目录中的文件或文件夹
/// </summary>
/// <param name="fileOrFolder">文件或文件夹</param>
/// <returns>绝对路径</returns>
public string Locate(string fileOrFolder)
{
return Path.GetFullPath(fileOrFolder, location.GetPath());
}
/// <summary>
/// 定位根目录下子文件夹中的文件
/// </summary>
/// <param name="folder">文件夹</param>
/// <param name="file">文件</param>
/// <returns>绝对路径</returns>
public string Locate(string folder, string file)
{
return Path.GetFullPath(Path.Combine(folder, file), location.GetPath());
}
/// <summary>
/// 将文件移动到指定的子目录
/// </summary>
/// <param name="file">文件</param>
/// <param name="folder">文件夹</param>
/// <param name="overwrite">是否覆盖</param>
/// <returns>是否成功 当文件不存在时会失败</returns>
public bool MoveToFolderOrIgnore(string file, string folder, bool overwrite = true)
{
string target = Locate(folder, file);
file = Locate(file);
if (File.Exists(file))
{
File.Move(file, target, overwrite);
return true;
}
return false;
}
/// <summary>
/// 创建文件,若已存在文件,则不会创建
/// </summary>
/// <param name="file">文件</param>
public void CreateFileOrIgnore(string file)
{
file = Locate(file);
if (!File.Exists(file))
{
File.Create(file).Dispose();
}
}
/// <summary>
/// 创建文件夹,若已存在文件,则不会创建
/// </summary>
/// <param name="folder">文件夹</param>
public void CreateFolderOrIgnore(string folder)
{
folder = Locate(folder);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
}
/// <summary>
/// 尝试删除文件夹
/// </summary>
/// <param name="folder">文件夹</param>
public void DeleteFolderOrIgnore(string folder)
{
folder = Locate(folder);
if (Directory.Exists(folder))
{
Directory.Delete(folder, true);
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 文件系统位置
/// </summary>
public interface IFileSystemLocation
{
/// <summary>
/// 获取路径
/// </summary>
/// <returns>路径</returns>
string GetPath();
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
public class MyDocument : IFileSystemLocation
{
private string? path;
/// <inheritdoc/>
public string GetPath()
{
if (string.IsNullOrEmpty(path))
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
path = Path.GetFullPath(Path.Combine(myDocument, "Hutao"));
}
return path;
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 我的文档上下文
/// </summary>
[Injection(InjectAs.Transient)]
internal class MyDocumentContext : FileSystemContext
{
/// <inheritdoc cref="FileSystemContext"/>
public MyDocumentContext(MyDocument myDocument)
: base(myDocument)
{
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Extension;
using System.Security.Cryptography;
using System.Text;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core;
/// <summary>
/// 核心环境参数
/// </summary>
internal static class CoreEnvironment
{
/// <summary>
/// 当前版本
/// </summary>
public static readonly Version Version;
/// <summary>
/// 设备Id
/// </summary>
public static readonly string DeviceId;
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography";
private const string MachineGuidValue = "MachineGuid";
static CoreEnvironment()
{
Version = Package.Current.Id.Version.ToVersion();
DeviceId = GetDeviceId();
}
/// <summary>
/// 获取设备的UUID
/// </summary>
/// <returns>设备的UUID</returns>
private static string GetDeviceId()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
byte[] bytes = Encoding.UTF8.GetBytes($"{userName}{machineGuid}");
byte[] hash = MD5.HashData(bytes);
return Convert.ToHexString(hash);
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 实现日期的转换
/// </summary>
internal class DateTimeConverter : JsonConverter<DateTime>
{
/// <inheritdoc/>
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is string dataTimeString)
{
return DateTime.Parse(dataTimeString);
}
return default(DateTime);
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
}
}

View File

@@ -3,7 +3,7 @@
using System.Net.Http;
namespace Snap.Hutao.Core;
namespace Snap.Hutao.Core.Json;
/// <summary>
/// Http Json 处理

View File

@@ -1,10 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using System.IO;
using System.Text.Json;
namespace Snap.Hutao.Core;
namespace Snap.Hutao.Core.Json;
/// <summary>
/// Json操作
@@ -12,20 +12,17 @@ namespace Snap.Hutao.Core;
[Injection(InjectAs.Transient)]
public class Json
{
private readonly JsonSerializerOptions jsonSerializerOptions;
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="jsonSerializerOptions">配置</param>
/// <param name="logger">日志器</param>
public Json(ILogger<Json> logger)
public Json(JsonSerializerOptions jsonSerializerOptions, ILogger<Json> logger)
{
this.jsonSerializerOptions = jsonSerializerOptions;
this.logger = logger;
}
@@ -34,19 +31,20 @@ public class Json
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="value">要反序列化的JSON</param>
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns>
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会返回 <see langword="default"/></returns>
public T? ToObject<T>(string value)
{
try
{
return JsonConvert.DeserializeObject<T>(value);
T? result = JsonSerializer.Deserialize<T>(value);
return result;
}
catch (Exception ex)
{
logger.LogError("反序列化Json时遇到问题:{ex}", ex);
logger.LogError("反序列化Json时遇到问题\n{ex}", ex);
}
return default;
return default(T);
}
/// <summary>
@@ -69,7 +67,7 @@ public class Json
/// <returns>对象的JSON字符串表示形式</returns>
public string Stringify(object? value)
{
return JsonConvert.SerializeObject(value, jsonSerializerSettings);
return JsonSerializer.Serialize(value, jsonSerializerOptions);
}
/// <summary>

View File

@@ -0,0 +1,113 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
using Windows.Storage;
namespace Snap.Hutao.Core.Setting;
/// <summary>
/// 本地设置
/// </summary>
internal static class LocalSetting
{
/// <summary>
/// 由于 <see cref="Windows.Foundation.Collections.IPropertySet"/> 没有启用 nullable,
/// 在处理引用类型时需要格外小心
/// 将值类型的操作与引用类型区分开,可以提升一定的性能
/// </summary>
private static readonly ApplicationDataContainer Container;
static LocalSetting()
{
Container = ApplicationData.Current.LocalSettings;
}
/// <summary>
/// 获取设置项的值
/// </summary>
/// <typeparam name="T">设置项的类型</typeparam>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>获取的值</returns>
public static T? Get<T>(string key, T? defaultValue = default)
where T : class
{
if (Container.Values.TryGetValue(key, out object? value))
{
return value is null ? defaultValue : value as T;
}
else
{
Set(key, defaultValue);
return defaultValue;
}
}
/// <summary>
/// 获取设置项的值
/// </summary>
/// <typeparam name="T">设置项的类型</typeparam>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>获取的值</returns>
public static T GetValueType<T>(string key, T defaultValue = default)
where T : struct
{
if (Container.Values.TryGetValue(key, out object? value))
{
if (value is null)
{
return defaultValue;
}
else
{
// 无法避免的拆箱操作
return (T)value;
}
}
else
{
Set(key, defaultValue);
return defaultValue;
}
}
/// <summary>
/// 设置设置项的值
/// </summary>
/// <typeparam name="T">设置项的类型</typeparam>
/// <param name="key">键</param>
/// <param name="value">值</param>
public static void Set<T>(string key, T? value)
where T : class
{
try
{
Container.Values[key] = value;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
/// <summary>
/// 设置设置项的值
/// </summary>
/// <typeparam name="T">设置项的类型</typeparam>
/// <param name="key">键</param>
/// <param name="value">值</param>
public static void SetValueType<T>(string key, T value)
where T : struct
{
try
{
Container.Values[key] = value;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Setting;
/// <summary>
/// 设置键
/// </summary>
internal static class SettingKeys
{
/// <summary>
/// 上次打开时App的版本
/// </summary>
public static readonly string LastAppVersion = "LastAppVersion";
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft;
using Snap.Hutao.Model;
namespace Snap.Hutao.Core.Threading;
@@ -13,6 +14,7 @@ namespace Snap.Hutao.Core.Threading;
public class Watcher : Observable
{
private readonly bool isReusable;
private bool hasUsed;
private bool isWorking;
private bool isCompleted;
@@ -48,33 +50,33 @@ public class Watcher : Observable
/// <summary>
/// 对某个操作进行监视,
/// 无法防止代码重入
/// </summary>
/// <returns>一个可释放的对象,用于在操作完成时自动提示监视器工作已经完成</returns>
/// <exception cref="InvalidOperationException">重用了一个不可重用的监视器</exception>
public IDisposable Watch()
{
Verify.Operation(!IsWorking, $"此 {nameof(Watcher)} 已经处于检查状态");
Verify.Operation(isReusable || !hasUsed, $"此 {nameof(Watcher)} 不允许多次使用");
hasUsed = true;
IsWorking = true;
return new WorkDisposable(this);
return new WatchDisposable(this);
}
private struct WorkDisposable : IDisposable
private struct WatchDisposable : IDisposable
{
private readonly Watcher work;
private readonly Watcher watcher;
public WorkDisposable(Watcher work)
public WatchDisposable(Watcher watcher)
{
this.work = work;
this.watcher = watcher;
}
public void Dispose()
{
work.IsWorking = false;
work.IsCompleted = true;
watcher.IsWorking = false;
watcher.IsCompleted = true;
}
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.ApplicationModel;
namespace Snap.Hutao.Extension;
/// <summary>
/// 包版本扩展
/// </summary>
public static class PackageVersionExtensions
{
/// <summary>
/// 将包版本转换为版本
/// </summary>
/// <param name="packageVersion">包版本</param>
/// <returns>版本</returns>
public static Version ToVersion(this PackageVersion packageVersion)
{
return new Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision);
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Storage.Pickers;
namespace Snap.Hutao.Factory.Abstraction;
/// <summary>
/// 文件选择器工厂
/// </summary>
internal interface IPickerFactory
{
/// <summary>
/// 获取 经过初始化的 <see cref="FileOpenPicker"/>
/// </summary>
/// <returns>经过初始化的 <see cref="FileOpenPicker"/></returns>
FileOpenPicker GetFileOpenPicker();
/// <summary>
/// 获取 经过初始化的 <see cref="FileSavePicker"/>
/// </summary>
/// <returns>经过初始化的 <see cref="FileSavePicker"/></returns>
FileSavePicker GetFileSavePicker();
/// <summary>
/// 获取 经过初始化的 <see cref="FolderPicker"/>
/// </summary>
/// <returns>经过初始化的 <see cref="FolderPicker"/></returns>
FolderPicker GetFolderPicker();
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Factory.Abstraction;
using Windows.Storage.Pickers;
using WinRT.Interop;
namespace Snap.Hutao.Factory;
/// <inheritdoc cref="IPickerFactory"/>
[Injection(InjectAs.Transient)]
internal class PickerFactory : IPickerFactory
{
private readonly MainWindow mainWindow;
/// <summary>
/// 构造一个新的文件选择器工厂
/// </summary>
/// <param name="mainWindow">主窗体的引用注入</param>
public PickerFactory(MainWindow mainWindow)
{
this.mainWindow = mainWindow;
}
/// <inheritdoc/>
public FileOpenPicker GetFileOpenPicker()
{
return GetInitializedPicker<FileOpenPicker>();
}
/// <inheritdoc/>
public FileSavePicker GetFileSavePicker()
{
return GetInitializedPicker<FileSavePicker>();
}
/// <inheritdoc/>
public FolderPicker GetFolderPicker()
{
return GetInitializedPicker<FolderPicker>();
}
private T GetInitializedPicker<T>()
where T : new()
{
// Create a folder picker.
T picker = new();
IntPtr hWnd = WindowNative.GetWindowHandle(mainWindow);
InitializeWithWindow.Initialize(picker, hWnd);
return picker;
}
}

View File

@@ -0,0 +1,109 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Web.Request;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao;
/// <summary>
/// <see cref="Ioc"/> 配置
/// </summary>
internal static class IocConfiguration
{
/// <summary>
/// 添加 <see cref="System.Net.Http.HttpClient"/>
/// </summary>
/// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddHttpClients(this IServiceCollection services)
{
// http json
services
.AddHttpClient<HttpJson>()
.ConfigureHttpClient(client =>
{
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Snap Hutao");
});
// requester & auth reuqester
services
.AddHttpClient<Requester>(nameof(Requester))
.AddTypedClient<AuthRequester>()
.ConfigureHttpClient(client => client.Timeout = Timeout.InfiniteTimeSpan);
return services;
}
/// <summary>
/// 添加默认的 <see cref="JsonSerializer"/> 配置
/// </summary>
/// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddDefaultJsonSerializerOptions(this IServiceCollection services)
{
// default json options, global configuration
return services
.AddSingleton(new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
WriteIndented = true,
});
}
/// <summary>
/// 添加数据库
/// </summary>
/// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddDatebase(this IServiceCollection services)
{
MyDocumentContext myDocument = new(new());
myDocument.EnsureDirectory();
string dbFile = myDocument.Locate("Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
bool shouldMigrate = false;
if (!myDocument.FileExists(dbFile))
{
shouldMigrate = true;
}
else
{
string? versionString = LocalSetting.Get<string>(SettingKeys.LastAppVersion);
if (Version.TryParse(versionString, out Version? lastVersion))
{
if (lastVersion < CoreEnvironment.Version)
{
shouldMigrate = true;
}
}
}
if (shouldMigrate)
{
// temporarily create a context
using (AppDbContext context = new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options))
{
context.Database.Migrate();
}
}
LocalSetting.Set(SettingKeys.LastAppVersion, CoreEnvironment.Version.ToString());
return services
.AddPooledDbContextFactory<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao;
/// <summary>
/// 主窗体
/// </summary>
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Singleton)]
public sealed partial class MainWindow : Window
{
/// <summary>

View File

@@ -4,7 +4,7 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core;
namespace Snap.Hutao.Model;
/// <summary>
/// 简单的实现了 <see cref="INotifyPropertyChanged"/> 接口

View File

@@ -61,13 +61,6 @@ public interface IInfoBarService
/// <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>

View File

@@ -61,5 +61,5 @@ public interface INavigationService
/// </summary>
/// <param name="pageType">同步的页面类型</param>
/// <returns>是否同步成功</returns>
bool SyncTabWith(Type pageType);
bool SyncSelectedNavigationViewItemWith(Type pageType);
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.VisualStudio.Threading;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service;
@@ -79,25 +78,25 @@ internal class InfoBarService : IInfoBarService
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)
{
if (infoBarStack is null)
{
return;
}
infoBarStack.DispatcherQueue.TryEnqueue(() => PrepareInfoBarAndShowInternal(severity, title, message, delay));
}
/// <summary>
/// 此方法应在主线程上运行
/// </summary>
/// <param name="severity">严重程度</param>
/// <param name="title">标题</param>
/// <param name="message">消息</param>
/// <param name="delay">关闭延迟</param>
[SuppressMessage("", "VSTHRD100", Justification ="只能通过 async void 方法使控件在主线程创建")]
private async void PrepareInfoBarAndShowInternal(InfoBarSeverity severity, string? title, string? message, int delay)
{
InfoBar infoBar = new()
{
@@ -107,7 +106,14 @@ internal class InfoBarService : IInfoBarService
IsOpen = true,
};
Show(infoBar, delay);
infoBar.Closed += OnInfoBarClosed;
Must.NotNull(infoBarStack!)!.Children.Add(infoBar);
if (delay > 0)
{
await Task.Delay(delay);
infoBar.IsOpen = false;
}
}
private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args)

View File

@@ -66,9 +66,9 @@ internal class NavigationService : INavigationService
public bool HasEverNavigated { get; set; }
/// <inheritdoc/>
public bool SyncTabWith(Type pageType)
public bool SyncSelectedNavigationViewItemWith(Type? pageType)
{
if (NavigationView is null)
if (NavigationView is null || pageType is null)
{
return false;
}
@@ -98,7 +98,7 @@ internal class NavigationService : INavigationService
return false;
}
_ = isSyncTabRequested && SyncTabWith(pageType);
_ = isSyncTabRequested && SyncSelectedNavigationViewItemWith(pageType);
bool result = false;
try
@@ -151,6 +151,7 @@ internal class NavigationService : INavigationService
if (Frame != null && Frame.CanGoBack)
{
Frame.GoBack();
SyncSelectedNavigationViewItemWith(Frame.Content.GetType());
}
}
}

View File

@@ -13,7 +13,7 @@
<Nullable>enable</Nullable>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<NeutralLanguage>zh-CN</NeutralLanguage>
<DefaultLanguage>zh-CN</DefaultLanguage>
<DefaultLanguage>zh-cn</DefaultLanguage>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
@@ -48,14 +48,16 @@
<ItemGroup>
<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.EntityFrameworkCore.Sqlite" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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" />
@@ -63,10 +65,7 @@
<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="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>
@@ -83,9 +82,6 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Folder Include="Model\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SettingsUI\SettingsUI.csproj" />
</ItemGroup>

View File

@@ -1,86 +0,0 @@
// 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

@@ -1,21 +0,0 @@
// 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

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Converters;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.View.Converter;

View File

@@ -10,26 +10,28 @@
<Grid>
<NavigationView
x:Name="NavView"
CompactPaneLength="48"
OpenPaneLength="172"
CompactModeThresholdWidth="360"
CompactModeThresholdWidth="128"
ExpandedModeThresholdWidth="720"
IsBackEnabled="{x:Bind ContentFrame.CanGoBack}">
IsPaneOpen="True"
IsBackEnabled="{Binding ElementName=ContentFrame,Path=CanGoBack}">
<!-- x:Bind can't get property update here seems like a WinUI 3 bug-->
<NavigationView.MenuItems>
<NavigationViewItem
Content="活动"
helper:NavHelper.NavigateTo="page:AnnouncementPage">
<NavigationViewItem Content="活动" helper:NavHelper.NavigateTo="page:AnnouncementPage">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xE7C4;"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<Frame x:Name="ContentFrame">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition>
<DrillInNavigationTransitionInfo/>
</NavigationThemeTransition>
</TransitionCollection>
<NavigationThemeTransition>
<DrillInNavigationTransitionInfo/>
</NavigationThemeTransition>
</Frame.ContentTransitions>
</Frame>
</NavigationView>
@@ -61,9 +63,6 @@
TintColor="#34424d"
FallbackColor="#34424d"/>
</StackPanel.Resources>
<StackPanel.OpacityTransition>
<ScalarTransition/>
</StackPanel.OpacityTransition>
<StackPanel.Transitions>
<TransitionCollection>
<AddDeleteThemeTransition/>

View File

@@ -21,10 +21,12 @@ public sealed partial class MainView : UserControl
{
InitializeComponent();
infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
infoBarService.Initialize(InfoBarStack);
navigationService = Ioc.Default.GetRequiredService<INavigationService>();
navigationService.Initialize(NavView, ContentFrame);
infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
infoBarService.Initialize(InfoBarStack);
navigationService.Navigate<Page.AnnouncementPage>();
}
}

View File

@@ -59,7 +59,7 @@
<DataTemplate>
<Border
CornerRadius="{StaticResource CompatCornerRadius}"
Background="{StaticResource SystemControlPageBackgroundAltHighBrush}"
Background="{ThemeResource SystemControlPageBackgroundAltHighBrush}"
cwui:UIElementExtensions.ClipToBounds="True">
<Grid>
<Grid.RowDefinitions>
@@ -105,31 +105,20 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Visibility="{Binding ShouldShowTimeDescription,Converter={StaticResource BoolToVisibilityConverter}}">
<Border.Background>
<!--<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#00000000"/>
<GradientStop Offset="1" Color="#A0000000"/>
</LinearGradientBrush>
</Border.Background>
</Border.Background>-->
<ProgressBar
Height="1"
MinHeight="1"
MinHeight="2"
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
@@ -170,13 +159,29 @@
TextTrimming="WordEllipsis"
Margin="4,6,0,0"
Opacity="0.6"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
FontSize="10"
Opacity="0.4"
Margin="4,4,0,4"
Text="{Binding TimeFormatted}"
TextWrapping="NoWrap"/>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
FontSize="10"
Opacity="0.8"
Margin="4,4,4,4"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding TimeDescription}" />
</Grid>
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
FontSize="10"
Opacity="0.4"
Margin="4,4,0,4"
Text="{Binding TimeFormatted}"/>
</StackPanel>
</Border>
</Grid>

View File

@@ -17,9 +17,8 @@
<ScrollViewer>
<StackPanel
Margin="32,0,24,0">
<controls:SettingsGroup
Header="关于 胡桃">
<controls:SettingsGroup Header="关于 胡桃">
<controls:SettingExpander>
<controls:SettingExpander.Header>
<controls:Setting

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page;
/// <summary>
@@ -13,6 +15,7 @@ public sealed partial class SettingPage : Microsoft.UI.Xaml.Controls.Page
/// </summary>
public SettingPage()
{
DataContext = Ioc.Default.GetRequiredService<SettingViewModel>();
InitializeComponent();
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel;
/// <summary>
/// 测试视图模型
/// </summary>
[Injection(InjectAs.Transient)]
internal class SettingViewModel
{
/// <summary>
/// 构造一个新的测试视图模型
/// </summary>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public SettingViewModel()
{
}
}

View File

@@ -1,7 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using Snap.Hutao.Core.Json.Converter;
using System.Text.Json.Serialization;
using System.Windows.Input;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
@@ -13,42 +14,6 @@ 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>
@@ -131,45 +96,83 @@ public class Announcement : AnnouncementContent
get => $"{StartTime:yyyy.MM.dd HH:mm} - {EndTime:yyyy.MM.dd HH:mm}";
}
/// <summary>
/// 类型标签
/// </summary>
[JsonPropertyName("type_label")]
public string? TypeLabel { get; set; }
/// <summary>
/// 标签文本
/// </summary>
[JsonPropertyName("tag_label")]
public string? TagLabel { get; set; }
/// <summary>
/// 标签图标
/// </summary>
[JsonPropertyName("tag_icon")]
public string? TagIcon { get; set; }
/// <summary>
/// 登录提醒
/// </summary>
[JsonPropertyName("login_alert")]
public int LoginAlert { get; set; }
/// <summary>
/// 开始时间
/// </summary>
[JsonPropertyName("start_time")]
[JsonConverter(typeof(DateTimeConverter))]
public DateTime StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
[JsonPropertyName("end_time")]
[JsonConverter(typeof(DateTimeConverter))]
public DateTime EndTime { get; set; }
/// <summary>
/// 类型
/// </summary>
[JsonProperty("type")]
[JsonPropertyName("type")]
public int Type { get; set; }
/// <summary>
/// 提醒
/// </summary>
[JsonProperty("remind")]
[JsonPropertyName("remind")]
public int Remind { get; set; }
/// <summary>
/// 通知
/// </summary>
[JsonProperty("alert")]
[JsonPropertyName("alert")]
public int Alert { get; set; }
/// <summary>
/// 标签开始时间
/// </summary>
[JsonProperty("tag_start_time")]
[JsonPropertyName("tag_start_time")]
public string? TagStartTime { get; set; }
/// <summary>
/// 标签结束时间
/// </summary>
[JsonProperty("tag_end_time")]
[JsonPropertyName("tag_end_time")]
public string? TagEndTime { get; set; }
/// <summary>
/// 提醒版本
/// </summary>
[JsonProperty("remind_ver")]
[JsonPropertyName("remind_ver")]
public int RemindVer { get; set; }
/// <summary>
/// 是否含有内容
/// </summary>
[JsonProperty("has_content")]
[JsonPropertyName("has_content")]
public bool HasContent { get; set; }
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
@@ -13,37 +13,37 @@ public class AnnouncementContent
/// <summary>
/// 公告Id
/// </summary>
[JsonProperty("ann_id")]
[JsonPropertyName("ann_id")]
public int AnnId { get; set; }
/// <summary>
/// 公告标题
/// </summary>
[JsonProperty("title")]
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <summary>
/// 副标题
/// </summary>
[JsonProperty("subtitle")]
[JsonPropertyName("subtitle")]
public string? Subtitle { get; set; }
/// <summary>
/// 横幅Url
/// </summary>
[JsonProperty("banner")]
[JsonPropertyName("banner")]
public string? Banner { get; set; }
/// <summary>
/// 内容字符串
/// 可能包含了一些html格式
/// </summary>
[JsonProperty("content")]
[JsonPropertyName("content")]
public string? Content { get; set; }
/// <summary>
/// 语言
/// </summary>
[JsonProperty("lang")]
[JsonPropertyName("lang")]
public string? Lang { get; set; }
}

View File

@@ -1,8 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using Snap.Hutao.Web.Response;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
@@ -14,12 +14,12 @@ public class AnnouncementListWrapper : ListWrapper<Announcement>
/// <summary>
/// 类型Id
/// </summary>
[JsonProperty("type_id")]
[JsonPropertyName("type_id")]
public int TypeId { get; set; }
/// <summary>
/// 类型标签
/// </summary>
[JsonProperty("type_label")]
[JsonPropertyName("type_label")]
public string? TypeLabel { get; set; }
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
@@ -13,18 +13,18 @@ public class AnnouncementType
/// <summary>
/// Id
/// </summary>
[JsonProperty("id")]
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 名称
/// </summary>
[JsonProperty("name")]
[JsonPropertyName("name")]
public string? Name { get; set; }
/// <summary>
/// 国际化名称
/// </summary>
[JsonProperty("mi18n_name")]
[JsonPropertyName("mi18n_name")]
public string? MI18NName { get; set; }
}

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using Snap.Hutao.Web.Response;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
@@ -15,36 +15,36 @@ public class AnnouncementWrapper : ListWrapper<AnnouncementListWrapper>
/// <summary>
/// 总数
/// </summary>
[JsonProperty("total")]
[JsonPropertyName("total")]
public int Total { get; set; }
/// <summary>
/// 类型列表
/// </summary>
[JsonProperty("type_list")]
[JsonPropertyName("type_list")]
public List<AnnouncementType>? TypeList { get; set; }
/// <summary>
/// 提醒
/// </summary>
[JsonProperty("alert")]
[JsonPropertyName("alert")]
public bool Alert { get; set; }
/// <summary>
/// 提醒Id
/// </summary>
[JsonProperty("alert_id")]
[JsonPropertyName("alert_id")]
public int AlertId { get; set; }
/// <summary>
/// 时区
/// </summary>
[JsonProperty("timezone")]
[JsonPropertyName("timezone")]
public int TimeZone { get; set; }
/// <summary>
/// 时间戳
/// </summary>
[JsonProperty("t")]
public long TimeStamp { get; set; }
[JsonPropertyName("t")]
public string? TimeStamp { get; set; }
}

View File

@@ -1,7 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Service.Abstraction;
using System.Net.Http;
namespace Snap.Hutao.Web.Request;
@@ -16,9 +17,10 @@ public class AuthRequester : Requester
/// </summary>
/// <param name="httpClient">Http 客户端</param>
/// <param name="json">Json 处理器</param>
/// <param name="infoBarService">信息条服务</param>
/// <param name="logger">消息器</param>
public AuthRequester(HttpClient httpClient, Json json, ILogger<Requester> logger)
: base(httpClient, json, logger)
public AuthRequester(HttpClient httpClient, Json json, IInfoBarService infoBarService, ILogger<Requester> logger)
: base(httpClient, json, infoBarService, logger)
{
}

View File

@@ -1,7 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Response;
using System.Net.Http;
using System.Text;
@@ -16,6 +17,7 @@ public class Requester
{
private readonly HttpClient httpClient;
private readonly Json json;
private readonly IInfoBarService infoBarService;
private readonly ILogger<Requester> logger;
/// <summary>
@@ -23,11 +25,13 @@ public class Requester
/// </summary>
/// <param name="httpClient">Http 客户端</param>
/// <param name="json">Json 处理器</param>
/// <param name="infoBarService">信息条服务</param>
/// <param name="logger">消息器</param>
public Requester(HttpClient httpClient, Json json, ILogger<Requester> logger)
public Requester(HttpClient httpClient, Json json, IInfoBarService infoBarService, ILogger<Requester> logger)
{
this.httpClient = httpClient;
this.json = json;
this.infoBarService = infoBarService;
this.logger = logger;
}
@@ -37,7 +41,7 @@ public class Requester
public RequestOptions Headers { get; set; } = new RequestOptions();
/// <summary>
/// 内部使用的 <see cref="HttpClient"/>
/// 内部使用的 <see cref="System.Net.Http.HttpClient"/>
/// </summary>
protected HttpClient HttpClient { get => httpClient; }
@@ -50,32 +54,15 @@ public class Requester
/// <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);
}
if (url is null)
{
return Response<TResult>.CreateForEmptyUrl();
}
/// <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);
Task<HttpResponseMessage> GetMethod(HttpClient client, CancellationToken token) => client.GetAsync(url, token);
return await RequestAsync<TResult>(GetMethod, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
@@ -88,13 +75,17 @@ public class Requester
/// <returns>响应</returns>
public async Task<Response<TResult>?> PostAsync<TResult>(string? url, object data, CancellationToken cancellationToken = default)
{
if (url is null)
{
return Response<TResult>.CreateForEmptyUrl();
}
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)
HttpContent content = new StringContent(dataString);
Task<HttpResponseMessage> PostMethod(HttpClient client, CancellationToken token) => client.PostAsync(url, content, token);
return await RequestAsync<TResult>(PostMethod, cancellationToken)
.ConfigureAwait(false);
}
@@ -109,13 +100,17 @@ public class Requester
/// <returns>响应</returns>
public async Task<Response<TResult>?> PostAsync<TResult>(string? url, object data, string contentType, CancellationToken cancellationToken = default)
{
if (url is null)
{
return Response<TResult>.CreateForEmptyUrl();
}
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)
HttpContent content = new StringContent(dataString, Encoding.UTF8, contentType);
Task<HttpResponseMessage> PostMethod(HttpClient client, CancellationToken token) => client.PostAsync(url, content, token);
return await RequestAsync<TResult>(PostMethod, cancellationToken)
.ConfigureAwait(false);
}
@@ -155,53 +150,38 @@ public class Requester
}
}
private async Task<Response<TResult>?> RequestAsync<TResult>(Func<HttpClient, RequestInfo> requestFunc, CancellationToken cancellationToken = default)
private async Task<Response<TResult>?> RequestAsync<TResult>(
Func<HttpClient, CancellationToken, Task<HttpResponseMessage>> requestFunc,
CancellationToken cancellationToken = default)
{
PrepareHttpClient();
RequestInfo? info = requestFunc(HttpClient);
try
{
HttpResponseMessage response = await info.RequestAsyncFunc.Invoke()
HttpResponseMessage response = await requestFunc
.Invoke(HttpClient, cancellationToken)
.ConfigureAwait(false);
string contentString = await response.Content.ReadAsStringAsync(cancellationToken)
string contentString = await response.Content
.ReadAsStringAsync(cancellationToken)
.ConfigureAwait(false);
if (info.Encoding is not null)
Response<TResult>? resp = json.ToObject<Response<TResult>>(contentString);
if (resp?.ToString() is string representable)
{
byte[] bytes = Encoding.UTF8.GetBytes(contentString);
info.Encoding.GetString(bytes);
infoBarService.Information(representable);
}
logger.LogInformation("Response String :{contentString}", contentString);
return json.ToObject<Response<TResult>>(contentString);
return resp;
}
catch (Exception ex)
{
logger.LogError(ex, "请求时遇到问题");
return Response<TResult>.CreateFail($"{ex.Message}");
return Response<TResult>.CreateForException($"{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

@@ -8,10 +8,15 @@ namespace Snap.Hutao.Web.Response;
/// </summary>
public enum KnownReturnCode
{
/// <summary>
/// Url为 空
/// </summary>
UrlIsEmpty = -2000000001,
/// <summary>
/// 内部错误
/// </summary>
InternalFailure = int.MinValue,
InternalFailure = -2000000000,
/// <summary>
/// 已经签到过了

View File

@@ -1,8 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Response;
@@ -15,5 +15,6 @@ public class ListWrapper<T>
/// <summary>
/// 列表
/// </summary>
[JsonProperty("list")] public List<T>? List { get; set; }
[JsonPropertyName("list")]
public List<T>? List { get; set; }
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Response;
@@ -13,13 +13,13 @@ public class Response
/// <summary>
/// 返回代码
/// </summary>
[JsonProperty("retcode")]
[JsonPropertyName("retcode")]
public int ReturnCode { get; set; }
/// <summary>
/// 消息
/// </summary>
[JsonProperty("message")]
[JsonPropertyName("message")]
public string? Message { get; set; }
/// <summary>
@@ -37,7 +37,7 @@ public class Response
/// </summary>
/// <param name="message">消息</param>
/// <returns>响应</returns>
public static Response CreateFail(string message)
public static Response CreateForException(string message)
{
return new Response()
{

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Response;
@@ -14,7 +14,7 @@ public class Response<TData> : Response
/// <summary>
/// 数据
/// </summary>
[JsonProperty("data")]
[JsonPropertyName("data")]
public TData? Data { get; set; }
/// <summary>
@@ -22,7 +22,7 @@ public class Response<TData> : Response
/// </summary>
/// <param name="message">消息</param>
/// <returns>响应</returns>
public static new Response<TData> CreateFail(string message)
public static new Response<TData> CreateForException(string message)
{
return new Response<TData>()
{
@@ -30,4 +30,17 @@ public class Response<TData> : Response
Message = message,
};
}
/// <summary>
/// 构造一个空Url的响应
/// </summary>
/// <returns>响应</returns>
public static Response<TData> CreateForEmptyUrl()
{
return new Response<TData>()
{
ReturnCode = (int)KnownReturnCode.UrlIsEmpty,
Message = "请求的 Url 不应为空",
};
}
}