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.Extensions.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
using Snap.Hutao.Web.Request;
namespace Snap.Hutao; namespace Snap.Hutao;
@@ -40,29 +38,13 @@ public partial class App : Application
private static void InitializeDependencyInjection() private static void InitializeDependencyInjection()
{ {
// prepare DI
IServiceProvider services = new ServiceCollection() IServiceProvider services = new ServiceCollection()
.AddLogging(builder => builder.AddDebug()) .AddLogging(builder => builder.AddDebug())
.AddHttpClients()
// http json .AddDefaultJsonSerializerOptions()
.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)) .AddInjections(typeof(App))
.BuildServiceProvider(); .BuildServiceProvider();
Ioc.Default.ConfigureServices(services); 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; using System.Net.Http;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Core.Json;
/// <summary> /// <summary>
/// Http Json 处理 /// Http Json 处理

View File

@@ -1,10 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Newtonsoft.Json;
using System.IO; using System.IO;
using System.Text.Json;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Core.Json;
/// <summary> /// <summary>
/// Json操作 /// Json操作
@@ -12,20 +12,17 @@ namespace Snap.Hutao.Core;
[Injection(InjectAs.Transient)] [Injection(InjectAs.Transient)]
public class Json public class Json
{ {
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly ILogger logger; private readonly ILogger logger;
private readonly JsonSerializerSettings jsonSerializerSettings = new()
{
DateFormatString = "yyyy'-'MM'-'dd' 'HH':'mm':'ss.FFFFFFFK",
Formatting = Formatting.Indented,
};
/// <summary> /// <summary>
/// 初始化一个新的 Json操作 实例 /// 初始化一个新的 Json操作 实例
/// </summary> /// </summary>
/// <param name="jsonSerializerOptions">配置</param>
/// <param name="logger">日志器</param> /// <param name="logger">日志器</param>
public Json(ILogger<Json> logger) public Json(JsonSerializerOptions jsonSerializerOptions, ILogger<Json> logger)
{ {
this.jsonSerializerOptions = jsonSerializerOptions;
this.logger = logger; this.logger = logger;
} }
@@ -34,19 +31,20 @@ public class Json
/// </summary> /// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam> /// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="value">要反序列化的JSON</param> /// <param name="value">要反序列化的JSON</param>
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns> /// <returns>Json字符串中的反序列化对象, 如果反序列化失败会返回 <see langword="default"/></returns>
public T? ToObject<T>(string value) public T? ToObject<T>(string value)
{ {
try try
{ {
return JsonConvert.DeserializeObject<T>(value); T? result = JsonSerializer.Deserialize<T>(value);
return result;
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError("反序列化Json时遇到问题:{ex}", ex); logger.LogError("反序列化Json时遇到问题\n{ex}", ex);
} }
return default; return default(T);
} }
/// <summary> /// <summary>
@@ -69,7 +67,7 @@ public class Json
/// <returns>对象的JSON字符串表示形式</returns> /// <returns>对象的JSON字符串表示形式</returns>
public string Stringify(object? value) public string Stringify(object? value)
{ {
return JsonConvert.SerializeObject(value, jsonSerializerSettings); return JsonSerializer.Serialize(value, jsonSerializerOptions);
} }
/// <summary> /// <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. // Licensed under the MIT license.
using Microsoft; using Microsoft;
using Snap.Hutao.Model;
namespace Snap.Hutao.Core.Threading; namespace Snap.Hutao.Core.Threading;
@@ -13,6 +14,7 @@ namespace Snap.Hutao.Core.Threading;
public class Watcher : Observable public class Watcher : Observable
{ {
private readonly bool isReusable; private readonly bool isReusable;
private bool hasUsed; private bool hasUsed;
private bool isWorking; private bool isWorking;
private bool isCompleted; private bool isCompleted;
@@ -48,33 +50,33 @@ public class Watcher : Observable
/// <summary> /// <summary>
/// 对某个操作进行监视, /// 对某个操作进行监视,
/// 无法防止代码重入
/// </summary> /// </summary>
/// <returns>一个可释放的对象,用于在操作完成时自动提示监视器工作已经完成</returns> /// <returns>一个可释放的对象,用于在操作完成时自动提示监视器工作已经完成</returns>
/// <exception cref="InvalidOperationException">重用了一个不可重用的监视器</exception> /// <exception cref="InvalidOperationException">重用了一个不可重用的监视器</exception>
public IDisposable Watch() public IDisposable Watch()
{ {
Verify.Operation(!IsWorking, $"此 {nameof(Watcher)} 已经处于检查状态");
Verify.Operation(isReusable || !hasUsed, $"此 {nameof(Watcher)} 不允许多次使用"); Verify.Operation(isReusable || !hasUsed, $"此 {nameof(Watcher)} 不允许多次使用");
hasUsed = true; hasUsed = true;
IsWorking = 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() public void Dispose()
{ {
work.IsWorking = false; watcher.IsWorking = false;
work.IsCompleted = true; 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>
/// 主窗体 /// 主窗体
/// </summary> /// </summary>
[Injection(InjectAs.Transient)] [Injection(InjectAs.Singleton)]
public sealed partial class MainWindow : Window public sealed partial class MainWindow : Window
{ {
/// <summary> /// <summary>

View File

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

View File

@@ -61,13 +61,6 @@ public interface IInfoBarService
/// <param name="container">信息条的目标容器</param> /// <param name="container">信息条的目标容器</param>
void Initialize(StackPanel container); void Initialize(StackPanel container);
/// <summary>
/// 显示特定的信息条
/// </summary>
/// <param name="infoBar">信息条</param>
/// <param name="delay">关闭延迟</param>
void Show(InfoBar infoBar, int delay = 0);
/// <summary> /// <summary>
/// 显示成功信息 /// 显示成功信息
/// </summary> /// </summary>

View File

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

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.VisualStudio.Threading;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service; namespace Snap.Hutao.Service;
@@ -79,25 +78,25 @@ internal class InfoBarService : IInfoBarService
PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay); 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) 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() InfoBar infoBar = new()
{ {
@@ -107,7 +106,14 @@ internal class InfoBarService : IInfoBarService
IsOpen = true, 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) private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args)

View File

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

View File

@@ -13,7 +13,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling> <EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<NeutralLanguage>zh-CN</NeutralLanguage> <NeutralLanguage>zh-CN</NeutralLanguage>
<DefaultLanguage>zh-CN</DefaultLanguage> <DefaultLanguage>zh-cn</DefaultLanguage>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile> <GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled> <AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
@@ -48,14 +48,16 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="7.1.2" /> <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.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.Behaviors" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" 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.Analytics" Version="4.5.1" />
<PackageReference Include="Microsoft.AppCenter.Crashes" 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.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" 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" Version="17.1.46" />
@@ -63,10 +65,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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.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"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -83,9 +82,6 @@
<ItemGroup> <ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" /> <None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Model\" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\SettingsUI\SettingsUI.csproj" /> <ProjectReference Include="..\..\SettingsUI\SettingsUI.csproj" />
</ItemGroup> </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. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Converters;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace Snap.Hutao.View.Converter; namespace Snap.Hutao.View.Converter;

View File

@@ -10,26 +10,28 @@
<Grid> <Grid>
<NavigationView <NavigationView
x:Name="NavView" x:Name="NavView"
CompactPaneLength="48"
OpenPaneLength="172" OpenPaneLength="172"
CompactModeThresholdWidth="360" CompactModeThresholdWidth="128"
ExpandedModeThresholdWidth="720" 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> <NavigationView.MenuItems>
<NavigationViewItem
Content="活动" <NavigationViewItem Content="活动" helper:NavHelper.NavigateTo="page:AnnouncementPage">
helper:NavHelper.NavigateTo="page:AnnouncementPage">
<NavigationViewItem.Icon> <NavigationViewItem.Icon>
<FontIcon Glyph="&#xE7C4;"/> <FontIcon Glyph="&#xE7C4;"/>
</NavigationViewItem.Icon> </NavigationViewItem.Icon>
</NavigationViewItem> </NavigationViewItem>
</NavigationView.MenuItems> </NavigationView.MenuItems>
<Frame x:Name="ContentFrame"> <Frame x:Name="ContentFrame">
<Frame.ContentTransitions> <Frame.ContentTransitions>
<TransitionCollection> <NavigationThemeTransition>
<NavigationThemeTransition> <DrillInNavigationTransitionInfo/>
<DrillInNavigationTransitionInfo/> </NavigationThemeTransition>
</NavigationThemeTransition>
</TransitionCollection>
</Frame.ContentTransitions> </Frame.ContentTransitions>
</Frame> </Frame>
</NavigationView> </NavigationView>
@@ -61,9 +63,6 @@
TintColor="#34424d" TintColor="#34424d"
FallbackColor="#34424d"/> FallbackColor="#34424d"/>
</StackPanel.Resources> </StackPanel.Resources>
<StackPanel.OpacityTransition>
<ScalarTransition/>
</StackPanel.OpacityTransition>
<StackPanel.Transitions> <StackPanel.Transitions>
<TransitionCollection> <TransitionCollection>
<AddDeleteThemeTransition/> <AddDeleteThemeTransition/>

View File

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

View File

@@ -59,7 +59,7 @@
<DataTemplate> <DataTemplate>
<Border <Border
CornerRadius="{StaticResource CompatCornerRadius}" CornerRadius="{StaticResource CompatCornerRadius}"
Background="{StaticResource SystemControlPageBackgroundAltHighBrush}" Background="{ThemeResource SystemControlPageBackgroundAltHighBrush}"
cwui:UIElementExtensions.ClipToBounds="True"> cwui:UIElementExtensions.ClipToBounds="True">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -105,31 +105,20 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Visibility="{Binding ShouldShowTimeDescription,Converter={StaticResource BoolToVisibilityConverter}}"> Visibility="{Binding ShouldShowTimeDescription,Converter={StaticResource BoolToVisibilityConverter}}">
<Border.Background> <!--<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#00000000"/> <GradientStop Color="#00000000"/>
<GradientStop Offset="1" Color="#A0000000"/> <GradientStop Offset="1" Color="#A0000000"/>
</LinearGradientBrush> </LinearGradientBrush>
</Border.Background> </Border.Background>-->
<ProgressBar <ProgressBar
Height="1" MinHeight="2"
MinHeight="1"
Value="{Binding TimePercent,Mode=OneWay}" Value="{Binding TimePercent,Mode=OneWay}"
CornerRadius="0" CornerRadius="0"
Maximum="1" Maximum="1"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Background="Transparent"/> Background="Transparent"/>
</Border> </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> </Grid>
<!--General Description--> <!--General Description-->
<Border <Border
@@ -170,13 +159,29 @@
TextTrimming="WordEllipsis" TextTrimming="WordEllipsis"
Margin="4,6,0,0" Margin="4,6,0,0"
Opacity="0.6"/> 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> </StackPanel>
</Border> </Border>
</Grid> </Grid>

View File

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

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page; namespace Snap.Hutao.View.Page;
/// <summary> /// <summary>
@@ -13,6 +15,7 @@ public sealed partial class SettingPage : Microsoft.UI.Xaml.Controls.Page
/// </summary> /// </summary>
public SettingPage() public SettingPage()
{ {
DataContext = Ioc.Default.GetRequiredService<SettingViewModel>();
InitializeComponent(); 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. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Newtonsoft.Json; using Snap.Hutao.Core.Json.Converter;
using System.Text.Json.Serialization;
using System.Windows.Input; using System.Windows.Input;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement; namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
@@ -13,42 +14,6 @@ public class Announcement : AnnouncementContent
{ {
private double timePercent; 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>
/// 启动展示窗口的命令 /// 启动展示窗口的命令
/// </summary> /// </summary>
@@ -131,45 +96,83 @@ public class Announcement : AnnouncementContent
get => $"{StartTime:yyyy.MM.dd HH:mm} - {EndTime:yyyy.MM.dd HH:mm}"; 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>
/// 类型 /// 类型
/// </summary> /// </summary>
[JsonProperty("type")] [JsonPropertyName("type")]
public int Type { get; set; } public int Type { get; set; }
/// <summary> /// <summary>
/// 提醒 /// 提醒
/// </summary> /// </summary>
[JsonProperty("remind")] [JsonPropertyName("remind")]
public int Remind { get; set; } public int Remind { get; set; }
/// <summary> /// <summary>
/// 通知 /// 通知
/// </summary> /// </summary>
[JsonProperty("alert")] [JsonPropertyName("alert")]
public int Alert { get; set; } public int Alert { get; set; }
/// <summary> /// <summary>
/// 标签开始时间 /// 标签开始时间
/// </summary> /// </summary>
[JsonProperty("tag_start_time")] [JsonPropertyName("tag_start_time")]
public string? TagStartTime { get; set; } public string? TagStartTime { get; set; }
/// <summary> /// <summary>
/// 标签结束时间 /// 标签结束时间
/// </summary> /// </summary>
[JsonProperty("tag_end_time")] [JsonPropertyName("tag_end_time")]
public string? TagEndTime { get; set; } public string? TagEndTime { get; set; }
/// <summary> /// <summary>
/// 提醒版本 /// 提醒版本
/// </summary> /// </summary>
[JsonProperty("remind_ver")] [JsonPropertyName("remind_ver")]
public int RemindVer { get; set; } public int RemindVer { get; set; }
/// <summary> /// <summary>
/// 是否含有内容 /// 是否含有内容
/// </summary> /// </summary>
[JsonProperty("has_content")] [JsonPropertyName("has_content")]
public bool HasContent { get; set; } public bool HasContent { get; set; }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // 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 Snap.Hutao.Web.Response;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
@@ -16,6 +17,7 @@ public class Requester
{ {
private readonly HttpClient httpClient; private readonly HttpClient httpClient;
private readonly Json json; private readonly Json json;
private readonly IInfoBarService infoBarService;
private readonly ILogger<Requester> logger; private readonly ILogger<Requester> logger;
/// <summary> /// <summary>
@@ -23,11 +25,13 @@ public class Requester
/// </summary> /// </summary>
/// <param name="httpClient">Http 客户端</param> /// <param name="httpClient">Http 客户端</param>
/// <param name="json">Json 处理器</param> /// <param name="json">Json 处理器</param>
/// <param name="infoBarService">信息条服务</param>
/// <param name="logger">消息器</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.httpClient = httpClient;
this.json = json; this.json = json;
this.infoBarService = infoBarService;
this.logger = logger; this.logger = logger;
} }
@@ -37,7 +41,7 @@ public class Requester
public RequestOptions Headers { get; set; } = new RequestOptions(); public RequestOptions Headers { get; set; } = new RequestOptions();
/// <summary> /// <summary>
/// 内部使用的 <see cref="HttpClient"/> /// 内部使用的 <see cref="System.Net.Http.HttpClient"/>
/// </summary> /// </summary>
protected HttpClient HttpClient { get => httpClient; } protected HttpClient HttpClient { get => httpClient; }
@@ -50,32 +54,15 @@ public class Requester
/// <returns>响应</returns> /// <returns>响应</returns>
public async Task<Response<TResult>?> GetAsync<TResult>(string? url, CancellationToken cancellationToken = default) public async Task<Response<TResult>?> GetAsync<TResult>(string? url, CancellationToken cancellationToken = default)
{ {
logger.LogInformation("GET {urlbase}", url?.Split('?')[0]); if (url is null)
return url is null {
? null return Response<TResult>.CreateForEmptyUrl();
: await RequestAsync<TResult>( }
client => new RequestInfo(url, () => client.GetAsync(url, cancellationToken)),
cancellationToken)
.ConfigureAwait(false);
}
/// <summary> Task<HttpResponseMessage> GetMethod(HttpClient client, CancellationToken token) => client.GetAsync(url, token);
/// GET 操作
/// </summary> return await RequestAsync<TResult>(GetMethod, cancellationToken)
/// <typeparam name="TResult">返回的类类型</typeparam> .ConfigureAwait(false);
/// <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> /// <summary>
@@ -88,13 +75,17 @@ public class Requester
/// <returns>响应</returns> /// <returns>响应</returns>
public async Task<Response<TResult>?> PostAsync<TResult>(string? url, object data, CancellationToken cancellationToken = default) 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); string dataString = json.Stringify(data);
logger.LogInformation("POST {urlbase} with\n{dataString}", url?.Split('?')[0], dataString); HttpContent content = new StringContent(dataString);
return url is null
? null Task<HttpResponseMessage> PostMethod(HttpClient client, CancellationToken token) => client.PostAsync(url, content, token);
: await RequestAsync<TResult>(
client => new RequestInfo(url, () => client.PostAsync(url, new StringContent(dataString), cancellationToken)), return await RequestAsync<TResult>(PostMethod, cancellationToken)
cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@@ -109,13 +100,17 @@ public class Requester
/// <returns>响应</returns> /// <returns>响应</returns>
public async Task<Response<TResult>?> PostAsync<TResult>(string? url, object data, string contentType, CancellationToken cancellationToken = default) 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); string dataString = json.Stringify(data);
logger.LogInformation("POST {urlbase} with\n{dataString}", url?.Split('?')[0], dataString); HttpContent content = new StringContent(dataString, Encoding.UTF8, contentType);
return url is null
? null Task<HttpResponseMessage> PostMethod(HttpClient client, CancellationToken token) => client.PostAsync(url, content, token);
: await RequestAsync<TResult>(
client => new RequestInfo(url, () => client.PostAsync(url, new StringContent(dataString, Encoding.UTF8, contentType), cancellationToken)), return await RequestAsync<TResult>(PostMethod, cancellationToken)
cancellationToken)
.ConfigureAwait(false); .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(); PrepareHttpClient();
RequestInfo? info = requestFunc(HttpClient);
try try
{ {
HttpResponseMessage response = await info.RequestAsyncFunc.Invoke() HttpResponseMessage response = await requestFunc
.Invoke(HttpClient, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
string contentString = await response.Content.ReadAsStringAsync(cancellationToken) string contentString = await response.Content
.ReadAsStringAsync(cancellationToken)
.ConfigureAwait(false); .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); infoBarService.Information(representable);
info.Encoding.GetString(bytes);
} }
logger.LogInformation("Response String :{contentString}", contentString); return resp;
return json.ToObject<Response<TResult>>(contentString);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "请求时遇到问题"); logger.LogError(ex, "请求时遇到问题");
return Response<TResult>.CreateFail($"{ex.Message}"); return Response<TResult>.CreateForException($"{ex.Message}");
} }
finally finally
{ {
logger.LogInformation("Request Completed"); 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> /// </summary>
public enum KnownReturnCode public enum KnownReturnCode
{ {
/// <summary>
/// Url为 空
/// </summary>
UrlIsEmpty = -2000000001,
/// <summary> /// <summary>
/// 内部错误 /// 内部错误
/// </summary> /// </summary>
InternalFailure = int.MinValue, InternalFailure = -2000000000,
/// <summary> /// <summary>
/// 已经签到过了 /// 已经签到过了

View File

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

View File

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