mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
use source generation to improve injection discover speed
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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]
|
||||
#### 命名样式 ####
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.0.3" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
23
src/Snap.Hutao/Snap.Hutao.SourceGeneration/stylecop.json
Normal file
23
src/Snap.Hutao/Snap.Hutao.SourceGeneration/stylecop.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core.Converting;
|
||||
namespace Snap.Hutao.Core.Convertion;
|
||||
|
||||
/// <summary>
|
||||
/// 支持Md5转换
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
46
src/Snap.Hutao/Snap.Hutao/Core/Threading/Result.cs
Normal file
46
src/Snap.Hutao/Snap.Hutao/Core/Threading/Result.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
24
src/Snap.Hutao/Snap.Hutao/Core/Threading/Results.cs
Normal file
24
src/Snap.Hutao/Snap.Hutao/Core/Threading/Results.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -77,4 +77,4 @@ public class Watcher : Observable
|
||||
watcher.IsCompleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
54
src/Snap.Hutao/Snap.Hutao/Migrations/20220618110357_SettingAndUser.Designer.cs
generated
Normal file
54
src/Snap.Hutao/Snap.Hutao/Migrations/20220618110357_SettingAndUser.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
37
src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml
Normal file
37
src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml
Normal 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=""
|
||||
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>
|
||||
47
src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml.cs
Normal file
47
src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml.cs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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=""/>
|
||||
</NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
|
||||
|
||||
</NavigationView.MenuItems>
|
||||
|
||||
<NavigationView.PaneFooter>
|
||||
<view:UserView IsExpanded="{Binding ElementName=NavView,Path=IsPaneOpen}"/>
|
||||
</NavigationView.PaneFooter>
|
||||
|
||||
<Frame x:Name="ContentFrame">
|
||||
<Frame.ContentTransitions>
|
||||
<TransitionCollection>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed partial class TitleView : UserControl
|
||||
/// </summary>
|
||||
public TitleView()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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无效!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -57,6 +57,6 @@ public class UserFullInfoWrapper
|
||||
/// <summary>
|
||||
/// 审核信息
|
||||
/// </summary>
|
||||
[JsonPropertyName("customer_service")]
|
||||
[JsonPropertyName("audit_info")]
|
||||
public AuditInfo AuditInfo { get; set; } = default!;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public class UserInfo
|
||||
/// 头像框
|
||||
/// </summary>
|
||||
[JsonPropertyName("pendant")]
|
||||
public Uri Pendant { get; set; } = default!;
|
||||
public Uri? Pendant { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否登出
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user