mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Merge pull request #1170 from DGP-Studio/develop
This commit is contained in:
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"cake.tool": {
|
||||
"version": "4.0.0",
|
||||
"commands": [
|
||||
"dotnet-cake"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
20
appveyor.yml
Normal file
20
appveyor.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 1.0.{build}
|
||||
branches:
|
||||
only:
|
||||
- "release"
|
||||
build_cloud: HUTAO-SERVER
|
||||
image: Visual Studio 2022
|
||||
clone_depth: 3
|
||||
clone_folder: D:\appveyor\project\Snap.Hutao.Project
|
||||
install:
|
||||
- pwsh: dotnet tool restore
|
||||
build_script:
|
||||
- pwsh: dotnet cake
|
||||
artifacts:
|
||||
- path: src/output/*.msix
|
||||
type: file
|
||||
deploy:
|
||||
- provider: Webhook
|
||||
url: https://app.signpath.io/API/v1/7a941fa3-64d8-4c45-bd03-92a02bcd4964/Integrations/AppVeyor?ProjectSlug=Snap.Hutao&SigningPolicySlug=release-signing&ArtifactConfigurationSlug=msix
|
||||
authorization:
|
||||
secure: j8srQ5/UYWhI+jlm3Vo3D3QfXoRyQ9hOn3ynJGtwusKui4+uDi4gykdUFYCITZxK+C/fOCAZNJ+YaKSm/OaiXw==
|
||||
@@ -7,30 +7,32 @@
|
||||
# 5. Connect the GitHub in project settings
|
||||
# 6. Run
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- README.md
|
||||
- azure-pipelines.yml
|
||||
- .github/ISSUE_TEMPLATE/*.yml
|
||||
- .github/workflows/*.yml
|
||||
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
||||
pr:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
paths:
|
||||
exclude:
|
||||
- README.md
|
||||
- azure-pipelines.yml
|
||||
- .github/ISSUE_TEMPLATE/*.yml
|
||||
- .github/workflows/*.yml
|
||||
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
||||
|
||||
trigger: none
|
||||
pr: none
|
||||
# trigger:
|
||||
# branches:
|
||||
# include:
|
||||
# - main
|
||||
# - develop
|
||||
# paths:
|
||||
# exclude:
|
||||
# - README.md
|
||||
# - azure-pipelines.yml
|
||||
# - .github/ISSUE_TEMPLATE/*.yml
|
||||
# - .github/workflows/*.yml
|
||||
# - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
||||
# pr:
|
||||
# branches:
|
||||
# include:
|
||||
# - main
|
||||
# paths:
|
||||
# exclude:
|
||||
# - README.md
|
||||
# - azure-pipelines.yml
|
||||
# - .github/ISSUE_TEMPLATE/*.yml
|
||||
# - .github/workflows/*.yml
|
||||
# - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
||||
|
||||
|
||||
pool:
|
||||
name: Default
|
||||
@@ -42,15 +44,9 @@ variables:
|
||||
project: $(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj'
|
||||
buildPlatform: 'x64'
|
||||
buildConfiguration: 'Release'
|
||||
build_date: $[ format('{0:yyyy}.{0:M}.{0:d}', pipeline.startTime) ]
|
||||
|
||||
|
||||
steps:
|
||||
- task: GetRevision@1
|
||||
displayName: get Pipelines revision number
|
||||
inputs:
|
||||
VariableName: 'rev_number'
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Install dotNet
|
||||
inputs:
|
||||
@@ -58,134 +54,66 @@ steps:
|
||||
version: '8.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: NuGetToolInstaller@1
|
||||
name: 'NuGetToolInstaller'
|
||||
displayName: 'NuGet Installer'
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: NuGet restore
|
||||
inputs:
|
||||
command: 'restore'
|
||||
restoreSolution: '$(solution)'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.Config'
|
||||
|
||||
- task: MsixPackaging@1
|
||||
displayName: Build binary package
|
||||
inputs:
|
||||
outputPath: '$(Build.ArtifactStagingDirectory)/'
|
||||
solution: '$(solution)'
|
||||
clean: false
|
||||
generateBundle: false
|
||||
buildConfiguration: 'Release'
|
||||
buildPlatform: 'x64'
|
||||
updateAppVersion: false
|
||||
appPackageDistributionMode: 'SideloadOnly'
|
||||
msbuildLocationMethod: 'location'
|
||||
msbuildLocation: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Msbuild\Current\Bin\MSBuild.exe'
|
||||
|
||||
- task: MagicChunks@2
|
||||
inputs:
|
||||
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\AppxManifest.xml'
|
||||
fileType: 'Xml'
|
||||
targetPathType: 'source'
|
||||
transformationType: 'json'
|
||||
transformations: |
|
||||
{
|
||||
"Package/Identity/@Name": "7f0db578-026f-4e0b-a75b-d5d06bb0a74c",
|
||||
"Package/Identity/@Publisher": "CN=DGP Studio CI",
|
||||
"Package/Identity/@Version": "$(build_date).$(rev_number)",
|
||||
"Package/Properties/DisplayName": "胡桃 Alpha",
|
||||
"Package/Properties/PublisherDisplayName":"DGP Studio CI",
|
||||
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
|
||||
}
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: Create resources folder
|
||||
displayName: dotnet cake
|
||||
inputs:
|
||||
script: |
|
||||
mkdir Assets
|
||||
|
||||
mkdir Resource
|
||||
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64'
|
||||
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Assets Folder
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Assets'
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\Assets'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Resource Folder
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Resource'
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\Resource'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: Build MSIX
|
||||
inputs:
|
||||
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
script: dotnet tool restore && dotnet cake
|
||||
|
||||
- task: MsixSigning@1
|
||||
name: signMsix
|
||||
displayName: Sign MSIX package
|
||||
inputs:
|
||||
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix'
|
||||
certificate: 'DGP_Studio_CI.pfx'
|
||||
passwordVariable: 'pw'
|
||||
condition: succeeded()
|
||||
|
||||
|
||||
#- task: PublishPipelineArtifact@1
|
||||
# displayName: 'Upload Output'
|
||||
# inputs:
|
||||
# targetPath: '$(Build.ArtifactStagingDirectory)/'
|
||||
# artifact: 'Output'
|
||||
# publishLocation: 'pipeline'
|
||||
|
||||
- task: DownloadSecureFile@1
|
||||
name: cerFile
|
||||
displayName: Download Root CA
|
||||
inputs:
|
||||
secureFile: 'Snap.Hutao.CI.cer'
|
||||
|
||||
- task: GitHubRelease@1
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
gitHubConnection: 'github.com_Masterain'
|
||||
repositoryName: 'DGP-Studio/Snap.Hutao'
|
||||
action: 'create'
|
||||
target: '$(Build.SourceVersion)'
|
||||
tagSource: 'userSpecifiedTag'
|
||||
tag: '$(build_date).$(rev_number)'
|
||||
title: '$(build_date).$(rev_number)'
|
||||
releaseNotesSource: 'inline'
|
||||
releaseNotesInline: |
|
||||
## 普通用户请勿下载
|
||||
该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
||||
targetPath: '$(Build.ArtifactStagingDirectory)'
|
||||
artifact: 'Snap.Hutao.Alpha-$(version).msix'
|
||||
publishLocation: 'pipeline'
|
||||
|
||||
普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
||||
|
||||
assets: |
|
||||
$(Build.ArtifactStagingDirectory)/*
|
||||
$(cerFile.secureFilePath)
|
||||
isPreRelease: true
|
||||
changeLogCompareToRelease: 'lastFullRelease'
|
||||
changeLogType: 'commitBased'
|
||||
#- task: GitHubRelease@1
|
||||
# inputs:
|
||||
# gitHubConnection: 'github.com_Masterain'
|
||||
# repositoryName: 'DGP-Automation/Hutao-Auto-Release'
|
||||
# action: 'create'
|
||||
# target: '$(Build.SourceVersion)'
|
||||
# tagSource: 'userSpecifiedTag'
|
||||
# tag: '$(version)'
|
||||
# title: '$(version)'
|
||||
# releaseNotesSource: 'inline'
|
||||
# releaseNotesInline: |
|
||||
# ## 普通用户请勿下载
|
||||
# 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
||||
#
|
||||
# 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
||||
#
|
||||
# assets: |
|
||||
# $(Build.ArtifactStagingDirectory)/*
|
||||
# $(cerFile.secureFilePath)
|
||||
# isPreRelease: true
|
||||
# changeLogCompareToRelease: 'lastFullRelease'
|
||||
# changeLogType: 'commitBased'
|
||||
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload CI via Rclone
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/Alpha/'
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix downloadDGPCN:/releases/Alpha/'
|
||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload PR CI via Rclone
|
||||
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/PR/'
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix downloadDGPCN:/releases/PR/'
|
||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
||||
|
||||
179
build.cake
Normal file
179
build.cake
Normal file
@@ -0,0 +1,179 @@
|
||||
#tool "nuget:?package=nuget.commandline&version=6.5.0"
|
||||
#addin nuget:?package=Cake.Http&version=3.0.2
|
||||
|
||||
var target = Argument("target", "Build");
|
||||
var configuration = Argument("configuration", "Release");
|
||||
|
||||
// Pre-define
|
||||
|
||||
var version = "version";
|
||||
|
||||
var repoDir = "repoDir";
|
||||
var outputPath = "outputPath";
|
||||
|
||||
string solution
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln");
|
||||
}
|
||||
string project
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Snap.Hutao.csproj");
|
||||
}
|
||||
string binPath
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "bin", "x64", "Release", "net8.0-windows10.0.22621.0", "win-x64");
|
||||
}
|
||||
string manifest
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest");
|
||||
}
|
||||
|
||||
if (AzurePipelines.IsRunningOnAzurePipelines)
|
||||
{
|
||||
repoDir = AzurePipelines.Environment.Build.SourcesDirectory.FullPath;
|
||||
outputPath = AzurePipelines.Environment.Build.ArtifactStagingDirectory.FullPath;
|
||||
|
||||
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
|
||||
version = HttpGet(
|
||||
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
|
||||
new HttpSettings
|
||||
{
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
{ "Authorization", versionAuth }
|
||||
}
|
||||
}
|
||||
);
|
||||
Information($"Version: {version}");
|
||||
|
||||
AzurePipelines.Commands.SetVariable("version", version);
|
||||
}
|
||||
else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
{
|
||||
repoDir = AppVeyor.Environment.Build.Folder;
|
||||
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
||||
|
||||
version = XmlPeek(manifest, "appx:Package/appx:Identity/@Version", new XmlPeekSettings
|
||||
{
|
||||
Namespaces = new Dictionary<string, string> { { "appx", "http://schemas.microsoft.com/appx/manifest/foundation/windows10" } }
|
||||
})[..^2];
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
|
||||
Task("Build")
|
||||
.IsDependentOn("Build binary package")
|
||||
.IsDependentOn("Copy files")
|
||||
.IsDependentOn("Build MSIX");
|
||||
|
||||
Task("NuGet Restore")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Restoring packages...");
|
||||
|
||||
var nugetConfig = System.IO.Path.Combine(repoDir, "NuGet.Config");
|
||||
DotNetRestore(project, new DotNetRestoreSettings
|
||||
{
|
||||
Verbosity = DotNetVerbosity.Detailed,
|
||||
Interactive = false,
|
||||
ConfigFile = nugetConfig
|
||||
});
|
||||
});
|
||||
|
||||
Task("Generate AppxManifest")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Generating AppxManifest...");
|
||||
|
||||
var content = System.IO.File.ReadAllText(manifest);
|
||||
|
||||
if (AzurePipelines.IsRunningOnAzurePipelines)
|
||||
{
|
||||
Information("Using CI configuraion");
|
||||
content = content
|
||||
.Replace("Snap Hutao", "Snap Hutao Alpha")
|
||||
.Replace("胡桃", "胡桃 Alpha")
|
||||
.Replace("DGP Studio", "DGP Studio CI");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"7f0db578-026f-4e0b-a75b-d5d06bb0a74c\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=DGP Studio CI\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
|
||||
}
|
||||
else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
{
|
||||
Information("Using Release configuration");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\"");
|
||||
}
|
||||
|
||||
System.IO.File.WriteAllText(manifest, content);
|
||||
|
||||
Information("Generated.");
|
||||
});
|
||||
|
||||
Task("Build binary package")
|
||||
.IsDependentOn("NuGet Restore")
|
||||
.IsDependentOn("Generate AppxManifest")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Building binary package...");
|
||||
|
||||
var settings = new DotNetBuildSettings
|
||||
{
|
||||
Configuration = configuration
|
||||
};
|
||||
|
||||
settings.MSBuildSettings = new DotNetMSBuildSettings
|
||||
{
|
||||
ArgumentCustomization = args => args.Append("/p:Platform=x64")
|
||||
.Append("/p:UapAppxPackageBuildMode=SideloadOnly")
|
||||
.Append("/p:AppxPackageSigningEnabled=false")
|
||||
.Append("/p:AppxBundle=Never")
|
||||
.Append("/p:AppxPackageOutput=" + outputPath)
|
||||
};
|
||||
|
||||
DotNetBuild(project, settings);
|
||||
});
|
||||
|
||||
Task("Copy files")
|
||||
.IsDependentOn("Build binary package")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Copying assets...");
|
||||
CopyDirectory(
|
||||
System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Assets"),
|
||||
System.IO.Path.Combine(binPath, "Assets")
|
||||
);
|
||||
|
||||
Information("Copying resource...");
|
||||
CopyDirectory(
|
||||
System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Resource"),
|
||||
System.IO.Path.Combine(binPath, "Resource")
|
||||
);
|
||||
});
|
||||
|
||||
Task("Build MSIX")
|
||||
.IsDependentOn("Build binary package")
|
||||
.IsDependentOn("Copy files")
|
||||
.Does(() =>
|
||||
{
|
||||
var arguments = "arguments";
|
||||
if (AzurePipelines.IsRunningOnAzurePipelines)
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix");
|
||||
}
|
||||
else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao-{version}.msix");
|
||||
}
|
||||
var p = StartProcess(
|
||||
"makeappx.exe",
|
||||
new ProcessSettings
|
||||
{
|
||||
Arguments = arguments
|
||||
}
|
||||
);
|
||||
if (p != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Build failed with exit code " + p);
|
||||
}
|
||||
});
|
||||
|
||||
RunTarget(target);
|
||||
@@ -10,17 +10,10 @@ public class CollectionsMarshalTest
|
||||
[TestMethod]
|
||||
public void DictionaryMarshalGetValueRefOrNullRefIsNullRef()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = [];
|
||||
#else
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = new();
|
||||
#endif
|
||||
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U)));
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U)));
|
||||
@@ -31,17 +24,10 @@ public class CollectionsMarshalTest
|
||||
[TestMethod]
|
||||
public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = [];
|
||||
#else
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = new();
|
||||
#endif
|
||||
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default);
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -7,8 +8,6 @@ namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
[TestClass]
|
||||
public sealed class JsonSerializeTest
|
||||
{
|
||||
public TestContext? TestContext { get; set; }
|
||||
|
||||
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
@@ -36,7 +35,7 @@ public sealed class JsonSerializeTest
|
||||
[TestMethod]
|
||||
public void DelegatePropertyCanSerialize()
|
||||
{
|
||||
Sample sample = JsonSerializer.Deserialize<Sample>(SmapleObjectJson)!;
|
||||
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SmapleObjectJson)!;
|
||||
Assert.AreEqual(sample.B, 1);
|
||||
}
|
||||
|
||||
@@ -44,7 +43,7 @@ public sealed class JsonSerializeTest
|
||||
[ExpectedException(typeof(JsonException))]
|
||||
public void EmptyStringCannotSerializeAsNumber()
|
||||
{
|
||||
StringNumberSample sample = JsonSerializer.Deserialize<StringNumberSample>(SmapleEmptyStringObjectJson)!;
|
||||
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SmapleEmptyStringObjectJson)!;
|
||||
Assert.AreEqual(sample.A, 0);
|
||||
}
|
||||
|
||||
@@ -58,37 +57,28 @@ public sealed class JsonSerializeTest
|
||||
[TestMethod]
|
||||
public void ByteArraySerializeAsBase64()
|
||||
{
|
||||
byte[] array =
|
||||
#if NET8_0_OR_GREATER
|
||||
[1, 2, 3, 4, 5];
|
||||
#else
|
||||
{ 1, 2, 3, 4, 5 };
|
||||
#endif
|
||||
ByteArraySample sample = new()
|
||||
SampleByteArrayPropertyClass sample = new()
|
||||
{
|
||||
Array = array,
|
||||
Array = [1, 2, 3, 4, 5],
|
||||
};
|
||||
|
||||
string result = JsonSerializer.Serialize(sample);
|
||||
TestContext!.WriteLine($"ByteArray Serialize Result: {result}");
|
||||
Assert.AreEqual(result, """
|
||||
{"Array":"AQIDBAU="}
|
||||
""");
|
||||
Assert.AreEqual(result, """{"Array":"AQIDBAU="}""");
|
||||
}
|
||||
|
||||
private sealed class Sample
|
||||
private sealed class SampleDelegatePropertyClass
|
||||
{
|
||||
public int A { get => B; set => B = value; }
|
||||
public int B { get; set; }
|
||||
}
|
||||
|
||||
private sealed class StringNumberSample
|
||||
private sealed class SampleStringReadWriteNumberPropertyClass
|
||||
{
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public int A { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ByteArraySample
|
||||
private sealed class SampleByteArrayPropertyClass
|
||||
{
|
||||
public byte[]? Array { get; set; }
|
||||
}
|
||||
|
||||
33
src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs
Normal file
33
src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public sealed class LinqTest
|
||||
{
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(InvalidOperationException))]
|
||||
public void LinqOrderByWithWrapperStructThrow()
|
||||
{
|
||||
List<MyUInt32> list = [1, 5, 2, 6, 3, 7, 4, 8];
|
||||
string result = string.Join(", ", list.OrderBy(i => i).Select(i => i.Value));
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
|
||||
private readonly struct MyUInt32
|
||||
{
|
||||
public readonly uint Value;
|
||||
|
||||
public MyUInt32(uint value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator MyUInt32(uint value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,19 +159,11 @@ public sealed class GeniusInvokationDecoding
|
||||
result.CopyTo(resultArray);
|
||||
|
||||
ushort[] testKnownResult =
|
||||
#if NET8_0_OR_GREATER
|
||||
[
|
||||
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
|
||||
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
|
||||
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
|
||||
];
|
||||
#else
|
||||
{
|
||||
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
|
||||
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
|
||||
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
|
||||
};
|
||||
#endif
|
||||
|
||||
CollectionAssert.AreEqual(resultArray, testKnownResult);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public class SpiralAbyssScheduleIdTest
|
||||
DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8);
|
||||
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期");
|
||||
}
|
||||
|
||||
public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
// Force time in UTC+08
|
||||
|
||||
@@ -23,6 +23,18 @@ public sealed class UnsafeRuntimeBehaviorTest
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void UInt32LayoutIsLittleEndian()
|
||||
{
|
||||
ulong testValue = 0x1234567887654321;
|
||||
ref BuildVersion version = ref Unsafe.As<ulong, BuildVersion>(ref testValue);
|
||||
|
||||
Assert.AreEqual(0x1234, version.Major);
|
||||
Assert.AreEqual(0x5678, version.Minor);
|
||||
Assert.AreEqual(0x8765, version.Patch);
|
||||
Assert.AreEqual(0x4321, version.Build);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void ReadOnlyStructCanBeModifiedInCtor()
|
||||
{
|
||||
@@ -34,6 +46,8 @@ public sealed class UnsafeRuntimeBehaviorTest
|
||||
Assert.AreEqual(1212, testStruct.Value4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private readonly struct TestStruct
|
||||
{
|
||||
public readonly int Value1;
|
||||
@@ -46,4 +60,12 @@ public sealed class UnsafeRuntimeBehaviorTest
|
||||
CollectionsMarshal.AsSpan(list).CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As<TestStruct, int>(ref this), 4));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct BuildVersion
|
||||
{
|
||||
public readonly ushort Build;
|
||||
public readonly ushort Patch;
|
||||
public readonly ushort Minor;
|
||||
public readonly ushort Major;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,11 @@ DwmSetWindowAttribute
|
||||
GetDeviceCaps
|
||||
|
||||
// KERNEL32
|
||||
AllocConsole
|
||||
CloseHandle
|
||||
CreateEventW
|
||||
CreateRemoteThread
|
||||
FreeConsole
|
||||
GetModuleHandleW
|
||||
GetProcAddress
|
||||
K32EnumProcessModules
|
||||
@@ -64,6 +66,7 @@ IMemoryBufferByteAccess
|
||||
|
||||
// Const value
|
||||
INFINITE
|
||||
RPC_E_WRONG_THREAD
|
||||
MAX_PATH
|
||||
WM_GETMINMAXINFO
|
||||
WM_HOTKEY
|
||||
|
||||
@@ -16,22 +16,7 @@ internal abstract class DependencyValueConverter<TFrom, TTo> : DependencyObject,
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
#if DEBUG
|
||||
try
|
||||
{
|
||||
return Convert((TFrom)value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
}
|
||||
|
||||
return null;
|
||||
#else
|
||||
return Convert((TFrom)value);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -24,7 +24,7 @@ internal struct ContentDialogHideToken : IDisposable, IAsyncDisposable
|
||||
if (!disposed && !disposing)
|
||||
{
|
||||
disposing = true;
|
||||
taskContext.InvokeOnMainThread(contentDialog.Hide); // Hide() must be called on main thread.
|
||||
taskContext.InvokeOnMainThread(contentDialog.Hide);
|
||||
disposing = false;
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
public CachedImage()
|
||||
{
|
||||
IsCacheEnabled = true;
|
||||
EnableLazyLoading = true;
|
||||
EnableLazyLoading = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
Name="PlaceholderImage"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Opacity="1.0"
|
||||
Source="{TemplateBinding PlaceholderSource}"
|
||||
Stretch="{TemplateBinding PlaceholderStretch}"/>
|
||||
<Image
|
||||
@@ -27,7 +26,6 @@
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
NineGrid="{TemplateBinding NineGrid}"
|
||||
Opacity="0.0"
|
||||
Stretch="{TemplateBinding Stretch}"/>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
|
||||
@@ -80,19 +80,22 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
ILogger<CompositionImage> logger = serviceProvider.GetRequiredService<ILogger<CompositionImage>>();
|
||||
|
||||
// source is valid
|
||||
if (arg.NewValue is Uri inner && !string.IsNullOrEmpty(inner.OriginalString))
|
||||
if (arg.NewValue is Uri inner)
|
||||
{
|
||||
// value is different from old one
|
||||
if (inner != (arg.OldValue as Uri))
|
||||
if (!string.IsNullOrEmpty(inner.OriginalString))
|
||||
{
|
||||
image
|
||||
.ApplyImageAsync(inner, token)
|
||||
.SafeForget(logger, ex => OnApplyImageFailed(serviceProvider, inner, ex));
|
||||
// value is different from old one
|
||||
if (inner != (arg.OldValue as Uri))
|
||||
{
|
||||
image
|
||||
.ApplyImageAsync(inner, token)
|
||||
.SafeForget(logger, ex => OnApplyImageFailed(serviceProvider, inner, ex));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
image.HideAsync(token).SafeForget(logger);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
image.HideAsync(token).SafeForget(logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +133,9 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
{
|
||||
imageSurface = await LoadImageSurfaceAsync(file, token).ConfigureAwait(true);
|
||||
}
|
||||
catch (COMException)
|
||||
catch (COMException ex)
|
||||
{
|
||||
_ = ex;
|
||||
imageCache.Remove(uri);
|
||||
}
|
||||
catch (IOException)
|
||||
@@ -163,7 +167,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
surface.LoadCompleted += loadedImageSourceLoadCompletedEventHandler;
|
||||
if (surface.DecodedPhysicalSize.Size() <= 0D)
|
||||
{
|
||||
await surfaceLoadTaskCompletionSource.Task.ConfigureAwait(true);
|
||||
await Task.WhenAny(surfaceLoadTaskCompletionSource.Task, Task.Delay(5000, token)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
LoadImageSurfaceCompleted(surface);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<SolidColorBrush x:Key="PurpleColorBrush" Color="{ThemeResource PurpleColor}"/>
|
||||
<SolidColorBrush x:Key="OrangeColorBrush" Color="{ThemeResource OrangeColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="GuaranteePullCoolorBrush" Color="{ThemeResource GuaranteePullColor}"/>
|
||||
<SolidColorBrush x:Key="GuaranteePullColorBrush" Color="{ThemeResource GuaranteePullColor}"/>
|
||||
<SolidColorBrush x:Key="UpPullColorBrush" Color="{ThemeResource UpPullColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="DarkOnlyOverlayMaskColorBrush" Color="{ThemeResource DarkOnlyOverlayMaskColor}"/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-fluent-icons-font -->
|
||||
<x:String x:Key="FontIconContentAdd"></x:String>
|
||||
<x:String x:Key="FontIconContentSetting"></x:String>
|
||||
<x:String x:Key="FontIconContentRefresh"></x:String>
|
||||
@@ -12,6 +13,7 @@
|
||||
<x:String x:Key="FontIconContentBulletedList"></x:String>
|
||||
<x:String x:Key="FontIconContentCheckList"></x:String>
|
||||
<x:String x:Key="FontIconContentWebsite"></x:String>
|
||||
<x:String x:Key="FontIconContentQRCode"></x:String>
|
||||
<x:String x:Key="FontIconContentHomeGroup"></x:String>
|
||||
<x:String x:Key="FontIconContentAsteriskBadge12"></x:String>
|
||||
<x:String x:Key="FontIconContentZipFolder"></x:String>
|
||||
|
||||
@@ -18,41 +18,30 @@ namespace Snap.Hutao.Core.Caching;
|
||||
/// The class's name will become the cache folder's name
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IImageCache))]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
|
||||
internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
|
||||
private static readonly FrozenDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
|
||||
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = new Dictionary<int, TimeSpan>()
|
||||
{
|
||||
[0] = TimeSpan.FromSeconds(4),
|
||||
[1] = TimeSpan.FromSeconds(16),
|
||||
[2] = TimeSpan.FromSeconds(64),
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<ImageCache> logger;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
private string? baseFolder;
|
||||
private string? cacheFolder;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageCache"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public ImageCache(IServiceProvider serviceProvider)
|
||||
{
|
||||
logger = serviceProvider.GetRequiredService<ILogger<ImageCache>>();
|
||||
httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RemoveInvalid()
|
||||
{
|
||||
@@ -62,7 +51,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public void Remove(Uri uriForCachedItem)
|
||||
{
|
||||
Remove(new ReadOnlySpan<Uri>(ref uriForCachedItem));
|
||||
Remove([uriForCachedItem]);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -191,7 +180,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry {Uri} after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
@@ -24,7 +24,7 @@ internal static class DependencyInjection
|
||||
ServiceProvider serviceProvider = new ServiceCollection()
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder.AddUnconditionalDebug())
|
||||
.AddLogging(builder => builder.AddConsoleWindow())
|
||||
.AddMemoryCache()
|
||||
|
||||
// Hutao extensions
|
||||
@@ -39,6 +39,7 @@ internal static class DependencyInjection
|
||||
|
||||
Ioc.Default.ConfigureServices(serviceProvider);
|
||||
|
||||
serviceProvider.InitializeConsoleWindow();
|
||||
serviceProvider.InitializeCulture();
|
||||
|
||||
return serviceProvider;
|
||||
@@ -52,17 +53,18 @@ internal static class DependencyInjection
|
||||
|
||||
CultureInfo cultureInfo = appOptions.CurrentCulture;
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||
CultureInfo.CurrentCulture = cultureInfo;
|
||||
CultureInfo.CurrentUICulture = cultureInfo;
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||
|
||||
ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name;
|
||||
|
||||
SH.Culture = cultureInfo;
|
||||
}
|
||||
|
||||
private static void InitializeConsoleWindow(this IServiceProvider serviceProvider)
|
||||
{
|
||||
_ = serviceProvider.GetRequiredService<ConsoleWindowLifeTime>();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,7 +62,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_name", string.Empty);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
|
||||
@@ -81,7 +81,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.OSVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-language", "zh-cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -11,8 +11,6 @@ namespace Snap.Hutao.Core.Diagnostics;
|
||||
/// </summary>
|
||||
internal readonly struct ValueStopwatch
|
||||
{
|
||||
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
|
||||
|
||||
private readonly long startTimestamp;
|
||||
|
||||
private ValueStopwatch(long startTimestamp)
|
||||
|
||||
@@ -31,7 +31,7 @@ internal sealed partial class ExceptionRecorder
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
ValueTask<string?> task = serviceProvider
|
||||
.GetRequiredService<Web.Hutao.Log.HomaLogUploadClient>()
|
||||
.GetRequiredService<Web.Hutao.Log.HutaoLogUploadClient>()
|
||||
.UploadLogAsync(e.Exception);
|
||||
|
||||
if (!task.IsCompleted)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Http.Sharding;
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
namespace Snap.Hutao.Core.Logging;
|
||||
|
||||
internal sealed class ConsoleWindowLifeTime : IDisposable
|
||||
{
|
||||
private readonly bool consoleWindowAllocated;
|
||||
|
||||
public ConsoleWindowLifeTime()
|
||||
{
|
||||
if (LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false))
|
||||
{
|
||||
consoleWindowAllocated = AllocConsole();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (consoleWindowAllocated)
|
||||
{
|
||||
FreeConsole();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// A logger that writes messages in the debug output window only when a debugger is attached.
|
||||
/// </summary>
|
||||
internal sealed class DebugLogger : ILogger
|
||||
{
|
||||
private readonly string name;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DebugLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the logger.</param>
|
||||
public DebugLogger(string name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
where TState : notnull
|
||||
{
|
||||
return NullScope.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
// If the filter is null, everything is enabled
|
||||
return logLevel != LogLevel.None;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[SuppressMessage("", "SH002")]
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(formatter);
|
||||
|
||||
string message = formatter(state, exception);
|
||||
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
message = $"{logLevel}: {message}";
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
message += Environment.NewLine + Environment.NewLine + exception;
|
||||
}
|
||||
|
||||
DebugWriteLine(message, name);
|
||||
}
|
||||
|
||||
private static void DebugWriteLine(string message, string name)
|
||||
{
|
||||
Debug.WriteLine(message, category: name);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Snap.Hutao.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="ILoggerFactory"/> class.
|
||||
/// </summary>
|
||||
internal static class DebugLoggerFactoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a debug logger named 'Debug' to the factory.
|
||||
/// </summary>
|
||||
/// <param name="builder">The extension method argument.</param>
|
||||
/// <returns>builder</returns>
|
||||
public static ILoggingBuilder AddUnconditionalDebug(this ILoggingBuilder builder)
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DebugLoggerProvider>());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// The provider for the <see cref="DebugLogger"/>.
|
||||
/// </summary>
|
||||
[ProviderAlias("Debug")]
|
||||
internal sealed class DebugLoggerProvider : ILoggerProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new DebugLogger(name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Logging;
|
||||
|
||||
internal static class LoggerFactoryExtensions
|
||||
{
|
||||
public static ILoggingBuilder AddConsoleWindow(this ILoggingBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<ConsoleWindowLifeTime>();
|
||||
|
||||
builder.AddSimpleConsole();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// An empty scope without any logic
|
||||
/// </summary>
|
||||
internal sealed class NullScope : IDisposable
|
||||
{
|
||||
private NullScope()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例
|
||||
/// </summary>
|
||||
public static NullScope Instance { get; } = new NullScope();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
@@ -12,28 +11,16 @@ using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 存储环境相关的选项
|
||||
/// 运行时运算得到的选项,无数据库交互
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class RuntimeOptions : IOptions<RuntimeOptions>
|
||||
internal sealed class RuntimeOptions
|
||||
{
|
||||
private readonly ILogger<RuntimeOptions> logger;
|
||||
|
||||
private readonly bool isWebView2Supported;
|
||||
private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected;
|
||||
|
||||
private bool? isElevated;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃选项
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
public RuntimeOptions(ILogger<RuntimeOptions> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
|
||||
AppLaunchTime = DateTimeOffset.UtcNow;
|
||||
|
||||
DataFolder = GetDataFolderPath();
|
||||
@@ -45,117 +32,95 @@ internal sealed class RuntimeOptions : IOptions<RuntimeOptions>
|
||||
UserAgent = $"Snap Hutao/{Version}";
|
||||
|
||||
DeviceId = GetUniqueUserId();
|
||||
DetectWebView2Environment(ref webView2Version, ref isWebView2Supported);
|
||||
DetectWebView2Environment(logger, out webView2Version, out isWebView2Supported);
|
||||
|
||||
static string GetDataFolderPath()
|
||||
{
|
||||
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(preferredPath))
|
||||
{
|
||||
Directory.CreateDirectory(preferredPath);
|
||||
return preferredPath;
|
||||
}
|
||||
|
||||
// Fallback to MyDocuments
|
||||
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
string path = Path.GetFullPath(Path.Combine(myDocuments, folderName));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
static string GetUniqueUserId()
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
}
|
||||
|
||||
static void DetectWebView2Environment(ILogger<RuntimeOptions> logger, out string webView2Version, out bool isWebView2Supported)
|
||||
{
|
||||
try
|
||||
{
|
||||
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
|
||||
isWebView2Supported = true;
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
webView2Version = SH.CoreWebView2HelperVersionUndetected;
|
||||
isWebView2Supported = false;
|
||||
logger.LogError(ex, "WebView2 Runtime not installed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前版本
|
||||
/// </summary>
|
||||
public Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准UA
|
||||
/// </summary>
|
||||
public string UserAgent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 安装位置
|
||||
/// </summary>
|
||||
public string InstalledLocation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据文件夹路径
|
||||
/// </summary>
|
||||
public string DataFolder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 本地缓存
|
||||
/// </summary>
|
||||
public string LocalCache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 包家族名称
|
||||
/// </summary>
|
||||
public string FamilyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 设备Id
|
||||
/// </summary>
|
||||
public string DeviceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// WebView2 版本
|
||||
/// </summary>
|
||||
public string WebView2Version { get => webView2Version; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持 WebView2
|
||||
/// </summary>
|
||||
public bool IsWebView2Supported { get => isWebView2Supported; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为提升的权限
|
||||
/// </summary>
|
||||
public bool IsElevated { get => isElevated ??= GetElevated(); }
|
||||
public bool IsElevated
|
||||
{
|
||||
get
|
||||
{
|
||||
return isElevated ??= GetElevated();
|
||||
|
||||
static bool GetElevated()
|
||||
{
|
||||
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
WindowsPrincipal principal = new(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset AppLaunchTime { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RuntimeOptions Value { get => this; }
|
||||
|
||||
private static string GetDataFolderPath()
|
||||
{
|
||||
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(preferredPath) && Directory.Exists(preferredPath))
|
||||
{
|
||||
return preferredPath;
|
||||
}
|
||||
|
||||
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static string GetUniqueUserId()
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
}
|
||||
|
||||
private static bool GetElevated()
|
||||
{
|
||||
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
WindowsPrincipal principal = new(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
}
|
||||
|
||||
private void DetectWebView2Environment(ref string webView2Version, ref bool isWebView2Supported)
|
||||
{
|
||||
try
|
||||
{
|
||||
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
|
||||
isWebView2Supported = true;
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
logger.LogError(ex, "WebView2 Runtime not installed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,59 +9,26 @@ namespace Snap.Hutao.Core.Setting;
|
||||
[HighQuality]
|
||||
internal static class SettingKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// 窗体矩形
|
||||
/// </summary>
|
||||
public const string WindowRect = "WindowRect";
|
||||
|
||||
/// <summary>
|
||||
/// 导航侧栏是否展开
|
||||
/// </summary>
|
||||
public const string IsNavPaneOpen = "IsNavPaneOpen";
|
||||
|
||||
/// <summary>
|
||||
/// 启动次数
|
||||
/// </summary>
|
||||
public const string LaunchTimes = "LaunchTimes";
|
||||
|
||||
/// <summary>
|
||||
/// 数据文件夹
|
||||
/// </summary>
|
||||
public const string DataFolderPath = "DataFolderPath";
|
||||
|
||||
/// <summary>
|
||||
/// 通行证用户名(邮箱)
|
||||
/// </summary>
|
||||
public const string PassportUserName = "PassportUserName";
|
||||
|
||||
/// <summary>
|
||||
/// 通行证密码
|
||||
/// </summary>
|
||||
public const string PassportPassword = "PassportPassword";
|
||||
|
||||
/// <summary>
|
||||
/// 消息是否显示
|
||||
/// </summary>
|
||||
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
|
||||
|
||||
/// <summary>
|
||||
/// 1.7.0 版本指引状态
|
||||
/// </summary>
|
||||
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
|
||||
|
||||
/// <summary>
|
||||
/// 排除的系统公告
|
||||
/// </summary>
|
||||
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
|
||||
|
||||
/// <summary>
|
||||
/// 禁用元数据更新检查
|
||||
/// </summary>
|
||||
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
|
||||
|
||||
/// <summary>
|
||||
/// 覆盖管理员权限执行命令
|
||||
/// </summary>
|
||||
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
|
||||
|
||||
public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent";
|
||||
@@ -83,4 +50,6 @@ internal static class SettingKeys
|
||||
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
|
||||
|
||||
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
|
||||
|
||||
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
|
||||
}
|
||||
@@ -8,25 +8,13 @@ namespace Snap.Hutao.Core.Threading;
|
||||
/// </summary>
|
||||
internal interface ITaskContext
|
||||
{
|
||||
SynchronizationContext GetSynchronizationContext();
|
||||
SynchronizationContext SynchronizationContext { get; }
|
||||
|
||||
void BeginInvokeOnMainThread(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// 在主线程上同步等待执行操作
|
||||
/// </summary>
|
||||
/// <param name="action">操作</param>
|
||||
void InvokeOnMainThread(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// 异步切换到 后台线程
|
||||
/// </summary>
|
||||
/// <remarks>使用 <see cref="SwitchToMainThreadAsync"/> 异步切换到 主线程</remarks>
|
||||
/// <returns>等待体</returns>
|
||||
ThreadPoolSwitchOperation SwitchToBackgroundAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步切换到 主线程
|
||||
/// </summary>
|
||||
/// <remarks>使用 <see cref="SwitchToBackgroundAsync"/> 异步切换到 后台线程</remarks>
|
||||
/// <returns>等待体</returns>
|
||||
DispatcherQueueSwitchOperation SwitchToMainThreadAsync();
|
||||
}
|
||||
@@ -24,6 +24,8 @@ internal sealed class TaskContext : ITaskContext
|
||||
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
|
||||
}
|
||||
|
||||
public SynchronizationContext SynchronizationContext { get => synchronizationContext; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ThreadPoolSwitchOperation SwitchToBackgroundAsync()
|
||||
{
|
||||
@@ -42,8 +44,8 @@ internal sealed class TaskContext : ITaskContext
|
||||
dispatcherQueue.Invoke(action);
|
||||
}
|
||||
|
||||
public SynchronizationContext GetSynchronizationContext()
|
||||
public void BeginInvokeOnMainThread(Action action)
|
||||
{
|
||||
return synchronizationContext;
|
||||
dispatcherQueue.TryEnqueue(() => action());
|
||||
}
|
||||
}
|
||||
61
src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs
Normal file
61
src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
internal static class Uuid
|
||||
{
|
||||
public static Guid NewV5(string name, Guid namespaceId)
|
||||
{
|
||||
Span<byte> namespaceBuffer = stackalloc byte[16];
|
||||
Verify.Operation(namespaceId.TryWriteBytes(namespaceBuffer), "Failed to copy namespace guid bytes");
|
||||
Span<byte> nameBytes = Encoding.UTF8.GetBytes(name);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
ReverseEndianness(namespaceBuffer);
|
||||
}
|
||||
|
||||
Span<byte> data = stackalloc byte[namespaceBuffer.Length + nameBytes.Length];
|
||||
namespaceBuffer.CopyTo(data);
|
||||
nameBytes.CopyTo(data[namespaceBuffer.Length..]);
|
||||
|
||||
Span<byte> temp = stackalloc byte[20];
|
||||
Verify.Operation(SHA1.TryHashData(data, temp, out _), "Failed to compute SHA1 hash of UUID");
|
||||
|
||||
Span<byte> hash = temp[..16];
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
ReverseEndianness(hash);
|
||||
}
|
||||
|
||||
hash[8] &= 0x3F;
|
||||
hash[8] |= 0x80;
|
||||
|
||||
int versionIndex = BitConverter.IsLittleEndian ? 7 : 6;
|
||||
|
||||
hash[versionIndex] &= 0x0F;
|
||||
hash[versionIndex] |= 0x50;
|
||||
|
||||
return new(hash);
|
||||
}
|
||||
|
||||
private static void ReverseEndianness(in Span<byte> guidByte)
|
||||
{
|
||||
ExchangeBytes(guidByte, 0, 3);
|
||||
ExchangeBytes(guidByte, 1, 2);
|
||||
ExchangeBytes(guidByte, 4, 5);
|
||||
ExchangeBytes(guidByte, 6, 7);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ExchangeBytes(in Span<byte> guid, int left, int right)
|
||||
{
|
||||
(guid[right], guid[left]) = (guid[left], guid[right]);
|
||||
}
|
||||
}
|
||||
@@ -11,35 +11,27 @@ internal static class DateTimeOffsetExtension
|
||||
{
|
||||
public static readonly DateTimeOffset DatebaseDefaultTime = new(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0));
|
||||
|
||||
/// <summary>
|
||||
/// 从Unix时间戳转换
|
||||
/// </summary>
|
||||
/// <param name="timestamp">时间戳</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>转换的时间</returns>
|
||||
public static DateTimeOffset FromUnixTime(long? timestamp, in DateTimeOffset defaultValue)
|
||||
public static DateTimeOffset UnsafeRelaxedFromUnixTime(long? timestamp, in DateTimeOffset defaultValue)
|
||||
{
|
||||
if (timestamp is { } value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(value);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeMilliseconds(value);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
if (timestamp is not { } value)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(value);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeMilliseconds(value);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ internal static partial class EnumerableExtension
|
||||
return results;
|
||||
}
|
||||
|
||||
public static async ValueTask<List<TResult>> SelectListAsync<TSource, TResult>(this List<TSource> list, Func<TSource, CancellationToken, ValueTask<TResult>> selector, CancellationToken token)
|
||||
public static async ValueTask<List<TResult>> SelectListAsync<TSource, TResult>(this List<TSource> list, Func<TSource, CancellationToken, ValueTask<TResult>> selector, CancellationToken token = default)
|
||||
{
|
||||
List<TResult> results = new(list.Count);
|
||||
|
||||
@@ -207,4 +207,4 @@ internal static partial class EnumerableExtension
|
||||
list.Sort((left, right) => keySelector(right).CompareTo(keySelector(left)));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Extension;
|
||||
|
||||
internal static partial class EnumerableExtension
|
||||
{
|
||||
public static bool TryGetValue(this NameValueCollection collection, string name, [NotNullWhen(true)] out string? value)
|
||||
public static bool TryGetSingleValue(this NameValueCollection collection, string name, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
if (collection.AllKeys.Contains(name))
|
||||
{
|
||||
|
||||
@@ -17,19 +17,6 @@ internal static partial class EnumerableExtension
|
||||
return source.ElementAtOrDefault(index) ?? source.LastOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 如果传入集合不为空则原路返回,
|
||||
/// 如果传入集合为空返回一个集合的空集
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <returns>源集合或空集</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IEnumerable<TSource> EmptyIfNull<TSource>(this IEnumerable<TSource>? source)
|
||||
{
|
||||
return source ?? Enumerable.Empty<TSource>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 寻找枚举中唯一的值,找不到时
|
||||
/// 回退到首个或默认值
|
||||
|
||||
@@ -10,13 +10,6 @@ namespace Snap.Hutao.Extension;
|
||||
/// </summary>
|
||||
internal static class MemoryCacheExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试从 IMemoryCache 中移除并返回具有指定键的值
|
||||
/// </summary>
|
||||
/// <param name="memoryCache">缓存</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>是否移除成功</returns>
|
||||
public static bool TryRemove(this IMemoryCache memoryCache, string key, out object? value)
|
||||
{
|
||||
if (!memoryCache.TryGetValue(key, out value))
|
||||
@@ -27,4 +20,16 @@ internal static class MemoryCacheExtension
|
||||
memoryCache.Remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetRequiredValue<T>(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out T? value)
|
||||
where T : class
|
||||
{
|
||||
if (!memoryCache.TryGetValue(key, out value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,22 @@ internal static class NullableExtension
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string ToStringOrEmpty<T>(this in T? nullable)
|
||||
where T : struct
|
||||
{
|
||||
string? result = default;
|
||||
|
||||
if (nullable.HasValue)
|
||||
{
|
||||
result = nullable.Value.ToString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
result = string.Empty;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
@@ -18,9 +17,9 @@ internal static class SpanExtension
|
||||
/// <param name="span">Span</param>
|
||||
/// <returns>最大值的下标</returns>
|
||||
public static int IndexOfMax<T>(this in ReadOnlySpan<T> span)
|
||||
where T : INumber<T>
|
||||
where T : INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
T max = T.Zero;
|
||||
T max = T.MinValue;
|
||||
int maxIndex = 0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
@@ -75,9 +74,4 @@ internal static class SpanExtension
|
||||
|
||||
return unchecked((byte)(sum / count));
|
||||
}
|
||||
|
||||
public static Span<T> AsSpan<T>(this List<T> list)
|
||||
{
|
||||
return CollectionsMarshal.AsSpan(list);
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,23 @@ internal static class StringBuilderExtension
|
||||
return condition ? sb.Append(value) : sb;
|
||||
}
|
||||
|
||||
public static string ToStringTrimEnd(this StringBuilder builder)
|
||||
{
|
||||
if (builder.Length > 1 && char.IsWhiteSpace(builder[^1]))
|
||||
{
|
||||
return builder.ToString(0, builder.Length - 1);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static string ToStringTrimEndReturn(this StringBuilder builder)
|
||||
{
|
||||
Must.Argument(builder.Length >= 1, "StringBuilder 的长度必须大于 0");
|
||||
if (builder.Length < 1)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
int remove = 0;
|
||||
if (builder[^1] is '\n')
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ internal static class WinRTExtension
|
||||
}
|
||||
|
||||
// protected bool disposed;
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name ="disposed")]
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "disposed")]
|
||||
private static extern ref bool GetProtectedDisposed(IObjectReference objRef);
|
||||
|
||||
// private object _disposedLock
|
||||
|
||||
@@ -11,6 +11,6 @@ internal sealed partial class ProgressFactory : IProgressFactory
|
||||
|
||||
public IProgress<T> CreateForMainThread<T>(Action<T> handler)
|
||||
{
|
||||
return new DispatcherQueueProgress<T>(handler, taskContext.GetSynchronizationContext());
|
||||
return new DispatcherQueueProgress<T>(handler, taskContext.SynchronizationContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Factory.QrCode;
|
||||
|
||||
internal interface IQRCodeFactory
|
||||
{
|
||||
byte[] Create(string source);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using QRCoder;
|
||||
|
||||
namespace Snap.Hutao.Factory.QrCode;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IQRCodeFactory))]
|
||||
internal class QRCodeFactory : IQRCodeFactory
|
||||
{
|
||||
public byte[] Create(string source)
|
||||
{
|
||||
using (QRCodeGenerator generator = new())
|
||||
{
|
||||
using (QRCodeData data = generator.CreateQrCode(source, QRCodeGenerator.ECCLevel.Q))
|
||||
{
|
||||
using (BitmapByteQRCode code = new(data))
|
||||
{
|
||||
return code.GetGraphic(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
612
src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs
generated
Normal file
612
src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs
generated
Normal file
@@ -0,0 +1,612 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20231207085530_AddCultivateEntryLevelInformation")]
|
||||
partial class AddCultivateEntryLevelInformation
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ArchiveId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("Current")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("achievements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("achievement_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CalculatorRefreshTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("GameRecordRefreshTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Info")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ShowcaseRefreshTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("avatar_infos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("cultivate_entries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("AvatarLevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("AvatarLevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("EntryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("SkillALevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillALevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillELevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillELevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillQLevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillQLevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("WeaponLevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("WeaponLevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("EntryId");
|
||||
|
||||
b.ToTable("cultivate_entry_level_informations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("EntryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsFinished")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("EntryId");
|
||||
|
||||
b.ToTable("cultivate_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachedUid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("cultivate_projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DailyNote")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DailyTaskNotify")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("DailyTaskNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ExpeditionNotify")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ExpeditionNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("HomeCoinNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("HomeCoinNotifyThreshold")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("RefreshTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ResinNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ResinNotifyThreshold")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TransformerNotify")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TransformerNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("daily_notes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("gacha_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ArchiveId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GachaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QueryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("gacha_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachUid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MihoyoSDK")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("game_accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppendPropIdList")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MainPropId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_reliquaries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PromoteLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_weapons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpireTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("object_cache");
|
||||
});
|
||||
|
||||
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.SpiralAbyssEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("ScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SpiralAbyss")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("spiral_abysses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Aid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CookieToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CookieTokenLastUpdateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Fingerprint")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("FingerprintLastUpdateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsOversea")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("Ltoken");
|
||||
|
||||
b.Property<string>("Mid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("Stoken");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
|
||||
.WithMany()
|
||||
.HasForeignKey("ArchiveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Archive");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||
.WithMany()
|
||||
.HasForeignKey("EntryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Entry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||
.WithMany()
|
||||
.HasForeignKey("EntryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Entry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
|
||||
.WithMany()
|
||||
.HasForeignKey("ArchiveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Archive");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCultivateEntryLevelInformation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "cultivate_entry_level_informations",
|
||||
columns: table => new
|
||||
{
|
||||
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
EntryId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
AvatarLevelFrom = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
AvatarLevelTo = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
SkillALevelFrom = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
SkillALevelTo = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
SkillELevelFrom = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
SkillELevelTo = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
SkillQLevelFrom = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
SkillQLevelTo = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
WeaponLevelFrom = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
WeaponLevelTo = table.Column<uint>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_cultivate_entry_level_informations", x => x.InnerId);
|
||||
table.ForeignKey(
|
||||
name: "FK_cultivate_entry_level_informations_cultivate_entries_EntryId",
|
||||
column: x => x.EntryId,
|
||||
principalTable: "cultivate_entries",
|
||||
principalColumn: "InnerId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_cultivate_entry_level_informations_EntryId",
|
||||
table: "cultivate_entry_level_informations",
|
||||
column: "EntryId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "cultivate_entry_level_informations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("achievements");
|
||||
b.ToTable("achievements", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
|
||||
@@ -60,7 +60,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("achievement_archives");
|
||||
b.ToTable("achievement_archives", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
|
||||
@@ -88,7 +88,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("avatar_infos");
|
||||
b.ToTable("avatar_infos", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||
@@ -110,7 +110,53 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("cultivate_entries");
|
||||
b.ToTable("cultivate_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("AvatarLevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("AvatarLevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("EntryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("SkillALevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillALevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillELevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillELevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillQLevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("SkillQLevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("WeaponLevelFrom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("WeaponLevelTo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("EntryId");
|
||||
|
||||
b.ToTable("cultivate_entry_level_informations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||
@@ -119,7 +165,7 @@ namespace Snap.Hutao.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Count")
|
||||
b.Property<uint>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("EntryId")
|
||||
@@ -128,14 +174,14 @@ namespace Snap.Hutao.Migrations
|
||||
b.Property<bool>("IsFinished")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
b.Property<uint>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("EntryId");
|
||||
|
||||
b.ToTable("cultivate_items");
|
||||
b.ToTable("cultivate_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
|
||||
@@ -156,7 +202,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("cultivate_projects");
|
||||
b.ToTable("cultivate_projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||
@@ -212,7 +258,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("daily_notes");
|
||||
b.ToTable("daily_notes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
|
||||
@@ -230,7 +276,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("gacha_archives");
|
||||
b.ToTable("gacha_archives", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
@@ -261,7 +307,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("gacha_items");
|
||||
b.ToTable("gacha_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||
@@ -286,7 +332,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("game_accounts");
|
||||
b.ToTable("game_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||
@@ -308,7 +354,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_items");
|
||||
b.ToTable("inventory_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
|
||||
@@ -337,7 +383,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_reliquaries");
|
||||
b.ToTable("inventory_reliquaries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
|
||||
@@ -362,7 +408,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_weapons");
|
||||
b.ToTable("inventory_weapons", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
|
||||
@@ -378,7 +424,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("object_cache");
|
||||
b.ToTable("object_cache", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||
@@ -391,7 +437,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("settings");
|
||||
b.ToTable("settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
|
||||
@@ -413,7 +459,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("spiral_abysses");
|
||||
b.ToTable("spiral_abysses", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
|
||||
@@ -456,7 +502,7 @@ namespace Snap.Hutao.Migrations
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("users");
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
@@ -481,6 +527,17 @@ namespace Snap.Hutao.Migrations
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||
.WithMany()
|
||||
.HasForeignKey("EntryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Entry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||
|
||||
@@ -33,6 +33,8 @@ internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry,
|
||||
[ForeignKey(nameof(ProjectId))]
|
||||
public CultivateProject Project { get; set; } = default!;
|
||||
|
||||
public CultivateEntryLevelInformation? LevelInformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 养成类型
|
||||
/// </summary>
|
||||
@@ -59,4 +61,10 @@ internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry,
|
||||
Id = id,
|
||||
};
|
||||
}
|
||||
|
||||
public static CultivateEntry Join(CultivateEntry entry, CultivateEntryLevelInformation levelInformation)
|
||||
{
|
||||
entry.LevelInformation = levelInformation;
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Snap.Hutao.Model.Entity;
|
||||
|
||||
[Table("cultivate_entry_level_informations")]
|
||||
internal sealed class CultivateEntryLevelInformation : IMappingFrom<CultivateEntryLevelInformation, Guid, CultivateType, LevelInformation>
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid InnerId { get; set; }
|
||||
|
||||
public Guid EntryId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(EntryId))]
|
||||
public CultivateEntry? Entry { get; set; }
|
||||
|
||||
public uint AvatarLevelFrom { get; set; }
|
||||
|
||||
public uint AvatarLevelTo { get; set; }
|
||||
|
||||
public uint SkillALevelFrom { get; set; }
|
||||
|
||||
public uint SkillALevelTo { get; set; }
|
||||
|
||||
public uint SkillELevelFrom { get; set; }
|
||||
|
||||
public uint SkillELevelTo { get; set; }
|
||||
|
||||
public uint SkillQLevelFrom { get; set; }
|
||||
|
||||
public uint SkillQLevelTo { get; set; }
|
||||
|
||||
public uint WeaponLevelFrom { get; set; }
|
||||
|
||||
public uint WeaponLevelTo { get; set; }
|
||||
|
||||
public static CultivateEntryLevelInformation From(Guid entryId, CultivateType type, LevelInformation source)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
CultivateType.AvatarAndSkill => new()
|
||||
{
|
||||
EntryId = entryId,
|
||||
AvatarLevelFrom = source.AvatarLevelFrom,
|
||||
AvatarLevelTo = source.AvatarLevelTo,
|
||||
SkillALevelFrom = source.SkillALevelFrom,
|
||||
SkillALevelTo = source.SkillALevelTo,
|
||||
SkillELevelFrom = source.SkillELevelFrom,
|
||||
SkillELevelTo = source.SkillELevelTo,
|
||||
SkillQLevelFrom = source.SkillQLevelFrom,
|
||||
SkillQLevelTo = source.SkillQLevelTo,
|
||||
},
|
||||
CultivateType.Weapon => new()
|
||||
{
|
||||
EntryId = entryId,
|
||||
WeaponLevelFrom = source.WeaponLevelFrom,
|
||||
WeaponLevelTo = source.WeaponLevelTo,
|
||||
},
|
||||
_ => throw Must.NeverHappen($"不支持的养成类型{type}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,12 @@ internal sealed class CultivateItem : IDbMappingForeignKeyFrom<CultivateItem, We
|
||||
/// <summary>
|
||||
/// 物品 Id
|
||||
/// </summary>
|
||||
public int ItemId { get; set; }
|
||||
public uint ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 物品个数
|
||||
/// </summary>
|
||||
public int Count { get; set; }
|
||||
public uint Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否完成此项
|
||||
|
||||
@@ -37,89 +37,40 @@ internal sealed class AppDbContext : DbContext
|
||||
logger.LogInformation("{Name}[{Id}] created", nameof(AppDbContext), ContextId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置
|
||||
/// </summary>
|
||||
public DbSet<SettingEntry> Settings { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 用户
|
||||
/// </summary>
|
||||
public DbSet<User> Users { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 成就
|
||||
/// </summary>
|
||||
public DbSet<Achievement> Achievements { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 成就存档
|
||||
/// </summary>
|
||||
public DbSet<AchievementArchive> AchievementArchives { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 卡池数据
|
||||
/// </summary>
|
||||
public DbSet<GachaItem> GachaItems { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 卡池存档
|
||||
/// </summary>
|
||||
public DbSet<GachaArchive> GachaArchives { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 角色信息
|
||||
/// </summary>
|
||||
public DbSet<AvatarInfo> AvatarInfos { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏内账号
|
||||
/// </summary>
|
||||
public DbSet<GameAccount> GameAccounts { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺
|
||||
/// </summary>
|
||||
public DbSet<DailyNoteEntry> DailyNotes { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 对象缓存
|
||||
/// </summary>
|
||||
public DbSet<ObjectCacheEntry> ObjectCache { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 培养计划
|
||||
/// </summary>
|
||||
public DbSet<CultivateProject> CultivateProjects { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 培养入口点
|
||||
/// </summary>
|
||||
public DbSet<CultivateEntry> CultivateEntries { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 培养消耗物品
|
||||
/// </summary>
|
||||
public DbSet<CultivateEntryLevelInformation> LevelInformations { get; set; } = default!;
|
||||
|
||||
public DbSet<CultivateItem> CultivateItems { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 背包内物品
|
||||
/// </summary>
|
||||
public DbSet<InventoryItem> InventoryItems { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 背包内武器
|
||||
/// </summary>
|
||||
public DbSet<InventoryWeapon> InventoryWeapons { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 背包内圣遗物
|
||||
/// </summary>
|
||||
public DbSet<InventoryReliquary> InventoryReliquaries { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 深渊记录
|
||||
/// </summary>
|
||||
public DbSet<SpiralAbyssEntry> SpiralAbysses { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -30,7 +30,7 @@ internal sealed class UIAFInfo : IMappingFrom<UIAFInfo, RuntimeOptions>
|
||||
[JsonIgnore]
|
||||
public DateTimeOffset ExportDateTime
|
||||
{
|
||||
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -38,7 +38,7 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
|
||||
[JsonIgnore]
|
||||
public DateTimeOffset ExportDateTime
|
||||
{
|
||||
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -35,7 +35,7 @@ internal sealed class UIIFInfo
|
||||
[JsonIgnore]
|
||||
public DateTimeOffset ExportDateTime
|
||||
{
|
||||
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ internal sealed class Material : DisplayItem
|
||||
/// <returns>是否为物品栏物品</returns>
|
||||
public bool IsInventoryItem()
|
||||
{
|
||||
// TODO: Add a pre-filtered metadata set to check if it's an inventory item
|
||||
|
||||
// 原质
|
||||
if (Id == 112001U)
|
||||
{
|
||||
|
||||
@@ -504,7 +504,7 @@
|
||||
<value>精炼 {0} 阶</value>
|
||||
</data>
|
||||
<data name="MustSelectUserAndUid" xml:space="preserve">
|
||||
<value>必须先选择一个用户与角色</value>
|
||||
<value>必须登录 米游社/HoYoLAB 并选择一个用户与角色</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceDeleteEntrySucceed" xml:space="preserve">
|
||||
<value>删除了 Uid:{0} 的 {1} 条祈愿记录</value>
|
||||
@@ -1265,6 +1265,9 @@
|
||||
<data name="ViewDialogLaunchGamePackageConvertTitle" xml:space="preserve">
|
||||
<value>正在转换客户端</value>
|
||||
</data>
|
||||
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
|
||||
<value>使用米游社扫描二维码</value>
|
||||
</data>
|
||||
<data name="ViewDialogSettingDeleteUserDataContent" xml:space="preserve">
|
||||
<value>该操作是不可逆的,所有用户登录状态会丢失</value>
|
||||
</data>
|
||||
@@ -1406,6 +1409,9 @@
|
||||
<data name="ViewModelCultivationEntryAddWarning" xml:space="preserve">
|
||||
<value>请先前往养成计划页面创建计划并选中</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryViewDescriptionDefault" xml:space="preserve">
|
||||
<value>重新添加物品以查看养成描述</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationProjectAdded" xml:space="preserve">
|
||||
<value>添加成功</value>
|
||||
</data>
|
||||
@@ -1562,6 +1568,9 @@
|
||||
<data name="ViewModelSettingCreateDesktopShortcutFailed" xml:space="preserve">
|
||||
<value>创建桌面快捷方式失败</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingFolderSizeDescription" xml:space="preserve">
|
||||
<value>已使用磁盘空间:{0}</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingGeetestCustomUrlSucceed" xml:space="preserve">
|
||||
<value>无感验证复合 Url 配置成功</value>
|
||||
</data>
|
||||
@@ -2127,7 +2136,7 @@
|
||||
<value>预下载</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
|
||||
<value>该账号尚未绑定 UID</value>
|
||||
<value>该账号尚未绑定实时便笺通知 UID</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidToolTip" xml:space="preserve">
|
||||
<value>绑定当前用户的角色</value>
|
||||
@@ -2202,7 +2211,7 @@
|
||||
<value>图片缓存 在此处存放</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingCacheFolderHeader" xml:space="preserve">
|
||||
<value>打开 缓存 文件夹</value>
|
||||
<value>缓存 文件夹</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingCopyDeviceIdAction" xml:space="preserve">
|
||||
<value>复制</value>
|
||||
@@ -2229,7 +2238,7 @@
|
||||
<value>用户数据/元数据 在此处存放</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingDataFolderHeader" xml:space="preserve">
|
||||
<value>打开 数据 文件夹</value>
|
||||
<value>数据 文件夹</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingDeleteCacheAction" xml:space="preserve">
|
||||
<value>删除</value>
|
||||
@@ -2249,6 +2258,12 @@
|
||||
<data name="ViewPageSettingDeviceIdHeader" xml:space="preserve">
|
||||
<value>设备 ID</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingDeviceIpDescription" xml:space="preserve">
|
||||
<value>IP:{0} 归属服务器:{1}</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
|
||||
<value>设备 IP</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
|
||||
<value>在祈愿记录页面显示或隐藏无记录的历史祈愿活动</value>
|
||||
</data>
|
||||
@@ -2516,6 +2531,15 @@
|
||||
<data name="ViewServiceHutaoUserLoginOrRegisterHint" xml:space="preserve">
|
||||
<value>立即登录或注册</value>
|
||||
</data>
|
||||
<data name="ViewSettingAllocConsoleDescription" xml:space="preserve">
|
||||
<value>控制胡桃启动时是否开启控制台,重启后生效</value>
|
||||
</data>
|
||||
<data name="ViewSettingAllocConsoleHeader" xml:space="preserve">
|
||||
<value>调试控制台</value>
|
||||
</data>
|
||||
<data name="ViewSettingFolderViewOpenFolderAction" xml:space="preserve">
|
||||
<value>打开文件夹</value>
|
||||
</data>
|
||||
<data name="ViewSpiralAbyssAvatarAppearanceRankDescription" xml:space="preserve">
|
||||
<value>角色出场率 = 本层上阵该角色次数(层内重复出现只记一次)/ 深渊记录总数</value>
|
||||
</data>
|
||||
@@ -2603,6 +2627,9 @@
|
||||
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
|
||||
<value>网页登录</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationLoginQRCodeAction" xml:space="preserve">
|
||||
<value>扫码登录</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationManualInputAction" xml:space="preserve">
|
||||
<value>手动输入</value>
|
||||
</data>
|
||||
@@ -2822,6 +2849,9 @@
|
||||
<data name="WebHoyolabInvalidUid" xml:space="preserve">
|
||||
<value>无效的 UID</value>
|
||||
</data>
|
||||
<data name="WebHutaoServiceUnAvailable" xml:space="preserve">
|
||||
<value>胡桃服务维护中</value>
|
||||
</data>
|
||||
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
|
||||
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
|
||||
</data>
|
||||
|
||||
@@ -116,7 +116,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
/// <param name="deserializer">反序列化器</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>值</returns>
|
||||
[return:NotNull]
|
||||
[return: NotNull]
|
||||
protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, [DisallowNull] T defaultValue)
|
||||
{
|
||||
if (storage is not null)
|
||||
@@ -128,7 +128,16 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value;
|
||||
storage = value is null ? defaultValue : deserializer(value)!;
|
||||
if (value is null)
|
||||
{
|
||||
storage = defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
T targetValue = deserializer(value);
|
||||
ArgumentNullException.ThrowIfNull(targetValue);
|
||||
storage = targetValue;
|
||||
}
|
||||
}
|
||||
|
||||
return storage;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
@@ -27,11 +28,9 @@ internal sealed partial class AnnouncementService : IAnnouncementService
|
||||
public async ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 缓存中存在记录,直接返回
|
||||
if (memoryCache.TryGetValue(CacheKey, out object? cache))
|
||||
if (memoryCache.TryGetRequiredValue(CacheKey, out AnnouncementWrapper? cache))
|
||||
{
|
||||
AnnouncementWrapper? wrapper = (AnnouncementWrapper?)cache;
|
||||
ArgumentNullException.ThrowIfNull(wrapper);
|
||||
return wrapper;
|
||||
return cache;
|
||||
}
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
@@ -39,38 +38,40 @@ internal sealed partial class AnnouncementService : IAnnouncementService
|
||||
.GetAnnouncementsAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (announcementWrapperResponse.IsOk())
|
||||
if (!announcementWrapperResponse.IsOk())
|
||||
{
|
||||
AnnouncementWrapper wrapper = announcementWrapperResponse.Data;
|
||||
Response<ListWrapper<AnnouncementContent>> announcementContentResponse = await announcementClient
|
||||
.GetAnnouncementContentsAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (announcementContentResponse.IsOk())
|
||||
{
|
||||
List<AnnouncementContent> contents = announcementContentResponse.Data.List;
|
||||
|
||||
Dictionary<int, string> contentMap = contents
|
||||
.ToDictionary(id => id.AnnId, content => content.Content);
|
||||
|
||||
// 将活动公告置于前方
|
||||
wrapper.List.Reverse();
|
||||
|
||||
PreprocessAnnouncements(contentMap, wrapper.List);
|
||||
|
||||
return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30));
|
||||
}
|
||||
return default!;
|
||||
}
|
||||
|
||||
return default!;
|
||||
AnnouncementWrapper wrapper = announcementWrapperResponse.Data;
|
||||
Response<ListWrapper<AnnouncementContent>> announcementContentResponse = await announcementClient
|
||||
.GetAnnouncementContentsAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!announcementContentResponse.IsOk())
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
|
||||
List<AnnouncementContent> contents = announcementContentResponse.Data.List;
|
||||
|
||||
Dictionary<int, string> contentMap = contents
|
||||
.ToDictionary(id => id.AnnId, content => content.Content);
|
||||
|
||||
// 将活动公告置于前方
|
||||
wrapper.List.Reverse();
|
||||
|
||||
PreprocessAnnouncements(contentMap, wrapper.List);
|
||||
|
||||
return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30));
|
||||
}
|
||||
|
||||
private static void PreprocessAnnouncements(Dictionary<int, string> contentMap, List<AnnouncementListWrapper> announcementListWrappers)
|
||||
{
|
||||
// 将公告内容联入公告列表
|
||||
foreach (ref AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
|
||||
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
|
||||
{
|
||||
foreach (ref Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
|
||||
foreach (ref readonly Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
|
||||
{
|
||||
contentMap.TryGetValue(item.AnnId, out string? rawContent);
|
||||
item.Content = rawContent ?? string.Empty;
|
||||
@@ -79,10 +80,11 @@ internal sealed partial class AnnouncementService : IAnnouncementService
|
||||
|
||||
AdjustAnnouncementTime(announcementListWrappers);
|
||||
|
||||
foreach (ref AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
|
||||
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
|
||||
{
|
||||
foreach (ref Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
|
||||
foreach (ref readonly Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
|
||||
{
|
||||
item.Subtitle = new StringBuilder(item.Subtitle).Replace("\r<br>", string.Empty).ToString();
|
||||
item.Content = AnnouncementRegex.XmlTimeTagRegex.Replace(item.Content, x => x.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
@@ -101,56 +103,60 @@ internal sealed partial class AnnouncementService : IAnnouncementService
|
||||
.List
|
||||
.Single(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title));
|
||||
|
||||
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is { Success: true } match)
|
||||
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } match)
|
||||
{
|
||||
DateTimeOffset versionUpdateTime = DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture);
|
||||
_ = 1;
|
||||
foreach (ref readonly Announcement announcement in CollectionsMarshal.AsSpan(activities))
|
||||
return;
|
||||
}
|
||||
|
||||
DateTimeOffset versionUpdateTime = DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture);
|
||||
|
||||
foreach (ref readonly Announcement announcement in CollectionsMarshal.AsSpan(activities))
|
||||
{
|
||||
if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent)
|
||||
{
|
||||
if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent)
|
||||
announcement.StartTime = versionUpdateTime;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (AnnouncementRegex.PersistentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } persistent)
|
||||
{
|
||||
announcement.StartTime = versionUpdateTime;
|
||||
announcement.EndTime = versionUpdateTime + TimeSpan.FromDays(42);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient)
|
||||
{
|
||||
announcement.StartTime = versionUpdateTime;
|
||||
announcement.EndTime = DateTimeOffset.Parse(transient.Groups[2].ValueSpan, CultureInfo.InvariantCulture);
|
||||
continue;
|
||||
}
|
||||
|
||||
MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex.Matches(announcement.Content);
|
||||
if (matches.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<DateTimeOffset> dateTimes = matches.Select(match => DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture)).ToList();
|
||||
DateTimeOffset min = DateTimeOffset.MaxValue;
|
||||
DateTimeOffset max = DateTimeOffset.MinValue;
|
||||
|
||||
foreach (DateTimeOffset time in dateTimes)
|
||||
{
|
||||
if (time < min)
|
||||
{
|
||||
announcement.StartTime = versionUpdateTime;
|
||||
continue;
|
||||
min = time;
|
||||
}
|
||||
|
||||
if (AnnouncementRegex.PersistentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } persistent)
|
||||
if (time > max)
|
||||
{
|
||||
announcement.StartTime = versionUpdateTime;
|
||||
announcement.EndTime = versionUpdateTime + TimeSpan.FromDays(42);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient)
|
||||
{
|
||||
announcement.StartTime = versionUpdateTime;
|
||||
announcement.EndTime = DateTimeOffset.Parse(transient.Groups[2].ValueSpan, CultureInfo.InvariantCulture);
|
||||
continue;
|
||||
}
|
||||
|
||||
MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex.Matches(announcement.Content);
|
||||
if (matches.Count >= 2)
|
||||
{
|
||||
List<DateTimeOffset> dateTimes = matches.Select(match => DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture)).ToList();
|
||||
DateTimeOffset min = DateTimeOffset.MaxValue;
|
||||
DateTimeOffset max = DateTimeOffset.MinValue;
|
||||
|
||||
foreach (DateTimeOffset time in dateTimes)
|
||||
{
|
||||
if (time < min)
|
||||
{
|
||||
min = time;
|
||||
}
|
||||
|
||||
if (time > max)
|
||||
{
|
||||
max = time;
|
||||
}
|
||||
}
|
||||
|
||||
announcement.StartTime = min;
|
||||
announcement.EndTime = max;
|
||||
max = time;
|
||||
}
|
||||
}
|
||||
|
||||
announcement.StartTime = min;
|
||||
announcement.EndTime = max;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,25 +11,10 @@ using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序选项
|
||||
/// 存储服务相关的选项
|
||||
/// </summary>
|
||||
[ConstructorGenerated(CallBaseConstructor = true)]
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class AppOptions : DbStoreOptions
|
||||
{
|
||||
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = CollectionsNameValue.FromEnum<BackdropType>();
|
||||
|
||||
private readonly List<NameValue<CultureInfo>> supportedCulturesInner =
|
||||
[
|
||||
ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")),
|
||||
ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")),
|
||||
ToNameValue(CultureInfo.GetCultureInfo("en")),
|
||||
ToNameValue(CultureInfo.GetCultureInfo("ko")),
|
||||
ToNameValue(CultureInfo.GetCultureInfo("ja")),
|
||||
];
|
||||
|
||||
private string? gamePath;
|
||||
private string? powerShellPath;
|
||||
private bool? isEmptyHistoryWishVisible;
|
||||
@@ -38,75 +23,67 @@ internal sealed partial class AppOptions : DbStoreOptions
|
||||
private bool? isAdvancedLaunchOptionsEnabled;
|
||||
private string? geetestCustomCompositeUrl;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public string GamePath
|
||||
{
|
||||
get => GetOption(ref gamePath, SettingEntry.GamePath);
|
||||
set => SetOption(ref gamePath, SettingEntry.GamePath, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PowerShell 路径
|
||||
/// </summary>
|
||||
public string PowerShellPath
|
||||
{
|
||||
get => GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetPowerShellLocationOrEmpty);
|
||||
get
|
||||
{
|
||||
return GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetDefaultPowerShellLocationOrEmpty);
|
||||
|
||||
static string GetDefaultPowerShellLocationOrEmpty()
|
||||
{
|
||||
string? paths = Environment.GetEnvironmentVariable("Path");
|
||||
if (!string.IsNullOrEmpty(paths))
|
||||
{
|
||||
foreach (StringSegment path in new StringTokenizer(paths, [';']))
|
||||
{
|
||||
if (path is { HasValue: true, Length: > 0 })
|
||||
{
|
||||
if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Path.Combine(path.Value, "powershell.exe");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
set => SetOption(ref powerShellPath, SettingEntry.PowerShellPath, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public bool IsEmptyHistoryWishVisible
|
||||
{
|
||||
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible);
|
||||
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 所有支持的背景样式
|
||||
/// </summary>
|
||||
public List<NameValue<BackdropType>> BackdropTypes { get => supportedBackdropTypesInner; }
|
||||
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>();
|
||||
|
||||
/// <summary>
|
||||
/// 背景类型 默认 Mica
|
||||
/// </summary>
|
||||
public BackdropType BackdropType
|
||||
{
|
||||
get => GetOption(ref backdropType, SettingEntry.SystemBackdropType, v => Enum.Parse<BackdropType>(v), BackdropType.Mica).Value;
|
||||
set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToString()!);
|
||||
set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToStringOrEmpty());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 所有支持的语言
|
||||
/// </summary>
|
||||
public List<NameValue<CultureInfo>> Cultures { get => supportedCulturesInner; }
|
||||
public List<NameValue<CultureInfo>> Cultures { get; } = SupportedCultures.Get();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化前的语言
|
||||
/// 通过设置与获取此属性,就可以获取到与系统同步的语言
|
||||
/// </summary>
|
||||
public CultureInfo PreviousCulture { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 当前语言
|
||||
/// 默认为系统语言
|
||||
/// </summary>
|
||||
public CultureInfo CurrentCulture
|
||||
{
|
||||
get => GetOption(ref currentCulture, SettingEntry.Culture, CultureInfo.GetCultureInfo, CultureInfo.CurrentCulture);
|
||||
set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用高级功能
|
||||
/// DO NOT MOVE TO OTHER CLASS
|
||||
/// We are binding this property in SettingPage
|
||||
/// </summary>
|
||||
public bool IsAdvancedLaunchOptionsEnabled
|
||||
{
|
||||
// DO NOT MOVE TO OTHER CLASS
|
||||
// We use this property in SettingPage binding
|
||||
get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
|
||||
set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
|
||||
}
|
||||
@@ -117,28 +94,5 @@ internal sealed partial class AppOptions : DbStoreOptions
|
||||
set => SetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl, value);
|
||||
}
|
||||
|
||||
private static NameValue<CultureInfo> ToNameValue(CultureInfo info)
|
||||
{
|
||||
return new(info.NativeName, info);
|
||||
}
|
||||
|
||||
private static string GetPowerShellLocationOrEmpty()
|
||||
{
|
||||
string? paths = Environment.GetEnvironmentVariable("Path");
|
||||
if (!string.IsNullOrEmpty(paths))
|
||||
{
|
||||
foreach (StringSegment path in new StringTokenizer(paths, [';']))
|
||||
{
|
||||
if (path is { HasValue: true, Length: > 0 })
|
||||
{
|
||||
if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Path.Combine(path.Value, "powershell.exe");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
internal CultureInfo PreviousCulture { get; set; } = default!;
|
||||
}
|
||||
@@ -51,6 +51,20 @@ internal sealed partial class CultivationDbService : ICultivationDbService
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<List<CultivateEntry>> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await appDbContext.CultivateEntries
|
||||
.AsNoTracking()
|
||||
.Where(e => e.ProjectId == projectId)
|
||||
.Include(e => e.LevelInformation)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
@@ -161,4 +175,22 @@ internal sealed partial class CultivationDbService : ICultivationDbService
|
||||
return appDbContext.CultivateProjects.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await appDbContext.LevelInformations.ExecuteDeleteWhereAsync(l => l.EntryId == entryId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await appDbContext.LevelInformations.AddAndSaveAsync(levelInformation).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata.Item;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal sealed class CultivationMetadataContext : ICultivationMetadataContext
|
||||
{
|
||||
public List<Material> Materials { get; set; } = default!;
|
||||
|
||||
public Dictionary<MaterialId, Material> IdMaterialMap { get; set; } = default!;
|
||||
|
||||
public Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> IdAvatarMap { get; set; } = default!;
|
||||
|
||||
public Dictionary<WeaponId, Model.Metadata.Weapon.Weapon> IdWeaponMap { get; set; } = default!;
|
||||
}
|
||||
@@ -7,8 +7,8 @@ using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.Model.Metadata.Item;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.Service.Inventroy;
|
||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
using Snap.Hutao.ViewModel.Cultivation;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
@@ -29,7 +29,7 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, List<Material> metadata, ICommand saveCommand)
|
||||
public List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
@@ -39,7 +39,7 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
List<InventoryItem> entities = cultivationDbService.GetInventoryItemListByProjectId(projectId);
|
||||
|
||||
List<InventoryItemView> results = [];
|
||||
foreach (Material meta in metadata.Where(m => m.IsInventoryItem()).OrderBy(m => m.Id.Value))
|
||||
foreach (Material meta in context.EnumerateInventroyMaterial())
|
||||
{
|
||||
InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.From(projectId, meta.Id);
|
||||
results.Add(new(entity, meta, saveCommand));
|
||||
@@ -50,36 +50,32 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(
|
||||
CultivateProject cultivateProject,
|
||||
List<Material> materials,
|
||||
Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap,
|
||||
Dictionary<WeaponId, Model.Metadata.Weapon.Weapon> idWeaponMap)
|
||||
public async ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
List<CultivateEntry> entries = await cultivationDbService
|
||||
.GetCultivateEntryListByProjectIdAsync(cultivateProject.InnerId)
|
||||
.GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(cultivateProject.InnerId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
List<CultivateEntryView> resultEntries = new(entries.Count);
|
||||
foreach (CultivateEntry entry in entries)
|
||||
{
|
||||
List<CultivateItemView> entryItems = [];
|
||||
foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false))
|
||||
foreach (CultivateItem cultivateItem in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false))
|
||||
{
|
||||
entryItems.Add(new(item, materials.Single(m => m.Id == item.ItemId)));
|
||||
entryItems.Add(new(cultivateItem, context.GetMaterial(cultivateItem.ItemId)));
|
||||
}
|
||||
|
||||
Item itemBase = entry.Type switch
|
||||
Item item = entry.Type switch
|
||||
{
|
||||
CultivateType.AvatarAndSkill => idAvatarMap[entry.Id].ToItem(),
|
||||
CultivateType.Weapon => idWeaponMap[entry.Id].ToItem(),
|
||||
CultivateType.AvatarAndSkill => context.GetAvatar(entry.Id).ToItem(),
|
||||
CultivateType.Weapon => context.GetWeapon(entry.Id).ToItem(),
|
||||
|
||||
// TODO: support furniture calc
|
||||
_ => default!,
|
||||
};
|
||||
|
||||
resultEntries.Add(new(entry, itemBase, entryItems));
|
||||
resultEntries.Add(new(entry, item, entryItems));
|
||||
}
|
||||
|
||||
return resultEntries
|
||||
@@ -89,9 +85,7 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ObservableCollection<StatisticsCultivateItem>> GetStatisticsCultivateItemCollectionAsync(
|
||||
CultivateProject cultivateProject,
|
||||
List<Material> materials,
|
||||
CancellationToken token)
|
||||
CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
List<StatisticsCultivateItem> resultItems = [];
|
||||
@@ -115,7 +109,7 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
}
|
||||
else
|
||||
{
|
||||
resultItems.Add(new(materials.Single(m => m.Id == item.ItemId), item));
|
||||
resultItems.Add(new(context.GetMaterial(item.ItemId), item));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +152,7 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Web.Hoyolab.Takumi.Event.Calculate.Item> items)
|
||||
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Web.Hoyolab.Takumi.Event.Calculate.Item> items, LevelInformation levelInformation)
|
||||
{
|
||||
if (items.Count == 0)
|
||||
{
|
||||
@@ -188,8 +182,12 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
}
|
||||
|
||||
Guid entryId = entry.InnerId;
|
||||
await cultivationDbService.RemoveCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false);
|
||||
|
||||
await cultivationDbService.RemoveLevelInformationByEntryIdAsync(entryId).ConfigureAwait(false);
|
||||
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, type, levelInformation);
|
||||
await cultivationDbService.AddLevelInformationAsync(entryLevelInformation).ConfigureAwait(false);
|
||||
|
||||
await cultivationDbService.RemoveCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false);
|
||||
IEnumerable<CultivateItem> toAdd = items.Select(item => CultivateItem.From(entryId, item));
|
||||
await cultivationDbService.AddCultivateItemRangeAsync(toAdd).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -35,4 +35,10 @@ internal interface ICultivationDbService
|
||||
void UpdateCultivateItem(CultivateItem item);
|
||||
|
||||
ValueTask UpdateCultivateItemAsync(CultivateItem item);
|
||||
|
||||
ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId);
|
||||
|
||||
ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation);
|
||||
|
||||
ValueTask<List<CultivateEntry>> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal interface ICultivationMetadataContext : IMetadataContext,
|
||||
IMetadataListMaterialSource,
|
||||
IMetadataDictionaryIdMaterialSource,
|
||||
IMetadataDictionaryIdAvatarSource,
|
||||
IMetadataDictionaryIdWeaponSource
|
||||
{
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.Model.Metadata.Item;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.ViewModel.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -27,38 +25,12 @@ internal interface ICultivationService
|
||||
/// </summary>
|
||||
ObservableCollection<CultivateProject> ProjectCollection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取绑定用的养成列表
|
||||
/// </summary>
|
||||
/// <param name="cultivateProject">养成计划</param>
|
||||
/// <param name="materials">材料</param>
|
||||
/// <param name="idAvatarMap">Id 角色映射</param>
|
||||
/// <param name="idWeaponMap">Id 武器映射</param>
|
||||
/// <returns>绑定用的养成列表</returns>
|
||||
ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(
|
||||
CultivateProject cultivateProject,
|
||||
List<Material> materials,
|
||||
Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap,
|
||||
Dictionary<WeaponId, Model.Metadata.Weapon.Weapon> idWeaponMap);
|
||||
ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context);
|
||||
|
||||
/// <summary>
|
||||
/// 获取物品列表
|
||||
/// </summary>
|
||||
/// <param name="cultivateProject">养成计划</param>
|
||||
/// <param name="metadata">元数据</param>
|
||||
/// <param name="saveCommand">保存命令</param>
|
||||
/// <returns>物品列表</returns>
|
||||
List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, List<Material> metadata, ICommand saveCommand);
|
||||
List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取统计物品列表
|
||||
/// </summary>
|
||||
/// <param name="cultivateProject">养成计划</param>
|
||||
/// <param name="materials">材料</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>统计物品列表</returns>
|
||||
ValueTask<ObservableCollection<StatisticsCultivateItem>> GetStatisticsCultivateItemCollectionAsync(
|
||||
CultivateProject cultivateProject, List<Material> materials, CancellationToken token);
|
||||
CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// 删除养成清单
|
||||
@@ -74,14 +46,7 @@ internal interface ICultivationService
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveProjectAsync(CultivateProject project);
|
||||
|
||||
/// <summary>
|
||||
/// 异步保存养成物品
|
||||
/// </summary>
|
||||
/// <param name="type">类型</param>
|
||||
/// <param name="itemId">主Id</param>
|
||||
/// <param name="items">待存物品</param>
|
||||
/// <returns>是否保存成功</returns>
|
||||
ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Item> items);
|
||||
ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Item> items, LevelInformation levelInformation);
|
||||
|
||||
/// <summary>
|
||||
/// 保存养成物品状态
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal sealed class LevelInformation : IMappingFrom<LevelInformation, AvatarPromotionDelta>
|
||||
{
|
||||
public uint AvatarLevelFrom { get; private set; }
|
||||
|
||||
public uint AvatarLevelTo { get; private set; }
|
||||
|
||||
public uint SkillALevelFrom { get; private set; }
|
||||
|
||||
public uint SkillALevelTo { get; private set; }
|
||||
|
||||
public uint SkillELevelFrom { get; private set; }
|
||||
|
||||
public uint SkillELevelTo { get; private set; }
|
||||
|
||||
public uint SkillQLevelFrom { get; private set; }
|
||||
|
||||
public uint SkillQLevelTo { get; private set; }
|
||||
|
||||
public uint WeaponLevelFrom { get; private set; }
|
||||
|
||||
public uint WeaponLevelTo { get; private set; }
|
||||
|
||||
public static LevelInformation From(AvatarPromotionDelta delta)
|
||||
{
|
||||
LevelInformation levelInformation = new();
|
||||
|
||||
if (delta.AvatarId != 0U)
|
||||
{
|
||||
levelInformation.AvatarLevelFrom = delta.AvatarLevelCurrent;
|
||||
levelInformation.AvatarLevelTo = delta.AvatarLevelTarget;
|
||||
}
|
||||
|
||||
if (delta.SkillList is [PromotionDelta skillA, PromotionDelta skillE, PromotionDelta skillQ, ..])
|
||||
{
|
||||
levelInformation.SkillALevelFrom = skillA.LevelCurrent;
|
||||
levelInformation.SkillALevelTo = skillA.LevelTarget;
|
||||
levelInformation.SkillELevelFrom = skillE.LevelCurrent;
|
||||
levelInformation.SkillELevelTo = skillE.LevelTarget;
|
||||
levelInformation.SkillQLevelFrom = skillQ.LevelCurrent;
|
||||
levelInformation.SkillQLevelTo = skillQ.LevelTarget;
|
||||
}
|
||||
|
||||
if (delta.Weapon is { } weapon)
|
||||
{
|
||||
levelInformation.WeaponLevelFrom = weapon.LevelCurrent;
|
||||
levelInformation.WeaponLevelTo = weapon.LevelTarget;
|
||||
}
|
||||
|
||||
return levelInformation;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.GachaLog;
|
||||
using Snap.Hutao.Web.Hutao.Response;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
|
||||
Time = i.Time,
|
||||
Id = i.Id,
|
||||
});
|
||||
return [..result];
|
||||
return [.. result];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ using Snap.Hutao.Service.GachaLog.Factory;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.ViewModel.GachaLog;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.GachaLog;
|
||||
using Snap.Hutao.Web.Hutao.Response;
|
||||
using Snap.Hutao.Web.Response;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.ViewModel.GachaLog;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.GachaLog;
|
||||
using Snap.Hutao.Web.Hutao.Response;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog;
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP
|
||||
{
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(queryString);
|
||||
|
||||
if (query.TryGetValue("auth_appid", out string? appId) && appId is "webview_gacha")
|
||||
if (query.TryGetSingleValue("auth_appid", out string? appId) && appId is "webview_gacha")
|
||||
{
|
||||
string? queryLanguageCode = query["lang"];
|
||||
if (metadataOptions.IsCurrentLocale(queryLanguageCode))
|
||||
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
|
||||
{
|
||||
return new(true, new(queryString));
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(result.TrimEnd("#/log"));
|
||||
string? queryLanguageCode = query["lang"];
|
||||
|
||||
if (metadataOptions.IsCurrentLocale(queryLanguageCode))
|
||||
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
|
||||
{
|
||||
return new(true, new(result));
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ internal sealed partial class UIGFImportService : IUIGFImportService
|
||||
// v2.1 only support CHS
|
||||
if (version is UIGFVersion.Major2Minor2OrLower)
|
||||
{
|
||||
if (!metadataOptions.IsCurrentLocale(uigf.Info.Language))
|
||||
if (!metadataOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
|
||||
{
|
||||
string message = SH.FormatServiceGachaUIGFImportLanguageNotMatch(uigf.Info.Language, metadataOptions.LanguageCode);
|
||||
ThrowHelper.InvalidOperation(message);
|
||||
|
||||
@@ -40,10 +40,6 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
private bool? useStarwardPlayTimeStatistics;
|
||||
private bool? setDiscordActivityWhenPlaying;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的启动游戏选项
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public LaunchOptions(IServiceProvider serviceProvider)
|
||||
: base(serviceProvider)
|
||||
{
|
||||
@@ -53,47 +49,66 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
|
||||
InitializeMonitors(Monitors);
|
||||
InitializeScreenFps(out primaryScreenFps);
|
||||
|
||||
static void InitializeMonitors(List<NameValue<int>> monitors)
|
||||
{
|
||||
try
|
||||
{
|
||||
// This list can't use foreach
|
||||
// https://github.com/microsoft/CsWinRT/issues/747
|
||||
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
|
||||
for (int i = 0; i < displayAreas.Count; i++)
|
||||
{
|
||||
DisplayArea displayArea = displayAreas[i];
|
||||
int index = i + 1;
|
||||
monitors.Add(new($"{displayArea.DisplayId.Value:X8}:{index}", index));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
monitors.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
static void InitializeScreenFps(out int fps)
|
||||
{
|
||||
HDC hDC = default;
|
||||
try
|
||||
{
|
||||
hDC = GetDC(HWND.Null);
|
||||
fps = GetDeviceCaps(hDC, GET_DEVICE_CAPS_INDEX.VREFRESH);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = ReleaseDC(HWND.Null, hDC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用启动参数
|
||||
/// </summary>
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true);
|
||||
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否全屏
|
||||
/// </summary>
|
||||
public bool IsFullScreen
|
||||
{
|
||||
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen);
|
||||
set => SetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否无边框
|
||||
/// </summary>
|
||||
public bool IsBorderless
|
||||
{
|
||||
get => GetOption(ref isBorderless, SettingEntry.LaunchIsBorderless);
|
||||
set => SetOption(ref isBorderless, SettingEntry.LaunchIsBorderless, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否独占全屏
|
||||
/// </summary>
|
||||
public bool IsExclusive
|
||||
{
|
||||
get => GetOption(ref isExclusive, SettingEntry.LaunchIsExclusive);
|
||||
set => SetOption(ref isExclusive, SettingEntry.LaunchIsExclusive, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 屏幕宽度
|
||||
/// </summary>
|
||||
public int ScreenWidth
|
||||
{
|
||||
get => GetOption(ref screenWidth, SettingEntry.LaunchScreenWidth, primaryScreenWidth);
|
||||
@@ -106,9 +121,6 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
set => SetOption(ref isScreenWidthEnabled, SettingEntry.LaunchIsScreenWidthEnabled, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 屏幕高度
|
||||
/// </summary>
|
||||
public int ScreenHeight
|
||||
{
|
||||
get => GetOption(ref screenHeight, SettingEntry.LaunchScreenHeight, primaryScreenHeight);
|
||||
@@ -121,32 +133,20 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
set => SetOption(ref isScreenHeightEnabled, SettingEntry.LaunchIsScreenHeightEnabled, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否全屏
|
||||
/// </summary>
|
||||
public bool UnlockFps
|
||||
{
|
||||
get => GetOption(ref unlockFps, SettingEntry.LaunchUnlockFps);
|
||||
set => SetOption(ref unlockFps, SettingEntry.LaunchUnlockFps, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标帧率
|
||||
/// </summary>
|
||||
public int TargetFps
|
||||
{
|
||||
get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps);
|
||||
set => SetOption(ref targetFps, SettingEntry.LaunchTargetFps, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 所有监视器
|
||||
/// </summary>
|
||||
public List<NameValue<int>> Monitors { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 目标帧率
|
||||
/// </summary>
|
||||
[AllowNull]
|
||||
public NameValue<int> Monitor
|
||||
{
|
||||
@@ -196,31 +196,4 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
get => GetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, true);
|
||||
set => SetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, value);
|
||||
}
|
||||
|
||||
private static void InitializeMonitors(List<NameValue<int>> monitors)
|
||||
{
|
||||
// This list can't use foreach
|
||||
// https://github.com/microsoft/CsWinRT/issues/747
|
||||
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
|
||||
for (int i = 0; i < displayAreas.Count; i++)
|
||||
{
|
||||
DisplayArea displayArea = displayAreas[i];
|
||||
int index = i + 1;
|
||||
monitors.Add(new($"{displayArea.DisplayId.Value:X8}:{index}", index));
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitializeScreenFps(out int fps)
|
||||
{
|
||||
HDC hDC = default;
|
||||
try
|
||||
{
|
||||
hDC = GetDC(HWND.Null);
|
||||
fps = GetDeviceCaps(hDC, GET_DEVICE_CAPS_INDEX.VREFRESH);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = ReleaseDC(HWND.Null, hDC);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ internal sealed partial class UnityLogGameLocator : IGameLocator
|
||||
// Fallback to the CN server.
|
||||
string logFilePathFinal = File.Exists(logFilePathOversea) ? logFilePathOversea : logFilePathChinese;
|
||||
|
||||
if (TempFile.CopyFrom(logFilePathFinal) is TempFile file)
|
||||
if (TempFile.CopyFrom(logFilePathFinal) is { } file)
|
||||
{
|
||||
using (file)
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
internal sealed partial class PackageConverter
|
||||
{
|
||||
private const string PackageVersion = "pkg_version";
|
||||
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Service.Discord;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Service.Game.Unlocker;
|
||||
@@ -33,8 +32,17 @@ internal sealed partial class GameProcessService : IGameProcessService
|
||||
return true;
|
||||
}
|
||||
|
||||
return System.Diagnostics.Process.GetProcessesByName(YuanShenProcessName).Length > 0
|
||||
|| System.Diagnostics.Process.GetProcessesByName(GenshinImpactProcessName).Length > 0;
|
||||
// Original two GetProcessesByName is O(2n)
|
||||
// GetProcesses once and manually loop is O(n)
|
||||
foreach (ref System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan())
|
||||
{
|
||||
if (process.ProcessName is YuanShenProcessName or GenshinImpactProcessName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.Response;
|
||||
using Snap.Hutao.Web.Hutao.SpiralAbyss;
|
||||
using Snap.Hutao.Web.Response;
|
||||
|
||||
@@ -19,7 +19,7 @@ internal sealed partial class HutaoSpiralAbyssService : IHutaoSpiralAbyssService
|
||||
private readonly TimeSpan cacheExpireTime = TimeSpan.FromHours(4);
|
||||
|
||||
private readonly IObjectCacheDbService objectCacheDbService;
|
||||
private readonly HomaSpiralAbyssClient homaClient;
|
||||
private readonly HutaoSpiralAbyssClient homaClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly IMemoryCache memoryCache;
|
||||
|
||||
|
||||
@@ -2,141 +2,38 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Snap.Hutao.Service.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃用户选项
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class HutaoUserOptions : ObservableObject, IOptions<HutaoUserOptions>
|
||||
internal sealed class HutaoUserOptions : ObservableObject
|
||||
{
|
||||
private readonly TaskCompletionSource initializedTaskCompletionSource = new();
|
||||
private readonly TaskCompletionSource initialization = new();
|
||||
|
||||
private string? userName = SH.ViewServiceHutaoUserLoginOrRegisterHint;
|
||||
private string? token;
|
||||
private bool isLoggedIn;
|
||||
private bool isHutaoCloudServiceAllowed;
|
||||
private bool isCloudServiceAllowed;
|
||||
private bool isLicensedDeveloper;
|
||||
private bool isMaintainer;
|
||||
private string? gachaLogExpireAt;
|
||||
private string? gachaLogExpireAtSlim;
|
||||
private bool isMaintainer;
|
||||
private string? token;
|
||||
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string? UserName { get => userName; set => SetProperty(ref userName, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 真正的用户名
|
||||
/// </summary>
|
||||
public string? ActualUserName { get => IsLoggedIn ? UserName : null; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已登录
|
||||
/// </summary>
|
||||
public bool IsLoggedIn { get => isLoggedIn; set => SetProperty(ref isLoggedIn, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃云服务是否可用
|
||||
/// </summary>
|
||||
public bool IsCloudServiceAllowed { get => isHutaoCloudServiceAllowed; set => SetProperty(ref isHutaoCloudServiceAllowed, value); }
|
||||
public bool IsCloudServiceAllowed { get => isCloudServiceAllowed; set => SetProperty(ref isCloudServiceAllowed, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为开发者
|
||||
/// </summary>
|
||||
public bool IsLicensedDeveloper { get => isLicensedDeveloper; set => SetProperty(ref isLicensedDeveloper, value); }
|
||||
|
||||
public bool IsMaintainer { get => isMaintainer; set => SetProperty(ref isMaintainer, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿记录服务到期时间
|
||||
/// </summary>
|
||||
public string? GachaLogExpireAt { get => gachaLogExpireAt; set => SetProperty(ref gachaLogExpireAt, value); }
|
||||
|
||||
public string? GachaLogExpireAtSlim { get => gachaLogExpireAtSlim; set => SetProperty(ref gachaLogExpireAtSlim, value); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HutaoUserOptions Value { get => this; }
|
||||
internal string? Token { get => token; set => token = value; }
|
||||
|
||||
public async ValueTask<bool> PostLoginSucceedAsync(HomaPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token)
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.PassportUserName, username);
|
||||
LocalSetting.Set(SettingKeys.PassportPassword, password);
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
UserName = username;
|
||||
this.token = token;
|
||||
IsLoggedIn = true;
|
||||
initializedTaskCompletionSource.TrySetResult();
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
Web.Response.Response<UserInfo> userInfoResponse = await passportClient.GetUserInfoAsync(default).ConfigureAwait(false);
|
||||
if (userInfoResponse.IsOk())
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
UpdateUserInfo(userInfoResponse.Data);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LogoutOrUnregister()
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.PassportUserName, string.Empty);
|
||||
LocalSetting.Set(SettingKeys.PassportPassword, string.Empty);
|
||||
|
||||
UserName = null;
|
||||
token = null;
|
||||
IsLoggedIn = false;
|
||||
ClearUserInfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登录失败
|
||||
/// </summary>
|
||||
public void LoginFailed()
|
||||
{
|
||||
UserName = SH.ViewServiceHutaoUserLoginFailHint;
|
||||
initializedTaskCompletionSource.TrySetResult();
|
||||
}
|
||||
|
||||
public void SkipLogin()
|
||||
{
|
||||
initializedTaskCompletionSource.TrySetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新用户信息
|
||||
/// </summary>
|
||||
/// <param name="userInfo">用户信息</param>
|
||||
public void UpdateUserInfo(UserInfo userInfo)
|
||||
{
|
||||
IsLicensedDeveloper = userInfo.IsLicensedDeveloper;
|
||||
IsMaintainer = userInfo.IsMaintainer;
|
||||
string unescaped = Regex.Unescape(SH.ServiceHutaoUserGachaLogExpiredAt);
|
||||
GachaLogExpireAt = string.Format(CultureInfo.CurrentCulture, unescaped, userInfo.GachaLogExpireAt);
|
||||
GachaLogExpireAtSlim = $"{userInfo.GachaLogExpireAt:yyyy.MM.dd HH:mm:ss}";
|
||||
IsCloudServiceAllowed = IsLicensedDeveloper || userInfo.GachaLogExpireAt > DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public async ValueTask<string?> GetTokenAsync()
|
||||
{
|
||||
await initializedTaskCompletionSource.Task.ConfigureAwait(false);
|
||||
return token;
|
||||
}
|
||||
|
||||
private void ClearUserInfo()
|
||||
{
|
||||
IsLicensedDeveloper = false;
|
||||
IsMaintainer = false;
|
||||
GachaLogExpireAt = null;
|
||||
GachaLogExpireAtSlim = null;
|
||||
IsCloudServiceAllowed = false;
|
||||
}
|
||||
internal TaskCompletionSource Initialization { get => initialization; }
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Snap.Hutao.Service.Hutao;
|
||||
|
||||
internal static class HutaoUserOptionsExtension
|
||||
{
|
||||
public static string? GetActualUserName(this HutaoUserOptions options)
|
||||
{
|
||||
return options.IsLoggedIn ? options.UserName : null;
|
||||
}
|
||||
|
||||
public static async ValueTask<string?> GetTokenAsync(this HutaoUserOptions options)
|
||||
{
|
||||
await options.Initialization.Task.ConfigureAwait(false);
|
||||
return options.Token;
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> PostLoginSucceedAsync(this HutaoUserOptions options, HutaoPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token)
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.PassportUserName, username);
|
||||
LocalSetting.Set(SettingKeys.PassportPassword, password);
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
options.UserName = username;
|
||||
options.Token = token;
|
||||
options.IsLoggedIn = true;
|
||||
options.Initialization.TrySetResult();
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
Web.Response.Response<UserInfo> userInfoResponse = await passportClient.GetUserInfoAsync(default).ConfigureAwait(false);
|
||||
if (userInfoResponse.IsOk())
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
UpdateUserInfo(options, userInfoResponse.Data);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
static void UpdateUserInfo(HutaoUserOptions options, UserInfo userInfo)
|
||||
{
|
||||
options.IsLicensedDeveloper = userInfo.IsLicensedDeveloper;
|
||||
options.IsMaintainer = userInfo.IsMaintainer;
|
||||
|
||||
options.IsCloudServiceAllowed = options.IsLicensedDeveloper || userInfo.GachaLogExpireAt > DateTimeOffset.UtcNow;
|
||||
string unescaped = Regex.Unescape(SH.ServiceHutaoUserGachaLogExpiredAt);
|
||||
options.GachaLogExpireAt = string.Format(CultureInfo.CurrentCulture, unescaped, userInfo.GachaLogExpireAt);
|
||||
options.GachaLogExpireAtSlim = $"{userInfo.GachaLogExpireAt:yyyy.MM.dd HH:mm:ss}";
|
||||
}
|
||||
}
|
||||
|
||||
public static void PostLogoutOrUnregister(this HutaoUserOptions options)
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.PassportUserName, string.Empty);
|
||||
LocalSetting.Set(SettingKeys.PassportPassword, string.Empty);
|
||||
|
||||
options.UserName = null;
|
||||
options.Token = null;
|
||||
options.IsLoggedIn = false;
|
||||
ClearUserInfo(options);
|
||||
|
||||
static void ClearUserInfo(HutaoUserOptions options)
|
||||
{
|
||||
options.IsLicensedDeveloper = false;
|
||||
options.IsMaintainer = false;
|
||||
options.GachaLogExpireAt = null;
|
||||
options.GachaLogExpireAtSlim = null;
|
||||
options.IsCloudServiceAllowed = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void PostLoginFailed(this HutaoUserOptions options)
|
||||
{
|
||||
options.UserName = SH.ViewServiceHutaoUserLoginFailHint;
|
||||
options.Initialization.TrySetResult();
|
||||
}
|
||||
|
||||
public static void PostLoginSkipped(this HutaoUserOptions options)
|
||||
{
|
||||
options.Initialization.TrySetResult();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
|
||||
{
|
||||
private readonly TaskCompletionSource initializeCompletionSource = new();
|
||||
|
||||
private readonly HomaPassportClient passportClient;
|
||||
private readonly HutaoPassportClient passportClient;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly HutaoUserOptions options;
|
||||
|
||||
@@ -36,7 +36,7 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
|
||||
|
||||
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
options.SkipLogin();
|
||||
options.PostLoginSkipped();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -52,7 +52,7 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
|
||||
else
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
options.LoginFailed();
|
||||
options.PostLoginFailed();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataContext;
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdAvatarSource
|
||||
{
|
||||
public Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> IdAvatarMap { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata.Item;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdMaterialSource
|
||||
{
|
||||
public Dictionary<MaterialId, Material> IdMaterialMap { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdWeaponSource
|
||||
{
|
||||
public Dictionary<WeaponId, Model.Metadata.Weapon.Weapon> IdWeaponMap { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata.Item;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataListMaterialSource
|
||||
{
|
||||
public List<Material> Materials { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Item;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal static class MetadataServiceContextExtension
|
||||
{
|
||||
public static async ValueTask<TContext> GetContextAsync<TContext>(this IMetadataService metadataService, CancellationToken token = default)
|
||||
where TContext : IMetadataContext, new()
|
||||
{
|
||||
TContext context = new();
|
||||
|
||||
// List
|
||||
{
|
||||
if (context is IMetadataListMaterialSource listMaterialSource)
|
||||
{
|
||||
listMaterialSource.Materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Dictionary
|
||||
{
|
||||
if (context is IMetadataDictionaryIdAvatarSource dictionaryAvatarSource)
|
||||
{
|
||||
dictionaryAvatarSource.IdAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdMaterialSource dictionaryMaterialSource)
|
||||
{
|
||||
dictionaryMaterialSource.IdMaterialMap = await metadataService.GetIdToMaterialMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdWeaponSource dictionaryWeaponSource)
|
||||
{
|
||||
dictionaryWeaponSource.IdWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
#pragma warning disable SH002
|
||||
public static IEnumerable<Material> EnumerateInventroyMaterial(this IMetadataListMaterialSource context)
|
||||
{
|
||||
return context.Materials.Where(m => m.IsInventoryItem()).OrderBy(m => m.Id.Value);
|
||||
}
|
||||
|
||||
public static Avatar GetAvatar(this IMetadataDictionaryIdAvatarSource context, AvatarId id)
|
||||
{
|
||||
return context.IdAvatarMap[id];
|
||||
}
|
||||
|
||||
public static Material GetMaterial(this IMetadataDictionaryIdMaterialSource context, MaterialId id)
|
||||
{
|
||||
return context.IdMaterialMap[id];
|
||||
}
|
||||
|
||||
public static Weapon GetWeapon(this IMetadataDictionaryIdWeaponSource context, WeaponId id)
|
||||
{
|
||||
return context.IdWeaponMap[id];
|
||||
}
|
||||
#pragma warning restore SH002
|
||||
}
|
||||
@@ -8,6 +8,8 @@ namespace Snap.Hutao.Service.Metadata;
|
||||
/// </summary>
|
||||
internal static class LocaleNames
|
||||
{
|
||||
public const string CHS = "CHS"; // Chinese (Simplified)
|
||||
public const string CHT = "CHT"; // Chinese (Traditional)
|
||||
public const string DE = "DE"; // German
|
||||
public const string EN = "EN"; // English
|
||||
public const string ES = "ES"; // Spanish
|
||||
@@ -21,8 +23,6 @@ internal static class LocaleNames
|
||||
public const string TH = "TH"; // Thai
|
||||
public const string TR = "TR"; // Turkish
|
||||
public const string VI = "VI"; // Vietnamese
|
||||
public const string CHS = "CHS"; // Chinese (Simplified)
|
||||
public const string CHT = "CHT"; // Chinese (Traditional)
|
||||
|
||||
public static bool TryGetLocaleNameFromLanguageName(string languageName, [NotNullWhen(true)] out string? localeName)
|
||||
{
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Snap.Hutao.Core;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// 元数据选项
|
||||
/// </summary>
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
|
||||
internal sealed partial class MetadataOptions
|
||||
{
|
||||
private readonly AppOptions appOptions;
|
||||
private readonly RuntimeOptions hutaoOptions;
|
||||
@@ -22,9 +17,6 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
|
||||
private string? fallbackDataFolder;
|
||||
private string? localizedDataFolder;
|
||||
|
||||
/// <summary>
|
||||
/// 中文数据文件夹
|
||||
/// </summary>
|
||||
public string FallbackDataFolder
|
||||
{
|
||||
get
|
||||
@@ -39,9 +31,6 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地化数据文件夹
|
||||
/// </summary>
|
||||
public string LocalizedDataFolder
|
||||
{
|
||||
get
|
||||
@@ -56,17 +45,11 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前使用的元数据本地化名称
|
||||
/// </summary>
|
||||
public string LocaleName
|
||||
{
|
||||
get => localeName ??= GetLocaleName(appOptions.CurrentCulture);
|
||||
get => localeName ??= MetadataOptionsExtension.GetLocaleName(appOptions.CurrentCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前语言代码
|
||||
/// </summary>
|
||||
public string LanguageCode
|
||||
{
|
||||
get
|
||||
@@ -79,63 +62,4 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
|
||||
throw new KeyNotFoundException($"Invalid localeName: '{LocaleName}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataOptions Value { get => this; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取语言名称
|
||||
/// </summary>
|
||||
/// <param name="cultureInfo">语言信息</param>
|
||||
/// <returns>元数据语言名称</returns>
|
||||
public static string GetLocaleName(CultureInfo cultureInfo)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (LocaleNames.TryGetLocaleNameFromLanguageName(cultureInfo.Name, out string? localeName))
|
||||
{
|
||||
return localeName;
|
||||
}
|
||||
else
|
||||
{
|
||||
cultureInfo = cultureInfo.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否为当前语言名称
|
||||
/// </summary>
|
||||
/// <param name="languageCode">语言代码</param>
|
||||
/// <returns>是否为当前语言名称</returns>
|
||||
public bool IsCurrentLocale(string? languageCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(languageCode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageCode);
|
||||
return GetLocaleName(cultureInfo) == LocaleName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地的本地化元数据文件
|
||||
/// </summary>
|
||||
/// <param name="fileNameWithExtension">文件名</param>
|
||||
/// <returns>本地的本地化元数据文件</returns>
|
||||
public string GetLocalizedLocalFile(string fileNameWithExtension)
|
||||
{
|
||||
return Path.Combine(LocalizedDataFolder, fileNameWithExtension);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取服务器上的本地化元数据文件
|
||||
/// </summary>
|
||||
/// <param name="fileNameWithExtension">文件名</param>
|
||||
/// <returns>服务器上的本地化元数据文件</returns>
|
||||
public string GetLocalizedRemoteFile(string fileNameWithExtension)
|
||||
{
|
||||
return Web.HutaoEndpoints.Metadata(LocaleName, fileNameWithExtension);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata;
|
||||
|
||||
internal static class MetadataOptionsExtension
|
||||
{
|
||||
public static string GetLocalizedLocalFile(this MetadataOptions options, string fileNameWithExtension)
|
||||
{
|
||||
return Path.Combine(options.LocalizedDataFolder, fileNameWithExtension);
|
||||
}
|
||||
|
||||
public static string GetLocalizedRemoteFile(this MetadataOptions options, string fileNameWithExtension)
|
||||
{
|
||||
return Web.HutaoEndpoints.Metadata(options.LocaleName, fileNameWithExtension);
|
||||
}
|
||||
|
||||
public static bool LanguageCodeFitsCurrentLocale(this MetadataOptions options, string? languageCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(languageCode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We want to make sure code fits in 1 of 15 metadata locales
|
||||
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageCode);
|
||||
return GetLocaleName(cultureInfo) == options.LocaleName;
|
||||
}
|
||||
|
||||
internal static string GetLocaleName(CultureInfo cultureInfo)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (LocaleNames.TryGetLocaleNameFromLanguageName(cultureInfo.Name, out string? localeName))
|
||||
{
|
||||
return localeName;
|
||||
}
|
||||
else
|
||||
{
|
||||
cultureInfo = cultureInfo.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,89 +6,10 @@ using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.Notification;
|
||||
|
||||
/// <summary>
|
||||
/// 消息条服务
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal interface IInfoBarService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取操作的集合
|
||||
/// </summary>
|
||||
ObservableCollection<InfoBar> Collection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Error(string message, int delay = 30000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Error(string title, string message, int delay = 30000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息
|
||||
/// </summary>
|
||||
/// <param name="exception">异常</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Error(Exception exception, int delay = 30000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息
|
||||
/// </summary>
|
||||
/// <param name="exception">异常</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Error(Exception exception, string title, int delay = 30000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示提示信息
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Information(string message, int delay = 5000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示提示信息
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Information(string title, string message, int delay = 5000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示成功信息
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Success(string message, int delay = 5000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示成功信息
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Success(string title, string message, int delay = 5000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示警告信息
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Warning(string message, int delay = 15000);
|
||||
|
||||
/// <summary>
|
||||
/// 显示警告信息
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
void Warning(string title, string message, int delay = 15000);
|
||||
void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay);
|
||||
}
|
||||
|
||||
@@ -34,84 +34,17 @@ internal sealed class InfoBarService : IInfoBarService
|
||||
get => collection ??= [];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Information(string message, int delay = 5000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Informational, null, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Information(string title, string message, int delay = 5000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Informational, title, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Success(string message, int delay = 5000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Success, null, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Success(string title, string message, int delay = 5000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Success, title, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Warning(string message, int delay = 30000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Warning, null, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Warning(string title, string message, int delay = 30000)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Warning, title, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(string message, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Error, null, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(string title, string message, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Error, title, message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(Exception ex, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, ex.Message, delay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Error(Exception ex, string title, int delay = 0)
|
||||
{
|
||||
PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay);
|
||||
}
|
||||
|
||||
private void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay)
|
||||
public void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay)
|
||||
{
|
||||
if (collection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PrepareInfoBarAndShowInternalAsync(severity, title, message, delay).SafeForget(logger);
|
||||
PrepareInfoBarAndShowCoreAsync(severity, title, message, delay).SafeForget(logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 准备信息条并显示
|
||||
/// </summary>
|
||||
/// <param name="severity">严重程度</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="delay">关闭延迟</param>
|
||||
private async ValueTask PrepareInfoBarAndShowInternalAsync(InfoBarSeverity severity, string? title, string? message, int delay)
|
||||
private async ValueTask PrepareInfoBarAndShowCoreAsync(InfoBarSeverity severity, string? title, string? message, int delay)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
@@ -139,7 +72,7 @@ internal sealed class InfoBarService : IInfoBarService
|
||||
private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
taskContext.InvokeOnMainThread(() => collection.Remove(sender));
|
||||
taskContext.BeginInvokeOnMainThread(() => collection.Remove(sender));
|
||||
sender.Closed -= infobarClosedEventHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Service.Notification;
|
||||
|
||||
internal static class InfoBarServiceExtension
|
||||
{
|
||||
public static void Information(this IInfoBarService infoBarService, string message, int delay = 5000)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Informational, null, message, delay);
|
||||
}
|
||||
|
||||
public static void Information(this IInfoBarService infoBarService, string title, string message, int delay = 5000)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Informational, title, message, delay);
|
||||
}
|
||||
|
||||
public static void Success(this IInfoBarService infoBarService, string message, int delay = 5000)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Success, null, message, delay);
|
||||
}
|
||||
|
||||
public static void Success(this IInfoBarService infoBarService, string title, string message, int delay = 5000)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Success, title, message, delay);
|
||||
}
|
||||
|
||||
public static void Warning(this IInfoBarService infoBarService, string message, int delay = 30000)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Warning, null, message, delay);
|
||||
}
|
||||
|
||||
public static void Warning(this IInfoBarService infoBarService, string title, string message, int delay = 30000)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Warning, title, message, delay);
|
||||
}
|
||||
|
||||
public static void Error(this IInfoBarService infoBarService, string message, int delay = 0)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Error, null, message, delay);
|
||||
}
|
||||
|
||||
public static void Error(this IInfoBarService infoBarService, string title, string message, int delay = 0)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Error, title, message, delay);
|
||||
}
|
||||
|
||||
public static void Error(this IInfoBarService infoBarService, Exception ex, int delay = 0)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, ex.Message, delay);
|
||||
}
|
||||
|
||||
public static void Error(this IInfoBarService infoBarService, Exception ex, string title, int delay = 0)
|
||||
{
|
||||
infoBarService.PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay);
|
||||
}
|
||||
}
|
||||
29
src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs
Normal file
29
src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
|
||||
internal static class SupportedCultures
|
||||
{
|
||||
private static readonly List<NameValue<CultureInfo>> Cultures =
|
||||
[
|
||||
ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")),
|
||||
ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")),
|
||||
ToNameValue(CultureInfo.GetCultureInfo("en")),
|
||||
ToNameValue(CultureInfo.GetCultureInfo("ko")),
|
||||
ToNameValue(CultureInfo.GetCultureInfo("ja")),
|
||||
];
|
||||
|
||||
public static List<NameValue<CultureInfo>> Get()
|
||||
{
|
||||
return Cultures;
|
||||
}
|
||||
|
||||
private static NameValue<CultureInfo> ToNameValue(CultureInfo info)
|
||||
{
|
||||
return new(info.NativeName, info);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.ObjectModel;
|
||||
using BindingUser = Snap.Hutao.ViewModel.User.User;
|
||||
|
||||
namespace Snap.Hutao.Service.User;
|
||||
|
||||
internal interface IUserCollectionService
|
||||
{
|
||||
BindingUser? CurrentUser { get; set; }
|
||||
|
||||
ValueTask<ObservableCollection<UserAndUid>> GetUserAndUidCollectionAsync();
|
||||
|
||||
ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync();
|
||||
|
||||
UserGameRole? GetUserGameRoleByUid(string uid);
|
||||
|
||||
ValueTask RemoveUserAsync(BindingUser user);
|
||||
ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea);
|
||||
bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user