diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..3f287339 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "cake.tool": { + "version": "4.0.0", + "commands": [ + "dotnet-cake" + ] + } + } +} diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..223fdb9e --- /dev/null +++ b/appveyor.yml @@ -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== diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 55f9c894..ca0b5b85 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -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' diff --git a/build.cake b/build.cake new file mode 100644 index 00000000..26a16afc --- /dev/null +++ b/build.cake @@ -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 + { + { "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 { { "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); diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs index a7090302..1d22ed83 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs @@ -10,17 +10,10 @@ public class CollectionsMarshalTest [TestMethod] public void DictionaryMarshalGetValueRefOrNullRefIsNullRef() { -#if NET8_0_OR_GREATER Dictionary dictionaryValueKeyRefValue = []; Dictionary dictionaryValueKeyValueValue = []; Dictionary dictionaryRefKeyValueValue = []; Dictionary dictionaryRefKeyRefValue = []; -#else - Dictionary dictionaryValueKeyRefValue = new(); - Dictionary dictionaryValueKeyValueValue = new(); - Dictionary dictionaryRefKeyValueValue = new(); - Dictionary 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 dictionaryValueKeyRefValue = []; Dictionary dictionaryValueKeyValueValue = []; Dictionary dictionaryRefKeyValueValue = []; Dictionary dictionaryRefKeyRefValue = []; -#else - Dictionary dictionaryValueKeyRefValue = new(); - Dictionary dictionaryValueKeyValueValue = new(); - Dictionary dictionaryRefKeyValueValue = new(); - Dictionary dictionaryRefKeyRefValue = new(); -#endif Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default); Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default); diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs index 656294cb..42b0ca1d 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs @@ -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(SmapleObjectJson)!; + SampleDelegatePropertyClass sample = JsonSerializer.Deserialize(SmapleObjectJson)!; Assert.AreEqual(sample.B, 1); } @@ -44,7 +43,7 @@ public sealed class JsonSerializeTest [ExpectedException(typeof(JsonException))] public void EmptyStringCannotSerializeAsNumber() { - StringNumberSample sample = JsonSerializer.Deserialize(SmapleEmptyStringObjectJson)!; + SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize(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; } } diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs new file mode 100644 index 00000000..c189ac21 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/LinqTest.cs @@ -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 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); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs index aaa2b7ce..7a72c3e5 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs @@ -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); } diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs index 31d30681..ac4b993d 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/SpiralAbyssScheduleIdTest.cs @@ -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 diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs index 36601cfa..86de485c 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs @@ -23,6 +23,18 @@ public sealed class UnsafeRuntimeBehaviorTest } } + [TestMethod] + public unsafe void UInt32LayoutIsLittleEndian() + { + ulong testValue = 0x1234567887654321; + ref BuildVersion version = ref Unsafe.As(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(ref this), 4)); } } + + private readonly struct BuildVersion + { + public readonly ushort Build; + public readonly ushort Patch; + public readonly ushort Minor; + public readonly ushort Major; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt index 6520488d..3a8308d4 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt +++ b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt @@ -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 diff --git a/src/Snap.Hutao/Snap.Hutao/Control/DependencyValueConverter.cs b/src/Snap.Hutao/Snap.Hutao/Control/DependencyValueConverter.cs index f42ffe48..4771e71f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/DependencyValueConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/DependencyValueConverter.cs @@ -16,22 +16,7 @@ internal abstract class DependencyValueConverter : DependencyObject, /// public object? Convert(object value, Type targetType, object parameter, string language) { -#if DEBUG - try - { - return Convert((TFrom)value); - } - catch (Exception ex) - { - Ioc.Default - .GetRequiredService>>() - .LogError(ex, "值转换器异常"); - } - - return null; -#else return Convert((TFrom)value); -#endif } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogHideToken.cs b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogHideToken.cs index c0178f3f..2b5f14af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogHideToken.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogHideToken.cs @@ -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; } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs index e05dc521..ef166a7e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs @@ -20,7 +20,7 @@ internal sealed class CachedImage : Implementation.ImageEx public CachedImage() { IsCacheEnabled = true; - EnableLazyLoading = true; + EnableLazyLoading = false; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml index e64ab600..b9540edd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml @@ -19,7 +19,6 @@ Name="PlaceholderImage" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}" - Opacity="1.0" Source="{TemplateBinding PlaceholderSource}" Stretch="{TemplateBinding PlaceholderStretch}"/> diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs index c049e8aa..b35ceec4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs @@ -80,19 +80,22 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co ILogger logger = serviceProvider.GetRequiredService>(); // 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); diff --git a/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs b/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs index 986de656..587070b6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs @@ -3,7 +3,6 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Snap.Hutao.Core; using Windows.Foundation; namespace Snap.Hutao.Control; diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml index 8557c744..5ea53fe5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml @@ -23,7 +23,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml index 555f142e..4f8f0190 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml @@ -1,4 +1,5 @@ + @@ -12,6 +13,7 @@ + diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs index eaaca065..debe0818 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs @@ -18,41 +18,30 @@ namespace Snap.Hutao.Core.Caching; /// The class's name will become the cache folder's name /// [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 RetryCountToDelay = new Dictionary() + private readonly FrozenDictionary retryCountToDelay = new Dictionary() { [0] = TimeSpan.FromSeconds(4), [1] = TimeSpan.FromSeconds(16), [2] = TimeSpan.FromSeconds(64), }.ToFrozenDictionary(); + private readonly ConcurrentDictionary concurrentTasks = new(); + private readonly IHttpClientFactory httpClientFactory; private readonly IServiceProvider serviceProvider; private readonly ILogger logger; - private readonly ConcurrentDictionary concurrentTasks = new(); - private string? baseFolder; private string? cacheFolder; - /// - /// Initializes a new instance of the class. - /// - /// 服务提供器 - public ImageCache(IServiceProvider serviceProvider) - { - logger = serviceProvider.GetRequiredService>(); - httpClientFactory = serviceProvider.GetRequiredService(); - - this.serviceProvider = serviceProvider; - } - /// public void RemoveInvalid() { @@ -62,7 +51,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation /// public void Remove(Uri uriForCachedItem) { - Remove(new ReadOnlySpan(ref uriForCachedItem)); + Remove([uriForCachedItem]); } /// @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs index d6bf777d..1f515abd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs @@ -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(); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs index 2a20113f..1d091b32 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -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); } /// @@ -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); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs index 83dc3e2a..dfc1c56a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs @@ -11,8 +11,6 @@ namespace Snap.Hutao.Core.Diagnostics; /// internal readonly struct ValueStopwatch { - private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - private readonly long startTimestamp; private ValueStopwatch(long startTimestamp) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs index fec24cb5..87514817 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs @@ -31,7 +31,7 @@ internal sealed partial class ExceptionRecorder private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { ValueTask task = serviceProvider - .GetRequiredService() + .GetRequiredService() .UploadLogAsync(e.Exception); if (!task.IsCompleted) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorkerOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorkerOptions.cs index afa5ab83..21df6075 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorkerOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorkerOptions.cs @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/ConsoleWindowLifeTime.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/ConsoleWindowLifeTime.cs new file mode 100644 index 00000000..66f4ceaf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/ConsoleWindowLifeTime.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs deleted file mode 100644 index b90e42ab..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.Diagnostics; - -namespace Snap.Hutao.Core.Logging; - -/// -/// A logger that writes messages in the debug output window only when a debugger is attached. -/// -internal sealed class DebugLogger : ILogger -{ - private readonly string name; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the logger. - public DebugLogger(string name) - { - this.name = name; - } - - /// - public IDisposable BeginScope(TState state) - where TState : notnull - { - return NullScope.Instance; - } - - /// - public bool IsEnabled(LogLevel logLevel) - { - // If the filter is null, everything is enabled - return logLevel != LogLevel.None; - } - - /// - [SuppressMessage("", "SH002")] - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func 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); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs deleted file mode 100644 index 852a90c9..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs +++ /dev/null @@ -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; - -/// -/// Extension methods for the class. -/// -internal static class DebugLoggerFactoryExtensions -{ - /// - /// Adds a debug logger named 'Debug' to the factory. - /// - /// The extension method argument. - /// builder - public static ILoggingBuilder AddUnconditionalDebug(this ILoggingBuilder builder) - { - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - return builder; - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs deleted file mode 100644 index 2cf5dd5e..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Core.Logging; - -/// -/// The provider for the . -/// -[ProviderAlias("Debug")] -internal sealed class DebugLoggerProvider : ILoggerProvider -{ - /// - public ILogger CreateLogger(string name) - { - return new DebugLogger(name); - } - - /// - public void Dispose() - { - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/LoggerFactoryExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LoggerFactoryExtensions.cs new file mode 100644 index 00000000..1dd99ff7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LoggerFactoryExtensions.cs @@ -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(); + + builder.AddSimpleConsole(); + return builder; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs deleted file mode 100644 index 65db7224..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Core.Logging; - -/// -/// An empty scope without any logic -/// -internal sealed class NullScope : IDisposable -{ - private NullScope() - { - } - - /// - /// 实例 - /// - public static NullScope Instance { get; } = new NullScope(); - - /// - public void Dispose() - { - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs index 49c50cab..9dd3079a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs @@ -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; -/// -/// 存储环境相关的选项 -/// 运行时运算得到的选项,无数据库交互 -/// [Injection(InjectAs.Singleton)] -internal sealed class RuntimeOptions : IOptions +internal sealed class RuntimeOptions { - private readonly ILogger logger; - private readonly bool isWebView2Supported; private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected; private bool? isElevated; - /// - /// 构造一个新的胡桃选项 - /// - /// 日志器 public RuntimeOptions(ILogger logger) { - this.logger = logger; - AppLaunchTime = DateTimeOffset.UtcNow; DataFolder = GetDataFolderPath(); @@ -45,117 +32,95 @@ internal sealed class RuntimeOptions : IOptions 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 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."); + } + } } - /// - /// 当前版本 - /// public Version Version { get; } - /// - /// 标准UA - /// public string UserAgent { get; } - /// - /// 安装位置 - /// public string InstalledLocation { get; } - /// - /// 数据文件夹路径 - /// public string DataFolder { get; } - /// - /// 本地缓存 - /// public string LocalCache { get; } - /// - /// 包家族名称 - /// public string FamilyName { get; } - /// - /// 设备Id - /// public string DeviceId { get; } - /// - /// WebView2 版本 - /// public string WebView2Version { get => webView2Version; } - /// - /// 是否支持 WebView2 - /// public bool IsWebView2Supported { get => isWebView2Supported; } - /// - /// 是否为提升的权限 - /// - 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; } - - /// - 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."); - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs index d67cc641..3565689c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs @@ -9,59 +9,26 @@ namespace Snap.Hutao.Core.Setting; [HighQuality] internal static class SettingKeys { - /// - /// 窗体矩形 - /// public const string WindowRect = "WindowRect"; - /// - /// 导航侧栏是否展开 - /// public const string IsNavPaneOpen = "IsNavPaneOpen"; - /// - /// 启动次数 - /// public const string LaunchTimes = "LaunchTimes"; - /// - /// 数据文件夹 - /// public const string DataFolderPath = "DataFolderPath"; - /// - /// 通行证用户名(邮箱) - /// public const string PassportUserName = "PassportUserName"; - /// - /// 通行证密码 - /// public const string PassportPassword = "PassportPassword"; - /// - /// 消息是否显示 - /// public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked"; - /// - /// 1.7.0 版本指引状态 - /// public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState"; - /// - /// 排除的系统公告 - /// public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds"; - /// - /// 禁用元数据更新检查 - /// public const string SuppressMetadataInitialization = "SuppressMetadataInitialization"; - /// - /// 覆盖管理员权限执行命令 - /// 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"; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs index 10cf42bb..1a52c409 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ITaskContext.cs @@ -8,25 +8,13 @@ namespace Snap.Hutao.Core.Threading; /// internal interface ITaskContext { - SynchronizationContext GetSynchronizationContext(); + SynchronizationContext SynchronizationContext { get; } + + void BeginInvokeOnMainThread(Action action); - /// - /// 在主线程上同步等待执行操作 - /// - /// 操作 void InvokeOnMainThread(Action action); - /// - /// 异步切换到 后台线程 - /// - /// 使用 异步切换到 主线程 - /// 等待体 ThreadPoolSwitchOperation SwitchToBackgroundAsync(); - /// - /// 异步切换到 主线程 - /// - /// 使用 异步切换到 后台线程 - /// 等待体 DispatcherQueueSwitchOperation SwitchToMainThreadAsync(); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs index c0d5e145..32b133ed 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs @@ -24,6 +24,8 @@ internal sealed class TaskContext : ITaskContext SynchronizationContext.SetSynchronizationContext(synchronizationContext); } + public SynchronizationContext SynchronizationContext { get => synchronizationContext; } + /// 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()); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs b/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs new file mode 100644 index 00000000..b0d74b4f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs @@ -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 namespaceBuffer = stackalloc byte[16]; + Verify.Operation(namespaceId.TryWriteBytes(namespaceBuffer), "Failed to copy namespace guid bytes"); + Span nameBytes = Encoding.UTF8.GetBytes(name); + + if (BitConverter.IsLittleEndian) + { + ReverseEndianness(namespaceBuffer); + } + + Span data = stackalloc byte[namespaceBuffer.Length + nameBytes.Length]; + namespaceBuffer.CopyTo(data); + nameBytes.CopyTo(data[namespaceBuffer.Length..]); + + Span temp = stackalloc byte[20]; + Verify.Operation(SHA1.TryHashData(data, temp, out _), "Failed to compute SHA1 hash of UUID"); + + Span 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 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 guid, int left, int right) + { + (guid[right], guid[left]) = (guid[left], guid[right]); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtension.cs index 867ef104..f31d6af8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtension.cs @@ -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)); - /// - /// 从Unix时间戳转换 - /// - /// 时间戳 - /// 默认值 - /// 转换的时间 - 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; + } + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs index 68b066d5..049bc56e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs @@ -167,7 +167,7 @@ internal static partial class EnumerableExtension return results; } - public static async ValueTask> SelectListAsync(this List list, Func> selector, CancellationToken token) + public static async ValueTask> SelectListAsync(this List list, Func> selector, CancellationToken token = default) { List results = new(list.Count); @@ -207,4 +207,4 @@ internal static partial class EnumerableExtension list.Sort((left, right) => keySelector(right).CompareTo(keySelector(left))); return list; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs index 9d3840c0..2f4bafda 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs @@ -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)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs index eaa5e998..3ff7c070 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.cs @@ -17,19 +17,6 @@ internal static partial class EnumerableExtension return source.ElementAtOrDefault(index) ?? source.LastOrDefault(); } - /// - /// 如果传入集合不为空则原路返回, - /// 如果传入集合为空返回一个集合的空集 - /// - /// 源类型 - /// 源 - /// 源集合或空集 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable EmptyIfNull(this IEnumerable? source) - { - return source ?? Enumerable.Empty(); - } - /// /// 寻找枚举中唯一的值,找不到时 /// 回退到首个或默认值 diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/MemoryCacheExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/MemoryCacheExtension.cs index 9ae998a5..77b7a755 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/MemoryCacheExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/MemoryCacheExtension.cs @@ -10,13 +10,6 @@ namespace Snap.Hutao.Extension; /// internal static class MemoryCacheExtension { - /// - /// 尝试从 IMemoryCache 中移除并返回具有指定键的值 - /// - /// 缓存 - /// 键 - /// 值 - /// 是否移除成功 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(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; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/NullableExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/NullableExtension.cs index 85f31b64..7c25a349 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/NullableExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/NullableExtension.cs @@ -17,4 +17,22 @@ internal static class NullableExtension value = default; return false; } + + public static string ToStringOrEmpty(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; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs index dee1a93c..9f60a0fd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs @@ -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 /// Span /// 最大值的下标 public static int IndexOfMax(this in ReadOnlySpan span) - where T : INumber + where T : INumber, IMinMaxValue { - 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 AsSpan(this List list) - { - return CollectionsMarshal.AsSpan(list); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs index 515666c7..f71001ec 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs @@ -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') { diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs index 608eb0a4..f07285eb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs @@ -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 diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs index fa35afd5..e655fe85 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs @@ -11,6 +11,6 @@ internal sealed partial class ProgressFactory : IProgressFactory public IProgress CreateForMainThread(Action handler) { - return new DispatcherQueueProgress(handler, taskContext.GetSynchronizationContext()); + return new DispatcherQueueProgress(handler, taskContext.SynchronizationContext); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs new file mode 100644 index 00000000..8ba3ea82 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs @@ -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); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs new file mode 100644 index 00000000..fb28e857 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs @@ -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); + } + } + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs new file mode 100644 index 00000000..c3e5e810 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.Designer.cs @@ -0,0 +1,612 @@ +// +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 + { + /// + 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("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CalculatorRefreshTime") + .HasColumnType("TEXT"); + + b.Property("GameRecordRefreshTime") + .HasColumnType("TEXT"); + + b.Property("Info") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShowcaseRefreshTime") + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("avatar_infos"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("cultivate_entries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("AvatarLevelTo") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("SkillALevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillALevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelTo") + .HasColumnType("INTEGER"); + + b.Property("WeaponLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("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("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("IsFinished") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("EntryId"); + + b.ToTable("cultivate_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AttachedUid") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("cultivate_projects"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DailyNote") + .HasColumnType("TEXT"); + + b.Property("DailyTaskNotify") + .HasColumnType("INTEGER"); + + b.Property("DailyTaskNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotify") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("RefreshTime") + .HasColumnType("TEXT"); + + b.Property("ResinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ResinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotify") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("UserId"); + + b.ToTable("daily_notes"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("gacha_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("GachaType") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("QueryType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("gacha_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AttachUid") + .HasColumnType("TEXT"); + + b.Property("MihoyoSDK") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("game_accounts"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AppendPropIdList") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("MainPropId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_reliquaries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("PromoteLevel") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_weapons"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ExpireTime") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("object_cache"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SpiralAbyss") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("spiral_abysses"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Aid") + .HasColumnType("TEXT"); + + b.Property("CookieToken") + .HasColumnType("TEXT"); + + b.Property("CookieTokenLastUpdateTime") + .HasColumnType("TEXT"); + + b.Property("Fingerprint") + .HasColumnType("TEXT"); + + b.Property("FingerprintLastUpdateTime") + .HasColumnType("TEXT"); + + b.Property("IsOversea") + .HasColumnType("INTEGER"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LToken") + .HasColumnType("TEXT") + .HasColumnName("Ltoken"); + + b.Property("Mid") + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.cs new file mode 100644 index 00000000..37a9b8f8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20231207085530_AddCultivateEntryLevelInformation.cs @@ -0,0 +1,56 @@ +// +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + /// + public partial class AddCultivateEntryLevelInformation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "cultivate_entry_level_informations", + columns: table => new + { + InnerId = table.Column(type: "TEXT", nullable: false), + EntryId = table.Column(type: "TEXT", nullable: false), + AvatarLevelFrom = table.Column(type: "INTEGER", nullable: false), + AvatarLevelTo = table.Column(type: "INTEGER", nullable: false), + SkillALevelFrom = table.Column(type: "INTEGER", nullable: false), + SkillALevelTo = table.Column(type: "INTEGER", nullable: false), + SkillELevelFrom = table.Column(type: "INTEGER", nullable: false), + SkillELevelTo = table.Column(type: "INTEGER", nullable: false), + SkillQLevelFrom = table.Column(type: "INTEGER", nullable: false), + SkillQLevelTo = table.Column(type: "INTEGER", nullable: false), + WeaponLevelFrom = table.Column(type: "INTEGER", nullable: false), + WeaponLevelTo = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "cultivate_entry_level_informations"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index 6c6a556f..81c6a057 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs @@ -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("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("AvatarLevelTo") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("SkillALevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillALevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelTo") + .HasColumnType("INTEGER"); + + b.Property("WeaponLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("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("Count") + b.Property("Count") .HasColumnType("INTEGER"); b.Property("EntryId") @@ -128,14 +174,14 @@ namespace Snap.Hutao.Migrations b.Property("IsFinished") .HasColumnType("INTEGER"); - b.Property("ItemId") + b.Property("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") diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs index cd892254..348ffdd4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs @@ -33,6 +33,8 @@ internal sealed class CultivateEntry : IDbMappingForeignKeyFrom /// 养成类型 /// @@ -59,4 +61,10 @@ internal sealed class CultivateEntry : IDbMappingForeignKeyFrom +{ + [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}"), + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateItem.cs index af0ffec5..eef1e917 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateItem.cs @@ -35,12 +35,12 @@ internal sealed class CultivateItem : IDbMappingForeignKeyFrom /// 物品 Id /// - public int ItemId { get; set; } + public uint ItemId { get; set; } /// /// 物品个数 /// - public int Count { get; set; } + public uint Count { get; set; } /// /// 是否完成此项 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs index 8d3b18e3..fc1146be 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs @@ -37,89 +37,40 @@ internal sealed class AppDbContext : DbContext logger.LogInformation("{Name}[{Id}] created", nameof(AppDbContext), ContextId); } - /// - /// 设置 - /// public DbSet Settings { get; set; } = default!; - /// - /// 用户 - /// public DbSet Users { get; set; } = default!; - /// - /// 成就 - /// public DbSet Achievements { get; set; } = default!; - /// - /// 成就存档 - /// public DbSet AchievementArchives { get; set; } = default!; - /// - /// 卡池数据 - /// public DbSet GachaItems { get; set; } = default!; - /// - /// 卡池存档 - /// public DbSet GachaArchives { get; set; } = default!; - /// - /// 角色信息 - /// public DbSet AvatarInfos { get; set; } = default!; - /// - /// 游戏内账号 - /// public DbSet GameAccounts { get; set; } = default!; - /// - /// 实时便笺 - /// public DbSet DailyNotes { get; set; } = default!; - /// - /// 对象缓存 - /// public DbSet ObjectCache { get; set; } = default!; - /// - /// 培养计划 - /// public DbSet CultivateProjects { get; set; } = default!; - /// - /// 培养入口点 - /// public DbSet CultivateEntries { get; set; } = default!; - /// - /// 培养消耗物品 - /// + public DbSet LevelInformations { get; set; } = default!; + public DbSet CultivateItems { get; set; } = default!; - /// - /// 背包内物品 - /// public DbSet InventoryItems { get; set; } = default!; - /// - /// 背包内武器 - /// public DbSet InventoryWeapons { get; set; } = default!; - /// - /// 背包内圣遗物 - /// public DbSet InventoryReliquaries { get; set; } = default!; - /// - /// 深渊记录 - /// public DbSet SpiralAbysses { get; set; } = default!; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs index e15c6147..0155e807 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs @@ -30,7 +30,7 @@ internal sealed class UIAFInfo : IMappingFrom [JsonIgnore] public DateTimeOffset ExportDateTime { - get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); + get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs index b5302513..f0eeae6e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFInfo.cs @@ -38,7 +38,7 @@ internal sealed class UIGFInfo : IMappingFrom DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); + get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs index a17ddfad..1ef79580 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Inventory/UIIFInfo.cs @@ -35,7 +35,7 @@ internal sealed class UIIFInfo [JsonIgnore] public DateTimeOffset ExportDateTime { - get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); + get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs index b17463bc..8a5a9225 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Intrinsic; -using System.Runtime.InteropServices; namespace Snap.Hutao.Model.Metadata.Avatar; diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/Material.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/Material.cs index 34e5908b..fc4cf24a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/Material.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/Material.cs @@ -34,6 +34,8 @@ internal sealed class Material : DisplayItem /// 是否为物品栏物品 public bool IsInventoryItem() { + // TODO: Add a pre-filtered metadata set to check if it's an inventory item + // 原质 if (Id == 112001U) { diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 1f7e8c14..ab31fad0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -504,7 +504,7 @@ 精炼 {0} 阶 - 必须先选择一个用户与角色 + 必须登录 米游社/HoYoLAB 并选择一个用户与角色 删除了 Uid:{0} 的 {1} 条祈愿记录 @@ -1265,6 +1265,9 @@ 正在转换客户端 + + 使用米游社扫描二维码 + 该操作是不可逆的,所有用户登录状态会丢失 @@ -1406,6 +1409,9 @@ 请先前往养成计划页面创建计划并选中 + + 重新添加物品以查看养成描述 + 添加成功 @@ -1562,6 +1568,9 @@ 创建桌面快捷方式失败 + + 已使用磁盘空间:{0} + 无感验证复合 Url 配置成功 @@ -2127,7 +2136,7 @@ 预下载 - 该账号尚未绑定 UID + 该账号尚未绑定实时便笺通知 UID 绑定当前用户的角色 @@ -2202,7 +2211,7 @@ 图片缓存 在此处存放 - 打开 缓存 文件夹 + 缓存 文件夹 复制 @@ -2229,7 +2238,7 @@ 用户数据/元数据 在此处存放 - 打开 数据 文件夹 + 数据 文件夹 删除 @@ -2249,6 +2258,12 @@ 设备 ID + + IP:{0} 归属服务器:{1} + + + 设备 IP + 在祈愿记录页面显示或隐藏无记录的历史祈愿活动 @@ -2516,6 +2531,15 @@ 立即登录或注册 + + 控制胡桃启动时是否开启控制台,重启后生效 + + + 调试控制台 + + + 打开文件夹 + 角色出场率 = 本层上阵该角色次数(层内重复出现只记一次)/ 深渊记录总数 @@ -2603,6 +2627,9 @@ 网页登录 + + 扫码登录 + 手动输入 @@ -2822,6 +2849,9 @@ 无效的 UID + + 胡桃服务维护中 + 验证失败,请手动验证或前往「米游社-我的角色」页面查看 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs index 3ea09669..56167c1b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/DbStoreOptions.cs @@ -116,7 +116,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions反序列化器 /// 默认值 /// - [return:NotNull] + [return: NotNull] protected T GetOption(ref T? storage, string key, Func deserializer, [DisallowNull] T defaultValue) { if (storage is not null) @@ -128,7 +128,16 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions(); 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; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs index 199ff6d3..df39f93b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AnnouncementService.cs @@ -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 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> announcementContentResponse = await announcementClient - .GetAnnouncementContentsAsync(cancellationToken) - .ConfigureAwait(false); - - if (announcementContentResponse.IsOk()) - { - List contents = announcementContentResponse.Data.List; - - Dictionary 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> announcementContentResponse = await announcementClient + .GetAnnouncementContentsAsync(cancellationToken) + .ConfigureAwait(false); + + if (!announcementContentResponse.IsOk()) + { + return default!; + } + + List contents = announcementContentResponse.Data.List; + + Dictionary 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 contentMap, List 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
", 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 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 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; } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs index 4b0f32d8..e02352d9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs @@ -11,25 +11,10 @@ using System.IO; namespace Snap.Hutao.Service; -/// -/// 应用程序选项 -/// 存储服务相关的选项 -/// [ConstructorGenerated(CallBaseConstructor = true)] [Injection(InjectAs.Singleton)] internal sealed partial class AppOptions : DbStoreOptions { - private readonly List> supportedBackdropTypesInner = CollectionsNameValue.FromEnum(); - - private readonly List> 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; - /// - /// 游戏路径 - /// public string GamePath { get => GetOption(ref gamePath, SettingEntry.GamePath); set => SetOption(ref gamePath, SettingEntry.GamePath, value); } - /// - /// PowerShell 路径 - /// 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); } - /// - /// 游戏路径 - /// public bool IsEmptyHistoryWishVisible { get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible); set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value); } - /// - /// 所有支持的背景样式 - /// - public List> BackdropTypes { get => supportedBackdropTypesInner; } + public List> BackdropTypes { get; } = CollectionsNameValue.FromEnum(); - /// - /// 背景类型 默认 Mica - /// public BackdropType BackdropType { get => GetOption(ref backdropType, SettingEntry.SystemBackdropType, v => Enum.Parse(v), BackdropType.Mica).Value; - set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToString()!); + set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToStringOrEmpty()); } - /// - /// 所有支持的语言 - /// - public List> Cultures { get => supportedCulturesInner; } + public List> Cultures { get; } = SupportedCultures.Get(); - /// - /// 初始化前的语言 - /// 通过设置与获取此属性,就可以获取到与系统同步的语言 - /// - public CultureInfo PreviousCulture { get; set; } = default!; - - /// - /// 当前语言 - /// 默认为系统语言 - /// public CultureInfo CurrentCulture { get => GetOption(ref currentCulture, SettingEntry.Culture, CultureInfo.GetCultureInfo, CultureInfo.CurrentCulture); set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name); } - /// - /// 是否启用高级功能 - /// DO NOT MOVE TO OTHER CLASS - /// We are binding this property in SettingPage - /// 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 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!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs index cba52ed6..164971a4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs @@ -51,6 +51,20 @@ internal sealed partial class CultivationDbService : ICultivationDbService } } + public async ValueTask> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + return await appDbContext.CultivateEntries + .AsNoTracking() + .Where(e => e.ProjectId == projectId) + .Include(e => e.LevelInformation) + .ToListAsync() + .ConfigureAwait(false); + } + } + public async ValueTask> 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(); + 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(); + await appDbContext.LevelInformations.AddAndSaveAsync(levelInformation).ConfigureAwait(false); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationMetadataContext.cs new file mode 100644 index 00000000..35996d60 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationMetadataContext.cs @@ -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 Materials { get; set; } = default!; + + public Dictionary IdMaterialMap { get; set; } = default!; + + public Dictionary IdAvatarMap { get; set; } = default!; + + public Dictionary IdWeaponMap { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 67b1ecb8..7491b43d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -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; /// - public List GetInventoryItemViews(CultivateProject cultivateProject, List metadata, ICommand saveCommand) + public List GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand) { using (IServiceScope scope = serviceProvider.CreateScope()) { @@ -39,7 +39,7 @@ internal sealed partial class CultivationService : ICultivationService List entities = cultivationDbService.GetInventoryItemListByProjectId(projectId); List 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 } /// - public async ValueTask> GetCultivateEntriesAsync( - CultivateProject cultivateProject, - List materials, - Dictionary idAvatarMap, - Dictionary idWeaponMap) + public async ValueTask> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context) { await taskContext.SwitchToBackgroundAsync(); List entries = await cultivationDbService - .GetCultivateEntryListByProjectIdAsync(cultivateProject.InnerId) + .GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(cultivateProject.InnerId) .ConfigureAwait(false); List resultEntries = new(entries.Count); foreach (CultivateEntry entry in entries) { List 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 /// public async ValueTask> GetStatisticsCultivateItemCollectionAsync( - CultivateProject cultivateProject, - List materials, - CancellationToken token) + CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token) { await taskContext.SwitchToBackgroundAsync(); List 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 } /// - public async ValueTask SaveConsumptionAsync(CultivateType type, uint itemId, List items) + public async ValueTask SaveConsumptionAsync(CultivateType type, uint itemId, List 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 toAdd = items.Select(item => CultivateItem.From(entryId, item)); await cultivationDbService.AddCultivateItemRangeAsync(toAdd).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs index ffb2ed1f..45c90413 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs @@ -35,4 +35,10 @@ internal interface ICultivationDbService void UpdateCultivateItem(CultivateItem item); ValueTask UpdateCultivateItemAsync(CultivateItem item); + + ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId); + + ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation); + + ValueTask> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationMetadataContext.cs new file mode 100644 index 00000000..43c05dfb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationMetadataContext.cs @@ -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 +{ +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs index 2b690bb0..2beb88ab 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs @@ -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 ///
ObservableCollection ProjectCollection { get; } - /// - /// 获取绑定用的养成列表 - /// - /// 养成计划 - /// 材料 - /// Id 角色映射 - /// Id 武器映射 - /// 绑定用的养成列表 - ValueTask> GetCultivateEntriesAsync( - CultivateProject cultivateProject, - List materials, - Dictionary idAvatarMap, - Dictionary idWeaponMap); + ValueTask> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context); - /// - /// 获取物品列表 - /// - /// 养成计划 - /// 元数据 - /// 保存命令 - /// 物品列表 - List GetInventoryItemViews(CultivateProject cultivateProject, List metadata, ICommand saveCommand); + List GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand); - /// - /// 异步获取统计物品列表 - /// - /// 养成计划 - /// 材料 - /// 取消令牌 - /// 统计物品列表 ValueTask> GetStatisticsCultivateItemCollectionAsync( - CultivateProject cultivateProject, List materials, CancellationToken token); + CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token); /// /// 删除养成清单 @@ -74,14 +46,7 @@ internal interface ICultivationService /// 任务 ValueTask RemoveProjectAsync(CultivateProject project); - /// - /// 异步保存养成物品 - /// - /// 类型 - /// 主Id - /// 待存物品 - /// 是否保存成功 - ValueTask SaveConsumptionAsync(CultivateType type, uint itemId, List items); + ValueTask SaveConsumptionAsync(CultivateType type, uint itemId, List items, LevelInformation levelInformation); /// /// 保存养成物品状态 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/LevelInformation.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/LevelInformation.cs new file mode 100644 index 00000000..13a5f527 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/LevelInformation.cs @@ -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 +{ + 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; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs index 32d141c3..5c964feb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilderContext.cs @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs index cfa27516..4d6b8abb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogDbService.cs @@ -287,7 +287,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService Time = i.Time, Id = i.Id, }); - return [..result]; + return [.. result]; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs index c5d95cc4..9998565e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogHutaoCloudService.cs @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogHutaoCloudService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogHutaoCloudService.cs index fd87d389..18fc51ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogHutaoCloudService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogHutaoCloudService.cs @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs index a146c2d3..45be4b6b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -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)); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs index b36ce4a4..247fe149 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs @@ -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)); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs index ed63e016..6f82560e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UIGFImportService.cs @@ -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); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index 5876b870..4241bba3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -40,10 +40,6 @@ internal sealed class LaunchOptions : DbStoreOptions private bool? useStarwardPlayTimeStatistics; private bool? setDiscordActivityWhenPlaying; - /// - /// 构造一个新的启动游戏选项 - /// - /// 服务提供器 public LaunchOptions(IServiceProvider serviceProvider) : base(serviceProvider) { @@ -53,47 +49,66 @@ internal sealed class LaunchOptions : DbStoreOptions InitializeMonitors(Monitors); InitializeScreenFps(out primaryScreenFps); + + static void InitializeMonitors(List> monitors) + { + try + { + // This list can't use foreach + // https://github.com/microsoft/CsWinRT/issues/747 + IReadOnlyList 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); + } + } } - /// - /// 是否启用启动参数 - /// public bool IsEnabled { get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true); set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value); } - /// - /// 是否全屏 - /// public bool IsFullScreen { get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen); set => SetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, value); } - /// - /// 是否无边框 - /// public bool IsBorderless { get => GetOption(ref isBorderless, SettingEntry.LaunchIsBorderless); set => SetOption(ref isBorderless, SettingEntry.LaunchIsBorderless, value); } - /// - /// 是否独占全屏 - /// public bool IsExclusive { get => GetOption(ref isExclusive, SettingEntry.LaunchIsExclusive); set => SetOption(ref isExclusive, SettingEntry.LaunchIsExclusive, value); } - /// - /// 屏幕宽度 - /// 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); } - /// - /// 屏幕高度 - /// 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); } - /// - /// 是否全屏 - /// public bool UnlockFps { get => GetOption(ref unlockFps, SettingEntry.LaunchUnlockFps); set => SetOption(ref unlockFps, SettingEntry.LaunchUnlockFps, value); } - /// - /// 目标帧率 - /// public int TargetFps { get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps); set => SetOption(ref targetFps, SettingEntry.LaunchTargetFps, value); } - /// - /// 所有监视器 - /// public List> Monitors { get; } = []; - /// - /// 目标帧率 - /// [AllowNull] public NameValue 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> monitors) - { - // This list can't use foreach - // https://github.com/microsoft/CsWinRT/issues/747 - IReadOnlyList 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); - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs index 38866257..ca41a9cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs @@ -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) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs index 68bbd48f..560c586b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs index 8053bc09..e9d6e28a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -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 progress) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs index 0566c9a0..c250c8a4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoSpiralAbyssService.cs @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs index 87074a42..71fabc3c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptions.cs @@ -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; -/// -/// 胡桃用户选项 -/// [Injection(InjectAs.Singleton)] -internal sealed class HutaoUserOptions : ObservableObject, IOptions +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; - /// - /// 用户名 - /// public string? UserName { get => userName; set => SetProperty(ref userName, value); } - /// - /// 真正的用户名 - /// - public string? ActualUserName { get => IsLoggedIn ? UserName : null; } - - /// - /// 是否已登录 - /// public bool IsLoggedIn { get => isLoggedIn; set => SetProperty(ref isLoggedIn, value); } - /// - /// 胡桃云服务是否可用 - /// - public bool IsCloudServiceAllowed { get => isHutaoCloudServiceAllowed; set => SetProperty(ref isHutaoCloudServiceAllowed, value); } + public bool IsCloudServiceAllowed { get => isCloudServiceAllowed; set => SetProperty(ref isCloudServiceAllowed, value); } - /// - /// 是否为开发者 - /// public bool IsLicensedDeveloper { get => isLicensedDeveloper; set => SetProperty(ref isLicensedDeveloper, value); } public bool IsMaintainer { get => isMaintainer; set => SetProperty(ref isMaintainer, value); } - /// - /// 祈愿记录服务到期时间 - /// public string? GachaLogExpireAt { get => gachaLogExpireAt; set => SetProperty(ref gachaLogExpireAt, value); } public string? GachaLogExpireAtSlim { get => gachaLogExpireAtSlim; set => SetProperty(ref gachaLogExpireAtSlim, value); } - /// - public HutaoUserOptions Value { get => this; } + internal string? Token { get => token; set => token = value; } - public async ValueTask 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 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(); - } - - /// - /// 登录失败 - /// - public void LoginFailed() - { - UserName = SH.ViewServiceHutaoUserLoginFailHint; - initializedTaskCompletionSource.TrySetResult(); - } - - public void SkipLogin() - { - initializedTaskCompletionSource.TrySetResult(); - } - - /// - /// 刷新用户信息 - /// - /// 用户信息 - 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 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; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptionsExtension.cs new file mode 100644 index 00000000..d7bf4d84 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserOptionsExtension.cs @@ -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 GetTokenAsync(this HutaoUserOptions options) + { + await options.Initialization.Task.ConfigureAwait(false); + return options.Token; + } + + public static async ValueTask 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 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(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs index 7d7638b6..34527a60 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoUserService.cs @@ -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(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataContext.cs new file mode 100644 index 00000000..ca35233e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataContext.cs @@ -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; \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdAvatarSource.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdAvatarSource.cs new file mode 100644 index 00000000..d8cb7428 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdAvatarSource.cs @@ -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 IdAvatarMap { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdMaterialSource.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdMaterialSource.cs new file mode 100644 index 00000000..dff1b88e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdMaterialSource.cs @@ -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 IdMaterialMap { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdWeaponSource.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdWeaponSource.cs new file mode 100644 index 00000000..d664ecd5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataDictionaryIdWeaponSource.cs @@ -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 IdWeaponMap { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataListMaterialSource.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataListMaterialSource.cs new file mode 100644 index 00000000..7a54f01d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/IMetadataListMaterialSource.cs @@ -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 Materials { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/MetadataServiceContextExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/MetadataServiceContextExtension.cs new file mode 100644 index 00000000..114b56f5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/ContextAbstraction/MetadataServiceContextExtension.cs @@ -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 GetContextAsync(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 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 +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs index c760a0df..86b29ebe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/LocaleNames.cs @@ -8,6 +8,8 @@ namespace Snap.Hutao.Service.Metadata; /// 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) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptions.cs index 5fe5a13b..3bb1b91c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptions.cs @@ -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; -/// -/// 元数据选项 -/// [ConstructorGenerated] [Injection(InjectAs.Singleton)] -internal sealed partial class MetadataOptions : IOptions +internal sealed partial class MetadataOptions { private readonly AppOptions appOptions; private readonly RuntimeOptions hutaoOptions; @@ -22,9 +17,6 @@ internal sealed partial class MetadataOptions : IOptions private string? fallbackDataFolder; private string? localizedDataFolder; - /// - /// 中文数据文件夹 - /// public string FallbackDataFolder { get @@ -39,9 +31,6 @@ internal sealed partial class MetadataOptions : IOptions } } - /// - /// 本地化数据文件夹 - /// public string LocalizedDataFolder { get @@ -56,17 +45,11 @@ internal sealed partial class MetadataOptions : IOptions } } - /// - /// 当前使用的元数据本地化名称 - /// public string LocaleName { - get => localeName ??= GetLocaleName(appOptions.CurrentCulture); + get => localeName ??= MetadataOptionsExtension.GetLocaleName(appOptions.CurrentCulture); } - /// - /// 当前语言代码 - /// public string LanguageCode { get @@ -79,63 +62,4 @@ internal sealed partial class MetadataOptions : IOptions throw new KeyNotFoundException($"Invalid localeName: '{LocaleName}'"); } } - - /// - public MetadataOptions Value { get => this; } - - /// - /// 获取语言名称 - /// - /// 语言信息 - /// 元数据语言名称 - public static string GetLocaleName(CultureInfo cultureInfo) - { - while (true) - { - if (LocaleNames.TryGetLocaleNameFromLanguageName(cultureInfo.Name, out string? localeName)) - { - return localeName; - } - else - { - cultureInfo = cultureInfo.Parent; - } - } - } - - /// - /// 检查是否为当前语言名称 - /// - /// 语言代码 - /// 是否为当前语言名称 - public bool IsCurrentLocale(string? languageCode) - { - if (string.IsNullOrEmpty(languageCode)) - { - return false; - } - - CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageCode); - return GetLocaleName(cultureInfo) == LocaleName; - } - - /// - /// 获取本地的本地化元数据文件 - /// - /// 文件名 - /// 本地的本地化元数据文件 - public string GetLocalizedLocalFile(string fileNameWithExtension) - { - return Path.Combine(LocalizedDataFolder, fileNameWithExtension); - } - - /// - /// 获取服务器上的本地化元数据文件 - /// - /// 文件名 - /// 服务器上的本地化元数据文件 - public string GetLocalizedRemoteFile(string fileNameWithExtension) - { - return Web.HutaoEndpoints.Metadata(LocaleName, fileNameWithExtension); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptionsExtension.cs new file mode 100644 index 00000000..59c17e93 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataOptionsExtension.cs @@ -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; + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/IInfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/IInfoBarService.cs index 432c943b..2cc22e7e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Notification/IInfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/IInfoBarService.cs @@ -6,89 +6,10 @@ using System.Collections.ObjectModel; namespace Snap.Hutao.Service.Notification; -/// -/// 消息条服务 -/// [HighQuality] internal interface IInfoBarService { - /// - /// 获取操作的集合 - /// ObservableCollection Collection { get; } - /// - /// 显示错误消息 - /// - /// 消息 - /// 关闭延迟 - void Error(string message, int delay = 30000); - - /// - /// 显示错误消息 - /// - /// 标题 - /// 消息 - /// 关闭延迟 - void Error(string title, string message, int delay = 30000); - - /// - /// 显示错误消息 - /// - /// 异常 - /// 关闭延迟 - void Error(Exception exception, int delay = 30000); - - /// - /// 显示错误消息 - /// - /// 异常 - /// 标题 - /// 关闭延迟 - void Error(Exception exception, string title, int delay = 30000); - - /// - /// 显示提示信息 - /// - /// 消息 - /// 关闭延迟 - void Information(string message, int delay = 5000); - - /// - /// 显示提示信息 - /// - /// 标题 - /// 消息 - /// 关闭延迟 - void Information(string title, string message, int delay = 5000); - - /// - /// 显示成功信息 - /// - /// 消息 - /// 关闭延迟 - void Success(string message, int delay = 5000); - - /// - /// 显示成功信息 - /// - /// 标题 - /// 消息 - /// 关闭延迟 - void Success(string title, string message, int delay = 5000); - - /// - /// 显示警告信息 - /// - /// 消息 - /// 关闭延迟 - void Warning(string message, int delay = 15000); - - /// - /// 显示警告信息 - /// - /// 标题 - /// 消息 - /// 关闭延迟 - void Warning(string title, string message, int delay = 15000); + void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs index f5ebe8f9..e7c3daf2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarService.cs @@ -34,84 +34,17 @@ internal sealed class InfoBarService : IInfoBarService get => collection ??= []; } - /// - public void Information(string message, int delay = 5000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Informational, null, message, delay); - } - - /// - public void Information(string title, string message, int delay = 5000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Informational, title, message, delay); - } - - /// - public void Success(string message, int delay = 5000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Success, null, message, delay); - } - - /// - public void Success(string title, string message, int delay = 5000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Success, title, message, delay); - } - - /// - public void Warning(string message, int delay = 30000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Warning, null, message, delay); - } - - /// - public void Warning(string title, string message, int delay = 30000) - { - PrepareInfoBarAndShow(InfoBarSeverity.Warning, title, message, delay); - } - - /// - public void Error(string message, int delay = 0) - { - PrepareInfoBarAndShow(InfoBarSeverity.Error, null, message, delay); - } - - /// - public void Error(string title, string message, int delay = 0) - { - PrepareInfoBarAndShow(InfoBarSeverity.Error, title, message, delay); - } - - /// - public void Error(Exception ex, int delay = 0) - { - PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, ex.Message, delay); - } - - /// - 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); } - /// - /// 准备信息条并显示 - /// - /// 严重程度 - /// 标题 - /// 消息 - /// 关闭延迟 - 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; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarServiceExtension.cs new file mode 100644 index 00000000..2a966d5f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Notification/InfoBarServiceExtension.cs @@ -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); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs b/src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs new file mode 100644 index 00000000..8e352ef8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/SupportedCultures.cs @@ -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> 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> Get() + { + return Cultures; + } + + private static NameValue ToNameValue(CultureInfo info) + { + return new(info.NativeName, info); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs new file mode 100644 index 00000000..8db6973f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserCollectionService.cs @@ -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> GetUserAndUidCollectionAsync(); + + ValueTask> GetUserCollectionAsync(); + + UserGameRole? GetUserGameRoleByUid(string uid); + + ValueTask RemoveUserAsync(BindingUser user); + ValueTask> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea); + bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs index e814d7d1..e89d0494 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs @@ -50,13 +50,6 @@ internal interface IUserService /// 处理的结果 ValueTask> ProcessInputCookieAsync(Cookie cookie, bool isOversea); - /// - /// 异步刷新 Cookie 的 CookieToken - /// - /// 用户 - /// 是否刷新成功 - ValueTask RefreshCookieTokenAsync(BindingUser user); - ValueTask RefreshCookieTokenAsync(Model.Entity.User user); /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs new file mode 100644 index 00000000..d0f689cb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserCollectionService.cs @@ -0,0 +1,185 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.Messaging; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Message; +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; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IUserCollectionService))] +internal sealed partial class UserCollectionService : IUserCollectionService +{ + private readonly ScopedDbCurrent dbCurrent; + private readonly IUserInitializationService userInitializationService; + private readonly IUserDbService userDbService; + private readonly ITaskContext taskContext; + private readonly IMessenger messenger; + + private readonly Throttler throttler = new(); + + private ObservableCollection? userCollection; + private Dictionary? midUserMap; + + private ObservableCollection? userAndUidCollection; + private Dictionary? uidUserGameRoleMap; + + public BindingUser? CurrentUser + { + get => dbCurrent.Current; + set => dbCurrent.Current = value; + } + + public async ValueTask> GetUserCollectionAsync() + { + using (await throttler.ThrottleAsync().ConfigureAwait(false)) + { + if (userCollection is null) + { + List entities = await userDbService.GetUserListAsync().ConfigureAwait(false); + List users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync).ConfigureAwait(false); + + midUserMap = []; + foreach (BindingUser user in users) + { + if (user.Entity.Mid is not null) + { + midUserMap[user.Entity.Mid] = user; + } + + if (user.NeedDbUpdateAfterResume) + { + await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); + user.NeedDbUpdateAfterResume = false; + } + } + + userCollection = users.ToObservableCollection(); + + try + { + CurrentUser = users.SelectedOrDefault(); + } + catch (InvalidOperationException ex) + { + ThrowHelper.UserdataCorrupted(SH.ServiceUserCurrentMultiMatched, ex); + } + } + } + + return userCollection; + } + + public async ValueTask> GetUserAndUidCollectionAsync() + { + if (userAndUidCollection is null) + { + await taskContext.SwitchToBackgroundAsync(); + ObservableCollection users = await GetUserCollectionAsync().ConfigureAwait(false); + List roles = []; + uidUserGameRoleMap = []; + + foreach (BindingUser user in users) + { + foreach (UserGameRole role in user.UserGameRoles) + { + roles.Add(UserAndUid.From(user.Entity, role)); + uidUserGameRoleMap[role.GameUid] = role; + } + } + + userAndUidCollection = roles.ToObservableCollection(); + } + + return userAndUidCollection; + } + + public async ValueTask RemoveUserAsync(BindingUser user) + { + // Sync cache + await taskContext.SwitchToMainThreadAsync(); + ArgumentNullException.ThrowIfNull(userCollection); + userCollection.Remove(user); + userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid); + if (user.Entity.Mid is not null) + { + midUserMap?.Remove(user.Entity.Mid); + } + + // Sync database + await taskContext.SwitchToBackgroundAsync(); + await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false); + + messenger.Send(new UserRemovedMessage(user.Entity)); + } + + public UserGameRole? GetUserGameRoleByUid(string uid) + { + if (uidUserGameRoleMap is null) + { + return default; + } + + try + { + return uidUserGameRoleMap[uid]; + } + catch (InvalidOperationException) + { + // Sequence contains more than one matching element + // TODO: return a specialize UserGameRole to indicate error + } + + return default; + } + + public bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user) + { + ArgumentNullException.ThrowIfNull(midUserMap); + return midUserMap.TryGetValue(mid, out user); + } + + public async ValueTask> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea) + { + await taskContext.SwitchToBackgroundAsync(); + BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false); + + if (newUser is null) + { + return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed); + } + + await GetUserCollectionAsync().ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(userCollection); + + // Sync cache + await taskContext.SwitchToMainThreadAsync(); + userCollection.Add(newUser); + if (newUser.Entity.Mid is not null) + { + midUserMap?.Add(newUser.Entity.Mid, newUser); + } + + if (userAndUidCollection is not null) + { + foreach (UserGameRole role in newUser.UserGameRoles) + { + userAndUidCollection.Add(new(newUser.Entity, role)); + uidUserGameRoleMap?.Add(role.GameUid, role); + } + } + + // Sync database + await taskContext.SwitchToBackgroundAsync(); + await userDbService.AddUserAsync(newUser.Entity).ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(newUser.UserInfo); + return new(UserOptionResult.Added, newUser.UserInfo.Uid); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs index 864346e5..a8a1f59e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs @@ -74,7 +74,7 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService SeedTime = $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}", ExtFields = JsonSerializer.Serialize(extendProperties), AppName = "bbs_cn", - BbsDeviceId = HoyolabOptions.DeviceId, + BbsDeviceId = HoyolabOptions.DeviceId36, DeviceFp = string.IsNullOrEmpty(user.Fingerprint) ? Core.Random.GetLowerHexString(13) : user.Fingerprint, }; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index 6ec475cf..ae2dc00d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -1,11 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.Mvvm.Messaging; -using Snap.Hutao.Core.Database; using Snap.Hutao.Core.DependencyInjection.Abstraction; -using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Message; using Snap.Hutao.ViewModel.User; using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab.Passport; @@ -23,39 +19,20 @@ namespace Snap.Hutao.Service.User; [Injection(InjectAs.Singleton, typeof(IUserService))] internal sealed partial class UserService : IUserService, IUserServiceUnsafe { - private readonly Throttler throttler = new(); - - private readonly ScopedDbCurrent dbCurrent; - private readonly IUserInitializationService userInitializationService; + private readonly IUserCollectionService userCollectionService; private readonly IServiceProvider serviceProvider; private readonly IUserDbService userDbService; private readonly ITaskContext taskContext; - private readonly IMessenger messenger; - private ObservableCollection? userCollection; - private ObservableCollection? userAndUidCollection; - - /// public BindingUser? Current { - get => dbCurrent.Current; - set => dbCurrent.Current = value; + get => userCollectionService.CurrentUser; + set => userCollectionService.CurrentUser = value; } - /// - public async ValueTask RemoveUserAsync(BindingUser user) + public ValueTask RemoveUserAsync(BindingUser user) { - // Sync cache - await taskContext.SwitchToMainThreadAsync(); - ArgumentNullException.ThrowIfNull(userCollection); - userCollection.Remove(user); - userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid); - - // Sync database - await taskContext.SwitchToBackgroundAsync(); - await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false); - - messenger.Send(new UserRemovedMessage(user.Entity)); + return userCollectionService.RemoveUserAsync(user); } public async ValueTask UnsafeRemoveUsersAsync() @@ -64,76 +41,21 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe await userDbService.RemoveUsersAsync().ConfigureAwait(false); } - /// - public async ValueTask> GetUserCollectionAsync() + public ValueTask> GetUserCollectionAsync() { - using (await throttler.ThrottleAsync().ConfigureAwait(false)) - { - if (userCollection is null) - { - List entities = await userDbService.GetUserListAsync().ConfigureAwait(false); - List users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync, default).ConfigureAwait(false); - - foreach (BindingUser user in users) - { - if (user.NeedDbUpdateAfterResume) - { - await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); - user.NeedDbUpdateAfterResume = false; - } - } - - userCollection = users.ToObservableCollection(); - - try - { - Current = users.SelectedOrDefault(); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceUserCurrentMultiMatched, ex); - } - } - } - - return userCollection; + return userCollectionService.GetUserCollectionAsync(); } - /// - public async ValueTask> GetRoleCollectionAsync() + public ValueTask> GetRoleCollectionAsync() { - await taskContext.SwitchToBackgroundAsync(); - if (userAndUidCollection is null) - { - ObservableCollection users = await GetUserCollectionAsync().ConfigureAwait(false); - userAndUidCollection = users - .SelectMany(user => user.UserGameRoles.Select(role => UserAndUid.From(user.Entity, role))) - .ToObservableCollection(); - } - - return userAndUidCollection; + return userCollectionService.GetUserAndUidCollectionAsync(); } - /// public UserGameRole? GetUserGameRoleByUid(string uid) { - if (userCollection is not null) - { - try - { - return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid); - } - catch (InvalidOperationException) - { - // Sequence contains more than one matching element - // TODO: return a specialize UserGameRole to indicate error - } - } - - return default; + return userCollectionService.GetUserGameRoleByUid(uid); } - /// public async ValueTask> ProcessInputCookieAsync(Cookie cookie, bool isOversea) { await taskContext.SwitchToBackgroundAsync(); @@ -145,33 +67,22 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe } // 检查 mid 对应用户是否存在 - ArgumentNullException.ThrowIfNull(userCollection); - if (TryGetUser(userCollection, mid, out BindingUser? user)) + if (!userCollectionService.TryGetUserByMid(mid, out BindingUser? user)) { - if (cookie.TryGetSToken(isOversea, out Cookie? stoken)) - { - user.SToken = stoken; - user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken; - user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; - - await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); - return new(UserOptionResult.Updated, mid); - } - else - { - return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken); - } + return await userCollectionService.TryCreateAndAddUserFromCookieAsync(cookie, isOversea).ConfigureAwait(false); } - else + + if (!cookie.TryGetSToken(isOversea, out Cookie? stoken)) { - return await TryCreateUserAndAddAsync(cookie, isOversea).ConfigureAwait(false); + return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken); } - } - /// - public ValueTask RefreshCookieTokenAsync(BindingUser user) - { - return RefreshCookieTokenAsync(user.Entity); + user.SToken = stoken; + user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken; + user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; + + await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); + return new(UserOptionResult.Updated, mid); } public async ValueTask RefreshCookieTokenAsync(Model.Entity.User user) @@ -183,65 +94,20 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe .GetCookieAccountInfoBySTokenAsync(user) .ConfigureAwait(false); - if (cookieTokenResponse.IsOk()) - { - string cookieToken = cookieTokenResponse.Data.CookieToken; - - // Check null and create a new one to avoid System.NullReferenceException - user.CookieToken ??= new(); - - // Sync ui and database - user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken; - await userDbService.UpdateUserAsync(user).ConfigureAwait(false); - - return true; - } - else + if (!cookieTokenResponse.IsOk()) { return false; } - } - private static bool TryGetUser(ObservableCollection users, string mid, [NotNullWhen(true)] out BindingUser? user) - { - // TODO: System.InvalidOperationException: Sequence contains more than one matching element - user = users.SingleOrDefault(u => u.Entity.Mid == mid); - return user is not null; - } + string cookieToken = cookieTokenResponse.Data.CookieToken; - private async ValueTask> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea) - { - await taskContext.SwitchToBackgroundAsync(); - BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false); + // Check null and create a new one to avoid System.NullReferenceException + user.CookieToken ??= new(); - if (newUser is not null) - { - // Sync cache - if (userCollection is not null) - { - await taskContext.SwitchToMainThreadAsync(); - { - userCollection.Add(newUser); + // Sync ui and database + user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken; + await userDbService.UpdateUserAsync(user).ConfigureAwait(false); - if (userAndUidCollection is not null) - { - foreach (UserGameRole role in newUser.UserGameRoles) - { - userAndUidCollection.Add(new(newUser.Entity, role)); - } - } - } - } - - // Sync database - await taskContext.SwitchToBackgroundAsync(); - await userDbService.AddUserAsync(newUser.Entity).ConfigureAwait(false); - ArgumentNullException.ThrowIfNull(newUser.UserInfo); - return new(UserOptionResult.Added, newUser.UserInfo.Uid); - } - else - { - return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed); - } + return true; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs new file mode 100644 index 00000000..d29f3272 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using BindingUser = Snap.Hutao.ViewModel.User.User; + +namespace Snap.Hutao.Service.User; + +internal static class UserServiceExtension +{ + public static ValueTask RefreshCookieTokenAsync(this IUserService userService, BindingUser user) + { + return userService.RefreshCookieTokenAsync(user.Entity); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index a1ed520d..29bea2fe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -174,6 +174,7 @@ + @@ -297,10 +298,12 @@ + + all @@ -329,6 +332,11 @@ + + + MSBuild:Compile + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml index 17aaaa0a..b2be538d 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml @@ -59,7 +59,7 @@ @@ -124,10 +124,10 @@ @@ -172,6 +172,6 @@ Value="{Binding LevelTarget, Mode=TwoWay}"/> - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportRegisterDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportRegisterDialog.xaml.cs index b2dbb09f..307c4353 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportRegisterDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportRegisterDialog.xaml.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Common; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Service.Notification; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; namespace Snap.Hutao.View.Dialog; @@ -13,7 +14,7 @@ namespace Snap.Hutao.View.Dialog; [DependencyProperty("VerifyCode", typeof(string))] internal sealed partial class HutaoPassportRegisterDialog : ContentDialog { - private readonly HomaPassportClient homaPassportClient; + private readonly HutaoPassportClient homaPassportClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; @@ -22,7 +23,7 @@ internal sealed partial class HutaoPassportRegisterDialog : ContentDialog InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - homaPassportClient = serviceProvider.GetRequiredService(); + homaPassportClient = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs index 480a71ce..0224ce4d 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportResetPasswordDialog.xaml.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Common; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Service.Notification; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; namespace Snap.Hutao.View.Dialog; @@ -13,7 +14,7 @@ namespace Snap.Hutao.View.Dialog; [DependencyProperty("VerifyCode", typeof(string))] internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog { - private readonly HomaPassportClient homaPassportClient; + private readonly HutaoPassportClient homaPassportClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; @@ -22,7 +23,7 @@ internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - homaPassportClient = serviceProvider.GetRequiredService(); + homaPassportClient = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs index b3c8b380..993196a1 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/HutaoPassportUnregisterDialog.xaml.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Common; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Service.Notification; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; namespace Snap.Hutao.View.Dialog; @@ -13,7 +14,7 @@ namespace Snap.Hutao.View.Dialog; [DependencyProperty("VerifyCode", typeof(string))] internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog { - private readonly HomaPassportClient homaPassportClient; + private readonly HutaoPassportClient homaPassportClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; @@ -22,7 +23,7 @@ internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - homaPassportClient = serviceProvider.GetRequiredService(); + homaPassportClient = serviceProvider.GetRequiredService(); infoBarService = serviceProvider.GetRequiredService(); } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml new file mode 100644 index 00000000..62e88581 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs new file mode 100644 index 00000000..dcbaa0fc --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs @@ -0,0 +1,157 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using Snap.Hutao.Factory.QrCode; +using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; +using Snap.Hutao.Web.Hoyolab.Passport; +using Snap.Hutao.Web.Response; +using System.Collections.Specialized; +using System.IO; +using System.Web; + +namespace Snap.Hutao.View.Dialog; + +[DependencyProperty("QRCodeSource", typeof(ImageSource))] +internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable +{ + private readonly ITaskContext taskContext; + private readonly PandaClient pandaClient; + private readonly IQRCodeFactory qrCodeFactory; + + private readonly CancellationTokenSource userManualCancellationTokenSource = new(); + private bool disposed; + + public UserQRCodeDialog(IServiceProvider serviceProvider) + { + InitializeComponent(); + + taskContext = serviceProvider.GetRequiredService(); + pandaClient = serviceProvider.GetRequiredService(); + qrCodeFactory = serviceProvider.GetRequiredService(); + } + + ~UserQRCodeDialog() + { + Dispose(); + } + + public void Dispose() + { + if (!disposed) + { + userManualCancellationTokenSource.Dispose(); + disposed = true; + } + + GC.SuppressFinalize(this); + } + + public async ValueTask> GetUidGameTokenAsync() + { + try + { + return await GetUidGameTokenCoreAsync().ConfigureAwait(false); + } + finally + { + userManualCancellationTokenSource.Dispose(); + } + } + + [Command("CancelCommand")] + private void Cancel() + { + userManualCancellationTokenSource.Cancel(); + } + + private async ValueTask> GetUidGameTokenCoreAsync() + { + await taskContext.SwitchToMainThreadAsync(); + await ShowAsync(); + + while (!userManualCancellationTokenSource.IsCancellationRequested) + { + try + { + CancellationToken token = userManualCancellationTokenSource.Token; + string ticket = await FetchQRCodeAndSetImageAsync(token).ConfigureAwait(false); + UidGameToken? uidGameToken = await WaitQueryQRCodeConfirmAsync(ticket, token).ConfigureAwait(false); + + if (uidGameToken is null) + { + continue; + } + + await taskContext.SwitchToMainThreadAsync(); + Hide(); + return new(true, uidGameToken); + } + catch (OperationCanceledException) + { + break; + } + } + + return new(false, default!); + } + + private async ValueTask FetchQRCodeAndSetImageAsync(CancellationToken token) + { + Response fetchResponse = await pandaClient.QRCodeFetchAsync(token).ConfigureAwait(false); + if (!fetchResponse.IsOk()) + { + return string.Empty; + } + + string url = fetchResponse.Data.Url; + string ticket = GetTicketFromUrl(fetchResponse.Data.Url); + + await taskContext.SwitchToMainThreadAsync(); + + BitmapImage bitmap = new(); + await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.Create(url)).AsRandomAccessStream()); + QRCodeSource = bitmap; + + return ticket; + + static string GetTicketFromUrl(in ReadOnlySpan urlSpan) + { + ReadOnlySpan querySpan = urlSpan[urlSpan.IndexOf('?')..]; + NameValueCollection queryCollection = HttpUtility.ParseQueryString(querySpan.ToString()); + if (queryCollection.TryGetSingleValue("ticket", out string? ticket)) + { + return ticket; + } + + return string.Empty; + } + } + + private async ValueTask WaitQueryQRCodeConfirmAsync(string ticket, CancellationToken token) + { + using (PeriodicTimer timer = new(new(0, 0, 3))) + { + while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) + { + Response query = await pandaClient.QRCodeQueryAsync(ticket, token).ConfigureAwait(false); + + if (query is { ReturnCode: 0, Data: { Stat: "Confirmed", Payload.Proto: "Account" } }) + { + UidGameToken? uidGameToken = JsonSerializer.Deserialize(query.Data.Payload.Raw); + ArgumentNullException.ThrowIfNull(uidGameToken); + return uidGameToken; + } + else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) + { + break; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml index 440c769b..1ef6d6b7 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml @@ -199,7 +199,7 @@ Margin="16,16,16,0" Style="{StaticResource TitleTextBlockStyle}" Text="{Binding GreetingText}"/> - + - + VerticalAlignment="Center"> + + + + public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); } - /// public CancellationToken CancellationToken { get; set; } - /// public SemaphoreSlim DisposeLock { get; set; } = new(1); - /// public bool IsViewDisposed { get; set; } protected TaskCompletionSource Initialization { get; } = new(); - /// - /// 异步初始化UI - /// - /// 任务 [Command("OpenUICommand")] protected virtual async Task OpenUIAsync() { @@ -42,20 +33,11 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel Initialization.TrySetResult(IsInitialized); } - /// - /// 异步初始化界面数据 - /// - /// 初始化是否成功 protected virtual ValueTask InitializeUIAsync() { return ValueTask.FromResult(true); } - /// - /// 保证 using scope 内的代码运行完成 - /// 防止 视图资源被回收 - /// - /// 解除执行限制 protected async ValueTask EnterCriticalExecutionAsync() { ThrowIfViewDisposed(); @@ -64,10 +46,28 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel return disposable; } - /// - /// 当页面被释放后抛出异常 - /// - /// 操作被用户取消 + protected bool SetProperty(ref T storage, T value, Action changedCallback, [CallerMemberName] string? propertyName = null) + { + if (SetProperty(ref storage, value, propertyName)) + { + changedCallback(value); + return true; + } + + return false; + } + + protected bool SetProperty(ref T storage, T value, Func changedAsyncCallback, [CallerMemberName] string? propertyName = null) + { + if (SetProperty(ref storage, value, propertyName)) + { + changedAsyncCallback(value).SafeForget(); + return true; + } + + return false; + } + private void ThrowIfViewDisposed() { if (IsViewDisposed) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs index 57bfc0af..7a4678cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementFinishPercent.cs @@ -21,41 +21,42 @@ internal static class AchievementFinishPercent int totalFinished = 0; int totalCount = 0; - if (viewModel.Achievements is { } achievements) + if (viewModel.Achievements is not { } achievements) { - if (viewModel.AchievementGoals is { } achievementGoals) + return; + } + + if (viewModel.AchievementGoals is not { } achievementGoals) + { + return; + } + + if (achievements.SourceCollection is not List list) + { + // Fast path + throw Must.NeverHappen("AchievementViewModel.Achievements.SourceCollection 应为 List"); + } + + Dictionary counter = achievementGoals.ToDictionary(x => x.Id, AchievementGoalStatistics.From); + + foreach (ref readonly AchievementView achievement in CollectionsMarshal.AsSpan(list)) + { + ref AchievementGoalStatistics goalStat = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal); + + goalStat.TotalCount += 1; + totalCount += 1; + if (achievement.IsChecked) { - Dictionary counter = achievementGoals.ToDictionary(x => x.Id, AchievementGoalStatistics.From); - - // Fast path - if (achievements.SourceCollection is List list) - { - foreach (ref readonly AchievementView achievement in CollectionsMarshal.AsSpan(list)) - { - // Make the state update as fast as possible - ref AchievementGoalStatistics stat = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal); - - stat.TotalCount += 1; - totalCount += 1; - if (achievement.IsChecked) - { - stat.Finished += 1; - totalFinished += 1; - } - } - } - else - { - Must.NeverHappen("AchievementViewModel.Achievements.SourceCollection 应为 List"); - } - - foreach (AchievementGoalStatistics statistics in counter.Values) - { - statistics.AchievementGoal.UpdateFinishDescriptionAndPercent(statistics); - } - - viewModel.FinishDescription = AchievementStatistics.Format(totalFinished, totalCount, out _); + goalStat.Finished += 1; + totalFinished += 1; } } + + foreach (AchievementGoalStatistics statistics in counter.Values) + { + statistics.AchievementGoal.UpdateFinishDescriptionAndPercent(statistics); + } + + viewModel.FinishDescription = AchievementStatistics.Format(totalFinished, totalCount, out _); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs index bd5e5c48..c89d7b26 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementGoalView.cs @@ -72,17 +72,7 @@ internal sealed class AchievementGoalView : ObservableObject, INameIcon, IMappin /// 统计 public void UpdateFinishDescriptionAndPercent(AchievementGoalStatistics statistics) { - UpdateFinishDescriptionAndPercent(statistics.Finished, statistics.TotalCount); - } - - /// - /// 更新进度 - /// - /// 完成项 - /// 总项 - private void UpdateFinishDescriptionAndPercent(int finished, int count) - { - FinishDescription = AchievementStatistics.Format(finished, count, out double finishPercent); + FinishDescription = AchievementStatistics.Format(statistics.Finished, statistics.TotalCount, out double finishPercent); FinishPercent = finishPercent; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index cf249276..c758b59f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -18,6 +18,7 @@ using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.User; using Snap.Hutao.View.Dialog; using Snap.Hutao.ViewModel.User; +using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; using Snap.Hutao.Web.Response; using Windows.Graphics.Imaging; using Windows.Storage.Streams; @@ -51,6 +52,13 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I private Summary? summary; private AvatarView? selectedAvatar; + private enum CultivateCoreResult + { + Ok, + ComputeConsumptionFailed, + SaveConsumptionFailed, + } + /// /// 简述对象 /// @@ -169,171 +177,147 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I if (userService.Current is null) { infoBarService.Warning(SH.MustSelectUserAndUid); + return; } - else + + if (avatar.Weapon is null) { - if (avatar.Weapon is null) - { - infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull); - return; - } + infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull); + return; + } - CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); - CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); - (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); + CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); + CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); + (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); - if (!isOk) - { - return; - } + if (!isOk) + { + return; + } - Response consumptionResponse = await calculatorClient - .ComputeAsync(userService.Current.Entity, delta) - .ConfigureAwait(false); + CultivateCoreResult result = await CultivateCoreAsync(userService.Current.Entity, delta, avatar).ConfigureAwait(false); - if (!consumptionResponse.IsOk()) - { - return; - } - - CalculatorConsumption consumption = consumptionResponse.Data; - - List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); - bool avatarSaved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) - .ConfigureAwait(false); - - try - { - // take a hot path if avatar is not saved. - bool avatarAndWeaponSaved = avatarSaved && await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) - .ConfigureAwait(false); - - if (avatarAndWeaponSaved) - { - infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); - } - else - { - infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); - } - } - catch (Core.ExceptionService.UserdataCorruptedException ex) - { - infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); - } + switch (result) + { + case CultivateCoreResult.Ok: + infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); + break; + case CultivateCoreResult.SaveConsumptionFailed: + infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); + break; } } [Command("BatchCultivateCommand")] private async Task BatchCultivateAsync() { - if (summary is { Avatars: { } avatars }) + if (summary is not { Avatars: { } avatars }) { - if (userService.Current is null) + return; + } + + if (userService.Current is null) + { + infoBarService.Warning(SH.MustSelectUserAndUid); + return; + } + + CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + (bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false); + + if (!isOk) + { + return; + } + + ArgumentNullException.ThrowIfNull(baseline.SkillList); + ArgumentNullException.ThrowIfNull(baseline.Weapon); + + ContentDialog progressDialog = await contentDialogFactory + .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle) + .ConfigureAwait(false); + using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false)) + { + BatchCultivateResult result = default; + foreach (AvatarView avatar in avatars) { - infoBarService.Warning(SH.MustSelectUserAndUid); + if (!baseline.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy)) + { + ++result.SkippedCount; + continue; + } + + CultivateCoreResult coreResult = await CultivateCoreAsync(userService.Current.Entity, copy, avatar).ConfigureAwait(false); + + switch (coreResult) + { + case CultivateCoreResult.Ok: + ++result.SucceedCount; + break; + case CultivateCoreResult.ComputeConsumptionFailed: + result.Interrupted = true; + break; + case CultivateCoreResult.SaveConsumptionFailed: + result.Interrupted = true; + break; + } + + if (result.Interrupted) + { + break; + } + } + + if (result.Interrupted) + { + infoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount)); } else { - CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - (bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false); - - if (isOk) - { - ArgumentNullException.ThrowIfNull(baseline.SkillList); - ArgumentNullException.ThrowIfNull(baseline.Weapon); - - ContentDialog progressDialog = await contentDialogFactory - .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle) - .ConfigureAwait(false); - using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false)) - { - BatchCultivateResult result = default; - foreach (AvatarView avatar in avatars) - { - baseline.AvatarId = avatar.Id; - baseline.AvatarLevelCurrent = Math.Min(avatar.LevelNumber, baseline.AvatarLevelTarget); - - if (avatar.Skills.Count < 3) - { - continue; - } - - baseline.SkillList[0].Id = avatar.Skills[0].GroupId; - baseline.SkillList[0].LevelCurrent = Math.Min(avatar.Skills[0].LevelNumber, baseline.SkillList[0].LevelTarget); - baseline.SkillList[1].Id = avatar.Skills[1].GroupId; - baseline.SkillList[1].LevelCurrent = Math.Min(avatar.Skills[1].LevelNumber, baseline.SkillList[1].LevelTarget); - baseline.SkillList[2].Id = avatar.Skills[2].GroupId; - baseline.SkillList[2].LevelCurrent = Math.Min(avatar.Skills[2].LevelNumber, baseline.SkillList[2].LevelTarget); - - if (avatar.Weapon is null) - { - result.SkippedCount++; - continue; - } - - baseline.Weapon.Id = avatar.Weapon.Id; - baseline.Weapon.LevelCurrent = Math.Min(avatar.Weapon.LevelNumber, baseline.Weapon.LevelTarget); - - Response consumptionResponse = await calculatorClient - .ComputeAsync(userService.Current.Entity, baseline) - .ConfigureAwait(false); - - if (!consumptionResponse.IsOk()) - { - result.Interrupted = true; - break; - } - else - { - CalculatorConsumption consumption = consumptionResponse.Data; - - List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); - bool avatarSaved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) - .ConfigureAwait(false); - - try - { - // take a hot path if avatar is not saved. - bool avatarAndWeaponSaved = avatarSaved && await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) - .ConfigureAwait(false); - - if (avatarAndWeaponSaved) - { - result.SucceedCount++; - } - else - { - result.Interrupted = true; - break; - } - } - catch (Core.ExceptionService.UserdataCorruptedException ex) - { - infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); - } - } - } - - if (result.Interrupted) - { - infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); - infoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount)); - } - else - { - infoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount)); - } - } - } + infoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount)); } } } + private async ValueTask CultivateCoreAsync(Model.Entity.User user, CalculatorAvatarPromotionDelta delta, AvatarView avatar) + { + Response consumptionResponse = await calculatorClient.ComputeAsync(user, delta).ConfigureAwait(false); + + if (!consumptionResponse.IsOk()) + { + return CultivateCoreResult.ComputeConsumptionFailed; + } + + CalculatorConsumption consumption = consumptionResponse.Data; + LevelInformation levelInformation = LevelInformation.From(delta); + + List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); + bool avatarSaved = await cultivationService + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation) + .ConfigureAwait(false); + + try + { + ArgumentNullException.ThrowIfNull(avatar.Weapon); + + // Take a hot path if avatar is not saved. + bool avatarAndWeaponSaved = avatarSaved && await cultivationService + .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation) + .ConfigureAwait(false); + + if (!avatarAndWeaponSaved) + { + return CultivateCoreResult.SaveConsumptionFailed; + } + } + catch (Core.ExceptionService.UserdataCorruptedException ex) + { + infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); + } + + return CultivateCoreResult.Ok; + } + [Command("ExportAsImageCommand")] private async Task ExportAsImageAsync(FrameworkElement? element) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs index 071018e9..a8960008 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using Snap.Hutao.Model; +using Snap.Hutao.Model.Entity; +using System.Text; namespace Snap.Hutao.ViewModel.Cultivation; @@ -11,13 +13,7 @@ namespace Snap.Hutao.ViewModel.Cultivation; [HighQuality] internal sealed class CultivateEntryView : Item { - /// - /// 构造一个新的养成清单入口 - /// - /// 实体入口 - /// 对应物品 - /// 物品列表 - public CultivateEntryView(Model.Entity.CultivateEntry entry, Item item, List items) + public CultivateEntryView(CultivateEntry entry, Item item, List items) { Id = entry.Id; EntryId = entry.InnerId; @@ -26,33 +22,75 @@ internal sealed class CultivateEntryView : Item Badge = item.Badge; Quality = item.Quality; Items = items; + + Description = ParseDescription(entry); + IsToday = items.Any(i => i.IsToday); + DaysOfWeek = items.FirstOrDefault(i => i.DaysOfWeek != DaysOfWeek.Any)?.DaysOfWeek ?? DaysOfWeek.Any; + + static string ParseDescription(CultivateEntry entry) + { + if (entry.LevelInformation is null) + { + return SH.ViewModelCultivationEntryViewDescriptionDefault; + } + + CultivateEntryLevelInformation info = entry.LevelInformation; + + switch (entry.Type) + { + case Model.Entity.Primitive.CultivateType.AvatarAndSkill: + { + StringBuilder stringBuilder = new(); + + if (info.AvatarLevelFrom != info.AvatarLevelTo) + { + stringBuilder.Append("Lv.").Append(info.AvatarLevelFrom).Append(" → Lv.").Append(info.AvatarLevelTo).Append(' '); + } + + if (info.SkillALevelFrom != info.SkillALevelTo) + { + stringBuilder.Append("A: ").Append(info.SkillALevelFrom).Append(" → ").Append(info.SkillALevelTo).Append(' '); + } + + if (info.SkillELevelFrom != info.SkillELevelTo) + { + stringBuilder.Append("E: ").Append(info.SkillELevelFrom).Append(" → ").Append(info.SkillELevelTo).Append(' '); + } + + if (info.SkillQLevelFrom != info.SkillQLevelTo) + { + stringBuilder.Append("Q: ").Append(info.SkillQLevelFrom).Append(" → ").Append(info.SkillQLevelTo).Append(' '); + } + + return stringBuilder.ToStringTrimEnd(); + } + + case Model.Entity.Primitive.CultivateType.Weapon: + { + StringBuilder stringBuilder = new(); + + if (info.WeaponLevelFrom != info.WeaponLevelTo) + { + stringBuilder.Append("Lv.").Append(info.WeaponLevelFrom).Append(" → Lv.").Append(info.WeaponLevelTo); + } + + return stringBuilder.ToString(); + } + } + + return string.Empty; + } } - /// - /// 实体 - /// public List Items { get; set; } = default!; - /// - /// 是否为今日的材料 - /// - public bool IsToday { get => Items.Any(i => i.IsToday); } + public bool IsToday { get; } - /// - /// 星期中的日期 - /// - public DaysOfWeek DaysOfWeek - { - get => Items.FirstOrDefault(i => i.DaysOfWeek != DaysOfWeek.Any)?.DaysOfWeek ?? DaysOfWeek.Any; - } + public DaysOfWeek DaysOfWeek { get; } + + public string Description { get; } - /// - /// Id - /// internal uint Id { get; set; } - /// - /// 入口Id - /// internal Guid EntryId { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs index 19b51d44..fa7424cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs @@ -3,10 +3,9 @@ using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Entity; -using Snap.Hutao.Model.Metadata.Item; -using Snap.Hutao.Model.Primitive; using Snap.Hutao.Service.Cultivation; using Snap.Hutao.Service.Metadata; +using Snap.Hutao.Service.Metadata.ContextAbstraction; using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Notification; using Snap.Hutao.View.Dialog; @@ -38,14 +37,8 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel private ObservableCollection? cultivateEntries; private ObservableCollection? statisticsItems; - /// - /// 项目 - /// public ObservableCollection? Projects { get => projects; set => SetProperty(ref projects, value); } - /// - /// 当前选中的计划 - /// public CultivateProject? SelectedProject { get => selectedProject; set @@ -58,19 +51,10 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel } } - /// - /// 物品列表 - /// public List? InventoryItems { get => inventoryItems; set => SetProperty(ref inventoryItems, value); } - /// - /// 养成列表 - /// public ObservableCollection? CultivateEntries { get => cultivateEntries; set => SetProperty(ref cultivateEntries, value); } - /// - /// 统计列表 - /// public ObservableCollection? StatisticsItems { get => statisticsItems; set => SetProperty(ref statisticsItems, value); } protected override async ValueTask InitializeUIAsync() @@ -95,60 +79,61 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel CultivateProjectDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); (bool isOk, CultivateProject project) = await dialog.CreateProjectAsync().ConfigureAwait(false); - if (isOk) + if (!isOk) { - ProjectAddResult result = await cultivationService.TryAddProjectAsync(project).ConfigureAwait(false); + return; + } - switch (result) - { - case ProjectAddResult.Added: - infoBarService.Success(SH.ViewModelCultivationProjectAdded); - await taskContext.SwitchToMainThreadAsync(); - SelectedProject = project; - break; - case ProjectAddResult.InvalidName: - infoBarService.Information(SH.ViewModelCultivationProjectInvalidName); - break; - case ProjectAddResult.AlreadyExists: - infoBarService.Information(SH.ViewModelCultivationProjectAlreadyExists); - break; - default: - throw Must.NeverHappen(); - } + switch (await cultivationService.TryAddProjectAsync(project).ConfigureAwait(false)) + { + case ProjectAddResult.Added: + infoBarService.Success(SH.ViewModelCultivationProjectAdded); + await taskContext.SwitchToMainThreadAsync(); + SelectedProject = project; + break; + case ProjectAddResult.InvalidName: + infoBarService.Information(SH.ViewModelCultivationProjectInvalidName); + break; + case ProjectAddResult.AlreadyExists: + infoBarService.Information(SH.ViewModelCultivationProjectAlreadyExists); + break; + default: + throw Must.NeverHappen(); } } [Command("RemoveProjectCommand")] private async Task RemoveProjectAsync(CultivateProject? project) { - if (project is not null) + if (project is null) { - await cultivationService.RemoveProjectAsync(project).ConfigureAwait(false); - - await taskContext.SwitchToMainThreadAsync(); - ArgumentNullException.ThrowIfNull(Projects); - SelectedProject = Projects.FirstOrDefault(); + return; } + + await cultivationService.RemoveProjectAsync(project).ConfigureAwait(false); + await taskContext.SwitchToMainThreadAsync(); + ArgumentNullException.ThrowIfNull(Projects); + SelectedProject = Projects.FirstOrDefault(); } private async ValueTask UpdateEntryCollectionAsync(CultivateProject? project) { - if (project is not null) + if (project is null) { - List materials = await metadataService.GetMaterialListAsync().ConfigureAwait(false); - Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); - Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false); - - ObservableCollection entries = await cultivationService - .GetCultivateEntriesAsync(project, materials, idAvatarMap, idWeaponMap) - .ConfigureAwait(false); - - await taskContext.SwitchToMainThreadAsync(); - CultivateEntries = entries; - InventoryItems = cultivationService.GetInventoryItemViews(project, materials, SaveInventoryItemCommand); - - await UpdateStatisticsItemsAsync().ConfigureAwait(false); + return; } + + CultivationMetadataContext context = await metadataService.GetContextAsync().ConfigureAwait(false); + + ObservableCollection entries = await cultivationService + .GetCultivateEntriesAsync(project, context) + .ConfigureAwait(false); + + await taskContext.SwitchToMainThreadAsync(); + CultivateEntries = entries; + InventoryItems = cultivationService.GetInventoryItemViews(project, context, SaveInventoryItemCommand); + + await UpdateStatisticsItemsAsync().ConfigureAwait(false); } [Command("RemoveEntryCommand")] @@ -194,8 +179,8 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel ObservableCollection statistics; try { - List materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false); - statistics = await cultivationService.GetStatisticsCultivateItemCollectionAsync(SelectedProject, materials, token).ConfigureAwait(false); + CultivationMetadataContext context = await metadataService.GetContextAsync().ConfigureAwait(false); + statistics = await cultivationService.GetStatisticsCultivateItemCollectionAsync(SelectedProject, context, token).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs index ea004849..359cb342 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs @@ -30,7 +30,7 @@ internal sealed class StatisticsCultivateItem /// /// 对应背包物品的个数 /// - public int Count { get; set; } + public uint Count { get; set; } /// /// 对应背包物品的个数 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs index 9ab591a2..0a08086e 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs @@ -26,8 +26,8 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel { private readonly IContentDialogFactory contentDialogFactory; private readonly IDailyNoteService dailyNoteService; + private readonly DailyNoteOptions dailyNoteOptions; private readonly IInfoBarService infoBarService; - private readonly DailyNoteOptions options; private readonly RuntimeOptions runtimeOptions; private readonly ITaskContext taskContext; private readonly IUserService userService; @@ -35,15 +35,11 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel private ObservableCollection? userAndUids; private ObservableCollection? dailyNoteEntries; - /// - /// 选项 - /// - public DailyNoteOptions Options { get => options; } + public DailyNoteOptions DailyNoteOptions { get => dailyNoteOptions; } public RuntimeOptions RuntimeOptions { get => runtimeOptions; } - [SuppressMessage("", "CA1822")] - public IWebViewerSource VerifyUrlSource { get => new DailyNoteWebViewerSource(); } + public IWebViewerSource VerifyUrlSource { get; } = new DailyNoteWebViewerSource(); /// /// 用户与角色集合 @@ -128,13 +124,13 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel private async Task ConfigDailyNoteWebhookUrlAsync() { DailyNoteWebhookDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(true); - dialog.Text = options.WebhookUrl; + dialog.Text = dailyNoteOptions.WebhookUrl; (bool isOk, string url) = await dialog.GetInputUrlAsync().ConfigureAwait(false); if (isOk) { await taskContext.SwitchToMainThreadAsync(); - options.WebhookUrl = url; + dailyNoteOptions.WebhookUrl = url; infoBarService.Information(SH.ViewModelDailyNoteConfigWebhookUrlComplete); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index a33cf0f8..01bb8cdd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -55,59 +55,48 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel private GameAccount? selectedGameAccount; private GameResource? gameResource; - /// - /// 已知的服务器方案 - /// - [SuppressMessage("", "CA1822")] - public List KnownSchemes { get => KnownLaunchSchemes.Get(); } + public List KnownSchemes { get; } = KnownLaunchSchemes.Get(); - /// - /// 当前选择的服务器方案 - /// public LaunchScheme? SelectedScheme { - get => selectedScheme; set + get => selectedScheme; + set { - if (SetProperty(ref selectedScheme, value)) + SetProperty(ref selectedScheme, value, UpdateGameResourceAsync); + + async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme) { - if (value is not null) + if (scheme is null) { - UpdateGameResourceAsync(value).SafeForget(); + return; + } + + await taskContext.SwitchToBackgroundAsync(); + Web.Response.Response response = await resourceClient + .GetResourceAsync(scheme) + .ConfigureAwait(false); + + if (response.IsOk()) + { + await taskContext.SwitchToMainThreadAsync(); + GameResource = response.Data; } } } } - /// - /// 游戏账号集合 - /// public ObservableCollection? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); } - /// - /// 选中的账号 - /// public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); } - /// - /// 启动选项 - /// - public LaunchOptions Options { get => launchOptions; } + public LaunchOptions LaunchOptions { get => launchOptions; } public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; } - /// - /// 胡桃选项 - /// - public RuntimeOptions HutaoOptions { get => runtimeOptions; } + public RuntimeOptions RuntimeOptions { get => runtimeOptions; } - /// - /// 应用选项 - /// public AppOptions AppOptions { get => appOptions; } - /// - /// 游戏资源 - /// public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); } protected override async ValueTask InitializeUIAsync() @@ -174,20 +163,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel return true; } - private async ValueTask UpdateGameResourceAsync(LaunchScheme scheme) - { - await taskContext.SwitchToBackgroundAsync(); - Web.Response.Response response = await resourceClient - .GetResourceAsync(scheme) - .ConfigureAwait(false); - - if (response.IsOk()) - { - await taskContext.SwitchToMainThreadAsync(); - GameResource = response.Data; - } - } - [Command("LaunchCommand")] private async Task LaunchAsync() { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs index aba0fd04..d18bd542 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs @@ -51,7 +51,7 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli } } - [Command("LaunchCommand", AllowConcurrentExecutions = true)] + [Command("LaunchCommand")] private async Task LaunchAsync() { IInfoBarService infoBarService = ServiceProvider.GetRequiredService(); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/GuideViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/GuideViewModel.cs index d87da8a9..4ac4a17c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/GuideViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Guide/GuideViewModel.cs @@ -140,11 +140,10 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel /// /// 下载信息 /// - public ObservableCollection? DownloadSummaries { get => downloadSummaries; set => SetProperty(ref downloadSummaries, value); } - - protected override ValueTask InitializeUIAsync() + public ObservableCollection? DownloadSummaries { - return ValueTask.FromResult(true); + get => downloadSummaries; + set => SetProperty(ref downloadSummaries, value); } [Command("NextOrCompleteCommand")] @@ -160,28 +159,21 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel private async ValueTask DownloadStaticResourceAsync() { - HashSet downloadSummaries = []; + DownloadSummaries = StaticResource + .GetUnfulfilledCategorySet() + .Select(category => new DownloadSummary(serviceProvider, category)) + .ToObservableCollection(); - HashSet categories = StaticResource.GetUnfulfilledCategorySet(); - - foreach (string category in categories) - { - downloadSummaries.Add(new(serviceProvider, category)); - } - - DownloadSummaries = downloadSummaries.ToObservableCollection(); - - await Parallel.ForEachAsync(downloadSummaries, async (summary, token) => + await Parallel.ForEachAsync(DownloadSummaries, async (summary, token) => { if (await summary.DownloadAndExtractAsync().ConfigureAwait(false)) { - taskContext.InvokeOnMainThread(() => DownloadSummaries.Remove(summary)); + taskContext.BeginInvokeOnMainThread(() => DownloadSummaries.Remove(summary)); } }).ConfigureAwait(false); StaticResource.FulfillAll(); - - LocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, (uint)GuideState.Completed); + UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Completed); AppInstance.Restart(string.Empty); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs index adf63276..36553850 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Home/AnnouncementViewModel.cs @@ -39,7 +39,7 @@ internal sealed partial class AnnouncementViewModel : Abstraction.ViewModel /// /// 用户选项 /// - public HutaoUserOptions UserOptions { get => hutaoUserOptions; } + public HutaoUserOptions HutaoUserOptions { get => hutaoUserOptions; } /// /// 欢迎语 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/FolderViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/FolderViewModel.cs new file mode 100644 index 00000000..14a69c72 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/FolderViewModel.cs @@ -0,0 +1,46 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Common; +using CommunityToolkit.Mvvm.ComponentModel; +using System.IO; +using Windows.System; + +namespace Snap.Hutao.ViewModel.Setting; + +internal sealed partial class FolderViewModel : ObservableObject +{ + private readonly string folder; + private string? size; + + public FolderViewModel(ITaskContext taskContext, string folder) + { + this.folder = folder; + + SetFolderSizeAsync().SafeForget(); + + async ValueTask SetFolderSizeAsync() + { + await taskContext.SwitchToBackgroundAsync(); + long totalSize = 0; + + foreach (string file in Directory.EnumerateFiles(folder, "*.*", SearchOption.AllDirectories)) + { + totalSize += new FileInfo(file).Length; + } + + await taskContext.SwitchToMainThreadAsync(); + Size = SH.FormatViewModelSettingFolderSizeDescription(Converters.ToFileSizeString(totalSize)); + } + } + + public string Folder { get => folder; } + + public string? Size { get => size; set => SetProperty(ref size, value); } + + [Command("OpenFolderCommand")] + private async Task OpenDataFolderAsync() + { + await Launcher.LaunchFolderPathAsync(folder); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs index e889afc7..8dcf373a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs @@ -7,6 +7,7 @@ using Snap.Hutao.Service.Notification; using Snap.Hutao.View.Dialog; using Snap.Hutao.Web; using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Windows.System; namespace Snap.Hutao.ViewModel.Setting; @@ -19,7 +20,7 @@ namespace Snap.Hutao.ViewModel.Setting; internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel { private readonly IContentDialogFactory contentDialogFactory; - private readonly HomaPassportClient homaPassportClient; + private readonly HutaoPassportClient homaPassportClient; private readonly HutaoUserOptions hutaoUserOptions; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; @@ -77,7 +78,7 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel infoBarService.Information(response.GetLocalizationMessageOrMessage()); await taskContext.SwitchToMainThreadAsync(); - hutaoUserOptions.LogoutOrUnregister(); + hutaoUserOptions.PostLogoutOrUnregister(); } } } @@ -110,7 +111,7 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel [Command("LogoutCommand")] private void LogoutAsync() { - hutaoUserOptions.LogoutOrUnregister(); + hutaoUserOptions.PostLogoutOrUnregister(); } [Command("ResetPasswordCommand")] diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index 26fc68e6..6b09771c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -23,6 +23,8 @@ using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.User; using Snap.Hutao.View.Dialog; using Snap.Hutao.ViewModel.Guide; +using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Response; using System.Globalization; using System.IO; using System.Runtime.InteropServices; @@ -41,6 +43,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel private readonly HomeCardOptions homeCardOptions = new(); private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; + private readonly HutaoInfrastructureClient hutaoInfrastructureClient; private readonly HutaoPassportViewModel hutaoPassportViewModel; private readonly IContentDialogFactory contentDialogFactory; private readonly IGameLocatorFactory gameLocatorFactory; @@ -57,11 +60,11 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel private NameValue? selectedBackdropType; private NameValue? selectedCulture; + private IPInformation? ipInformation; + private FolderViewModel? cacheFolderView; + private FolderViewModel? dataFolderView; - /// - /// 应用程序设置 - /// - public AppOptions Options { get => appOptions; } + public AppOptions AppOptions { get => appOptions; } public RuntimeOptions HutaoOptions { get => runtimeOptions; } @@ -75,34 +78,70 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel public NameValue? SelectedBackdropType { - get => selectedBackdropType ??= Options.BackdropTypes.Single(t => t.Value == Options.BackdropType); + get => selectedBackdropType ??= AppOptions.BackdropTypes.Single(t => t.Value == AppOptions.BackdropType); set { if (SetProperty(ref selectedBackdropType, value) && value is not null) { - Options.BackdropType = value.Value; + AppOptions.BackdropType = value.Value; } } } public NameValue? SelectedCulture { - get => selectedCulture ??= Options.GetCurrentCultureForSelectionOrDefault(); + get => selectedCulture ??= AppOptions.GetCurrentCultureForSelectionOrDefault(); set { if (SetProperty(ref selectedCulture, value) && value is not null) { - Options.CurrentCulture = value.Value; + AppOptions.CurrentCulture = value.Value; AppInstance.Restart(string.Empty); } } } + public FolderViewModel? CacheFolderView { get => cacheFolderView; set => SetProperty(ref cacheFolderView, value); } + + public FolderViewModel? DataFolderView { get => dataFolderView; set => SetProperty(ref dataFolderView, value); } + + public IPInformation? IPInformation { get => ipInformation; private set => SetProperty(ref ipInformation, value); } + + [SuppressMessage("", "CA1822")] + public bool IsAllocConsoleDebugModeEnabled + { + get => LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false); + set => LocalSetting.Set(SettingKeys.IsAllocConsoleDebugModeEnabled, value); + } + + protected override async ValueTask InitializeUIAsync() + { + CacheFolderView = new(taskContext, runtimeOptions.LocalCache); + DataFolderView = new(taskContext, runtimeOptions.DataFolder); + + Response resp = await hutaoInfrastructureClient.GetIPInformationAsync().ConfigureAwait(false); + IPInformation info; + + if (resp.IsOk()) + { + info = resp.Data; + } + else + { + info = IPInformation.Default; + } + + await taskContext.SwitchToMainThreadAsync(); + IPInformation = info; + + return true; + } + [Command("ResetStaticResourceCommand")] private static void ResetStaticResource() { StaticResource.FailAll(); - LocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, (uint)GuideState.StaticResourceBegin); + UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.StaticResourceBegin); AppInstance.Restart(string.Empty); } @@ -112,6 +151,12 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel await Launcher.LaunchUriAsync(new("ms-windows-store://review/?ProductId=9PH4NXJ2JN52")); } + [Command("UpdateCheckCommand")] + private static async Task CheckUpdateAsync() + { + await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52")); + } + [Command("SetGamePathCommand")] private async Task SetGamePathAsync() { @@ -123,7 +168,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel await taskContext.SwitchToMainThreadAsync(); try { - Options.GamePath = path; + AppOptions.GamePath = path; } catch (SqliteException ex) { @@ -141,14 +186,14 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel if (isOk && Path.GetFileNameWithoutExtension(file).EqualsAny(["POWERSHELL", "PWSH"], StringComparison.OrdinalIgnoreCase)) { await taskContext.SwitchToMainThreadAsync(); - Options.PowerShellPath = file; + AppOptions.PowerShellPath = file; } } [Command("DeleteGameWebCacheCommand")] private void DeleteGameWebCache() { - string gamePath = Options.GamePath; + string gamePath = AppOptions.GamePath; if (!string.IsNullOrEmpty(gamePath)) { @@ -176,19 +221,12 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel } } - [Command("UpdateCheckCommand")] - private async Task CheckUpdateAsync() + [Command("OpenTestPageCommand")] + private async Task OpenTestPageAsync() { - if (hutaoUserOptions.IsMaintainer) - { - await navigationService - .NavigateAsync(INavigationAwaiter.Default) - .ConfigureAwait(false); - } - else - { - await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52")); - } + await navigationService + .NavigateAsync(INavigationAwaiter.Default) + .ConfigureAwait(false); } [Command("SetDataFolderCommand")] @@ -217,18 +255,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel } } - [Command("OpenCacheFolderCommand")] - private async Task OpenCacheFolderAsync() - { - await Launcher.LaunchFolderPathAsync(runtimeOptions.LocalCache); - } - - [Command("OpenDataFolderCommand")] - private async Task OpenDataFolderAsync() - { - await Launcher.LaunchFolderPathAsync(runtimeOptions.DataFolder); - } - [Command("DeleteUsersCommand")] private async Task DangerousDeleteUsersAsync() { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs index 301024dc..aeb7db5b 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/SpiralAbyssRecordViewModel.cs @@ -8,7 +8,7 @@ using Snap.Hutao.Service.SpiralAbyss; using Snap.Hutao.Service.User; using Snap.Hutao.ViewModel.Complex; using Snap.Hutao.ViewModel.User; -using Snap.Hutao.Web.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Hutao.SpiralAbyss; using Snap.Hutao.Web.Hutao.SpiralAbyss.Post; using System.Collections.ObjectModel; @@ -24,7 +24,7 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss; internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient { private readonly ISpiralAbyssRecordService spiralAbyssRecordService; - private readonly HomaSpiralAbyssClient spiralAbyssClient; + private readonly HutaoSpiralAbyssClient spiralAbyssClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; private readonly IUserService userService; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs index e894f15b..ddd8383f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs @@ -16,10 +16,10 @@ namespace Snap.Hutao.ViewModel; [Injection(InjectAs.Scoped)] internal sealed partial class TestViewModel : Abstraction.ViewModel { - private readonly MainWindow mainWindow; + private readonly HutaoAsAServiceClient homaAsAServiceClient; private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; - private readonly HutaoAsAServiceClient homaAsAServiceClient; + private readonly MainWindow mainWindow; private UploadAnnouncement announcement = new(); @@ -39,15 +39,10 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel set => LocalSetting.Set(SettingKeys.OverrideElevationRequirement, value); } - protected override ValueTask InitializeUIAsync() - { - return ValueTask.FromResult(true); - } - [Command("ResetGuideStateCommand")] private static void ResetGuideState() { - LocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, (uint)GuideState.Language); + UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language); } [Command("ExceptionCommand")] @@ -59,11 +54,12 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel [Command("ResetMainWindowSizeCommand")] private void ResetMainWindowSize() { - mainWindow.AppWindow.Resize(new(1280, 720)); + double scale = mainWindow.WindowOptions.GetRasterizationScale(); + mainWindow.AppWindow.Resize(new Windows.Graphics.SizeInt32(1280, 720).Scale(scale)); } [Command("UploadAnnouncementCommand")] - private async void UploadAnnouncementAsync() + private async Task UploadAnnouncementAsync() { Web.Response.Response response = await homaAsAServiceClient.UploadAnnouncementAsync(Announcement).ConfigureAwait(false); if (response.IsOk()) @@ -75,7 +71,7 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel } [Command("CompensationGachaLogServiceTimeCommand")] - private async void CompensationGachaLogServiceTimeAsync() + private async Task CompensationGachaLogServiceTimeAsync() { Web.Response.Response response = await homaAsAServiceClient.GachaLogCompensationAsync(15).ConfigureAwait(false); if (response.IsOk()) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs index d656bece..88bd31c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/SignInWebViewerSouce.cs @@ -21,6 +21,6 @@ internal sealed class SignInWebViewerSouce : DependencyObject, IWebViewerSource { return userAndUid.User.IsOversea ? "https://act.hoyolab.com/ys/event/signin-sea-v3/index.html?act_id=e202102251931481" - : "https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?act_id=e202009291139501"; + : "https://act.mihoyo.com/bbs/event/signin/hk4e/index.html?act_id=e202311201442471"; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs index f3c31406..d02df2ad 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs @@ -15,6 +15,8 @@ using Snap.Hutao.Service.User; using Snap.Hutao.View.Dialog; using Snap.Hutao.View.Page; using Snap.Hutao.Web.Hoyolab; +using Snap.Hutao.Web.Hoyolab.Passport; +using Snap.Hutao.Web.Response; using System.Collections.ObjectModel; using System.Text; using Windows.System; @@ -50,14 +52,12 @@ internal sealed partial class UserViewModel : ObservableObject get => selectedUser ??= userService.Current; set { + // Pre select the chosen role to avoid multiple UserChangedMessage + value?.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false); + if (SetProperty(ref selectedUser, value)) { userService.Current = value; - - if (value is not null) - { - value.SelectedUserGameRole = value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen); - } } } } @@ -173,6 +173,32 @@ internal sealed partial class UserViewModel : ObservableObject } } + [Command("LoginByQRCodeCommand")] + private async Task LoginByQRCode() + { + UserQRCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + (bool isOk, UidGameToken? token) = await dialog.GetUidGameTokenAsync().ConfigureAwait(false); + + if (!isOk) + { + return; + } + + Response sTokenResponse = await serviceProvider + .GetRequiredService() + .GetSTokenByGameTokenAsync(token) + .ConfigureAwait(false); + + if (sTokenResponse.IsOk()) + { + Cookie stokenV2 = Cookie.FromLoginResult(sTokenResponse.Data); + + (UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false); + + await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false); + } + } + [Command("RemoveUserCommand")] private async Task RemoveUserAsync(User? user) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs index 68d0116d..e0b4bad6 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs @@ -135,8 +135,6 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel return; } - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); CalculableOptions options = new(avatar.ToCalculable(), null); CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); (bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); @@ -156,11 +154,12 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel } CalculateConsumption consumption = consumptionResponse.Data; + LevelInformation levelInformation = LevelInformation.From(delta); List items = CalculateItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); try { bool saved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation) .ConfigureAwait(false); if (saved) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs index 17544deb..d64196c8 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs @@ -148,10 +148,11 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel } CalculateConsumption consumption = consumptionResponse.Data; + LevelInformation levelInformation = LevelInformation.From(delta); try { bool saved = await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, weapon.Id, consumption.WeaponConsume.EmptyIfNull()) + .SaveConsumptionAsync(CultivateType.Weapon, weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation) .ConfigureAwait(false); if (saved) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index 64ffdee8..2774436b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -40,6 +40,7 @@ internal static class ApiEndpoints { return $"{ApiTakumiAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3"; } + #endregion #region ApiTaKumiBindingApi @@ -282,6 +283,14 @@ internal static class ApiEndpoints public const string AnnContent = $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery}"; #endregion + #region Hk4eSdk + + public const string QrCodeFetch = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/fetch"; + + public const string QrCodeQuery = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/query"; + + #endregion + #region PassportApi | PassportApiV4 /// @@ -294,6 +303,11 @@ internal static class ApiEndpoints /// public const string AccountGetLTokenBySToken = $"{PassportApiAuthApi}/getLTokenBySToken"; + /// + /// 通过GameToken获取V2SToken + /// + public const string AccountGetSTokenByGameToken = $"{PassportApi}/account/ma-cn-session/app/getTokenByGameToken"; + /// /// 获取V2SToken /// @@ -382,6 +396,8 @@ internal static class ApiEndpoints private const string Hk4eApi = "https://hk4e-api.mihoyo.com"; private const string Hk4eApiAnnouncementApi = $"{Hk4eApi}/common/hk4e_cn/announcement/api"; + private const string Hk4eSdk = "https://hk4e-sdk.mihoyo.com"; + private const string PassportApi = "https://passport-api.mihoyo.com"; private const string PassportApiAuthApi = $"{PassportApi}/account/auth/api"; private const string PassportApiV4 = "https://passport-api-v4.mihoyo.com"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs index d9992754..3466901d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs @@ -233,7 +233,7 @@ internal class MiHoYoJSBridge // Skip x-rpc-lifecycle_id { "x-rpc-app_id", "bll8iq97cem8" }, { "x-rpc-client_type", "5" }, - { "x-rpc-device_id", HoyolabOptions.DeviceId }, + { "x-rpc-device_id", HoyolabOptions.DeviceId36 }, { "x-rpc-app_version", userAndUid.IsOversea ? SaltConstants.OSVersion : SaltConstants.CNVersion }, { "x-rpc-sdk_version", "2.16.0" }, }; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs index 0faf5d10..7290ebc3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs @@ -59,33 +59,35 @@ internal sealed partial class EnkaClient .SetRequestUri(url) .Get(); - HttpResponseMessage response = await httpClient.SendAsync(builder.HttpRequestMessage, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false); - if (response.IsSuccessStatusCode) + using (HttpResponseMessage response = await httpClient.SendAsync(builder.HttpRequestMessage, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false)) { - return await response.Content.ReadFromJsonAsync(options, token).ConfigureAwait(false); - } - else - { - // https://github.com/yoimiya-kokomi/miao-plugin/pull/441 - // Additionally, HTTP codes for UID requests: - // 400 = wrong UID format - // 404 = player does not exist(MHY server told that) - // 429 = rate - limit - // 424 = game maintenance / everything is broken after the update - // 500 = general server error - // 503 = I screwed up massively - string message = response.StatusCode switch + if (response.IsSuccessStatusCode) { - HttpStatusCode.BadRequest => SH.WebEnkaResponseStatusCode400, - HttpStatusCode.NotFound => SH.WebEnkaResponseStatusCode404, - HttpStatusCode.FailedDependency => SH.WebEnkaResponseStatusCode424, - HttpStatusCode.TooManyRequests => SH.WebEnkaResponseStatusCode429, - HttpStatusCode.InternalServerError => SH.WebEnkaResponseStatusCode500, - HttpStatusCode.ServiceUnavailable => SH.WebEnkaResponseStatusCode503, - _ => SH.WebEnkaResponseStatusCodeUnknown, - }; + return await response.Content.ReadFromJsonAsync(options, token).ConfigureAwait(false); + } + else + { + // https://github.com/yoimiya-kokomi/miao-plugin/pull/441 + // Additionally, HTTP codes for UID requests: + // 400 = wrong UID format + // 404 = player does not exist(MHY server told that) + // 429 = rate - limit + // 424 = game maintenance / everything is broken after the update + // 500 = general server error + // 503 = I screwed up massively + string message = response.StatusCode switch + { + HttpStatusCode.BadRequest => SH.WebEnkaResponseStatusCode400, + HttpStatusCode.NotFound => SH.WebEnkaResponseStatusCode404, + HttpStatusCode.FailedDependency => SH.WebEnkaResponseStatusCode424, + HttpStatusCode.TooManyRequests => SH.WebEnkaResponseStatusCode429, + HttpStatusCode.InternalServerError => SH.WebEnkaResponseStatusCode500, + HttpStatusCode.ServiceUnavailable => SH.WebEnkaResponseStatusCode503, + _ => SH.WebEnkaResponseStatusCodeUnknown, + }; - return new() { Message = message, }; + return new() { Message = message, }; + } } } catch (HttpRequestException) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs new file mode 100644 index 00000000..479d4218 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs @@ -0,0 +1,27 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; + +[HighQuality] +internal sealed class GameLoginRequest +{ + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; + + [JsonPropertyName("ticket")] + public string? Ticket { get; set; } + + public static GameLoginRequest Create(int appId, string device, string? ticket = null) + { + return new() + { + AppId = appId, + Device = device, + Ticket = ticket, + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs new file mode 100644 index 00000000..92e6456e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; + +/// +/// 扫码登录结果 +/// +[HighQuality] +internal sealed class GameLoginResult +{ + [JsonPropertyName("stat")] + public string Stat { get; set; } = default!; + + [JsonPropertyName("payload")] + public GameLoginResultPayload Payload { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs new file mode 100644 index 00000000..80580331 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; + +[HighQuality] +internal sealed partial class GameLoginResultPayload +{ + [JsonPropertyName("proto")] + public string Proto { get; set; } = default!; + + [JsonPropertyName("raw")] + public string Raw { get; set; } = default!; + + [JsonPropertyName("ext")] + public string Ext { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs new file mode 100644 index 00000000..90ec1cfc --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs @@ -0,0 +1,49 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Web.Request.Builder; +using Snap.Hutao.Web.Request.Builder.Abstraction; +using Snap.Hutao.Web.Response; +using System.Net.Http; + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; + +[ConstructorGenerated(ResolveHttpClient = true)] +[HttpClient(HttpClientConfiguration.XRpc2)] +internal sealed partial class PandaClient +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + public async ValueTask> QRCodeFetchAsync(CancellationToken token = default) + { + GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeFetch) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + public async ValueTask> QRCodeQueryAsync(string ticket, CancellationToken token = default) + { + GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40, ticket); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeQuery) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs new file mode 100644 index 00000000..9a962e6a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs @@ -0,0 +1,10 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; + +internal sealed class UrlWrapper +{ + [JsonPropertyName("url")] + public string Url { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs index f5e391a6..ad3dff78 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs @@ -1,16 +1,16 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.Extensions.Options; using Snap.Hutao.Web.Hoyolab.DataSigning; using System.Collections.Frozen; +using System.Security.Cryptography; namespace Snap.Hutao.Web.Hoyolab; /// /// 米游社选项 /// -internal sealed class HoyolabOptions : IOptions +internal static class HoyolabOptions { /// /// 米游社请求UA @@ -35,7 +35,12 @@ internal sealed class HoyolabOptions : IOptions /// /// 米游社设备Id /// - public static string DeviceId { get; } = Guid.NewGuid().ToString(); + public static string DeviceId36 { get; } = Guid.NewGuid().ToString(); + + /// + /// 扫码登录设备Id + /// + public static string DeviceId40 { get; } = GenerateDeviceId40(); /// /// 盐 @@ -56,6 +61,16 @@ internal sealed class HoyolabOptions : IOptions [SaltType.OSX6] = "okr4obncj8bw5a65hbnn5oo6ixjc3l9w", }.ToFrozenDictionary(); - /// - public HoyolabOptions Value { get => this; } + [SuppressMessage("", "CA1308")] + private static string GenerateDeviceId40() + { + Guid uuid = Core.Uuid.NewV5(DeviceId36, new("9450ea74-be9c-35c0-9568-f97407856768")); + + Span uuidSpan = stackalloc byte[16]; + Span hash = stackalloc byte[20]; + + Verify.Operation(uuid.TryWriteBytes(uuidSpan), "Failed to write UUID bytes"); + Verify.Operation(SHA1.TryHashData(uuidSpan, hash, out _), "Failed to write SHA1 hash"); + return Convert.ToHexString(hash).ToLowerInvariant(); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs new file mode 100644 index 00000000..0c76cc52 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +internal sealed class AccountIdGameToken +{ + [JsonPropertyName("account_id")] + public int AccountId { get; set; } = default!; + + [JsonPropertyName("game_token")] + public string GameToken { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs index f6e3713f..be1ad870 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs @@ -5,9 +5,11 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Model.Entity; using Snap.Hutao.Web.Hoyolab.Annotation; using Snap.Hutao.Web.Hoyolab.DataSigning; +using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using Snap.Hutao.Web.Response; +using System.Globalization; using System.Net.Http; namespace Snap.Hutao.Web.Hoyolab.Passport; @@ -68,6 +70,25 @@ internal sealed partial class PassportClient2 return Response.Response.DefaultIfNull(resp); } + public async ValueTask> GetSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default) + { + AccountIdGameToken data = new() + { + AccountId = int.Parse(account.Uid, CultureInfo.InvariantCulture), + GameToken = account.GameToken, + }; + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken) + .PostJson(data); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + private class Timestamp { [JsonPropertyName("t")] diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs new file mode 100644 index 00000000..7f2ffc47 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +[HighQuality] +internal sealed class UidGameToken +{ + [JsonPropertyName("uid")] + public string Uid { get; set; } = default!; + + [JsonPropertyName("token")] + public string GameToken { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDeltaExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDeltaExtension.cs new file mode 100644 index 00000000..1c5452d2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDeltaExtension.cs @@ -0,0 +1,61 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.ViewModel.AvatarProperty; + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; + +internal static class AvatarPromotionDeltaExtension +{ + public static bool TryGetNonErrorCopy(this AvatarPromotionDelta source, AvatarView avatar, [NotNullWhen(true)] out AvatarPromotionDelta? copy) + { + copy = new() + { + AvatarId = avatar.Id, + AvatarLevelCurrent = Math.Min(avatar.LevelNumber, source.AvatarLevelTarget), + AvatarLevelTarget = source.AvatarLevelTarget, + }; + + if (avatar.Skills is not ([SkillView skillViewA, SkillView skillViewE, SkillView skillViewQ, ..]) || + source.SkillList is not ([PromotionDelta deltaA, PromotionDelta deltaE, PromotionDelta deltaQ, ..])) + { + return false; + } + + copy.SkillList = new(3) + { + new() + { + Id = skillViewA.GroupId, + LevelCurrent = Math.Min(skillViewA.LevelNumber, deltaA.LevelTarget), + LevelTarget = deltaA.LevelTarget, + }, + new() + { + Id = skillViewE.GroupId, + LevelCurrent = Math.Min(skillViewE.LevelNumber, deltaE.LevelTarget), + LevelTarget = deltaE.LevelTarget, + }, + new() + { + Id = skillViewQ.GroupId, + LevelCurrent = Math.Min(skillViewQ.LevelNumber, deltaQ.LevelTarget), + LevelTarget = deltaQ.LevelTarget, + }, + }; + + if (avatar.Weapon is not WeaponView weaponView || source.Weapon is not { } deltaWeapon) + { + return false; + } + + copy.Weapon = new() + { + Id = weaponView.Id, + LevelCurrent = Math.Min(weaponView.LevelNumber, deltaWeapon.LevelTarget), + LevelTarget = Math.Min(weaponView.MaxLevel, deltaWeapon.LevelTarget), + }; + + return true; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs index da9af651..8d08d3be 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs @@ -187,9 +187,9 @@ internal sealed partial class CalculateClient private class IdCount { [JsonPropertyName("cnt")] - public int Count { get; set; } + public uint Count { get; set; } [JsonPropertyName("id")] - public int Id { get; set; } + public uint Id { get; set; } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/Item.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/Item.cs index 2ef8573d..ba897985 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/Item.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/Item.cs @@ -15,7 +15,7 @@ internal sealed class Item /// Id /// [JsonPropertyName("id")] - public int Id { get; set; } + public uint Id { get; set; } /// /// 名称 @@ -33,7 +33,7 @@ internal sealed class Item /// 数量 /// [JsonPropertyName("num")] - public int Num { get; set; } + public uint Num { get; set; } /// /// 物品星级 仅有家具为有效值 diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/ItemHelper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/ItemHelper.cs index cb79da8c..a27f98b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/ItemHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/ItemHelper.cs @@ -1,25 +1,18 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using System.Runtime.InteropServices; + namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; -/// -/// 物品帮助类 -/// [HighQuality] internal static class ItemHelper { - /// - /// 合并两个物品列表 - /// - /// 左列表 - /// 右列表 - /// 合并且排序好的列表 public static List Merge(List? left, List? right) { if (left.IsNullOrEmpty() && right.IsNullOrEmpty()) { - return new(0); + return []; } if (left.IsNullOrEmpty() && !right.IsNullOrEmpty()) @@ -38,9 +31,10 @@ internal static class ItemHelper List result = new(left.Count + right.Count); result.AddRange(left); - foreach (Item item in right) + foreach (ref readonly Item item in CollectionsMarshal.AsSpan(right)) { - if (result.SingleOrDefault(i => i.Id == item.Id) is { } existed) + uint id = item.Id; + if (result.SingleOrDefault(i => i.Id == id) is { } existed) { existed.Num += item.Num; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/HomaGachaLogClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/HomaGachaLogClient.cs index 6634724a..a85f2acc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/HomaGachaLogClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/HomaGachaLogClient.cs @@ -3,6 +3,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Service.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using System.Net.Http; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs index 87eda9bb..995b1ce9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Geetest/GeetestResponse.cs @@ -5,7 +5,7 @@ namespace Snap.Hutao.Web.Hutao.Geetest; internal sealed class GeetestResponse { - public static GeetestResponse InternalFailure { get; } = new() { Code = Response.Response.InternalFailure }; + public static GeetestResponse InternalFailure { get; } = new() { Code = Web.Response.Response.InternalFailure }; [JsonPropertyName("code")] public int Code { get; set; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoAsAService/HutaoAsAServiceClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoAsAService/HutaoAsAServiceClient.cs index c5d83080..c1290fb4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoAsAService/HutaoAsAServiceClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoAsAService/HutaoAsAServiceClient.cs @@ -4,6 +4,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Service.Hutao; using Snap.Hutao.Service.Metadata; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using System.Net.Http; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs new file mode 100644 index 00000000..7a1c3e55 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoInfrastructureClient.cs @@ -0,0 +1,29 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Web.Request.Builder; +using Snap.Hutao.Web.Request.Builder.Abstraction; +using Snap.Hutao.Web.Response; +using System.Net.Http; + +namespace Snap.Hutao.Web.Hutao; + +[HttpClient(HttpClientConfiguration.Default)] +[ConstructorGenerated(ResolveHttpClient = true)] +internal sealed partial class HutaoInfrastructureClient +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + public async ValueTask> GetIPInformationAsync(CancellationToken token = default) + { + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(HutaoEndpoints.Ip) + .Get(); + + Response? resp = await builder.TryCatchSendAsync>(httpClient, logger, token).ConfigureAwait(false); + return Web.Response.Response.DefaultIfNull(resp); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs similarity index 98% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs index 6e93e96a..f3be6276 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportClient.cs @@ -3,6 +3,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Service.Hutao; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using System.Net.Http; @@ -17,7 +18,7 @@ namespace Snap.Hutao.Web.Hutao; [HighQuality] [HttpClient(HttpClientConfiguration.Default)] [ConstructorGenerated(ResolveHttpClient = true)] -internal sealed partial class HomaPassportClient +internal sealed partial class HutaoPassportClient { /// /// 通行证请求公钥 @@ -35,7 +36,7 @@ internal sealed partial class HomaPassportClient """; private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; + private readonly ILogger logger; private readonly HutaoUserOptions hutaoUserOptions; private readonly HttpClient httpClient; @@ -203,4 +204,4 @@ internal sealed partial class HomaPassportClient return Convert.ToBase64String(encryptedBytes); } } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportHttpRequestMessageBuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportHttpRequestMessageBuilderExtension.cs similarity index 88% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportHttpRequestMessageBuilderExtension.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportHttpRequestMessageBuilderExtension.cs index a99499b3..91d9edeb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportHttpRequestMessageBuilderExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPassportHttpRequestMessageBuilderExtension.cs @@ -6,7 +6,7 @@ using Snap.Hutao.Web.Request.Builder; namespace Snap.Hutao.Web.Hutao; -internal static class HomaPassportHttpRequestMessageBuilderExtension +internal static class HutaoPassportHttpRequestMessageBuilderExtension { public static async ValueTask TrySetTokenAsync(this HttpRequestMessageBuilder builder, HutaoUserOptions hutaoUserOptions) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs new file mode 100644 index 00000000..f7ee777d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/IPInformation.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hutao; + +internal sealed class IPInformation +{ + private const string Unknown = "Unknown"; + + public static IPInformation Default { get; } = new() + { + Ip = Unknown, + Division = Unknown, + }; + + [JsonPropertyName("ip")] + public string Ip { get; set; } = default!; + + [JsonPropertyName("division")] + public string Division { get; set; } = default!; + + public override string ToString() + { + if (Ip is Unknown && Division is Unknown) + { + return SH.WebHutaoServiceUnAvailable; + } + + return SH.FormatViewPageSettingDeviceIpDescription(Ip, Division); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HomaLogUploadClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HomaLogUploadClient.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs index 30d110e4..6f198105 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HomaLogUploadClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLogUploadClient.cs @@ -16,10 +16,10 @@ namespace Snap.Hutao.Web.Hutao.Log; [HighQuality] [ConstructorGenerated(ResolveHttpClient = true)] [HttpClient(HttpClientConfiguration.Default)] -internal sealed partial class HomaLogUploadClient +internal sealed partial class HutaoLogUploadClient { private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; + private readonly ILogger logger; private readonly RuntimeOptions runtimeOptions; private readonly HttpClient httpClient; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/HutaoResponse.cs similarity index 89% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoResponse.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/HutaoResponse.cs index e73529e0..906a7845 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/HutaoResponse.cs @@ -3,9 +3,9 @@ using System.Runtime.CompilerServices; -namespace Snap.Hutao.Web.Hutao; +namespace Snap.Hutao.Web.Hutao.Response; -internal sealed class HutaoResponse : Response.Response, ILocalizableResponse +internal sealed class HutaoResponse : Web.Response.Response, ILocalizableResponse { [JsonConstructor] public HutaoResponse(int returnCode, string message, string? localizationKey) @@ -38,7 +38,7 @@ internal sealed class HutaoResponse : Response.Response, ILocalizableResponse } [SuppressMessage("", "SA1402")] -internal sealed class HutaoResponse : Response.Response, ILocalizableResponse +internal sealed class HutaoResponse : Web.Response.Response, ILocalizableResponse { [JsonConstructor] public HutaoResponse(int returnCode, string message, TData? data, string? localizationKey) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/ILocalizableResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/ILocalizableResponse.cs similarity index 81% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/ILocalizableResponse.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/ILocalizableResponse.cs index 49eaf8c5..1ee0c724 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/ILocalizableResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/ILocalizableResponse.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hutao; +namespace Snap.Hutao.Web.Hutao.Response; internal interface ILocalizableResponse { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/LocalizableResponseExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/LocalizableResponseExtension.cs similarity index 90% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/LocalizableResponseExtension.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/LocalizableResponseExtension.cs index 8bffc613..d28440fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/LocalizableResponseExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Response/LocalizableResponseExtension.cs @@ -3,7 +3,7 @@ using System.Globalization; -namespace Snap.Hutao.Web.Hutao; +namespace Snap.Hutao.Web.Hutao.Response; internal static class LocalizableResponseExtension { @@ -19,7 +19,7 @@ internal static class LocalizableResponseExtension } public static string GetLocalizationMessageOrMessage(this TResponse localizableResponse) - where TResponse : Response.Response, ILocalizableResponse + where TResponse : Web.Response.Response, ILocalizableResponse { return localizableResponse.GetLocalizationMessageOrDefault() ?? localizableResponse.Message; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HomaSpiralAbyssClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs similarity index 97% rename from src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HomaSpiralAbyssClient.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs index d4841cd1..427575a2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HomaSpiralAbyssClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/SpiralAbyss/HutaoSpiralAbyssClient.cs @@ -8,6 +8,7 @@ using Snap.Hutao.ViewModel.User; using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar; +using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Hutao.SpiralAbyss.Post; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; @@ -22,10 +23,10 @@ namespace Snap.Hutao.Web.Hutao.SpiralAbyss; [HighQuality] [ConstructorGenerated(ResolveHttpClient = true)] [HttpClient(HttpClientConfiguration.Default)] -internal sealed partial class HomaSpiralAbyssClient +internal sealed partial class HutaoSpiralAbyssClient { private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; + private readonly ILogger logger; private readonly IServiceProvider serviceProvider; private readonly HttpClient httpClient; @@ -233,7 +234,7 @@ internal sealed partial class HomaSpiralAbyssClient if (spiralAbyssResponse.IsOk()) { HutaoUserOptions options = serviceProvider.GetRequiredService(); - return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data, options.ActualUserName); + return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data, options.GetActualUserName()); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs index 1ee466fb..c6867f9e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs @@ -11,6 +11,7 @@ namespace Snap.Hutao.Web; /// [HighQuality] [SuppressMessage("", "SA1201")] +[SuppressMessage("", "SA1203")] [SuppressMessage("", "SA1124")] internal static class HutaoEndpoints { @@ -258,6 +259,7 @@ internal static class HutaoEndpoints return $"{ApiSnapGenshinEnka}/{uid}"; } + public const string Ip = $"{ApiSnapGenshin}/ip"; private const string ApiSnapGenshin = "https://api.snapgenshin.com"; private const string ApiSnapGenshinMetadata = $"{ApiSnapGenshin}/metadata"; private const string ApiSnapGenshinStaticRaw = $"{ApiSnapGenshin}/static/raw"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs index 469b5e72..f25bb246 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpRequestMessageBuilderExtension.cs @@ -1,7 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Web.Hutao.Response; using System.IO; +using System.Net; using System.Net.Http; using System.Net.Sockets; @@ -12,6 +14,7 @@ internal static class HttpRequestMessageBuilderExtension private const string RequestErrorMessage = "请求异常已忽略"; internal static async ValueTask TryCatchSendAsync(this HttpRequestMessageBuilder builder, HttpClient httpClient, ILogger logger, CancellationToken token) + where TResult : class { try { @@ -26,6 +29,22 @@ internal static class HttpRequestMessageBuilderExtension catch (HttpRequestException ex) { logger.LogWarning(ex, RequestErrorMessage); + + if (ex.StatusCode is HttpStatusCode.BadGateway) + { + Type resultType = typeof(TResult); + + if (resultType == typeof(HutaoResponse)) + { + return Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default) as TResult; + } + + if (resultType.IsConstructedGenericType && resultType.GetGenericTypeDefinition() == typeof(HutaoResponse<>)) + { + return Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default, default) as TResult; + } + } + return default; } catch (IOException ex) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs new file mode 100644 index 00000000..53eb0ea1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs @@ -0,0 +1,10 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Request.Builder; + +internal enum HttpTryCatchSendStrategy +{ + Default, + HutaoApi, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/NameValueCollectionExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/NameValueCollectionExtension.cs index 8375b0ef..1ac71767 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/NameValueCollectionExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/NameValueCollectionExtension.cs @@ -18,21 +18,21 @@ internal static class NameValueCollectionExtension } StringBuilder sb = new(); - string?[] keys = collection.AllKeys; - for (int i = 0; i < count; i++) + foreach (string? key in collection.AllKeys) { - string? key = keys[i]; - if (collection.GetValues(key) is { } values) + if (collection.GetValues(key) is not { } values) { - foreach (ref readonly string value in values.AsSpan()) - { - if (!string.IsNullOrEmpty(key)) - { - sb.Append(key).Append('='); - } + continue; + } - sb.Append(HttpUtility.UrlEncode(value)).Append('&'); + foreach (ref readonly string value in values.AsSpan()) + { + if (!string.IsNullOrEmpty(key)) + { + sb.Append(key).Append('='); } + + sb.Append(HttpUtility.UrlEncode(value)).Append('&'); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index b5cc3540..5725680a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -84,6 +84,11 @@ internal enum KnownReturnCode /// AppIdError = -109, + /// + /// 二维码已过期 + /// + QrCodeExpired = -106, + /// /// 验证密钥过期 ///