Merge pull request #1170 from DGP-Studio/develop

This commit is contained in:
DismissedLight
2023-12-12 17:08:56 +08:00
committed by GitHub
174 changed files with 3735 additions and 2065 deletions

12
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "4.0.0",
"commands": [
"dotnet-cake"
]
}
}
}

20
appveyor.yml Normal file
View 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==

View File

@@ -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
View 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);

View File

@@ -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);

View File

@@ -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; }
}

View 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);
}
}
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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/>

View File

@@ -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;
}

View File

@@ -20,7 +20,7 @@ internal sealed class CachedImage : Implementation.ImageEx
public CachedImage()
{
IsCacheEnabled = true;
EnableLazyLoading = true;
EnableLazyLoading = false;
}
/// <inheritdoc/>

View File

@@ -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">

View File

@@ -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);

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Windows.Foundation;
namespace Snap.Hutao.Control;

View File

@@ -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}"/>

View File

@@ -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">&#xE710;</x:String>
<x:String x:Key="FontIconContentSetting">&#xE713;</x:String>
<x:String x:Key="FontIconContentRefresh">&#xE72C;</x:String>
@@ -12,6 +13,7 @@
<x:String x:Key="FontIconContentBulletedList">&#xE8FD;</x:String>
<x:String x:Key="FontIconContentCheckList">&#xE9D5;</x:String>
<x:String x:Key="FontIconContentWebsite">&#xEB41;</x:String>
<x:String x:Key="FontIconContentQRCode">&#xED14;</x:String>
<x:String x:Key="FontIconContentHomeGroup">&#xEC26;</x:String>
<x:String x:Key="FontIconContentAsteriskBadge12">&#xEDAD;</x:String>
<x:String x:Key="FontIconContentZipFolder">&#xF012;</x:String>

View File

@@ -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;

View File

@@ -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>();
}
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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()
{
}
}

View File

@@ -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;
}
}

View File

@@ -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()
{
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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";
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View 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]);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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))
{

View File

@@ -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>
/// 寻找枚举中唯一的值,找不到时
/// 回退到首个或默认值

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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')
{

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}
}
}

View 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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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")

View File

@@ -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;
}
}

View File

@@ -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}"),
};
}
}

View File

@@ -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>
/// 是否完成此项

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Model.Metadata.Avatar;

View File

@@ -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)
{

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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!;
}

View File

@@ -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);
}
}
}

View File

@@ -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!;
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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
{
}

View File

@@ -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>
/// 保存养成物品状态

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -287,7 +287,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
Time = i.Time,
Id = i.Id,
});
return [..result];
return [.. result];
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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));
}

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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)
{

View 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;

View File

@@ -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)

View File

@@ -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;

View File

@@ -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; }
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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
}

View File

@@ -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)
{

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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