mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afbebe4222 | ||
|
|
7da778699b | ||
|
|
5bfc790ea2 | ||
|
|
fc13b85739 | ||
|
|
df999dbf51 | ||
|
|
688562c1dd | ||
|
|
051a115f84 | ||
|
|
b3d75f9fa5 | ||
|
|
0b90bdaa42 | ||
|
|
77067d27d0 | ||
|
|
e04542606e | ||
|
|
5be958ff64 | ||
|
|
a57933388d | ||
|
|
86c6c9574b | ||
|
|
47e451df2f | ||
|
|
c8592c798b | ||
|
|
5fad960b20 | ||
|
|
44ba0a90a6 | ||
|
|
883c1ca95f | ||
|
|
1a29908e5d | ||
|
|
3e9edd2f62 | ||
|
|
f9c18d2555 | ||
|
|
b5afca256a | ||
|
|
a0e79344b1 | ||
|
|
3b8eba3bb1 | ||
|
|
08a3db7dc9 | ||
|
|
c02e7b0db3 | ||
|
|
a51ede5048 | ||
|
|
1f31c946cc | ||
|
|
7dee4a0ea5 | ||
|
|
2fdeaa2557 | ||
|
|
fbffadd546 | ||
|
|
00abfe6695 | ||
|
|
f2a4d2fa53 | ||
|
|
4246fa3d13 | ||
|
|
e0a5898b3a | ||
|
|
71ac87539f | ||
|
|
3959ce1c0a | ||
|
|
a56f382f8b | ||
|
|
04c3498b54 | ||
|
|
f304e0920f | ||
|
|
6fb276af9d | ||
|
|
4bd55c308a | ||
|
|
98a9f5fec9 | ||
|
|
0420568e73 | ||
|
|
45242ff8ce | ||
|
|
074cc1194b | ||
|
|
bd4a0f0d8e | ||
|
|
bbc2d7655c | ||
|
|
588aba1395 | ||
|
|
611469beb3 | ||
|
|
1b80f79189 | ||
|
|
a8cfb7fcc4 | ||
|
|
10445a73b4 | ||
|
|
7d00cec7c6 | ||
|
|
05674fb01a | ||
|
|
2c6682574f | ||
|
|
53a95ddcb9 | ||
|
|
bbfd5096d7 | ||
|
|
24407ecc05 | ||
|
|
ff2521c02c | ||
|
|
5954c1a0ab | ||
|
|
4dc753bf5a | ||
|
|
bd3617c15a | ||
|
|
70da292f21 | ||
|
|
97c5e7d37f | ||
|
|
388f9d5657 | ||
|
|
74e11f3823 | ||
|
|
c1305cda43 | ||
|
|
b0d5051957 | ||
|
|
6a42c36a76 | ||
|
|
7cf106ec50 | ||
|
|
f12cd63c92 | ||
|
|
c441fdb6b0 | ||
|
|
09a880525b | ||
|
|
15212d8f21 | ||
|
|
a60c4bff08 | ||
|
|
e02985926d | ||
|
|
09448b7137 | ||
|
|
6487df776a | ||
|
|
2be11c22df | ||
|
|
9ecb3d5821 | ||
|
|
ca64c3e0ef | ||
|
|
3fe726aa63 | ||
|
|
6b7ffe9fe9 | ||
|
|
5ed5729c4e | ||
|
|
9ba0066f40 | ||
|
|
9c639fbaa4 | ||
|
|
a592816661 | ||
|
|
ec9c5ebee1 | ||
|
|
9475c19b64 | ||
|
|
9bfe7f78ef | ||
|
|
88d7f0bcc7 | ||
|
|
4920da4ea2 | ||
|
|
99b2ccb33b | ||
|
|
9b94a75d6f | ||
|
|
e390ad2839 | ||
|
|
3086d59674 | ||
|
|
cb00fdbda0 | ||
|
|
1702dfcdc6 | ||
|
|
4b54b343f9 | ||
|
|
292b21a759 | ||
|
|
29e9413022 | ||
|
|
7c923aaa5e | ||
|
|
c6618be0fc | ||
|
|
45dd276b89 | ||
|
|
8a9fb38f49 | ||
|
|
1249216491 | ||
|
|
4bf3f4151e | ||
|
|
89d909b04f | ||
|
|
2cec0f5e0e | ||
|
|
bbb97cd802 | ||
|
|
67b058f126 | ||
|
|
b98611ccd9 | ||
|
|
80d6d5eb2b | ||
|
|
f682bb57e8 | ||
|
|
d8310b784f | ||
|
|
486c6eb308 | ||
|
|
c5d04e09da | ||
|
|
0629f7c4c9 | ||
|
|
b49288a98f | ||
|
|
df61aa3968 | ||
|
|
ee99d0b665 | ||
|
|
72b62aa9c6 | ||
|
|
6b031e1866 | ||
|
|
59c03c7f3b | ||
|
|
c03a96b44f | ||
|
|
d5a97903d3 | ||
|
|
7f998dc87f | ||
|
|
2367c4759d | ||
|
|
043e3f07d8 | ||
|
|
cd075c4dab | ||
|
|
65252f1f69 | ||
|
|
f5dd5f4c1d | ||
|
|
27ce55f3f7 | ||
|
|
311941bb89 | ||
|
|
07e2489cab | ||
|
|
699ac60aaf | ||
|
|
dc7bc7e35d | ||
|
|
bf67fcf3a2 | ||
|
|
5093246571 | ||
|
|
94fe192581 | ||
|
|
c679032387 | ||
|
|
d119b056c7 | ||
|
|
ab95ce8ce8 | ||
|
|
0ede5b158f | ||
|
|
6c9a98c2c9 | ||
|
|
a725fc0e9e | ||
|
|
5251dd9343 | ||
|
|
0b71053bf9 | ||
|
|
a91499171d | ||
|
|
b6c474cc12 | ||
|
|
e041def070 | ||
|
|
2e81ddf3f4 | ||
|
|
4d988e5500 | ||
|
|
36aff679c0 | ||
|
|
d490393108 |
21
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
21
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
@@ -40,7 +40,7 @@ body:
|
||||
attributes:
|
||||
label: Snap Hutao 版本
|
||||
description: 在应用标题,应用程序的反馈中心界面中可以找到
|
||||
placeholder: 例:1.4.15.0
|
||||
placeholder: 例:1.9.9.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -62,20 +62,19 @@ body:
|
||||
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
|
||||
options:
|
||||
- 安装和环境
|
||||
- 成就管理
|
||||
- 角色信息面板
|
||||
- 游戏启动器
|
||||
- 祈愿记录
|
||||
- 成就管理
|
||||
- 我的角色
|
||||
- 实时便笺
|
||||
- 养成计算
|
||||
- 文件缓存
|
||||
- 祈愿记录
|
||||
- 玩家查询
|
||||
- 胡桃数据库
|
||||
- 用户界面
|
||||
- 胡桃云
|
||||
- 胡桃帐号
|
||||
- 签到
|
||||
- 深境螺旋/胡桃数据库
|
||||
- Wiki
|
||||
- 米游社账号面板
|
||||
- 每日签到奖励
|
||||
- 胡桃通行证/胡桃云
|
||||
- 用户界面
|
||||
- 文件缓存
|
||||
- 公告
|
||||
- 其它
|
||||
validations:
|
||||
|
||||
19
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
19
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
@@ -40,7 +40,7 @@ body:
|
||||
attributes:
|
||||
label: Snap Hutao Version
|
||||
description: You can find the version in application's title bar
|
||||
placeholder: e.g. 1.4.15.0
|
||||
placeholder: e.g. 1.9.9.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -62,20 +62,19 @@ body:
|
||||
description: Please select the most associated category of your issue
|
||||
options:
|
||||
- Installation and Environment
|
||||
- Game Launcher
|
||||
- Wish Export
|
||||
- Achievement
|
||||
- My Character
|
||||
- Game Launcher
|
||||
- Realtime Note
|
||||
- Develop Plan
|
||||
- File Cache
|
||||
- Wish Export
|
||||
- Game Record
|
||||
- Hutao Database
|
||||
- User Interface
|
||||
- Snap Hutao Cloud
|
||||
- Snap Hutao Account
|
||||
- Checkin
|
||||
- Spiral Abyss
|
||||
- Wiki
|
||||
- MiHoYo Account Panel
|
||||
- Daily Checkin Reward
|
||||
- Hutao Passport/Hutao Cloud
|
||||
- User Interface
|
||||
- File Cache
|
||||
- Announcement
|
||||
- Other
|
||||
validations:
|
||||
|
||||
15
.github/pull_request_template.md
vendored
Normal file
15
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
|
||||
<!--- Before you create this PR, please fill the following form and checklist -->
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe your changes -->
|
||||
|
||||
## Related Issue
|
||||
|
||||
<!--- If there's an associated issue, please use [GitHub Keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) to link it -->
|
||||
<!-- e.g. fix #999, resolve #999, close #999 -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] The target PR branch is `develop` branch
|
||||
@@ -17,7 +17,7 @@ You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-sta
|
||||
|
||||
## 本地化翻译 / Localization
|
||||
|
||||
].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
|
||||
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
|
||||
|
||||
|
||||
BIN
res/Banner3-large-cn.psd
Normal file
BIN
res/Banner3-large-cn.psd
Normal file
Binary file not shown.
BIN
res/Banner3-large.psd
Normal file
BIN
res/Banner3-large.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/abyss.psd
Normal file
BIN
res/Store/chs/abyss.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/achievement.psd
Normal file
BIN
res/Store/chs/achievement.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/character-data.psd
Normal file
BIN
res/Store/chs/character-data.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/lancher.psd
Normal file
BIN
res/Store/chs/lancher.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/realtime-notes.psd
Normal file
BIN
res/Store/chs/realtime-notes.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/wish.psd
Normal file
BIN
res/Store/chs/wish.psd
Normal file
Binary file not shown.
BIN
res/Store/en/abyss.psd
Normal file
BIN
res/Store/en/abyss.psd
Normal file
Binary file not shown.
BIN
res/Store/en/achievement.psd
Normal file
BIN
res/Store/en/achievement.psd
Normal file
Binary file not shown.
BIN
res/Store/en/character-data.psd
Normal file
BIN
res/Store/en/character-data.psd
Normal file
Binary file not shown.
BIN
res/Store/en/lancher.psd
Normal file
BIN
res/Store/en/lancher.psd
Normal file
Binary file not shown.
BIN
res/Store/en/realtime-notes.psd
Normal file
BIN
res/Store/en/realtime-notes.psd
Normal file
Binary file not shown.
BIN
res/Store/en/wish.psd
Normal file
BIN
res/Store/en/wish.psd
Normal file
Binary file not shown.
@@ -110,7 +110,6 @@ dotnet_diagnostic.SA1642.severity = none
|
||||
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
dotnet_diagnostic.IDE0060.severity = none
|
||||
dotnet_diagnostic.IDE0290.severity = none
|
||||
|
||||
# SA1208: System using directives should be placed before other using directives
|
||||
dotnet_diagnostic.SA1208.severity = none
|
||||
@@ -321,7 +320,8 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
||||
|
||||
# CA2251: 使用 “string.Equals”
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
|
||||
csharp_style_prefer_primary_constructors = false:none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.PlatformExtensions;
|
||||
@@ -11,6 +12,7 @@ public sealed class DependencyInjectionTest
|
||||
.AddSingleton<IService, ServiceB>()
|
||||
.AddScoped<IScopedService, ServiceA>()
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.AddLogging(builder => builder.AddConsole())
|
||||
.BuildServiceProvider();
|
||||
|
||||
[TestMethod]
|
||||
@@ -41,6 +43,13 @@ public sealed class DependencyInjectionTest
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LoggerWithInterfaceTypeCanBeResolved()
|
||||
{
|
||||
Assert.IsNotNull(services.GetService<ILogger<IScopedService>>());
|
||||
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
|
||||
}
|
||||
|
||||
private interface IService
|
||||
{
|
||||
Guid Id { get; }
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -10,8 +10,6 @@ using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Shell;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using static Snap.Hutao.Core.Logging.ConsoleVirtualTerminalSequences;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -50,6 +48,8 @@ public sealed partial class App : Application
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public App(IServiceProvider serviceProvider)
|
||||
{
|
||||
// DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
|
||||
|
||||
// Load app resource
|
||||
InitializeComponent();
|
||||
activation = serviceProvider.GetRequiredService<IActivation>();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal class ButtonBaseBuilder<TButton> : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase, new()
|
||||
{
|
||||
public TButton Button { get; } = new();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction.Extension;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal static class ButtonBaseBuilderExtension
|
||||
{
|
||||
public static TBuilder SetContent<TBuilder, TButton>(this TBuilder builder, object? content)
|
||||
where TBuilder : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
builder.Configure(builder => builder.Button.Content = content);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder SetCommand<TBuilder, TButton>(this TBuilder builder, ICommand command)
|
||||
where TBuilder : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
builder.Configure(builder => builder.Button.Command = command);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal sealed class ButtonBuilder : ButtonBaseBuilder<Button>;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal static class ButtonBuilderExtension
|
||||
{
|
||||
public static ButtonBuilder SetContent(this ButtonBuilder builder, object? content)
|
||||
{
|
||||
return builder.SetContent<ButtonBuilder, Button>(content);
|
||||
}
|
||||
|
||||
public static ButtonBuilder SetCommand(this ButtonBuilder builder, ICommand command)
|
||||
{
|
||||
return builder.SetCommand<ButtonBuilder, Button>(command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal interface IButtonBaseBuilder<TButton> : IBuilder
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
TButton Button { get; }
|
||||
}
|
||||
@@ -28,4 +28,21 @@ internal static class FrameworkElementExtension
|
||||
frameworkElement.IsRightTapEnabled = false;
|
||||
frameworkElement.IsTabStop = false;
|
||||
}
|
||||
|
||||
public static void InitializeDataContext<TDataContext>(this FrameworkElement frameworkElement, IServiceProvider? serviceProvider = default)
|
||||
where TDataContext : class
|
||||
{
|
||||
IServiceProvider service = serviceProvider ?? Ioc.Default;
|
||||
try
|
||||
{
|
||||
frameworkElement.DataContext = service.GetRequiredService<TDataContext>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
ILogger? logger = service.GetRequiredService(typeof(ILogger<>).MakeGenericType([frameworkElement.GetType()])) as ILogger;
|
||||
logger?.LogError(ex, "Failed to initialize DataContext");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
|
||||
try
|
||||
{
|
||||
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), HutaoExceptionKind.ImageCacheInvalidUri, SH.ControlImageCachedImageInvalidResourceUri);
|
||||
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
|
||||
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
|
||||
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
|
||||
23
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapItem.cs
Normal file
23
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapItem.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Licensed to the .NET Fou// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class WrapItem
|
||||
{
|
||||
public WrapItem(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public int Index { get; }
|
||||
|
||||
public Size? Size { get; set; }
|
||||
|
||||
public Point? Position { get; set; }
|
||||
|
||||
public UIElement? Element { get; set; }
|
||||
}
|
||||
213
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayout.cs
Normal file
213
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayout.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections.Specialized;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
[DependencyProperty("HorizontalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))]
|
||||
[DependencyProperty("VerticalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))]
|
||||
internal sealed partial class WrapLayout : VirtualizingLayout
|
||||
{
|
||||
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = new WrapLayoutState(context);
|
||||
base.InitializeForContextCore(context);
|
||||
}
|
||||
|
||||
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = default;
|
||||
base.UninitializeForContextCore(context);
|
||||
}
|
||||
|
||||
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
|
||||
state.RemoveFromIndex(minIndex);
|
||||
state.RecycleElementAt(args.OldStartingIndex);
|
||||
state.RecycleElementAt(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
state.RemoveFromIndex(args.OldStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
state.RecycleElementAt(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
state.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
base.OnItemsChangedCore(context, source, args);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
|
||||
{
|
||||
Size spacing = new(HorizontalSpacing, VerticalSpacing);
|
||||
|
||||
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
|
||||
|
||||
if (spacing != state.Spacing || state.AvailableWidth != availableSize.Width)
|
||||
{
|
||||
state.ClearPositions();
|
||||
state.Spacing = spacing;
|
||||
state.AvailableWidth = availableSize.Height;
|
||||
}
|
||||
|
||||
double currentHeight = 0;
|
||||
Point position = default;
|
||||
for (int i = 0; i < context.ItemCount; ++i)
|
||||
{
|
||||
bool measured = false;
|
||||
WrapItem item = state.GetItemAt(i);
|
||||
if (item.Size is null)
|
||||
{
|
||||
item.Element = context.GetOrCreateElementAt(i);
|
||||
item.Element.Measure(availableSize);
|
||||
item.Size = item.Element.DesiredSize;
|
||||
measured = true;
|
||||
}
|
||||
|
||||
Size currentSize = item.Size.Value;
|
||||
|
||||
if (item.Position is null)
|
||||
{
|
||||
if (availableSize.Width < position.X + currentSize.Height)
|
||||
{
|
||||
// New Row
|
||||
position.X = 0;
|
||||
position.Y += currentHeight + spacing.Height;
|
||||
currentHeight = 0;
|
||||
}
|
||||
|
||||
item.Position = position;
|
||||
}
|
||||
|
||||
position = item.Position.Value;
|
||||
|
||||
double vEnd = position.Y + currentSize.Width;
|
||||
if (vEnd < context.RealizationRect.Top)
|
||||
{
|
||||
// Item is "above" the bounds
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = default;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (position.Y > context.RealizationRect.Bottom)
|
||||
{
|
||||
// Item is "below" the bounds.
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = default;
|
||||
}
|
||||
|
||||
// We don't need to measure anything below the bounds
|
||||
break;
|
||||
}
|
||||
else if (!measured)
|
||||
{
|
||||
// Always measure elements that are within the bounds
|
||||
item.Element = context.GetOrCreateElementAt(i);
|
||||
item.Element.Measure(availableSize);
|
||||
|
||||
currentSize = item.Element.DesiredSize;
|
||||
if (currentSize != item.Size)
|
||||
{
|
||||
// this item changed size; we need to recalculate layout for everything after this
|
||||
state.RemoveFromIndex(i + 1);
|
||||
item.Size = currentSize;
|
||||
|
||||
// did the change make it go into the new row?
|
||||
if (availableSize.Width < position.X + currentSize.Width)
|
||||
{
|
||||
// New Row
|
||||
position.X = 0;
|
||||
position.Y += currentHeight + spacing.Height;
|
||||
currentHeight = 0;
|
||||
}
|
||||
|
||||
item.Position = position;
|
||||
}
|
||||
}
|
||||
|
||||
position.X += currentSize.Width + spacing.Width;
|
||||
currentHeight = Math.Max(currentSize.Height, currentHeight);
|
||||
}
|
||||
|
||||
return new Size(double.IsInfinity(availableSize.Width) ? 0 : Math.Ceiling(availableSize.Width), state.GetHeight());
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
|
||||
{
|
||||
if (context.ItemCount > 0)
|
||||
{
|
||||
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
|
||||
|
||||
bool ArrangeItem(WrapItem item)
|
||||
{
|
||||
if (item is { Size: null } or { Position: null })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Size desiredSize = item.Size.Value;
|
||||
|
||||
Point position = item.Position.Value;
|
||||
|
||||
if (context.RealizationRect.Top <= position.Y + desiredSize.Height && position.Y <= context.RealizationRect.Bottom)
|
||||
{
|
||||
// place the item
|
||||
UIElement child = context.GetOrCreateElementAt(item.Index);
|
||||
child.Arrange(new Rect(position, desiredSize));
|
||||
}
|
||||
else if (position.Y > context.RealizationRect.Bottom)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < context.ItemCount; ++i)
|
||||
{
|
||||
if (!ArrangeItem(state.GetItemAt(i)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WrapLayout wp)
|
||||
{
|
||||
wp.InvalidateMeasure();
|
||||
wp.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayoutState.cs
Normal file
110
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayoutState.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class WrapLayoutState
|
||||
{
|
||||
private readonly List<WrapItem> items = [];
|
||||
private readonly VirtualizingLayoutContext context;
|
||||
|
||||
public WrapLayoutState(VirtualizingLayoutContext context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Orientation Orientation { get; private set; }
|
||||
|
||||
public Size Spacing { get; set; }
|
||||
|
||||
public double AvailableWidth { get; set; }
|
||||
|
||||
public WrapItem GetItemAt(int index)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
|
||||
if (index <= (items.Count - 1))
|
||||
{
|
||||
return items[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
WrapItem item = new(index);
|
||||
items.Add(item);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
public void RemoveFromIndex(int index)
|
||||
{
|
||||
if (index >= items.Count)
|
||||
{
|
||||
// Item was added/removed but we haven't realized that far yet
|
||||
return;
|
||||
}
|
||||
|
||||
int numToRemove = items.Count - index;
|
||||
items.RemoveRange(index, numToRemove);
|
||||
}
|
||||
|
||||
public void ClearPositions()
|
||||
{
|
||||
foreach (ref readonly WrapItem item in CollectionsMarshal.AsSpan(items))
|
||||
{
|
||||
item.Position = default;
|
||||
}
|
||||
}
|
||||
|
||||
public double GetHeight()
|
||||
{
|
||||
if (items.Count is 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Point? lastPosition = default;
|
||||
double maxHeight = 0;
|
||||
|
||||
for (int i = items.Count - 1; i >= 0; --i)
|
||||
{
|
||||
WrapItem item = items[i];
|
||||
|
||||
if (item.Position is null || item.Size is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastPosition is not null && lastPosition.Value.Y > item.Position.Value.Y)
|
||||
{
|
||||
// This is a row above the last item.
|
||||
break;
|
||||
}
|
||||
|
||||
lastPosition = item.Position;
|
||||
maxHeight = Math.Max(maxHeight, item.Size.Value.Height);
|
||||
}
|
||||
|
||||
return lastPosition?.Y + maxHeight ?? 0;
|
||||
}
|
||||
|
||||
public void RecycleElementAt(int index)
|
||||
{
|
||||
UIElement element = context.GetOrCreateElementAt(index);
|
||||
context.RecycleElement(element);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
@@ -18,13 +19,14 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
foreach (UIElement child in Children)
|
||||
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
|
||||
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
|
||||
{
|
||||
// ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation.
|
||||
double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width;
|
||||
double childAvailableWidth = (availableWidth + Spacing) / Children.Count;
|
||||
double childAvailableWidth = (availableWidth + Spacing) / visibleChildren.Count;
|
||||
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
|
||||
child.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
|
||||
visibleChild.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
|
||||
}
|
||||
|
||||
return base.MeasureOverride(availableSize);
|
||||
@@ -32,14 +34,14 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
int itemCount = Children.Count;
|
||||
double availableItemWidth = (finalSize.Width - (Spacing * (itemCount - 1))) / itemCount;
|
||||
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
|
||||
double availableItemWidth = (finalSize.Width - (Spacing * (visibleChildren.Count - 1))) / visibleChildren.Count;
|
||||
double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth);
|
||||
|
||||
double offset = 0;
|
||||
foreach (UIElement child in Children)
|
||||
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
|
||||
{
|
||||
child.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
|
||||
visibleChild.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
|
||||
offset += actualItemWidth + Spacing;
|
||||
}
|
||||
|
||||
@@ -49,7 +51,8 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
|
||||
private static void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HorizontalEqualPanel panel = (HorizontalEqualPanel)sender;
|
||||
panel.MinWidth = (panel.MinItemWidth * panel.Children.Count) + (panel.Spacing * (panel.Children.Count - 1));
|
||||
int vivibleChildrenCount = panel.Children.Count(child => child.Visibility is Visibility.Visible);
|
||||
panel.MinWidth = (panel.MinItemWidth * vivibleChildrenCount) + (panel.Spacing * (vivibleChildrenCount - 1));
|
||||
}
|
||||
|
||||
private static void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
@@ -15,7 +15,7 @@ internal class ScopedPage : Page
|
||||
{
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly CancellationTokenSource viewCancellationTokenSource = new();
|
||||
private readonly IServiceScope currentScope;
|
||||
private readonly IServiceScope pageScope;
|
||||
|
||||
private bool inFrame = true;
|
||||
|
||||
@@ -23,7 +23,7 @@ internal class ScopedPage : Page
|
||||
{
|
||||
unloadEventHandler = OnUnloaded;
|
||||
Unloaded += unloadEventHandler;
|
||||
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
|
||||
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
|
||||
}
|
||||
|
||||
public async ValueTask NotifyRecipientAsync(INavigationData extra)
|
||||
@@ -44,9 +44,17 @@ internal class ScopedPage : Page
|
||||
protected void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
try
|
||||
{
|
||||
IViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -95,7 +103,8 @@ internal class ScopedPage : Page
|
||||
viewModel.IsViewDisposed = true;
|
||||
|
||||
// Dispose the scope
|
||||
currentScope.Dispose();
|
||||
pageScope.Dispose();
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScope
|
||||
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
|
||||
IServiceScope currentScope = serviceProvider.CreateScope();
|
||||
|
||||
// In case previous one is not disposed.
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
<x:String x:Key="UI_Icon_Intee_Explore_1">https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png</x:String>
|
||||
<x:String x:Key="UI_ImgSign_ItemIcon">https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png</x:String>
|
||||
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png</x:String>
|
||||
|
||||
<!-- Mark -->
|
||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Events_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Main_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Main_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
|
||||
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Mark/UI_MarkTower.png</x:String>
|
||||
|
||||
<!-- ItemIcon -->
|
||||
<x:String x:Key="UI_ItemIcon_201">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png</x:String>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Web.Request.Builder;
|
||||
namespace Snap.Hutao.Core.Abstraction.Extension;
|
||||
|
||||
internal static class BuilderExtension
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
internal interface IBuilder;
|
||||
@@ -1,10 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Hashing;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.ViewModel.Guide;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.IO;
|
||||
@@ -25,6 +29,7 @@ namespace Snap.Hutao.Core.Caching;
|
||||
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
private const string CacheFailedDownloadTasksName = $"{nameof(ImageCache)}.FailedDownloadTasks";
|
||||
|
||||
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = FrozenDictionary.ToFrozenDictionary(
|
||||
[
|
||||
@@ -35,9 +40,11 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<ImageCache> logger;
|
||||
private readonly IMemoryCache memoryCache;
|
||||
|
||||
private string? baseFolder;
|
||||
private string? cacheFolder;
|
||||
@@ -169,40 +176,76 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
while (retryCount < 3)
|
||||
{
|
||||
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
if (message.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||
{
|
||||
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||
}
|
||||
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
|
||||
.Create()
|
||||
.SetRequestUri(uri)
|
||||
|
||||
if (message.IsSuccessStatusCode)
|
||||
// These headers are only available for our own api
|
||||
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
|
||||
.Get();
|
||||
|
||||
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
|
||||
{
|
||||
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||
{
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||
}
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
#if DEBUG
|
||||
DebugTrack(uri);
|
||||
#endif
|
||||
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
switch (responseMessage.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
#if DEBUG
|
||||
DebugTrack(uri);
|
||||
#endif
|
||||
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
internal partial class ImageCache
|
||||
{
|
||||
private void DebugTrack(Uri uri)
|
||||
{
|
||||
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => entry.Value ??= new HashSet<string>()) as HashSet<string>;
|
||||
set?.Add(uri.ToString());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Collection;
|
||||
|
||||
internal sealed class TwoEnumerbleEnumerator<TFirst, TSecond> : IDisposable
|
||||
{
|
||||
private readonly IEnumerator<TFirst> firstEnumerator;
|
||||
private readonly IEnumerator<TSecond> secondEnumerator;
|
||||
|
||||
public TwoEnumerbleEnumerator(IEnumerable<TFirst> firstEnumerable, IEnumerable<TSecond> secondEnumerable)
|
||||
{
|
||||
firstEnumerator = firstEnumerable.GetEnumerator();
|
||||
secondEnumerator = secondEnumerable.GetEnumerator();
|
||||
}
|
||||
|
||||
public (TFirst First, TSecond Second) Current { get => (firstEnumerator.Current, secondEnumerator.Current); }
|
||||
|
||||
public bool MoveNext(ref bool moveFirst, ref bool moveSecond)
|
||||
{
|
||||
moveFirst = moveFirst && firstEnumerator.MoveNext();
|
||||
moveSecond = moveSecond && secondEnumerator.MoveNext();
|
||||
|
||||
return moveFirst || moveSecond;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
firstEnumerator.Dispose();
|
||||
secondEnumerator.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 支持Md5转换
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class Convert
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取字符串的MD5计算结果
|
||||
/// </summary>
|
||||
/// <param name="source">源字符串</param>
|
||||
/// <returns>计算的结果</returns>
|
||||
public static string ToMd5HexString(string source)
|
||||
{
|
||||
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
|
||||
return System.Convert.ToHexString(hash);
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,6 @@ namespace Snap.Hutao.Core.Database;
|
||||
[HighQuality]
|
||||
internal static class DbSetExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -27,27 +20,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步添加并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加列表并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entities">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -55,27 +34,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步添加列表并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entities">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.AddRange(entities);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -83,27 +48,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步移除并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -111,18 +62,11 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步更新并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -136,11 +80,11 @@ internal static class DbSetExtension
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposedSlow())
|
||||
if (serviceProvider.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -96,7 +96,7 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposedSlow())
|
||||
if (serviceProvider.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,13 @@ internal static class DependencyInjection
|
||||
ServiceProvider serviceProvider = new ServiceCollection()
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder.AddDebug().AddConsoleWindow())
|
||||
.AddLogging(builder =>
|
||||
{
|
||||
builder
|
||||
.SetMinimumLevel(LogLevel.Trace)
|
||||
.AddDebug()
|
||||
.AddConsoleWindow();
|
||||
})
|
||||
.AddMemoryCache()
|
||||
|
||||
// Hutao extensions
|
||||
|
||||
@@ -33,17 +33,17 @@ internal static class IocConfiguration
|
||||
return services
|
||||
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
|
||||
.AddTransient(typeof(Database.ScopedDbCurrent<,,>))
|
||||
.AddDbContext<AppDbContext>(AddDbContextCore);
|
||||
.AddDbContextPool<AppDbContext>(AddDbContextCore);
|
||||
}
|
||||
|
||||
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
|
||||
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
|
||||
{
|
||||
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
// Temporarily create a context
|
||||
using (AppDbContext context = AppDbContext.Create(sqlConnectionString))
|
||||
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
|
||||
{
|
||||
if (context.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
|
||||
@@ -18,13 +18,22 @@ internal static class ServiceProviderExtension
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDisposedSlow(this IServiceProvider? serviceProvider)
|
||||
public static bool IsDisposed(this IServiceProvider? serviceProvider)
|
||||
{
|
||||
if (serviceProvider is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (serviceProvider is ServiceProvider serviceProviderImpl)
|
||||
{
|
||||
return GetPrivateDisposed(serviceProviderImpl);
|
||||
}
|
||||
|
||||
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
|
||||
}
|
||||
|
||||
// private bool _disposed;
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
|
||||
private static extern ref bool GetPrivateDisposed(ServiceProvider serviceProvider);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
using Snap.Hutao.Core.Logging;
|
||||
|
||||
namespace Snap.Hutao.Core.Diagnostics;
|
||||
|
||||
internal readonly struct MeasureExecutionToken : IDisposable
|
||||
@@ -17,6 +19,6 @@ internal readonly struct MeasureExecutionToken : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
logger.LogDebug("{Caller} toke {Time} ms", callerName, stopwatch.GetElapsedTime().TotalMilliseconds);
|
||||
logger.LogColorizedDebug(("{Caller} toke {Time} ms", ConsoleColor.Gray), (callerName, ConsoleColor.Yellow), (stopwatch.GetElapsedTime().TotalMilliseconds, ConsoleColor.DarkGreen));
|
||||
}
|
||||
}
|
||||
@@ -5,50 +5,67 @@ namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
internal sealed class HutaoException : Exception
|
||||
{
|
||||
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
|
||||
: this(message, innerException)
|
||||
{
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
private HutaoException(string message, Exception? innerException)
|
||||
public HutaoException(string message, Exception? innerException)
|
||||
: base($"{message}\n{innerException?.Message}", innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public HutaoExceptionKind Kind { get; private set; }
|
||||
|
||||
[DoesNotReturn]
|
||||
public static HutaoException Throw(HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||
public static HutaoException Throw(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new HutaoException(kind, message, innerException);
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
|
||||
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||
public static void ThrowIf([DoesNotReturnIf(true)] bool condition, string message, Exception? innerException = default)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
throw new HutaoException(kind, message, innerException);
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ThrowIfNot(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||
public static void ThrowIfNot([DoesNotReturnIf(false)] bool condition, string message, Exception? innerException = default)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new HutaoException(kind, message, innerException);
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
}
|
||||
|
||||
public static HutaoException ServiceTypeCastFailed<TFrom, TTo>(string name, Exception? innerException = default)
|
||||
{
|
||||
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
|
||||
throw new HutaoException(HutaoExceptionKind.ServiceTypeCastFailed, message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static HutaoException GachaStatisticsInvalidItemId(uint id, Exception? innerException = default)
|
||||
{
|
||||
string message = SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id);
|
||||
throw new HutaoException(HutaoExceptionKind.GachaStatisticsInvalidItemId, message, innerException);
|
||||
throw new HutaoException(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id), innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static HutaoException UserdataCorrupted(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static InvalidCastException InvalidCast<TFrom, TTo>(string name, Exception? innerException = default)
|
||||
{
|
||||
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
|
||||
throw new InvalidCastException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static InvalidOperationException InvalidOperation(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new InvalidOperationException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static NotSupportedException NotSupported(string? message = default, Exception? innerException = default)
|
||||
{
|
||||
throw new NotSupportedException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new OperationCanceledException(message, innerException);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
internal enum HutaoExceptionKind
|
||||
{
|
||||
None,
|
||||
|
||||
// Foundation
|
||||
ServiceTypeCastFailed,
|
||||
ImageCacheInvalidUri,
|
||||
|
||||
// IO
|
||||
FileSystemCreateFileInsufficientPermissions,
|
||||
PrivateNamedPipeContentHashIncorrect,
|
||||
|
||||
// Service
|
||||
GachaStatisticsInvalidItemId,
|
||||
GameFpsUnlockingFailed,
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.System.Com;
|
||||
using Snap.Hutao.Win32.UI.Shell;
|
||||
using System.IO;
|
||||
using static Snap.Hutao.Win32.Macros;
|
||||
using static Snap.Hutao.Win32.Ole32;
|
||||
using static Snap.Hutao.Win32.Shell32;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
@@ -39,4 +44,57 @@ internal static class FileOperation
|
||||
File.Move(sourceFileName, destFileName, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static unsafe bool UnsafeMove(string sourceFileName, string destFileName)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
|
||||
{
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(sourceFileName, default, in IShellItem.IID, out IShellItem* pSourceShellItem)))
|
||||
{
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(destFileName, default, in IShellItem.IID, out IShellItem* pDestShellItem)))
|
||||
{
|
||||
pFileOperation->MoveItem(pSourceShellItem, pDestShellItem, default, default);
|
||||
|
||||
if (SUCCEEDED(pFileOperation->PerformOperations()))
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pDestShellItem);
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pSourceShellItem);
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pFileOperation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static unsafe bool UnsafeDelete(string path)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
|
||||
{
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
|
||||
{
|
||||
pFileOperation->DeleteItem(pShellItem, default);
|
||||
|
||||
if (SUCCEEDED(pFileOperation->PerformOperations()))
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pShellItem);
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pFileOperation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,17 @@ namespace Snap.Hutao.Core.IO.Hashing;
|
||||
|
||||
internal static class Hash
|
||||
{
|
||||
public static string SHA1HexString(string input)
|
||||
public static unsafe string SHA1HexString(string input)
|
||||
{
|
||||
return HashCore(BitConverter.ToString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
|
||||
return HashCore(Convert.ToHexString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
|
||||
}
|
||||
|
||||
private static TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)
|
||||
public static unsafe string MD5HexString(string input)
|
||||
{
|
||||
return HashCore(Convert.ToHexString, System.Security.Cryptography.MD5.HashData, Encoding.UTF8.GetBytes, input);
|
||||
}
|
||||
|
||||
private static unsafe TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)
|
||||
{
|
||||
return resultConverter(hashMethod(bytesConverter(input)));
|
||||
}
|
||||
|
||||
@@ -34,6 +34,6 @@ internal static class MD5
|
||||
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
|
||||
{
|
||||
byte[] bytes = await System.Security.Cryptography.MD5.HashDataAsync(stream, token).ConfigureAwait(false);
|
||||
return System.Convert.ToHexString(bytes);
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,6 @@ internal static class SHA256
|
||||
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
|
||||
{
|
||||
byte[] bytes = await System.Security.Cryptography.SHA256.HashDataAsync(stream, token).ConfigureAwait(false);
|
||||
return System.Convert.ToHexString(bytes);
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -77,34 +78,36 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
||||
using (HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
Memory<byte> buffer = new byte[bufferSize];
|
||||
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent())
|
||||
{
|
||||
int totalBytesRead = 0;
|
||||
int bytesReadAfterPreviousReport = 0;
|
||||
do
|
||||
Memory<byte> buffer = memoryOwner.Memory;
|
||||
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
|
||||
if (bytesRead <= 0)
|
||||
int totalBytesRead = 0;
|
||||
int bytesReadAfterPreviousReport = 0;
|
||||
do
|
||||
{
|
||||
progress.Report(new(bytesReadAfterPreviousReport));
|
||||
bytesReadAfterPreviousReport = 0;
|
||||
break;
|
||||
}
|
||||
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
|
||||
if (bytesRead <= 0)
|
||||
{
|
||||
progress.Report(new(bytesReadAfterPreviousReport));
|
||||
bytesReadAfterPreviousReport = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
|
||||
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
bytesReadAfterPreviousReport += bytesRead;
|
||||
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
|
||||
{
|
||||
progress.Report(new(bytesReadAfterPreviousReport));
|
||||
bytesReadAfterPreviousReport = 0;
|
||||
stopwatch = ValueStopwatch.StartNew();
|
||||
totalBytesRead += bytesRead;
|
||||
bytesReadAfterPreviousReport += bytesRead;
|
||||
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
|
||||
{
|
||||
progress.Report(new(bytesReadAfterPreviousReport));
|
||||
bytesReadAfterPreviousReport = 0;
|
||||
stopwatch = ValueStopwatch.StartNew();
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
@@ -51,26 +52,30 @@ internal class StreamCopyWorker<TStatus>
|
||||
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
Memory<byte> buffer = new byte[bufferSize];
|
||||
|
||||
do
|
||||
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(bufferSize))
|
||||
{
|
||||
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
break;
|
||||
}
|
||||
Memory<byte> buffer = memoryOwner.Memory;
|
||||
|
||||
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
|
||||
do
|
||||
{
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
stopwatch = ValueStopwatch.StartNew();
|
||||
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
|
||||
if (bytesRead is 0)
|
||||
{
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
break;
|
||||
}
|
||||
|
||||
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
|
||||
{
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
stopwatch = ValueStopwatch.StartNew();
|
||||
}
|
||||
}
|
||||
while (bytesRead > 0);
|
||||
}
|
||||
while (bytesRead > 0);
|
||||
}
|
||||
}
|
||||
40
src/Snap.Hutao/Snap.Hutao/Core/IO/StreamReaderWriter.cs
Normal file
40
src/Snap.Hutao/Snap.Hutao/Core/IO/StreamReaderWriter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
internal sealed class StreamReaderWriter : IDisposable
|
||||
{
|
||||
private readonly StreamReader reader;
|
||||
private readonly StreamWriter writer;
|
||||
|
||||
public StreamReaderWriter(StreamReader reader, StreamWriter writer)
|
||||
{
|
||||
this.reader = reader;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
public StreamReader Reader { get => reader; }
|
||||
|
||||
public StreamWriter Writer { get => writer; }
|
||||
|
||||
/// <inheritdoc cref="StreamReader.ReadLineAsync(CancellationToken)"/>
|
||||
public ValueTask<string?> ReadLineAsync(CancellationToken token)
|
||||
{
|
||||
return reader.ReadLineAsync(token);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="StreamWriter.WriteAsync(string?)"/>
|
||||
public Task WriteAsync(string value)
|
||||
{
|
||||
return writer.WriteAsync(value);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
writer.Dispose();
|
||||
reader.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ internal readonly struct TempFile : IDisposable
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
HutaoException.Throw(HutaoExceptionKind.FileSystemCreateFileInsufficientPermissions, SH.CoreIOTempFileCreateFail, ex);
|
||||
HutaoException.Throw(SH.CoreIOTempFileCreateFail, ex);
|
||||
}
|
||||
|
||||
if (delete)
|
||||
|
||||
21
src/Snap.Hutao/Snap.Hutao/Core/LazySlim.cs
Normal file
21
src/Snap.Hutao/Snap.Hutao/Core/LazySlim.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
internal sealed class LazySlim<T>
|
||||
{
|
||||
private readonly Func<T> valueFactory;
|
||||
|
||||
[MaybeNull]
|
||||
private T value;
|
||||
private bool initialized;
|
||||
private object? syncRoot;
|
||||
|
||||
public LazySlim(Func<T> valueFactory)
|
||||
{
|
||||
this.valueFactory = valueFactory;
|
||||
}
|
||||
|
||||
public T Value { get => LazyInitializer.EnsureInitialized(ref value, ref initialized, ref syncRoot, valueFactory); }
|
||||
}
|
||||
@@ -116,15 +116,15 @@ internal sealed partial class Activation : IActivation
|
||||
|
||||
// If it's the first time launch, we show the guide window anyway.
|
||||
// Otherwise, we check if there's any unfulfilled resource category present.
|
||||
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
|
||||
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
|
||||
{
|
||||
if (StaticResource.IsAnyUnfulfilledCategoryPresent())
|
||||
{
|
||||
UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.StaticResourceBegin);
|
||||
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
|
||||
}
|
||||
}
|
||||
|
||||
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) < GuideState.Completed)
|
||||
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
serviceProvider.GetRequiredService<GuideWindow>();
|
||||
|
||||
@@ -49,7 +49,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
|
||||
{
|
||||
byte[] content = new byte[header->ContentLength];
|
||||
serverStream.ReadAtLeast(content, header->ContentLength, false);
|
||||
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, HutaoExceptionKind.PrivateNamedPipeContentHashIncorrect, "PipePacket Content Hash incorrect");
|
||||
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect");
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ internal readonly struct LogArgument
|
||||
return new(argument);
|
||||
}
|
||||
|
||||
public static implicit operator LogArgument(double argument)
|
||||
{
|
||||
return new(argument);
|
||||
}
|
||||
|
||||
public static implicit operator LogArgument((object? Argument, ConsoleColor Foreground) tuple)
|
||||
{
|
||||
return new(tuple.Argument, tuple.Foreground);
|
||||
|
||||
@@ -151,7 +151,7 @@ internal static class LoggerExtension
|
||||
}
|
||||
else
|
||||
{
|
||||
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.Default);
|
||||
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.ForegroundWhite);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ internal static class LoggerExtension
|
||||
// Restore default colors
|
||||
if (message.ForegroundColor.HasValue || message.BackgroundColor.HasValue)
|
||||
{
|
||||
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.Default);
|
||||
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.ForegroundWhite);
|
||||
}
|
||||
|
||||
return resultMessageBuilder.ToString();
|
||||
|
||||
@@ -9,7 +9,11 @@ internal static class LoggerFactoryExtension
|
||||
{
|
||||
builder.Services.AddSingleton<ConsoleWindowLifeTime>();
|
||||
|
||||
builder.AddSimpleConsole();
|
||||
builder.AddSimpleConsole(options =>
|
||||
{
|
||||
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff ";
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
27
src/Snap.Hutao/Snap.Hutao/Core/ReadOnlySpan2D.cs
Normal file
27
src/Snap.Hutao/Snap.Hutao/Core/ReadOnlySpan2D.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
internal readonly ref struct ReadOnlySpan2D<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly ref T reference;
|
||||
private readonly int length;
|
||||
private readonly int columns;
|
||||
|
||||
public unsafe ReadOnlySpan2D(void* pointer, int length, int columns)
|
||||
{
|
||||
reference = ref *(T*)pointer;
|
||||
this.length = length;
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<T> this[int row]
|
||||
{
|
||||
get => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref reference, row * columns), columns);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.IO.Hashing;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using System.IO;
|
||||
using System.Security.Principal;
|
||||
@@ -15,15 +16,13 @@ namespace Snap.Hutao.Core;
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class RuntimeOptions
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly Lazy<(Version Version, string UserAgent)> lazyVersionAndUserAgent = new(() =>
|
||||
private readonly LazySlim<(Version Version, string UserAgent)> lazyVersionAndUserAgent = new(() =>
|
||||
{
|
||||
Version version = Package.Current.Id.Version.ToVersion();
|
||||
return (version, $"Snap Hutao/{version}");
|
||||
});
|
||||
|
||||
private readonly Lazy<string> lazyDataFolder = new(() =>
|
||||
private readonly LazySlim<string> lazyDataFolder = new(() =>
|
||||
{
|
||||
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
|
||||
|
||||
@@ -48,14 +47,14 @@ internal sealed class RuntimeOptions
|
||||
return path;
|
||||
});
|
||||
|
||||
private readonly Lazy<string> lazyDeviceId = new(() =>
|
||||
private readonly LazySlim<string> lazyDeviceId = new(() =>
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
return Hash.MD5HexString($"{userName}{machineGuid}");
|
||||
});
|
||||
|
||||
private readonly Lazy<(string Version, bool Supported)> lazyWebViewEnvironment = new(() =>
|
||||
private readonly LazySlim<(string Version, bool Supported)> lazyWebViewEnvironment = new(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -68,7 +67,7 @@ internal sealed class RuntimeOptions
|
||||
}
|
||||
});
|
||||
|
||||
private readonly Lazy<bool> lazyElevated = new(() =>
|
||||
private readonly LazySlim<bool> lazyElevated = new(() =>
|
||||
{
|
||||
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
|
||||
{
|
||||
@@ -82,18 +81,18 @@ internal sealed class RuntimeOptions
|
||||
}
|
||||
});
|
||||
|
||||
private readonly Lazy<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
|
||||
private readonly Lazy<string> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
|
||||
private readonly Lazy<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
|
||||
private readonly LazySlim<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
|
||||
private readonly LazySlim<string> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
|
||||
private readonly LazySlim<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
|
||||
|
||||
private bool isToastAvailable;
|
||||
private bool isToastAvailableInitialized;
|
||||
private object isToastAvailableLock = new();
|
||||
|
||||
public RuntimeOptions(IServiceProvider serviceProvider, ILogger<RuntimeOptions> logger)
|
||||
private readonly LazySlim<bool> lazyToastAvailable = new(() =>
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
ITaskContext taskContext = Ioc.Default.GetRequiredService<ITaskContext>();
|
||||
return taskContext.InvokeOnMainThread(() => ToastNotificationManager.CreateToastNotifier().Setting is NotificationSetting.Enabled);
|
||||
});
|
||||
|
||||
public RuntimeOptions()
|
||||
{
|
||||
AppLaunchTime = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
@@ -117,19 +116,7 @@ internal sealed class RuntimeOptions
|
||||
|
||||
public bool IsElevated { get => lazyElevated.Value; }
|
||||
|
||||
public bool IsToastAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
return LazyInitializer.EnsureInitialized(ref isToastAvailable, ref isToastAvailableInitialized, ref isToastAvailableLock, GetIsToastAvailable);
|
||||
|
||||
bool GetIsToastAvailable()
|
||||
{
|
||||
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
return taskContext.InvokeOnMainThread(() => ToastNotificationManager.CreateToastNotifier().Setting is NotificationSetting.Enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool IsToastAvailable { get => lazyToastAvailable.Value; }
|
||||
|
||||
public DateTimeOffset AppLaunchTime { get; }
|
||||
}
|
||||
@@ -16,11 +16,15 @@ internal static class RuntimeOptionsExtension
|
||||
|
||||
public static string GetDataFolderServerCacheFolder(this RuntimeOptions options)
|
||||
{
|
||||
return Path.Combine(options.DataFolder, "ServerCache");
|
||||
string directory = Path.Combine(options.DataFolder, "ServerCache");
|
||||
Directory.CreateDirectory(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
public static string GetDataFolderBackgroundFolder(this RuntimeOptions options)
|
||||
{
|
||||
return Path.Combine(options.DataFolder, "Background");
|
||||
string directory = Path.Combine(options.DataFolder, "Background");
|
||||
Directory.CreateDirectory(directory);
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,9 @@ internal static class SettingKeys
|
||||
#region Application
|
||||
public const string LaunchTimes = "LaunchTimes";
|
||||
public const string DataFolderPath = "DataFolderPath";
|
||||
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
|
||||
public const string Major1Minor10Revision0GuideState = "Major1Minor10Revision0GuideState1";
|
||||
public const string StaticResourceImageQuality = "StaticResourceImageQuality";
|
||||
public const string StaticResourceImageArchive = "StaticResourceImageArchive";
|
||||
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
|
||||
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled2";
|
||||
#endregion
|
||||
@@ -60,6 +62,10 @@ internal static class SettingKeys
|
||||
#endregion
|
||||
|
||||
#region Obsolete
|
||||
|
||||
[Obsolete("重置新手引导状态")]
|
||||
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
|
||||
|
||||
[Obsolete("重置调试控制台开关")]
|
||||
public const string IsAllocConsoleDebugModeEnabledLegacy1 = "IsAllocConsoleDebugModeEnabled";
|
||||
#endregion
|
||||
|
||||
@@ -21,22 +21,27 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
|
||||
{
|
||||
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
|
||||
string elevatedLauncherPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.Elevated.Launcher.exe");
|
||||
|
||||
try
|
||||
{
|
||||
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
|
||||
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
|
||||
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
|
||||
|
||||
Uri elevatedLauncherUri = "ms-appx:///Snap.Hutao.Elevated.Launcher.exe".ToUri();
|
||||
StorageFile launcherFile = await StorageFile.GetFileFromApplicationUriAsync(elevatedLauncherUri);
|
||||
await launcherFile.OverwriteCopyAsync(elevatedLauncherPath).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath);
|
||||
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath, elevatedLauncherPath);
|
||||
}
|
||||
|
||||
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath)
|
||||
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath, string elevatedLauncherPath)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
@@ -44,18 +49,12 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
HRESULT hr = CoCreateInstance(in ShellLink.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IShellLinkW.IID, out IShellLinkW* pShellLink);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
pShellLink->SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
|
||||
pShellLink->SetPath(elevatedLauncherPath);
|
||||
pShellLink->SetArguments(runtimeOptions.FamilyName);
|
||||
pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
|
||||
pShellLink->SetIconLocation(targetLogoPath, 0);
|
||||
|
||||
if (SUCCEEDED(pShellLink->QueryInterface(in IShellLinkDataList.IID, out IShellLinkDataList* pShellLinkDataList)))
|
||||
{
|
||||
pShellLinkDataList->GetFlags(out uint flags);
|
||||
pShellLinkDataList->SetFlags(flags | (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER);
|
||||
pShellLinkDataList->Release();
|
||||
}
|
||||
|
||||
if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile)))
|
||||
if (SUCCEEDED(IUnknownMarshal.QueryInterface(pShellLink, in IPersistFile.IID, out IPersistFile* pPersistFile)))
|
||||
{
|
||||
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
|
||||
@@ -65,10 +64,10 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
result = true;
|
||||
}
|
||||
|
||||
pPersistFile->Release();
|
||||
IUnknownMarshal.Release(pPersistFile);
|
||||
}
|
||||
|
||||
pShellLink->Release();
|
||||
uint value = IUnknownMarshal.Release(pShellLink);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
internal delegate bool SpinWaitPredicate<T>(ref readonly T state);
|
||||
@@ -15,4 +17,23 @@ internal static class SpinWaitPolyfill
|
||||
spinner.SpinOnce();
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH002")]
|
||||
public static unsafe bool SpinUntil<T>(ref T state, delegate*<ref readonly T, bool> condition, TimeSpan timeout)
|
||||
{
|
||||
long startTime = Stopwatch.GetTimestamp();
|
||||
|
||||
SpinWait spinner = default;
|
||||
while (!condition(ref state))
|
||||
{
|
||||
spinner.SpinOnce();
|
||||
|
||||
if (timeout < Stopwatch.GetElapsedTime(startTime))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ using Snap.Hutao.Win32;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.Graphics.Dwm;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using System.Collections.Frozen;
|
||||
using System.IO;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
@@ -29,6 +28,7 @@ internal sealed class WindowController
|
||||
private readonly WindowOptions options;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly WindowSubclass subclass;
|
||||
private readonly WindowNonRudeHWND windowNonRudeHWND;
|
||||
|
||||
public WindowController(Window window, in WindowOptions options, IServiceProvider serviceProvider)
|
||||
{
|
||||
@@ -39,6 +39,8 @@ internal sealed class WindowController
|
||||
// Window reference must be set before Window Subclass created
|
||||
serviceProvider.GetRequiredService<ICurrentWindowReference>().Window = window;
|
||||
subclass = new(window, options, serviceProvider);
|
||||
windowNonRudeHWND = new(options.Hwnd);
|
||||
|
||||
InitializeCore();
|
||||
}
|
||||
|
||||
@@ -137,27 +139,19 @@ internal sealed class WindowController
|
||||
{
|
||||
SaveOrSkipWindowSize();
|
||||
subclass?.Dispose();
|
||||
windowNonRudeHWND?.Dispose();
|
||||
}
|
||||
|
||||
private void ExtendsContentIntoTitleBar()
|
||||
{
|
||||
if (options.UseLegacyDragBarImplementation)
|
||||
{
|
||||
// use normal Window method to extend.
|
||||
window.ExtendsContentIntoTitleBar = true;
|
||||
window.SetTitleBar(options.TitleBar);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
|
||||
appTitleBar.IconShowOptions = IconShowOptions.HideIconAndSystemMenu;
|
||||
appTitleBar.ExtendsContentIntoTitleBar = true;
|
||||
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
|
||||
appTitleBar.IconShowOptions = IconShowOptions.HideIconAndSystemMenu;
|
||||
appTitleBar.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
UpdateTitleButtonColor();
|
||||
UpdateDragRectangles();
|
||||
options.TitleBar.ActualThemeChanged += (_, _) => UpdateTitleButtonColor();
|
||||
options.TitleBar.SizeChanged += (_, _) => UpdateDragRectangles();
|
||||
}
|
||||
UpdateTitleButtonColor();
|
||||
UpdateDragRectangles();
|
||||
options.TitleBar.ActualThemeChanged += (_, _) => UpdateTitleButtonColor();
|
||||
options.TitleBar.SizeChanged += (_, _) => UpdateDragRectangles();
|
||||
}
|
||||
|
||||
private bool UpdateSystemBackdrop(BackdropType backdropType)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
internal sealed class WindowNonRudeHWND : IDisposable
|
||||
{
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist2-markfullscreenwindow#remarks
|
||||
private const string NonRudeHWND = "NonRudeHWND";
|
||||
private readonly HWND hwnd;
|
||||
|
||||
public WindowNonRudeHWND(HWND hwnd)
|
||||
{
|
||||
this.hwnd = hwnd;
|
||||
SetPropW(hwnd, NonRudeHWND, BOOL.TRUE);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RemovePropW(hwnd, NonRudeHWND);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Windows.Graphics;
|
||||
@@ -41,11 +40,6 @@ internal readonly struct WindowOptions
|
||||
/// </summary>
|
||||
public readonly bool PersistSize;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用 Win UI 3 自带的拓展标题栏实现
|
||||
/// </summary>
|
||||
public readonly bool UseLegacyDragBarImplementation = !AppWindowTitleBar.IsCustomizationSupported();
|
||||
|
||||
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false)
|
||||
{
|
||||
Hwnd = WindowNative.GetWindowHandle(window);
|
||||
|
||||
@@ -9,7 +9,6 @@ using Snap.Hutao.Win32.UI.Shell;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using static Snap.Hutao.Win32.ComCtl32;
|
||||
using static Snap.Hutao.Win32.ConstValues;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
@@ -20,7 +19,6 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
internal sealed class WindowSubclass : IDisposable
|
||||
{
|
||||
private const int WindowSubclassId = 101;
|
||||
private const int DragBarSubclassId = 102;
|
||||
|
||||
private readonly Window window;
|
||||
private readonly WindowOptions options;
|
||||
@@ -29,7 +27,6 @@ internal sealed class WindowSubclass : IDisposable
|
||||
|
||||
// We have to explicitly hold a reference to SUBCLASSPROC
|
||||
private SUBCLASSPROC windowProc = default!;
|
||||
private SUBCLASSPROC legacyDragBarProc = default!;
|
||||
|
||||
public WindowSubclass(Window window, in WindowOptions options, IServiceProvider serviceProvider)
|
||||
{
|
||||
@@ -50,26 +47,7 @@ internal sealed class WindowSubclass : IDisposable
|
||||
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0);
|
||||
hotKeyController.RegisterAll();
|
||||
|
||||
bool titleBarHooked = true;
|
||||
|
||||
// only hook up drag bar proc when use legacy Window.ExtendsContentIntoTitleBar
|
||||
if (!options.UseLegacyDragBarImplementation)
|
||||
{
|
||||
return windowHooked && titleBarHooked;
|
||||
}
|
||||
|
||||
titleBarHooked = false;
|
||||
HWND hwndDragBar = FindWindowExW(options.Hwnd, default, "DRAG_BAR_WINDOW_CLASS", default);
|
||||
|
||||
if (hwndDragBar.IsNull)
|
||||
{
|
||||
return windowHooked && titleBarHooked;
|
||||
}
|
||||
|
||||
legacyDragBarProc = OnLegacyDragBarProcedure;
|
||||
titleBarHooked = SetWindowSubclass(hwndDragBar, legacyDragBarProc, DragBarSubclassId, 0);
|
||||
|
||||
return windowHooked && titleBarHooked;
|
||||
return windowHooked;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -79,12 +57,6 @@ internal sealed class WindowSubclass : IDisposable
|
||||
|
||||
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
|
||||
windowProc = default!;
|
||||
|
||||
if (options.UseLegacyDragBarImplementation)
|
||||
{
|
||||
RemoveWindowSubclass(options.Hwnd, legacyDragBarProc, DragBarSubclassId);
|
||||
legacyDragBarProc = default!;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH002")]
|
||||
@@ -127,19 +99,4 @@ internal sealed class WindowSubclass : IDisposable
|
||||
|
||||
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH002")]
|
||||
private LRESULT OnLegacyDragBarProcedure(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam, nuint uIdSubclass, nuint dwRefData)
|
||||
{
|
||||
switch (uMsg)
|
||||
{
|
||||
case WM_NCRBUTTONDOWN:
|
||||
case WM_NCRBUTTONUP:
|
||||
{
|
||||
return (LRESULT)(nint)WM_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
}
|
||||
@@ -118,14 +118,6 @@ internal static partial class EnumerableExtension
|
||||
collection.RemoveAt(collection.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到新类型的列表
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">原始类型</typeparam>
|
||||
/// <typeparam name="TResult">新类型</typeparam>
|
||||
/// <param name="list">列表</param>
|
||||
/// <param name="selector">选择器</param>
|
||||
/// <returns>新类型的列表</returns>
|
||||
[Pure]
|
||||
public static List<TResult> SelectList<TSource, TResult>(this List<TSource> list, Func<TSource, TResult> selector)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
xmlns:shvg="using:Snap.Hutao.View.Guide"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid x:Name="RootGrid">
|
||||
<Grid x:Name="RootGrid" Background="{ThemeResource SolidBackgroundFillColorBaseBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
|
||||
@@ -14,10 +14,10 @@ namespace Snap.Hutao;
|
||||
internal sealed partial class GuideWindow : Window, IWindowOptionsSource, IMinMaxInfoHandler
|
||||
{
|
||||
private const int MinWidth = 1000;
|
||||
private const int MinHeight = 600;
|
||||
private const int MinHeight = 650;
|
||||
|
||||
private const int MaxWidth = 1200;
|
||||
private const int MaxHeight = 750;
|
||||
private const int MaxHeight = 800;
|
||||
|
||||
private readonly WindowOptions windowOptions;
|
||||
|
||||
|
||||
@@ -27,4 +27,28 @@ internal sealed partial class IdentifyMonitorWindow : Window
|
||||
}
|
||||
|
||||
public string Monitor { get; private set; }
|
||||
|
||||
public static async ValueTask IdentifyAllMonitorsAsync(int secondsDelay)
|
||||
{
|
||||
List<IdentifyMonitorWindow> windows = [];
|
||||
|
||||
// TODO: the order here is not sync with unity.
|
||||
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
|
||||
for (int i = 0; i < displayAreas.Count; i++)
|
||||
{
|
||||
windows.Add(new IdentifyMonitorWindow(displayAreas[i], i + 1));
|
||||
}
|
||||
|
||||
foreach (IdentifyMonitorWindow window in windows)
|
||||
{
|
||||
window.Activate();
|
||||
}
|
||||
|
||||
await Delay.FromSeconds(secondsDelay).ConfigureAwait(true);
|
||||
|
||||
foreach (IdentifyMonitorWindow window in windows)
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,18 @@
|
||||
"Equatable": true,
|
||||
"EqualityOperators": true
|
||||
},
|
||||
{
|
||||
"Name": "ChapterId",
|
||||
"Documentation": "章节 Id",
|
||||
"Equatable": true,
|
||||
"EqualityOperators": true
|
||||
},
|
||||
{
|
||||
"Name": "ChapterGroupId",
|
||||
"Documentation": "章节分组 Id",
|
||||
"Equatable": true,
|
||||
"EqualityOperators": true
|
||||
},
|
||||
{
|
||||
"Name": "EquipAffixId",
|
||||
"Documentation": "装备属性 Id",
|
||||
@@ -113,6 +125,12 @@
|
||||
"Equatable": true,
|
||||
"EqualityOperators": true
|
||||
},
|
||||
{
|
||||
"Name": "QuestId",
|
||||
"Documentation": "任务 Id",
|
||||
"Equatable": true,
|
||||
"EqualityOperators": true
|
||||
},
|
||||
{
|
||||
"Name": "ReliquaryLevel",
|
||||
"Documentation": "圣遗物等级 1 - 21",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.ViewModel.Game;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
@@ -35,7 +36,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
|
||||
scope = serviceProvider.CreateScope();
|
||||
windowOptions = new(this, DragableGrid, new(MaxWidth, MaxHeight));
|
||||
this.InitializeController(serviceProvider);
|
||||
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
|
||||
RootGrid.InitializeDataContext<LaunchGameViewModel>(scope.ServiceProvider);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Entity.Abstraction;
|
||||
|
||||
internal interface IAppDbEntity
|
||||
{
|
||||
Guid InnerId { get; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Entity.Abstraction;
|
||||
|
||||
internal interface IAppDbEntityHasArchive : IAppDbEntity
|
||||
{
|
||||
Guid ArchiveId { get; }
|
||||
}
|
||||
@@ -16,8 +16,8 @@ namespace Snap.Hutao.Model.Entity;
|
||||
[HighQuality]
|
||||
[Table("achievements")]
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal sealed class Achievement
|
||||
: IEquatable<Achievement>,
|
||||
internal sealed class Achievement : IAppDbEntityHasArchive,
|
||||
IEquatable<Achievement>,
|
||||
IDbMappingForeignKeyFrom<Achievement, AchievementId>,
|
||||
IDbMappingForeignKeyFrom<Achievement, UIAFItem>
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Table("cultivate_entries")]
|
||||
internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry, CultivateType, uint>
|
||||
internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry, CultivateType, uint>, IAppDbEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Entity.Abstraction;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
@@ -13,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Table("cultivate_projects")]
|
||||
internal sealed class CultivateProject : ISelectable, IMappingFrom<CultivateProject, string, string>
|
||||
internal sealed class CultivateProject : ISelectable, IMappingFrom<CultivateProject, string, string>, IAppDbEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Model.Entity.Abstraction;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.ViewModel.DailyNote;
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
|
||||
@@ -16,7 +19,7 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Table("daily_notes")]
|
||||
internal sealed class DailyNoteEntry : ObservableObject, IMappingFrom<DailyNoteEntry, UserAndUid>
|
||||
internal sealed class DailyNoteEntry : ObservableObject, IMappingFrom<DailyNoteEntry, UserAndUid>, IAppDbEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
@@ -52,6 +55,9 @@ internal sealed class DailyNoteEntry : ObservableObject, IMappingFrom<DailyNoteE
|
||||
/// </summary>
|
||||
public DailyNote? DailyNote { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public DailyNoteArchonQuestView ArchonQuestView { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新时间
|
||||
/// </summary>
|
||||
@@ -68,61 +74,26 @@ internal sealed class DailyNoteEntry : ObservableObject, IMappingFrom<DailyNoteE
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 树脂提醒阈值
|
||||
/// </summary>
|
||||
public int ResinNotifyThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于判断树脂是否继续提醒
|
||||
/// </summary>
|
||||
public bool ResinNotifySuppressed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 洞天宝钱提醒阈值
|
||||
/// </summary>
|
||||
public int HomeCoinNotifyThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于判断洞天宝钱是否继续提醒
|
||||
/// </summary>
|
||||
public bool HomeCoinNotifySuppressed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 参量质变仪提醒
|
||||
/// </summary>
|
||||
public bool TransformerNotify { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于判断参量质变仪是否继续提醒
|
||||
/// </summary>
|
||||
public bool TransformerNotifySuppressed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每日委托提醒
|
||||
/// </summary>
|
||||
public bool DailyTaskNotify { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于判断每日委托是否继续提醒
|
||||
/// </summary>
|
||||
public bool DailyTaskNotifySuppressed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 探索派遣提醒
|
||||
/// </summary>
|
||||
public bool ExpeditionNotify { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于判断探索派遣是否继续提醒
|
||||
/// </summary>
|
||||
public bool ExpeditionNotifySuppressed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的实时便笺
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <returns>新的实时便笺</returns>
|
||||
public static DailyNoteEntry From(UserAndUid userAndUid)
|
||||
{
|
||||
return new()
|
||||
@@ -134,10 +105,6 @@ internal sealed class DailyNoteEntry : ObservableObject, IMappingFrom<DailyNoteE
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新实时便笺
|
||||
/// </summary>
|
||||
/// <param name="dailyNote">新的值</param>
|
||||
public void UpdateDailyNote(DailyNote? dailyNote)
|
||||
{
|
||||
DailyNote = dailyNote;
|
||||
@@ -146,4 +113,24 @@ internal sealed class DailyNoteEntry : ObservableObject, IMappingFrom<DailyNoteE
|
||||
RefreshTime = DateTimeOffset.UtcNow;
|
||||
OnPropertyChanged(nameof(RefreshTimeFormatted));
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(DailyNoteEntry other)
|
||||
{
|
||||
other.UpdateDailyNote(DailyNote);
|
||||
|
||||
other.ResinNotifySuppressed = ResinNotifySuppressed;
|
||||
other.OnPropertyChanged(nameof(ResinNotifySuppressed));
|
||||
|
||||
other.HomeCoinNotifySuppressed = HomeCoinNotifySuppressed;
|
||||
other.OnPropertyChanged(nameof(HomeCoinNotifySuppressed));
|
||||
|
||||
other.TransformerNotifySuppressed = TransformerNotifySuppressed;
|
||||
other.OnPropertyChanged(nameof(TransformerNotifySuppressed));
|
||||
|
||||
other.DailyTaskNotifySuppressed = DailyTaskNotifySuppressed;
|
||||
other.OnPropertyChanged(nameof(DailyTaskNotifySuppressed));
|
||||
|
||||
other.ExpeditionNotifySuppressed = ExpeditionNotifySuppressed;
|
||||
other.OnPropertyChanged(nameof(ExpeditionNotifySuppressed));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Model.Entity.Configuration;
|
||||
using System.Diagnostics;
|
||||
@@ -24,18 +25,8 @@ internal sealed class AppDbContext : DbContext
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的应用程序数据库上下文
|
||||
/// </summary>
|
||||
/// <param name="options">选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options, ILogger<AppDbContext> logger)
|
||||
: this(options)
|
||||
{
|
||||
this.logger = logger;
|
||||
logger.LogColorizedInformation("{Name}[{Id}] {Action}", nameof(AppDbContext), (ContextId, ConsoleColor.DarkCyan), ("created", ConsoleColor.Green));
|
||||
logger = this.GetService<ILogger<AppDbContext>>();
|
||||
logger?.LogColorizedInformation("{Name}[{Id}] {Action}", nameof(AppDbContext), (ContextId, ConsoleColor.DarkCyan), ("created", ConsoleColor.Green));
|
||||
}
|
||||
|
||||
public DbSet<SettingEntry> Settings { get; set; } = default!;
|
||||
@@ -74,14 +65,14 @@ internal sealed class AppDbContext : DbContext
|
||||
|
||||
public DbSet<SpiralAbyssEntry> SpiralAbysses { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个临时的应用程序数据库上下文
|
||||
/// </summary>
|
||||
/// <param name="sqlConnectionString">连接字符串</param>
|
||||
/// <returns>应用程序数据库上下文</returns>
|
||||
public static AppDbContext Create(string sqlConnectionString)
|
||||
public static AppDbContext Create(IServiceProvider serviceProvider, string sqlConnectionString)
|
||||
{
|
||||
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
|
||||
DbContextOptions<AppDbContext> options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseApplicationServiceProvider(serviceProvider)
|
||||
.UseSqlite(sqlConnectionString)
|
||||
.Options;
|
||||
|
||||
return new(options);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -18,7 +18,7 @@ internal sealed class AppDbContextDesignTimeFactory : IDesignTimeDbContextFactor
|
||||
#if DEBUG
|
||||
// TODO: replace with your own database file path.
|
||||
string userdataDbName = @"D:\Hutao\Userdata.db";
|
||||
return AppDbContext.Create($"Data Source={userdataDbName}");
|
||||
return AppDbContext.Create(default!, $"Data Source={userdataDbName}");
|
||||
#else
|
||||
throw Must.NeverHappen();
|
||||
#endif
|
||||
|
||||
@@ -19,6 +19,7 @@ internal sealed partial class SettingEntry
|
||||
public const string AnnouncementRegion = "AnnouncementRegion";
|
||||
|
||||
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
|
||||
public const string IsUnobtainedWishItemVisible = "IsUnobtainedWishItemVisible";
|
||||
|
||||
public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl";
|
||||
|
||||
|
||||
44
src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/QuestType.cs
Normal file
44
src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/QuestType.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
internal enum QuestType : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// Archon Quest 魔神任务
|
||||
/// </summary>
|
||||
AQ,
|
||||
|
||||
/// <summary>
|
||||
/// Fractions Quest 帮派任务
|
||||
/// </summary>
|
||||
FQ,
|
||||
|
||||
/// <summary>
|
||||
/// Legend Quest 传说任务
|
||||
/// </summary>
|
||||
LQ,
|
||||
|
||||
/// <summary>
|
||||
/// Event Quest 活动任务
|
||||
/// </summary>
|
||||
EQ,
|
||||
|
||||
/// <summary>
|
||||
/// Daily Quest 日常任务
|
||||
/// </summary>
|
||||
DQ,
|
||||
|
||||
/// <summary>
|
||||
/// Indescribable Quest 不可描述的任务?
|
||||
/// </summary>
|
||||
IQ,
|
||||
|
||||
VQ,
|
||||
|
||||
/// <summary>
|
||||
/// World Quest 世界任务
|
||||
/// </summary>
|
||||
WQ,
|
||||
}
|
||||
34
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Chapter.cs
Normal file
34
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Chapter.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata;
|
||||
|
||||
internal sealed class Chapter
|
||||
{
|
||||
public ChapterId Id { get; set; }
|
||||
|
||||
public ChapterGroupId GroupId { get; set; }
|
||||
|
||||
public QuestId BeginQuestId { get; set; }
|
||||
|
||||
public QuestId EndQuestId { get; set; }
|
||||
|
||||
public uint NeedPlayerLevel { get; set; }
|
||||
|
||||
public string Number { get; set; } = default!;
|
||||
|
||||
public string Title { get; set; } = default!;
|
||||
|
||||
public string Icon { get; set; } = default!;
|
||||
|
||||
public string ImageTitle { get; set; } = default!;
|
||||
|
||||
public string SerialNumberIcon { get; set; } = default!;
|
||||
|
||||
public City CityId { get; set; }
|
||||
|
||||
public QuestType QuestType { get; set; }
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace Snap.Hutao.Model.Metadata;
|
||||
// CityTaskOpenExcelConfig
|
||||
internal enum City : uint
|
||||
{
|
||||
None = 0,
|
||||
Mondstadt = 1,
|
||||
Liyue = 2,
|
||||
Inazuma = 3,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using System.Collections.Frozen;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
|
||||
|
||||
@@ -11,11 +11,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
[HighQuality]
|
||||
internal sealed class AvatarNameCardPicConverter : ValueConverter<Avatar.Avatar?, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 从角色转换到名片
|
||||
/// </summary>
|
||||
/// <param name="avatar">角色</param>
|
||||
/// <returns>名片</returns>
|
||||
public static Uri AvatarToUri(Avatar.Avatar? avatar)
|
||||
{
|
||||
if (avatar is null)
|
||||
|
||||
@@ -34,8 +34,6 @@ internal sealed class Material : DisplayItem
|
||||
/// <returns>是否为物品栏物品</returns>
|
||||
public bool IsInventoryItem()
|
||||
{
|
||||
// TODO: Add a pre-filtered metadata set to check if it's an inventory item
|
||||
|
||||
// 原质
|
||||
if (Id == 112001U)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutao"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.9.9.0" />
|
||||
Version="1.10.1.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao</DisplayName>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutaoDev"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.9.9.0" />
|
||||
Version="1.10.1.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao Dev</DisplayName>
|
||||
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>Open UIAF Json File</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>Multiple identical achievement IDs found in a single achievement archive</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
@@ -1334,6 +1334,12 @@
|
||||
<data name="ViewDialogLaunchGameAccountTitle" xml:space="preserve">
|
||||
<value>Name the account</value>
|
||||
</data>
|
||||
<data name="ViewDialogLaunchGameConfigurationFixDialogHint" xml:space="preserve">
|
||||
<value>Select game server of the current gane client path</value>
|
||||
</data>
|
||||
<data name="ViewDialogLaunchGameConfigurationFixDialogTitle" xml:space="preserve">
|
||||
<value>Fixing configuration file</value>
|
||||
</data>
|
||||
<data name="ViewDialogLaunchGamePackageConvertHint" xml:space="preserve">
|
||||
<value>Conversion may take some time. Please don't close HuTao.</value>
|
||||
</data>
|
||||
@@ -1355,6 +1361,21 @@
|
||||
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
|
||||
<value>Delete user data permanently?</value>
|
||||
</data>
|
||||
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
|
||||
<value>Log in Now</value>
|
||||
</data>
|
||||
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
|
||||
<value>No Hutao Passport logged in currently, uploading Abyss Records will not grant you Hutao Cloud privilege extension.</value>
|
||||
</data>
|
||||
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
|
||||
<value>Continue to Upload</value>
|
||||
</data>
|
||||
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">
|
||||
<value>Upload Abyss Records</value>
|
||||
</data>
|
||||
<data name="ViewDialogUpdatePackageDownloadUpdatelogLinkContent" xml:space="preserve">
|
||||
<value>Check Update Logs</value>
|
||||
</data>
|
||||
<data name="ViewDialogUserDocumentAction" xml:space="preserve">
|
||||
<value>Take me there</value>
|
||||
</data>
|
||||
@@ -1376,6 +1397,9 @@
|
||||
<data name="ViewGachaLogHeader" xml:space="preserve">
|
||||
<value>Wish History</value>
|
||||
</data>
|
||||
<data name="ViewGuideStaticResourceDownloadSize" xml:space="preserve">
|
||||
<value>Expected asset size: {0}</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepAgreementIHaveReadText" xml:space="preserve">
|
||||
<value>I've read and agreed to</value>
|
||||
</data>
|
||||
@@ -1391,6 +1415,12 @@
|
||||
<data name="ViewGuideStepAgreementTermOfService" xml:space="preserve">
|
||||
<value>User Agreement and Legal Notices</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepCommonSetting" xml:space="preserve">
|
||||
<value>Basic Settings</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepCommonSettingHint" xml:space="preserve">
|
||||
<value>You may make changes in Settings</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepDocument" xml:space="preserve">
|
||||
<value>Document</value>
|
||||
</data>
|
||||
@@ -1418,6 +1448,24 @@
|
||||
<data name="ViewGuideStepStaticResource" xml:space="preserve">
|
||||
<value>Assets</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSetting" xml:space="preserve">
|
||||
<value>Image Assets Settings</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingHint" xml:space="preserve">
|
||||
<value>* Unless uninstall and reinstall Snap Hutao, you cannot change this setting</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingMinimumHeader" xml:space="preserve">
|
||||
<value>Image Assets Archive</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingMinimumOff" xml:space="preserve">
|
||||
<value>Full Archive</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingMinimumOn" xml:space="preserve">
|
||||
<value>Minimum Archive</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingQualityHeader" xml:space="preserve">
|
||||
<value>Image Assets Quality</value>
|
||||
</data>
|
||||
<data name="ViewHutaoDatabaseHeader" xml:space="preserve">
|
||||
<value>Abyss Stats</value>
|
||||
</data>
|
||||
@@ -1607,6 +1655,12 @@
|
||||
<data name="ViewModelGuideActionStaticResourceBegin" xml:space="preserve">
|
||||
<value>Dowloading Assets</value>
|
||||
</data>
|
||||
<data name="ViewModelGuideStaticResourceQualityHigh" xml:space="preserve">
|
||||
<value>Raw</value>
|
||||
</data>
|
||||
<data name="ViewModelGuideStaticResourceQualityRaw" xml:space="preserve">
|
||||
<value>High Quality</value>
|
||||
</data>
|
||||
<data name="ViewModelHutaoPassportEmailNotValidHint" xml:space="preserve">
|
||||
<value>Please enter correct email address</value>
|
||||
</data>
|
||||
@@ -1625,6 +1679,12 @@
|
||||
<data name="ViewModelLaunchGameEnsureGameResourceFail" xml:space="preserve">
|
||||
<value>Convert server failed</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameFixConfigurationFileButtonText" xml:space="preserve">
|
||||
<value>Fix Configuration File</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameFixConfigurationFileSuccess" xml:space="preserve">
|
||||
<value>Fix Complete</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameIdentifyMonitorsAction" xml:space="preserve">
|
||||
<value>Identify Monitors</value>
|
||||
</data>
|
||||
@@ -1637,6 +1697,9 @@
|
||||
<data name="ViewModelLaunchGameSchemeNotSelected" xml:space="preserve">
|
||||
<value>No server selected</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameSetGamePathButtonText" xml:space="preserve">
|
||||
<value>Set Game Path</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>Switch game account failed</value>
|
||||
</data>
|
||||
@@ -1673,6 +1736,9 @@
|
||||
<data name="ViewModelSettingGeetestCustomUrlSucceed" xml:space="preserve">
|
||||
<value>CAPTCHA Verification composite URL successfully configured</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingResetStaticResourceProgress" xml:space="preserve">
|
||||
<value>Resetting Static Resource</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
|
||||
<value>Set data directory successfully. Restart to apply changes.</value>
|
||||
</data>
|
||||
@@ -2654,6 +2720,12 @@
|
||||
<data name="ViewPageSettingTranslateNavigate" xml:space="preserve">
|
||||
<value>Contribute Translations</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
|
||||
<value>Display undrawn wish items in Character and Weapon tabs in Wish Export</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
|
||||
<value>Undrawn Wish Items</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUpdateCheckAction" xml:space="preserve">
|
||||
<value>Store Page</value>
|
||||
</data>
|
||||
@@ -2834,6 +2906,15 @@
|
||||
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
|
||||
<value>Upload Data</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageDownloadContent" xml:space="preserve">
|
||||
<value>Download Now?</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageDownloadFailedMessage" xml:space="preserve">
|
||||
<value>Failed to Download Update Patch</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageDownloadTitle" xml:space="preserve">
|
||||
<value>Snap Hutao {0} is Released</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
|
||||
<value>Install now?</value>
|
||||
</data>
|
||||
@@ -2843,6 +2924,9 @@
|
||||
<data name="ViewTitleAutoClicking" xml:space="preserve">
|
||||
<value>Auto Click</value>
|
||||
</data>
|
||||
<data name="ViewTitleUpdatePackageInstallingContent" xml:space="preserve">
|
||||
<value>Installing New Version Patch</value>
|
||||
</data>
|
||||
<data name="ViewToolHeader" xml:space="preserve">
|
||||
<value>Tools</value>
|
||||
</data>
|
||||
@@ -2922,13 +3006,19 @@
|
||||
<value>Weapon WIKI</value>
|
||||
</data>
|
||||
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
|
||||
<value>(?:〓Event Duration〓|〓Quest Start Time〓).*?\d\.\dthe Version update(?:after|)Permanently available</value>
|
||||
<value>(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)后永久开放</value>
|
||||
</data>
|
||||
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
|
||||
<value>〓Event Duration〓.*?\d\.\d Available throughout the entirety of Version</value>
|
||||
<value>〓活动时间〓.*?(\d\.\d)版本期间持续开放</value>
|
||||
</data>
|
||||
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
|
||||
<value>(?:〓Event Duration〓|Event Wish Duration|【Availability Duration】|〓Discount Period〓).*?(\d\.\dAfter the Version update).*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt;</value>
|
||||
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt;</value>
|
||||
</data>
|
||||
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
|
||||
<value>将于&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;进行版本更新维护</value>
|
||||
</data>
|
||||
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
|
||||
<value>\d\.\d版本更新维护预告</value>
|
||||
</data>
|
||||
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
|
||||
<value>〓Update Maintenance Duration〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;</value>
|
||||
@@ -2988,7 +3078,7 @@
|
||||
<value>{0} mins</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteExtraTaskRewardNotAllowed" xml:space="preserve">
|
||||
<value>Incomplete Daily Commissions</value>
|
||||
<value>Daily Commissions are not Completed</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteExtraTaskRewardNotTaken" xml:space="preserve">
|
||||
<value>Daily Commission Reward not claimed</value>
|
||||
|
||||
3230
src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.fr.resx
Normal file
3230
src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.fr.resx
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user