use source generation to improve injection discover speed

This commit is contained in:
DismissedLight
2022-06-21 11:42:03 +08:00
parent 7a4777cf8f
commit 98473af32f
75 changed files with 1330 additions and 569 deletions

7
.gitignore vendored
View File

@@ -4,9 +4,12 @@
.vs/
src/SettingsUI/bin
src/SettingsUI/obj
src/Snap.Hutao/SettingsUI/bin
src/Snap.Hutao/SettingsUI/obj
src/Snap.Hutao/Snap.Hutao/bin/
src/Snap.Hutao/Snap.Hutao/obj/
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/

View File

@@ -94,6 +94,9 @@ dotnet_diagnostic.SA1636.severity = none
# SA1414: Tuple types in signatures should have element names
dotnet_diagnostic.SA1414.severity = none
# SA0001: XML comment analysis disabled
dotnet_diagnostic.SA0001.severity = none
[*.vb]
#### 命名样式 ####

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.0.3" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,98 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
/// <summary>
/// 注入代码生成器
/// 旨在使用源生成器提高注入效率
/// 防止在运行时动态查找注入类型
/// </summary>
[Generator]
public class InjectionGenerator : ISourceGenerator
{
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new InjectionSyntaxContextReceiver());
}
/// <inheritdoc/>
public void Execute(GeneratorExecutionContext context)
{
// retrieve the populated receiver
if (context.SyntaxContextReceiver is not InjectionSyntaxContextReceiver receiver)
{
return;
}
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append(@"// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class ServiceCollectionExtensions
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Snap.Hutao.SourceGeneration.DedendencyInjection.InjectionGenerator"","" 1.0.0.0"")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddInjections(this IServiceCollection services)
{");
sourceCodeBuilder.Append("\r\n");
FillWithInjectionServices(receiver, sourceCodeBuilder);
sourceCodeBuilder.Append(@" return services;
}
}");
sourceCodeBuilder.Append("\r\n");
context.AddSource("ServiceCollectionExtensions.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
private static void FillWithInjectionServices(InjectionSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
{
foreach (INamedTypeSymbol classSymbol in receiver.Classes.OrderByDescending(symbol => symbol.ToDisplayString()))
{
AttributeData injectionInfo = classSymbol
.GetAttributes()
.Single(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
{
case "Snap.Hutao.Core.DependencyInjection.InjectAs.Singleton":
sourceCodeBuilder.Append(@" services.AddSingleton(");
break;
case "Snap.Hutao.Core.DependencyInjection.InjectAs.Transient":
sourceCodeBuilder.Append(@" services.AddTransient(");
break;
default:
throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]。");
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
sourceCodeBuilder.Append($"{interfaceType.ToCSharpString()}, ");
}
sourceCodeBuilder.Append($"typeof({classSymbol.ToDisplayString()}));\r\n");
}
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
/// <summary>
/// Created on demand before each generation pass
/// </summary>
public class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
{
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="stylecop.json" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": "DGP Studio",
"copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license.",
"xmlHeader": false,
"variables": {
"licenseName": "MIT"
}
},
"orderingRules": {
"elementOrder": [
"kind",
"accessibility",
"constant",
"static",
"readonly"
],
"usingDirectivesPlacement": "outsideNamespace"
}
}
}

View File

@@ -10,7 +10,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SettingsUI", "..\SettingsUI\SettingsUI.csproj", "{FC2E96B6-775E-465C-82FB-9931826C8049}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SettingsUI", "SettingsUI\SettingsUI.csproj", "{DCA5678C-896E-49FB-97A7-5A504A5CFF17}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -48,22 +50,38 @@ Global
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86
{FC2E96B6-775E-465C-82FB-9931826C8049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Debug|arm64.ActiveCfg = Debug|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Debug|arm64.Build.0 = Debug|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Debug|x64.ActiveCfg = Debug|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Debug|x64.Build.0 = Debug|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Debug|x86.ActiveCfg = Debug|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Debug|x86.Build.0 = Debug|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Release|Any CPU.Build.0 = Release|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Release|arm64.ActiveCfg = Release|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Release|arm64.Build.0 = Release|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Release|x64.ActiveCfg = Release|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Release|x64.Build.0 = Release|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Release|x86.ActiveCfg = Release|Any CPU
{FC2E96B6-775E-465C-82FB-9931826C8049}.Release|x86.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|arm64.ActiveCfg = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|arm64.Build.0 = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|x64.ActiveCfg = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|x64.Build.0 = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|x86.ActiveCfg = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|x86.Build.0 = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|Any CPU.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|arm64.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|arm64.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x64.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x64.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x86.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x86.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao;
/// </summary>
public partial class App : Application
{
private Window? mainWindow;
private static Window? window;
/// <summary>
/// Initializes the singleton application object.
@@ -25,6 +25,11 @@ public partial class App : Application
InitializeDependencyInjection();
}
/// <summary>
/// 当前窗口
/// </summary>
public static Window? Window { get => window; set => window = value; }
/// <summary>
/// Invoked when the application is launched normally by the end user.
/// Other entry points will be used such as when the application is launched to open a specific file.
@@ -32,8 +37,8 @@ public partial class App : Application
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
mainWindow.Activate();
Window = Ioc.Default.GetRequiredService<MainWindow>();
Window.Activate();
}
private static void InitializeDependencyInjection()
@@ -45,7 +50,7 @@ public partial class App : Application
.AddDatebase()
.AddHttpClients()
.AddDefaultJsonSerializerOptions()
.AddInjections(typeof(App))
.AddInjections()
.BuildServiceProvider();
Ioc.Default.ConfigureServices(services);

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Context.Database;
/// <summary>
/// 应用程序数据库上下文
/// </summary>
internal class AppDbContext : DbContext
public class AppDbContext : DbContext
{
/// <summary>
/// 构造一个新的应用程序数据库上下文
@@ -25,6 +25,11 @@ internal class AppDbContext : DbContext
/// </summary>
public DbSet<SettingEntry> Settings { get; set; } = default!;
/// <summary>
/// 用户表
/// </summary>
public DbSet<User> Users { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
@@ -34,4 +39,4 @@ internal class AppDbContext : DbContext
{
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Design;
using Snap.Hutao.Context.FileSystem;
namespace Snap.Hutao.Context.Database;
public class AppDbContextDesignTimeFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args)
{
MyDocumentContext myDocument = new(new());
myDocument.EnsureDirectory();
string dbFile = myDocument.Locate("Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
return AppDbContext.CreateFrom(sqlConnectionString);
}
}

View File

@@ -1,4 +1,7 @@
using CommunityToolkit.WinUI.UI;
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI;
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.UI.Composition;
using System.Numerics;

View File

@@ -1,4 +1,7 @@
using CommunityToolkit.WinUI.UI;
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI;
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.UI.Composition;
using System.Numerics;

View File

@@ -4,7 +4,7 @@
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core.Converting;
namespace Snap.Hutao.Core.Convertion;
/// <summary>
/// 支持Md5转换

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.Converting;
using Snap.Hutao.Core.Convertion;
using Snap.Hutao.Extension;
using Windows.ApplicationModel;

View File

@@ -32,10 +32,10 @@ public class InjectionAttribute : Attribute
/// <summary>
/// 注入类型
/// </summary>
public InjectAs InjectAs { get; set; }
public InjectAs InjectAs { get; }
/// <summary>
/// 该类实现的接口类型
/// </summary>
public Type? InterfaceType { get; set; }
public Type? InterfaceType { get; }
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Extension;
namespace Snap.Hutao.Core.DependencyInjection;
@@ -10,50 +9,12 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// 服务管理器
/// 依赖注入的核心管理类
/// </summary>
internal static class ServiceCollectionExtensions
internal static partial class ServiceCollectionExtensions
{
/// <summary>
/// 向容器注册服务, 调用 <see cref="Register(IServiceCollection, Type)"/>
/// 向容器注册服务
/// </summary>
/// <param name="services">容器</param>
/// <param name="entryType">入口类型,该类型所在的程序集均会被扫描</param>
/// <returns>可继续操作的服务集合</returns>
public static IServiceCollection AddInjections(this IServiceCollection services, Type entryType)
{
entryType.Assembly.ForEachType(type => Register(services, type));
return services;
}
/// <summary>
/// 向容器注册类型
/// </summary>
/// <param name="services">容器</param>
/// <param name="type">待检测的类型</param>
/// <returns>可继续操作的服务集合</returns>
public static IServiceCollection Register(this IServiceCollection services, Type type)
{
if (type.TryGetAttribute(out InjectionAttribute? attr))
{
if (attr.InterfaceType is not null)
{
return attr.InjectAs switch
{
InjectAs.Singleton => services.AddSingleton(attr.InterfaceType, type),
InjectAs.Transient => services.AddTransient(attr.InterfaceType, type),
_ => Must.NeverHappen<IServiceCollection>(),
};
}
else
{
return attr.InjectAs switch
{
InjectAs.Singleton => services.AddSingleton(type),
InjectAs.Transient => services.AddTransient(type),
_ => throw Must.NeverHappen(),
};
}
}
return services;
}
public static partial IServiceCollection AddInjections(this IServiceCollection services);
}

View File

@@ -1,131 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using System.Text.Json;
namespace Snap.Hutao.Core.Json;
/// <summary>
/// Json操作
/// </summary>
[Injection(InjectAs.Transient)]
public class Json
{
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly ILogger logger;
/// <summary>
/// 初始化一个新的 Json操作 实例
/// </summary>
/// <param name="jsonSerializerOptions">配置</param>
/// <param name="logger">日志器</param>
public Json(JsonSerializerOptions jsonSerializerOptions, ILogger<Json> logger)
{
this.jsonSerializerOptions = jsonSerializerOptions;
this.logger = logger;
}
/// <summary>
/// 将JSON反序列化为指定的.NET类型
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="value">要反序列化的JSON</param>
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会返回 <see langword="default"/></returns>
public T? ToObject<T>(string value)
{
try
{
T? result = JsonSerializer.Deserialize<T>(value);
return result;
}
catch (Exception ex)
{
logger.LogError("反序列化Json时遇到问题\n{ex}", ex);
}
return default(T);
}
/// <summary>
/// 将JSON反序列化为指定的.NET类型
/// 若为null则返回一个新建的实例
/// </summary>
/// <typeparam name="T">指定的类型</typeparam>
/// <param name="value">字符串</param>
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns>
public T ToObjectOrNew<T>(string value)
where T : new()
{
return ToObject<T>(value) ?? new T();
}
/// <summary>
/// 将指定的对象序列化为JSON字符串
/// </summary>
/// <param name="value">要序列化的对象</param>
/// <returns>对象的JSON字符串表示形式</returns>
public string Stringify(object? value)
{
return JsonSerializer.Serialize(value, jsonSerializerOptions);
}
/// <summary>
/// 使用 <see cref="FileMode.Open"/>, <see cref="FileAccess.Read"/> 和 <see cref="FileShare.Read"/> 从文件中读取后转化为实体类
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="fileName">存放JSON数据的文件路径</param>
/// <returns>JSON字符串中的反序列化对象, 如果反序列化失败则抛出异常,若文件不存在则返回 <see langword="null"/></returns>
public T? FromFile<T>(string fileName)
{
if (File.Exists(fileName))
{
// FileShare.Read is important to read some file
using (StreamReader sr = new(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)))
{
return ToObject<T>(sr.ReadToEnd());
}
}
else
{
return default;
}
}
/// <summary>
/// 使用 <see cref="FileMode.Open"/>, <see cref="FileAccess.Read"/> 和 <see cref="FileShare.Read"/> 从文件中读取后转化为实体类
/// 若为null则返回一个新建的实例
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="fileName">存放JSON数据的文件路径</param>
/// <returns>JSON字符串中的反序列化对象</returns>
public T FromFileOrNew<T>(string fileName)
where T : new()
{
return FromFile<T>(fileName) ?? new T();
}
/// <summary>
/// 从文件中读取后转化为实体类
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="file">存放JSON数据的文件</param>
/// <returns>JSON字符串中的反序列化对象</returns>
public T? FromFile<T>(FileInfo file)
{
using (StreamReader sr = file.OpenText())
{
return ToObject<T>(sr.ReadToEnd());
}
}
/// <summary>
/// 将对象保存到文件
/// </summary>
/// <param name="fileName">文件名称</param>
/// <param name="value">对象</param>
public void ToFile(string fileName, object? value)
{
File.WriteAllText(fileName, Stringify(value));
}
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 用于包装异步操作的结果
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <typeparam name="TValue"></typeparam>
public record Result<TResult, TValue>
where TResult : notnull
where TValue : notnull
{
/// <summary>
/// 构造一个新的结果
/// </summary>
/// <param name="isOk">是否成功</param>
/// <param name="value">值</param>
public Result(TResult isOk, TValue value)
{
IsOk = isOk;
Value = value;
}
/// <summary>
/// 是否成功
/// </summary>
public TResult IsOk { get; }
/// <summary>
/// 值
/// </summary>
public TValue Value { get; }
/// <summary>
/// 用于元组析构
/// </summary>
/// <param name="isOk">是否成功</param>
/// <param name="value">值</param>
public void Deconstruct(out TResult isOk, out TValue value)
{
isOk = IsOk;
value = Value;
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 构造复杂的结果
/// </summary>
public static class Results
{
/// <summary>
/// 根据条件构造结果
/// </summary>
/// <typeparam name="T">结果的类型</typeparam>
/// <param name="condition">条件</param>
/// <param name="trueValue">条件符合时的值</param>
/// <param name="falseValue">条件不符合时的值</param>
/// <returns>结果</returns>
public static Result<bool, T> Condition<T>(bool condition, T trueValue, T falseValue)
where T : notnull
{
return new(condition, condition ? trueValue : falseValue);
}
}

View File

@@ -77,4 +77,4 @@ public class Watcher : Observable
watcher.IsCompleted = true;
}
}
}
}

View File

@@ -11,17 +11,6 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// 将二维可枚举对象一维化
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>扁平的对象</returns>
public static IEnumerable<TSource> Flatten<TSource>(this IEnumerable<IEnumerable<TSource>> source)
{
return source.SelectMany(x => x);
}
/// <summary>
/// 计数
/// </summary>
@@ -66,18 +55,75 @@ public static class EnumerableExtensions
return source ?? new();
}
/// <summary>
/// 寻找枚举中唯一的值,找不到时
/// 回退到首个或默认值
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <param name="predicate">谓语</param>
/// <returns>目标项</returns>
public static TSource? FirstOrFirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
}
/// <summary>
/// 将二维可枚举对象一维化
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>扁平的对象</returns>
public static IEnumerable<TSource> Flatten<TSource>(this IEnumerable<IEnumerable<TSource>> source)
{
return source.SelectMany(x => x);
}
/// <summary>
/// 对集合中的每个物品执行指定的操作
/// </summary>
/// <typeparam name="TSource">集合类型</typeparam>
/// <param name="source">集合</param>
/// <param name="action">指定的操作</param>
public static void ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> action)
/// <returns>修改后的集合</returns>
public static IEnumerable<TSource> ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> action)
{
foreach (TSource item in source)
{
action(item);
}
return source;
}
/// <summary>
/// 对集合中的每个物品执行指定的操作
/// </summary>
/// <typeparam name="TSource">集合类型</typeparam>
/// <param name="source">集合</param>
/// <param name="func">指定的操作</param>
/// <returns>修改后的集合</returns>
public static async Task<IEnumerable<TSource>> ForEachAsync<TSource>(this IEnumerable<TSource> source, Func<TSource, Task> func)
{
foreach (TSource item in source)
{
await func(item);
}
return source;
}
/// <summary>
/// 寻找枚举中唯一的值,找不到时
/// 回退到首个或默认值
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <param name="predicate">谓语</param>
/// <returns>目标项</returns>
public static TSource? SingleOrFirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
return source.SingleOrDefault(predicate) ?? source.FirstOrDefault();
}
/// <summary>

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Extension;
/// <summary>

View File

@@ -12,3 +12,4 @@ global using System.ComponentModel;
global using System.Diagnostics.CodeAnalysis;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Windows.Input;

View File

@@ -5,8 +5,8 @@ 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.Setting;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -47,44 +47,20 @@ internal static class IocConfiguration
string dbFile = myDocument.Locate("Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
if (ShouldMigrate(myDocument, dbFile))
// temporarily create a context
using (AppDbContext context = AppDbContext.CreateFrom(sqlConnectionString))
{
// temporarily create a context
using (AppDbContext context = AppDbContext.CreateFrom(sqlConnectionString))
Debug.WriteLine("Migrate started");
if (context.Database.GetPendingMigrations().Any())
{
context.Database.Migrate();
}
Debug.WriteLine("Migrate completed");
}
LocalSetting.Set(SettingKeys.LastAppVersion, CoreEnvironment.Version.ToString());
// LocalSetting.Set(SettingKeys.LastAppVersion, CoreEnvironment.Version.ToString());
return services
.AddDbContextPool<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
}
private static bool ShouldMigrate(MyDocumentContext myDocument, string dbFile)
{
bool shouldMigrate = false;
// 数据库文件存在
if (myDocument.FileExists(dbFile))
{
string? versionString = LocalSetting.Get<string>(SettingKeys.LastAppVersion);
// 版本更新后
if (Version.TryParse(versionString, out Version? lastVersion))
{
if (lastVersion < CoreEnvironment.Version)
{
shouldMigrate = true;
}
}
}
else
{
shouldMigrate = true;
}
return shouldMigrate;
}
}

View File

@@ -8,9 +8,6 @@
mc:Ignorable="d">
<Grid>
<Grid.Background>
<ImageBrush ImageSource="https://i.loli.net/2020/07/07/2wSuTsFci3ZKNMl.jpg"/>
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition Height="48.8"/>
<RowDefinition/>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Context.Database;
namespace Snap.Hutao;
@@ -19,5 +20,13 @@ public sealed partial class MainWindow : Window
InitializeComponent();
ExtendsContentIntoTitleBar = true;
SetTitleBar(TitleBarView.DragableArea);
Closed += MainWindowClosed;
}
}
private void MainWindowClosed(object sender, WindowEventArgs args)
{
// save datebase
Ioc.Default.GetRequiredService<AppDbContext>().SaveChanges();
}
}

View File

@@ -0,0 +1,54 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20220618110357_SettingAndUser")]
partial class SettingAndUser
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.6");
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
public partial class SettingAndUser : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "settings",
columns: table => new
{
Key = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_settings", x => x.Key);
});
migrationBuilder.CreateTable(
name: "users",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
IsSelected = table.Column<bool>(type: "INTEGER", nullable: false),
Cookie = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_users", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "settings");
migrationBuilder.DropTable(
name: "users");
}
}
}

View File

@@ -0,0 +1,52 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.6");
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,8 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
namespace Snap.Hutao.Model.Entity;
@@ -10,8 +15,26 @@ namespace Snap.Hutao.Model.Entity;
/// 用户
/// </summary>
[Table("users")]
public class User
public class User : Observable
{
/// <summary>
/// 无用户
/// </summary>
public static readonly User None = new();
private UserGameRole? selectedUserGameRole;
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 是否被选中
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// 用户的Cookie
/// </summary>
@@ -21,5 +44,92 @@ public class User
/// 用户信息
/// </summary>
[NotMapped]
public UserInfo? UserInfo { get; set; }
public UserInfo? UserInfo { get; private set; }
/// <summary>
/// 用户信息
/// </summary>
[NotMapped]
public List<UserGameRole>? UserGameRoles { get; private set; }
/// <summary>
/// 用户信息
/// </summary>
[NotMapped]
public UserGameRole? SelectedUserGameRole
{
get => selectedUserGameRole;
private set => Set(ref selectedUserGameRole, value);
}
/// <summary>
/// 判断用户是否为空用户
/// </summary>
/// <param name="user">待检测的用户</param>
/// <returns>是否为空用户</returns>
public static bool IsNone([NotNullWhen(false)] User? user)
{
return ReferenceEquals(NoneIfNullOrNoCookie(user), None);
}
/// <summary>
/// 设置用户的选中状态
/// 同时更新用户选择的角色信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="isSelected">是否选中</param>
public static void SetSelectionState(User user, bool isSelected)
{
Verify.Operation(!IsNone(user), "尝试设置一个空的用户");
user!.IsSelected = isSelected;
if (isSelected)
{
user.SelectedUserGameRole ??= user.UserGameRoles!.FirstOrFirstOrDefault(role => role.IsChosen);
}
}
/// <summary>
/// 初始化此用户
/// </summary>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>用户是否初始化完成若Cookie失效会返回 <see langword="false"/> </returns>
internal async Task<bool> InitializeAsync(UserClient userClient, UserGameRoleClient userGameRoleClient, CancellationToken token = default)
{
if (IsNone(this))
{
return false;
}
UserInfo = await userClient
.GetUserFullInfoAsync(this, token)
.ConfigureAwait(false);
UserGameRoles = await userGameRoleClient
.GetUserGameRolesAsync(this, token)
.ConfigureAwait(false);
SelectedUserGameRole = UserGameRoles.FirstOrDefault(role => role.IsChosen) ?? UserGameRoles.FirstOrDefault();
return UserInfo != null && UserGameRoles != null;
}
/// <summary>
/// 尝试尽可能转换为 <see cref="None"/>
/// </summary>
/// <param name="user">用户</param>
/// <returns>转换后的用户</returns>
private static User NoneIfNullOrNoCookie(User? user)
{
if (user is null || user.Cookie == null)
{
return None;
}
else
{
return user;
}
}
}

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.0.2.0" />
Version="1.0.3.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using System.Windows.Input;
namespace Snap.Hutao.Service.Abstraction;

View File

@@ -2,8 +2,8 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Abstraction;
@@ -15,12 +15,34 @@ public interface IUserService
/// <summary>
/// 获取当前用户信息
/// </summary>
User Current { get; }
User? CurrentUser { get; set; }
/// <summary>
/// 获取用户信息枚举
/// 异步获取用户信息枚举
/// 每个用户信息都应准备完成
/// 此操作不能取消
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>准备完成的用户信息枚举</returns>
Task<IEnumerable<User>> GetInitializedUsersAsync();
Task<ObservableCollection<User>> GetInitializedUsersAsync();
/// <summary>
/// 异步添加用户
/// </summary>
/// <param name="user">待添加的用户</param>
/// <returns>用户初始化是否成功</returns>
Task<bool> TryAddUserAsync(User user);
/// <summary>
/// 异步移除用户
/// </summary>
/// <param name="user">待移除的用户</param>
void RemoveUser(User user);
/// <summary>
/// 将cookie的字符串形式转换为字典
/// </summary>
/// <param name="cookie">cookie的字符串形式</param>
/// <returns>包含cookie信息的字典</returns>
IDictionary<string, string> ParseCookie(string cookie);
}

View File

@@ -7,7 +7,6 @@ using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Input;
namespace Snap.Hutao.Service;

View File

@@ -1,20 +1,116 @@
using Snap.Hutao.Model.Entity;
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using System;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Service;
/// <summary>
/// 用户服务
/// </summary>
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Transient, typeof(IUserService))]
internal class UserService : IUserService
{
private readonly AppDbContext appDbContext;
private readonly UserClient userClient;
private readonly UserGameRoleClient userGameRoleClient;
private User? currentUser;
private ObservableCollection<User>? cachedUser = null;
public User Current { get => throw new NotImplementedException(); }
}
/// <summary>
/// 构造一个新的用户服务
/// </summary>
/// <param name="appDbContext">应用程序数据库上下文</param>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
public UserService(AppDbContext appDbContext, UserClient userClient, UserGameRoleClient userGameRoleClient)
{
this.appDbContext = appDbContext;
this.userClient = userClient;
this.userGameRoleClient = userGameRoleClient;
}
/// <inheritdoc/>
public User? CurrentUser
{
get => currentUser;
set
{
if (!User.IsNone(currentUser))
{
User.SetSelectionState(currentUser, false);
appDbContext.Users.Update(currentUser);
}
if (!User.IsNone(value))
{
currentUser = value;
User.SetSelectionState(currentUser, true);
appDbContext.Users.Update(currentUser);
}
}
}
/// <inheritdoc/>
public async Task<bool> TryAddUserAsync(User user)
{
if (await user.InitializeAsync(userClient, userGameRoleClient))
{
appDbContext.Users.Add(user);
return true;
}
return false;
}
/// <inheritdoc/>
public void RemoveUser(User user)
{
appDbContext.Users.Remove(user);
}
/// <inheritdoc/>
public async Task<ObservableCollection<User>> GetInitializedUsersAsync()
{
if (cachedUser == null)
{
appDbContext.Users.Load();
cachedUser = appDbContext.Users.Local.ToObservableCollection();
foreach (User user in cachedUser)
{
await user.InitializeAsync(userClient, userGameRoleClient);
}
CurrentUser = await appDbContext.Users.SingleOrDefaultAsync(user => user.IsSelected);
}
return cachedUser;
}
/// <inheritdoc/>
public IDictionary<string, string> ParseCookie(string cookie)
{
Dictionary<string, string> cookieDictionary = new();
string[] values = cookie.TrimEnd(';').Split(';');
foreach (string[] parts in values.Select(c => c.Split(new[] { '=' }, 2)))
{
string cookieName = parts[0].Trim();
string cookieValue = parts.Length == 1 ? string.Empty : parts[1].Trim();
cookieDictionary[cookieName] = cookieValue;
}
return cookieDictionary;
}
}

View File

@@ -26,8 +26,10 @@
<StartupObject>Snap.Hutao.Program</StartupObject>
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Remove="stylecop.json" />
<None Remove="View\Dialog\UserDialog.xaml" />
<None Remove="View\MainView.xaml" />
<None Remove="View\Page\AnnouncementContentPage.xaml" />
<None Remove="View\Page\AnnouncementPage.xaml" />
@@ -35,6 +37,7 @@
<None Remove="View\TitleView.xaml" />
<None Remove="View\UserView.xaml" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>
@@ -57,6 +60,7 @@
<PackageReference Include="Microsoft.AppCenter.Analytics" Version="4.5.1" />
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.5.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
@@ -82,9 +86,6 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SettingsUI\SettingsUI.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="View\UserView.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -125,4 +126,13 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SettingsUI\SettingsUI.csproj" />
<ProjectReference Include="..\Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\UserDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.UserDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao.View.Dialog"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="using:SettingsUI.Controls"
mc:Ignorable="d"
IsPrimaryButtonEnabled="False"
Title="设置米游社Cookie"
DefaultButton="Primary"
PrimaryButtonText="请输入Cookie"
SecondaryButtonText="取消"
Style="{StaticResource DefaultContentDialogStyle}">
<StackPanel>
<TextBox
Margin="0,0,0,8"
x:Name="InputText"
TextChanged="InputTextChanged"
PlaceholderText="在此处输入"
VerticalAlignment="Top"/>
<settings:Setting
Margin="0,8,0,0"
Icon="&#xEB41;"
Header="手动获取"
Description="进入我们的文档页面并按指示操作"
HorizontalAlignment="Stretch">
<HyperlinkButton
Margin="12,0,0,0"
Padding="4"
Content="立即前往"
NavigateUri="https://www.snapgenshin.com/documents/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/>
</settings:Setting>
</StackPanel>
</ContentDialog>

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Threading;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// 添加用户对话框
/// </summary>
public sealed partial class UserDialog : ContentDialog
{
/// <summary>
/// 构造一个新的添加用户对话框
/// </summary>
public UserDialog()
{
InitializeComponent();
XamlRoot = App.Window!.Content.XamlRoot;
}
/// <summary>
/// 获取输入的Cookie
/// </summary>
/// <returns>输入的结果</returns>
public async Task<Result<bool, string>> GetInputCookieAsync()
{
ContentDialogResult result = await ShowAsync();
string cookie = InputText.Text;
return new(result != ContentDialogResult.Secondary, cookie);
}
private void InputTextChanged(object sender, TextChangedEventArgs e)
{
string text = InputText.Text;
bool inputEmpty = string.IsNullOrEmpty(text);
(PrimaryButtonText, IsPrimaryButtonEnabled) = inputEmpty switch
{
true => ("请输入Cookie", false),
false => ("确认", true),
};
}
}

View File

@@ -20,20 +20,20 @@
IsPaneOpen="True"
IsBackEnabled="{Binding ElementName=ContentFrame,Path=CanGoBack}">
<NavigationView.PaneCustomContent>
<view:UserView IsExpanded="{Binding ElementName=NavView,Path=IsPaneOpen}"/>
</NavigationView.PaneCustomContent>
<NavigationView.MenuItems>
<NavigationViewItem Content="活动" helper:NavHelper.NavigateTo="page:AnnouncementPage">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xE7C4;"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<NavigationView.PaneFooter>
<view:UserView IsExpanded="{Binding ElementName=NavView,Path=IsPaneOpen}"/>
</NavigationView.PaneFooter>
<Frame x:Name="ContentFrame">
<Frame.ContentTransitions>
<TransitionCollection>

View File

@@ -31,150 +31,149 @@
<ScrollViewer
Padding="0,0,4,0"
Visibility="{Binding OpeningUI.IsWorking,Converter={StaticResource BoolToVisibilityRevertConverter}}">
<StackPanel>
<ItemsControl
HorizontalAlignment="Stretch"
ItemsSource="{Binding Announcement.List}"
Padding="0"
Margin="12,12,0,-12">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock
Text="{Binding TypeLabel}"
Margin="0,0,0,12"
Style="{StaticResource TitleTextBlockStyle}"/>
<cwucont:AdaptiveGridView
cwua:ItemsReorderAnimation.Duration="0:0:0.06"
SelectionMode="None"
DesiredWidth="320"
HorizontalAlignment="Stretch"
ItemsSource="{Binding List}"
Margin="0,0,0,0">
<cwucont:AdaptiveGridView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
</cwucont:AdaptiveGridView.ItemContainerStyle>
<cwucont:AdaptiveGridView.ItemTemplate>
<DataTemplate>
<Border
CornerRadius="{StaticResource CompatCornerRadius}"
Background="{ThemeResource SystemControlPageBackgroundAltHighBrush}"
cwu:UIElementExtensions.ClipToBounds="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<!--Image Layer-->
<ItemsControl
HorizontalAlignment="Stretch"
ItemsSource="{Binding Announcement.List}"
Padding="0"
Margin="12,12,0,-12">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock
Text="{Binding TypeLabel}"
Margin="0,0,0,12"
Style="{StaticResource TitleTextBlockStyle}"/>
<cwucont:AdaptiveGridView
cwua:ItemsReorderAnimation.Duration="0:0:0.06"
SelectionMode="None"
DesiredWidth="320"
HorizontalAlignment="Stretch"
ItemsSource="{Binding List}"
Margin="0,0,2,0">
<cwucont:AdaptiveGridView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
</cwucont:AdaptiveGridView.ItemContainerStyle>
<cwucont:AdaptiveGridView.ItemTemplate>
<DataTemplate>
<Border
CornerRadius="{StaticResource CompatCornerRadius}"
Background="{ThemeResource SystemControlPageBackgroundAltHighBrush}"
cwu:UIElementExtensions.ClipToBounds="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<!--Image Layer-->
<Border
cwu:UIElementExtensions.ClipToBounds="True">
<Border
cwu:UIElementExtensions.ClipToBounds="True">
<Border
VerticalAlignment="Top"
cwu:VisualExtensions.NormalizedCenterPoint="0.5">
<mxi:Interaction.Behaviors>
<shcb:AutoHeightBehavior TargetWidth="1080" TargetHeight="390"/>
</mxi:Interaction.Behaviors>
<Border.Background>
<ImageBrush
ImageSource="{Binding Banner}"
Stretch="UniformToFill"/>
</Border.Background>
<cwua:Explicit.Animations>
<cwua:AnimationSet x:Name="ImageZoomInAnimation">
<shca:ImageZoomInAnimation/>
</cwua:AnimationSet>
<cwua:AnimationSet x:Name="ImageZoomOutAnimation">
<shca:ImageZoomOutAnimation/>
</cwua:AnimationSet>
</cwua:Explicit.Animations>
</Border>
CompositeMode="SourceOver"
VerticalAlignment="Top"
cwu:VisualExtensions.NormalizedCenterPoint="0.5">
<mxi:Interaction.Behaviors>
<shcb:AutoHeightBehavior TargetWidth="1080" TargetHeight="390"/>
</mxi:Interaction.Behaviors>
<Border.Background>
<ImageBrush
ImageSource="{Binding Banner}"
Stretch="UniformToFill"/>
</Border.Background>
<cwua:Explicit.Animations>
<cwua:AnimationSet x:Name="ImageZoomInAnimation">
<shca:ImageZoomInAnimation/>
</cwua:AnimationSet>
<cwua:AnimationSet x:Name="ImageZoomOutAnimation">
<shca:ImageZoomOutAnimation/>
</cwua:AnimationSet>
</cwua:Explicit.Animations>
</Border>
<!--Time Description-->
<Grid Grid.Row="0">
<Border
Height="24"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Visibility="{Binding ShouldShowTimeDescription,Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressBar
MinHeight="2"
Value="{Binding TimePercent,Mode=OneWay}"
CornerRadius="0"
Maximum="1"
VerticalAlignment="Bottom"
Background="Transparent"/>
</Border>
</Grid>
<!--General Description-->
</Border>
<!--Time Description-->
<Grid Grid.Row="0">
<Border
Grid.Row="1"
CornerRadius="{StaticResource CompatCornerRadiusBottom}">
<StackPanel Margin="4" VerticalAlignment="Bottom">
<TextBlock
Margin="4,6,0,0"
HorizontalAlignment="Stretch"
Text="{Binding Subtitle}"
Style="{StaticResource SubtitleTextBlockStyle}"
TextWrapping="NoWrap"
TextTrimming="WordEllipsis"/>
<TextBlock
Text="{Binding Title}"
Style="{StaticResource BodyTextBlockStyle}"
TextWrapping="NoWrap"
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>
</StackPanel>
Height="24"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Visibility="{Binding ShouldShowTimeDescription,Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressBar
MinHeight="2"
Value="{Binding TimePercent,Mode=OneWay}"
CornerRadius="0"
Maximum="1"
VerticalAlignment="Bottom"
Background="Transparent"/>
</Border>
</Grid>
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Tapped">
<mxic:InvokeCommandAction
Command="{Binding OpenAnnouncementUICommand}"
CommandParameter="{Binding Content}"/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerEntered">
<cwub:StartAnimationAction Animation="{Binding ElementName=ImageZoomInAnimation}" />
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerExited">
<cwub:StartAnimationAction Animation="{Binding ElementName=ImageZoomOutAnimation}" />
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</Border>
</DataTemplate>
</cwucont:AdaptiveGridView.ItemTemplate>
</cwucont:AdaptiveGridView>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!--General Description-->
<Border
Grid.Row="1"
CornerRadius="{StaticResource CompatCornerRadiusBottom}">
<StackPanel Margin="4" VerticalAlignment="Bottom">
<TextBlock
Margin="4,6,0,0"
HorizontalAlignment="Stretch"
Text="{Binding Subtitle}"
Style="{StaticResource SubtitleTextBlockStyle}"
TextWrapping="NoWrap"
TextTrimming="WordEllipsis"/>
<TextBlock
Text="{Binding Title}"
Style="{StaticResource BodyTextBlockStyle}"
TextWrapping="NoWrap"
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>
</StackPanel>
</Border>
</Grid>
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Tapped">
<mxic:InvokeCommandAction
Command="{Binding OpenAnnouncementUICommand}"
CommandParameter="{Binding Content}"/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerEntered">
<cwub:StartAnimationAction Animation="{Binding ElementName=ImageZoomInAnimation}" />
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerExited">
<cwub:StartAnimationAction Animation="{Binding ElementName=ImageZoomOutAnimation}" />
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</Border>
</DataTemplate>
</cwucont:AdaptiveGridView.ItemTemplate>
</cwucont:AdaptiveGridView>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</shcc:CancellablePage>

View File

@@ -16,7 +16,7 @@ public sealed partial class TitleView : UserControl
/// </summary>
public TitleView()
{
this.InitializeComponent();
InitializeComponent();
}
/// <summary>

View File

@@ -5,7 +5,14 @@
xmlns:local="using:Snap.Hutao.View"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Loaded">
<mxic:InvokeCommandAction Command="{Binding OpenUICommand}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
<Grid Height="48">
<Grid Height="48">
<Grid.ColumnDefinitions>
@@ -14,18 +21,23 @@
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<PersonPicture
ProfilePicture="https://upload-bbs.mihoyo.com/game_record/genshin/character_icon/UI_AvatarIcon_Hutao.png"
ProfilePicture="{Binding SelectedUser.UserInfo.AvatarUrl,Mode=OneWay}"
HorizontalAlignment="Left"
Margin="4,0,4,0"
Height="40"
Initials="LB"/>
Height="40"/>
<!--<PersonPicture
ProfilePicture="{Binding SelectedUser.UserInfo.Pendant,FallbackValue={x:Null},Mode=OneWay}"
HorizontalAlignment="Left"
Margin="4,0,4,0"
Height="40"/>-->
<TextBlock
VerticalAlignment="Center"
Margin="0,0,0,2"
Grid.Column="1"
Text="胡桃胡桃胡桃胡桃胡桃胡桃"
Text="{Binding SelectedUser.UserInfo.Nickname,Mode=OneWay}"
TextTrimming="CharacterEllipsis"/>
<Button
x:Name="UsersFlyoutButton"
Background="Transparent"
BorderBrush="{x:Null}"
Height="38.4"
@@ -35,7 +47,7 @@
Margin="4">
<Button.Flyout>
<Flyout
Placement="BottomEdgeAlignedRight"
Placement="TopEdgeAlignedRight"
LightDismissOverlayMode="On">
<Flyout.FlyoutPresenterStyle>
<Style
@@ -48,25 +60,60 @@
<ListView
Grid.Row="1"
Margin="4"
CanReorderItems="True">
<ListViewItem Content="角色1"/>
<ListViewItem Content="角色2"/>
SelectionMode="Single"
CanReorderItems="True"
ItemsSource="{Binding SelectedUser.UserGameRoles}"
SelectedItem="{Binding SelectedUser.SelectedUserGameRole,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Padding="0,6">
<TextBlock Text="{Binding Nickname}"/>
<TextBlock
Margin="0,2,0,0"
Opacity="0.6"
Text="{Binding Description}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<MenuFlyoutSeparator/>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton Icon="Add" Label="添加新用户"/>
</CommandBar>
<ListView
MaxHeight="224"
Grid.Row="1"
Margin="4"
CanReorderItems="True">
<ListViewItem Content="用户1"/>
<ListViewItem Content="用户2"/>
<ListViewItem Content="用户3"/>
<ListViewItem Content="用户4"/>
<ListViewItem Content="用户1"/>
<ListViewItem Content="用户1"/>
SelectionMode="Single"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<PersonPicture
ProfilePicture="{Binding UserInfo.AvatarUrl,Mode=OneWay}"
HorizontalAlignment="Left"
Margin="4,0"
Height="32"/>
<TextBlock
Margin="12,0,0,0"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding UserInfo.Nickname}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton
Icon="Add"
Label="添加新用户"
Command="{Binding AddUserCommand}"
CommandParameter="{x:Bind UsersFlyoutButton.Flyout}"/>
</CommandBar>
</StackPanel>
</Flyout>
</Button.Flyout>

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View;
@@ -20,6 +21,7 @@ public sealed partial class UserView : UserControl
public UserView()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<UserViewModel>();
}
/// <summary>

View File

@@ -10,7 +10,6 @@ using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Abstraction.Navigation;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using System.Windows.Input;
namespace Snap.Hutao.ViewModel;

View File

@@ -1,12 +1,16 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Extension;
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.View.Dialog;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace Snap.Hutao.ViewModel;
@@ -17,26 +21,24 @@ namespace Snap.Hutao.ViewModel;
internal class UserViewModel : ObservableObject
{
private readonly IUserService userService;
private readonly UserGameRoleClient userGameRoleClient;
private readonly ILogger<UserViewModel> logger;
private readonly IInfoBarService infoBarService;
private User? selectedUserInfo;
private User? selectedUser;
private ObservableCollection<User>? userInfos;
private UserGameRole? selectedUserGameRole;
private ObservableCollection<UserGameRole>? userGameRoles;
/// <summary>
/// 构造一个新的用户视图模型
/// </summary>
/// <param name="userService">用户服务</param>
/// <param name="userGameRoleClient">用户角色信息客户端</param>
/// <param name="infoBarService">信息条服务</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public UserViewModel(IUserService userService, UserGameRoleClient userGameRoleClient, IAsyncRelayCommandFactory asyncRelayCommandFactory, ILogger<UserViewModel> logger)
public UserViewModel(IUserService userService, IInfoBarService infoBarService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
this.userService = userService;
this.userGameRoleClient = userGameRoleClient;
this.logger = logger;
this.infoBarService = infoBarService;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
AddUserCommand = asyncRelayCommandFactory.Create<Flyout>(AddUserAsync);
}
/// <summary>
@@ -44,12 +46,12 @@ internal class UserViewModel : ObservableObject
/// </summary>
public User? SelectedUser
{
get => selectedUserInfo;
get => selectedUser;
set
{
if (SetProperty(ref selectedUserInfo, value) && value != null)
if (SetProperty(ref selectedUser, value))
{
UpdateUserGameRolesAsync().SafeForget(logger);
userService.CurrentUser = value;
}
}
}
@@ -59,46 +61,81 @@ internal class UserViewModel : ObservableObject
/// </summary>
public ObservableCollection<User>? Users { get => userInfos; set => SetProperty(ref userInfos, value); }
/// <summary>
/// 选择的角色信息
/// </summary>
public UserGameRole? SelectedUserGameRole { get => selectedUserGameRole; set => SetProperty(ref selectedUserGameRole, value); }
/// <summary>
/// 角色信息集合
/// </summary>
public ObservableCollection<UserGameRole>? UserGameRoles
{
get => userGameRoles;
set
{
if (SetProperty(ref userGameRoles, value))
{
if (value != null)
{
SelectedUserGameRole = value.FirstOrDefault(role => role.IsChosen) ?? value.FirstOrDefault();
}
else
{
SelectedUserGameRole = null;
}
}
}
}
/// <summary>
/// 打开界面命令
/// </summary>
public ICommand OpenUICommand { get; }
/// <summary>
/// 添加用户命令
/// </summary>
public ICommand AddUserCommand { get; }
private static bool TryValidateCookie(IDictionary<string, string> map, [NotNullWhen(true)] out SortedDictionary<string, string>? filteredCookie)
{
int validFlag = 4;
filteredCookie = new();
// O(1) to validate cookie
foreach ((string key, string value) in map)
{
if (key == "account_id")
{
validFlag--;
filteredCookie[key] = value;
}
if (key == "cookie_token")
{
validFlag--;
filteredCookie[key] = value;
}
if (key == "ltoken")
{
validFlag--;
filteredCookie[key] = value;
}
if (key == "ltuid")
{
validFlag--;
filteredCookie[key] = value;
}
}
return validFlag == 0;
}
private async Task OpenUIAsync()
{
Users = new(await userService.GetInitializedUsersAsync());
Users = await userService.GetInitializedUsersAsync();
SelectedUser = Users.FirstOrDefault();
}
private async Task UpdateUserGameRolesAsync()
private async Task AddUserAsync(Flyout? flyout)
{
UserGameRoles = new(await userGameRoleClient.GetUserGameRolesAsync());
// hide the flyout, otherwise dialog can't open.
flyout?.Hide();
Result<bool, string> result = await new UserDialog().GetInputCookieAsync();
// user confirms the input
if (result.IsOk)
{
IDictionary<string, string> map = userService.ParseCookie(result.Value);
if (TryValidateCookie(map, out SortedDictionary<string, string>? filteredCookie))
{
string simplifiedCookie = string.Join(';', filteredCookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
User user = new() { Cookie = simplifiedCookie };
if (!await userService.TryAddUserAsync(user))
{
infoBarService.Warning("提供的Cookie无效");
}
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
@@ -16,7 +15,6 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
[Injection(InjectAs.Transient)]
internal class UserClient
{
private readonly IUserService userService;
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions jsonSerializerOptions;
@@ -26,9 +24,8 @@ internal class UserClient
/// <param name="userService">用户服务</param>
/// <param name="httpClient">http客户端</param>
/// <param name="jsonSerializerOptions">Json序列化选项</param>
public UserClient(IUserService userService, HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions)
public UserClient(HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions)
{
this.userService = userService;
this.httpClient = httpClient;
this.jsonSerializerOptions = jsonSerializerOptions;
}
@@ -36,13 +33,14 @@ internal class UserClient
/// <summary>
/// 获取当前用户详细信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
public async Task<UserInfo?> GetUserFullInfoAsync(CancellationToken token = default)
public async Task<UserInfo?> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
{
Response<UserFullInfoWrapper>? resp = await httpClient
.UsingDynamicSecret()
.SetUser(userService.Current)
.SetUser(user)
.GetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiEndpoints.UserFullInfo, jsonSerializerOptions, token)
.ConfigureAwait(false);
@@ -59,7 +57,7 @@ internal class UserClient
{
Response<UserFullInfoWrapper>? resp = await httpClient
.UsingDynamicSecret()
.SetUser(userService.Current)
/*.SetUser(userService.CurrentUser)*/
.GetFromJsonAsync<Response<UserFullInfoWrapper>>(string.Format(ApiEndpoints.UserFullInfoQuery, uid), jsonSerializerOptions, token)
.ConfigureAwait(false);

View File

@@ -57,6 +57,6 @@ public class UserFullInfoWrapper
/// <summary>
/// 审核信息
/// </summary>
[JsonPropertyName("customer_service")]
[JsonPropertyName("audit_info")]
public AuditInfo AuditInfo { get; set; } = default!;
}

View File

@@ -87,7 +87,7 @@ public class UserInfo
/// 头像框
/// </summary>
[JsonPropertyName("pendant")]
public Uri Pendant { get; set; } = default!;
public Uri? Pendant { get; set; }
/// <summary>
/// 是否登出

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Converting;
using Snap.Hutao.Core.Convertion;
using System.Text;
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Converting;
using Snap.Hutao.Core.Convertion;
using System.Linq;
using System.Text.Json;

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using System.Text.Json.Serialization;
using System.Windows.Input;
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;

View File

@@ -20,7 +20,11 @@ internal static class HttpClientCookieExtensions
/// <returns>客户端</returns>
internal static HttpClient SetUser(this HttpClient httpClient, User user)
{
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie);
if (!User.IsNone(user))
{
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie);
}
return httpClient;
}
}

View File

@@ -56,7 +56,15 @@ public record UserGameRole
/// 是否为官服
/// </summary>
[JsonPropertyName("is_official")]
public string IsOfficial { get; set; } = default!;
public bool IsOfficial { get; set; } = default!;
/// <summary>
/// 玩家服务器与等级简述
/// </summary>
public string Description
{
get => $"{RegionName} | Lv.{Level}";
}
public static explicit operator PlayerUid(UserGameRole userGameRole)
{

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Response;
using System.Collections.Generic;
using System.Net.Http;
@@ -16,7 +15,6 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
[Injection(InjectAs.Transient)]
internal class UserGameRoleClient
{
private readonly IUserService userService;
private readonly HttpClient httpClient;
/// <summary>
@@ -24,21 +22,21 @@ internal class UserGameRoleClient
/// </summary>
/// <param name="userService">用户服务</param>
/// <param name="httpClient">请求器</param>
public UserGameRoleClient(IUserService userService, HttpClient httpClient)
public UserGameRoleClient(HttpClient httpClient)
{
this.userService = userService;
this.httpClient = httpClient;
}
/// <summary>
/// 获取用户角色信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
public async Task<List<UserGameRole>> GetUserGameRolesAsync(CancellationToken token = default)
public async Task<List<UserGameRole>> GetUserGameRolesAsync(Model.Entity.User user, CancellationToken token = default)
{
Response<ListWrapper<UserGameRole>>? resp = await httpClient
.SetUser(userService.Current)
.SetUser(user)
.GetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiEndpoints.UserGameRoles, token)
.ConfigureAwait(false);

View File

@@ -13,50 +13,60 @@ public class Weapon
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 名称
/// </summary>
[JsonPropertyName("name")] public string Name { get; set; } = default!;
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")] public string Icon { get; set; } = default!;
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
/// <summary>
/// 类型
/// </summary>
[JsonPropertyName("type")] public WeaponType Type { get; set; }
[JsonPropertyName("type")]
public WeaponType Type { get; set; }
/// <summary>
/// 稀有度
/// </summary>
[JsonPropertyName("rarity")] public Rarity Rarity { get; set; }
[JsonPropertyName("rarity")]
public Rarity Rarity { get; set; }
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")] public int Level { get; set; }
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 突破等级
/// </summary>
[JsonPropertyName("promote_level")] public int PromoteLevel { get; set; }
[JsonPropertyName("promote_level")]
public int PromoteLevel { get; set; }
/// <summary>
/// 类型名称
/// </summary>
[JsonPropertyName("type_name")] public string TypeName { get; set; } = default!;
[JsonPropertyName("type_name")]
public string TypeName { get; set; } = default!;
/// <summary>
/// 武器介绍
/// </summary>
[JsonPropertyName("desc")] public string Description { get; set; } = default!;
[JsonPropertyName("desc")]
public string Description { get; set; } = default!;
/// <summary>
/// 精炼等级
/// </summary>
[JsonPropertyName("affix_level")] public int AffixLevel { get; set; }
[JsonPropertyName("affix_level")]
public int AffixLevel { get; set; }
}

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Response;
@@ -21,18 +21,16 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
[Injection(InjectAs.Transient)]
internal class GameRecordClient
{
private readonly IUserService userService;
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions jsonSerializerOptions;
/// <summary>
/// 构造一个新的游戏记录提供器
/// </summary>
/// <param name="userService">用户服务</param>
/// <param name="httpClient">请求器</param>
public GameRecordClient(IUserService userService, HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions)
/// <param name="jsonSerializerOptions">json序列化选项</param>
public GameRecordClient(HttpClient httpClient, JsonSerializerOptions jsonSerializerOptions)
{
this.userService = userService;
this.httpClient = httpClient;
this.jsonSerializerOptions = jsonSerializerOptions;
}
@@ -40,15 +38,28 @@ internal class GameRecordClient
/// <summary>
/// 获取玩家基础信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家的基础信息</returns>
public Task<PlayerInfo?> GetPlayerInfoAsync(User user, CancellationToken token = default)
{
PlayerUid uid = Must.NotNull(user.SelectedUserGameRole!).AsPlayerUid();
return GetPlayerInfoAsync(user, uid, token);
}
/// <summary>
/// 获取玩家基础信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家的基础信息</returns>
public async Task<PlayerInfo?> GetPlayerInfoAsync(PlayerUid uid, CancellationToken token)
public async Task<PlayerInfo?> GetPlayerInfoAsync(User user, PlayerUid uid, CancellationToken token = default)
{
string url = string.Format(ApiEndpoints.GameRecordIndex, uid.Value, uid.Region);
Response<PlayerInfo>? resp = await httpClient
.SetUser(userService.Current)
.SetUser(user)
.UsingDynamicSecret2(jsonSerializerOptions, url)
.GetFromJsonAsync<Response<PlayerInfo>>(url, jsonSerializerOptions, token)
.ConfigureAwait(false);
@@ -59,16 +70,30 @@ internal class GameRecordClient
/// <summary>
/// 获取玩家深渊信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="schedule">1当期2上期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
public Task<SpiralAbyss.SpiralAbyss?> GetSpiralAbyssAsync(User user, SpiralAbyssSchedule schedule, CancellationToken token = default)
{
PlayerUid uid = Must.NotNull(user.SelectedUserGameRole!).AsPlayerUid();
return GetSpiralAbyssAsync(user, uid, schedule, token);
}
/// <summary>
/// 获取玩家深渊信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="uid">uid</param>
/// <param name="schedule">1当期2上期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
public async Task<SpiralAbyss.SpiralAbyss?> GetSpiralAbyssAsync(PlayerUid uid, SpiralAbyssSchedule schedule, CancellationToken token = default)
public async Task<SpiralAbyss.SpiralAbyss?> GetSpiralAbyssAsync(User user, PlayerUid uid, SpiralAbyssSchedule schedule, CancellationToken token = default)
{
string url = string.Format(ApiEndpoints.SpiralAbyss, (int)schedule, uid.Value, uid.Region);
Response<SpiralAbyss.SpiralAbyss>? resp = await httpClient
.SetUser(userService.Current)
.SetUser(user)
.UsingDynamicSecret2(jsonSerializerOptions, url)
.GetFromJsonAsync<Response<SpiralAbyss.SpiralAbyss>>(url, jsonSerializerOptions, token)
.ConfigureAwait(false);
@@ -79,16 +104,30 @@ internal class GameRecordClient
/// <summary>
/// 获取玩家角色详细信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="playerInfo">玩家的基础信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public Task<List<Character>> GetCharactersAsync(User user, PlayerInfo playerInfo, CancellationToken token = default)
{
PlayerUid uid = Must.NotNull(user.SelectedUserGameRole!).AsPlayerUid();
return GetCharactersAsync(user, uid, playerInfo, token);
}
/// <summary>
/// 获取玩家角色详细信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="uid">uid</param>
/// <param name="playerInfo">玩家的基础信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public async Task<List<Character>> GetCharactersAsync(PlayerUid uid, PlayerInfo playerInfo, CancellationToken token = default)
public async Task<List<Character>> GetCharactersAsync(User user, PlayerUid uid, PlayerInfo playerInfo, CancellationToken token = default)
{
CharacterData data = new(uid, playerInfo.Avatars.Select(x => x.Id));
HttpResponseMessage? response = await httpClient
.SetUser(userService.Current)
.SetUser(user)
.UsingDynamicSecret2(jsonSerializerOptions, ApiEndpoints.Character, data)
.PostAsJsonAsync(ApiEndpoints.Character, data, token)
.ConfigureAwait(false);

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Takumi;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
@@ -291,60 +292,26 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <summary>
/// 异步获取角色的深渊记录
/// </summary>
/// <param name="role">角色</param>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家记录</returns>
public async Task<PlayerRecord> GetPlayerRecordAsync(UserGameRole role, CancellationToken token = default)
public async Task<PlayerRecord> GetPlayerRecordAsync(User user, CancellationToken token = default)
{
PlayerInfo? playerInfo = await gameRecordClient
.GetPlayerInfoAsync((PlayerUid)role, token)
.GetPlayerInfoAsync(user, token)
.ConfigureAwait(false);
Must.NotNull(playerInfo!);
List<Character> characters = await gameRecordClient
.GetCharactersAsync((PlayerUid)role, playerInfo, token)
.GetCharactersAsync(user, playerInfo, token)
.ConfigureAwait(false);
SpiralAbyss? spiralAbyssInfo = await gameRecordClient
.GetSpiralAbyssAsync((PlayerUid)role, SpiralAbyssSchedule.Current, token)
.GetSpiralAbyssAsync(user, SpiralAbyssSchedule.Current, token)
.ConfigureAwait(false);
Must.NotNull(spiralAbyssInfo!);
return PlayerRecord.Create(role.GameUid, characters, spiralAbyssInfo);
}
/// <summary>
/// 异步获取所有记录并上传到数据库
/// </summary>
/// <param name="confirmAsyncFunc">异步确认委托</param>
/// <param name="resultAsyncFunc">结果确认委托</param>
/// <param name="token">取消令牌</param>
/// <returns>任务</returns>
[Obsolete("上传任务应交由视图模型完成")]
[EditorBrowsable(EditorBrowsableState.Never)]
public async Task GetAllRecordsAndUploadAsync(Func<PlayerRecord, Task<bool>> confirmAsyncFunc, Func<Response.Response, Task> resultAsyncFunc, CancellationToken token = default)
{
// 由于此方法需要直接与UI线程交互
// 内部的异步方法均不使用 .ConfigureAwait(false);
List<UserGameRole> userGameRoles = await userGameRoleClient
.GetUserGameRolesAsync(token);
foreach (UserGameRole role in userGameRoles)
{
PlayerRecord playerRecord = await GetPlayerRecordAsync(role, token);
if (await confirmAsyncFunc(playerRecord))
{
Response<string>? resp = null;
if (await playerRecord.UploadItemsAsync(this, token))
{
await playerRecord.UploadRecordAsync(this, token);
}
// await resultAsyncFunc(resp ?? Response.Response.CreateForException($"{role.GameUid}-记录提交失败。"));
}
}
return PlayerRecord.Create(Must.NotNull(user.SelectedUserGameRole!).GameUid, characters, spiralAbyssInfo);
}
/// <summary>