mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb01f3a3cb | ||
|
|
f7f2d9c867 | ||
|
|
01b7e58b3e | ||
|
|
2518ae0b90 | ||
|
|
7d4a8cdcd9 | ||
|
|
623893e00e | ||
|
|
0d34c81bcf | ||
|
|
5f3d0126b3 | ||
|
|
5d1fe3f38a | ||
|
|
c810ffa625 | ||
|
|
ee70205245 | ||
|
|
06c8b347d3 | ||
|
|
5c6ab1dee9 | ||
|
|
ad440e0561 | ||
|
|
ca56d8c636 | ||
|
|
da0ee0cca6 | ||
|
|
5d00d9cc0d | ||
|
|
e8b27e6655 | ||
|
|
0ac79012d1 | ||
|
|
bb2665b75e | ||
|
|
d22ac39c1d | ||
|
|
a312603d61 | ||
|
|
0732ea0e06 | ||
|
|
e4d2b3055c | ||
|
|
5668931230 | ||
|
|
5126337138 | ||
|
|
4d634d3264 | ||
|
|
15a69fd0de | ||
|
|
c232891fe7 | ||
|
|
c35c2a5700 | ||
|
|
42305094f8 | ||
|
|
9ef48ab05c | ||
|
|
eec010870a | ||
|
|
a24fbf535d | ||
|
|
f7bd184a3c | ||
|
|
267f285101 | ||
|
|
2a1e77a9db | ||
|
|
abdc8e2e9f | ||
|
|
64f1af293b | ||
|
|
e0336d6b30 | ||
|
|
23f3e5df77 | ||
|
|
4a027a8d3f | ||
|
|
80459708a7 | ||
|
|
650b67bea0 | ||
|
|
18b3d23b1c | ||
|
|
bf08ffa89e | ||
|
|
af4180bdeb | ||
|
|
a70593c529 |
6
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -15,7 +15,7 @@ body:
|
||||
description: |-
|
||||
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
|
||||
options:
|
||||
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/most-frequent-questions.html),并认为我的问题没有在文档中得到解答
|
||||
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/),并认为我的问题没有在文档中得到解答
|
||||
required: true
|
||||
|
||||
- label: 我使用的操作系统是[受支持的版本](https://hut.ao/quick-start.html#%E6%9C%80%E4%BD%8E%E7%B3%BB%E7%BB%9F%E8%A6%81%E6%B1%82)
|
||||
@@ -33,7 +33,7 @@ body:
|
||||
label: Windows 版本
|
||||
description: |
|
||||
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
|
||||
placeholder: 例:22000.556
|
||||
placeholder: 例:22621.1105
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -42,7 +42,7 @@ body:
|
||||
attributes:
|
||||
label: Snap Hutao 版本
|
||||
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
|
||||
placeholder: 例:1.1.0
|
||||
placeholder: 例:1.3.13.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/network-issue.yml
vendored
8
.github/ISSUE_TEMPLATE/network-issue.yml
vendored
@@ -22,7 +22,7 @@ body:
|
||||
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
- 你可以点击下面的链接以下载网络诊断工具:
|
||||
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-tool.exe)
|
||||
- [GitHub](https://github.com/DGP-Studio/Snap.Hutao/files/10081999/network-diagnosis-hutao.zip)
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -41,7 +41,7 @@ body:
|
||||
id: user-isp
|
||||
attributes:
|
||||
label: 你的运营商
|
||||
description: 中国用户请精确到省级行政区,海外地区请精确到国家
|
||||
description: 海外用户请选其它
|
||||
options:
|
||||
- 中国电信
|
||||
- 中国联通
|
||||
@@ -60,7 +60,9 @@ body:
|
||||
- 完全无法连接服务器
|
||||
- 连接速度慢
|
||||
- 获取到了不正确的页面或数据
|
||||
- 图片下载错误(429 Error)
|
||||
- 客户端提示 429 Error
|
||||
- 客户端图片下载错误
|
||||
- 客户端图片预下载错误
|
||||
- 其它
|
||||
validations:
|
||||
required: true
|
||||
|
||||
18
.github/workflows/PublishDistribution.yml
vendored
18
.github/workflows/PublishDistribution.yml
vendored
@@ -2,7 +2,7 @@ name: PublishDistribution
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
types: [released]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Download Publish.zip
|
||||
# Download Assets
|
||||
- name: Download Release
|
||||
timeout-minutes: 5
|
||||
uses: robinraju/release-downloader@v1.5
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
repository: "DGP-Studio/Snap.Hutao"
|
||||
latest: true
|
||||
@@ -38,4 +38,14 @@ jobs:
|
||||
$RCCONF
|
||||
EOF
|
||||
|
||||
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/
|
||||
rclone copy ./release-download/* dgpODCN:/releases/
|
||||
|
||||
# Purge Patch System Cache
|
||||
- name: Purge Patch
|
||||
env:
|
||||
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
|
||||
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
|
||||
PURGE_URL: ${{ secrets.PURGE_URL }}
|
||||
run: |
|
||||
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
|
||||
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# [Snap.Hutao](https://hut.ao)
|
||||
|
||||

|
||||
|
||||
> 唷,找本堂主有何贵干呀?
|
||||
|
||||

|
||||
|
||||
# 特别感谢
|
||||
|
||||
### 原神组织与个人
|
||||
|
||||
* [HolographicHat](https://github.com/HolographicHat)
|
||||
* [UIGF organization](https://uigf.org)
|
||||
|
||||
|
||||
@@ -17,6 +17,16 @@ trigger:
|
||||
- azure-pipelines.yml
|
||||
- .github/ISSUE_TEMPLATE/*.yml
|
||||
- .github/workflows/*.yml
|
||||
pr:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
paths:
|
||||
exclude:
|
||||
- README.md
|
||||
- azure-pipelines.yml
|
||||
- .github/ISSUE_TEMPLATE/*.yml
|
||||
- .github/workflows/*.yml
|
||||
|
||||
|
||||
pool:
|
||||
@@ -81,7 +91,8 @@ steps:
|
||||
"Package/Identity/@Publisher": "CN=DGP Studio CI",
|
||||
"Package/Identity/@Version": "$(build_date).$(rev_number)",
|
||||
"Package/Properties/DisplayName": "胡桃 Alpha",
|
||||
"Package/Properties/PublisherDisplayName":"DGP Studio CI"
|
||||
"Package/Properties/PublisherDisplayName":"DGP Studio CI",
|
||||
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
|
||||
}
|
||||
|
||||
- task: CmdLine@2
|
||||
@@ -134,6 +145,7 @@ steps:
|
||||
secureFile: 'Snap.Hutao.CI.cer'
|
||||
|
||||
- task: GitHubRelease@1
|
||||
condition: or(eq(variables['Build.Reason'], 'Manual'), eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'))
|
||||
inputs:
|
||||
gitHubConnection: 'github.com_Masterain'
|
||||
repositoryName: 'DGP-Studio/Snap.Hutao'
|
||||
@@ -144,13 +156,26 @@ steps:
|
||||
title: '$(build_date).$(rev_number)'
|
||||
releaseNotesSource: 'inline'
|
||||
releaseNotesInline: |
|
||||
## 提示 (Hint)
|
||||
该发布版本由 CI 程序自动打包生成,属于 `Alpha` 测试版,仅用于开发调试和内部测试用途。使用该版本可能存在意料之外的风险,请仅在有明确用途的情况下使用该版本。
|
||||
|
||||
This release is a Alpha Testing version generated by CI program automatically in a purpose of debugging and interal testing. Using this release may have unexpected risk, please only use it when you know what you are doing.
|
||||
## 普通用户请勿下载
|
||||
该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
||||
|
||||
普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
||||
|
||||
assets: |
|
||||
$(Build.ArtifactStagingDirectory)/*
|
||||
$(cerFile.secureFilePath)
|
||||
isPreRelease: true
|
||||
changeLogCompareToRelease: 'lastFullRelease'
|
||||
changeLogType: 'commitBased'
|
||||
|
||||
- task: DownloadSecureFile@1
|
||||
name: RcloneConfigFile
|
||||
displayName: Download Rclone Config
|
||||
inputs:
|
||||
secureFile: 'rclone.conf'
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload CI via Rclone
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/Alpha/'
|
||||
configPath: '$(RcloneConfigFile.secureFilePath)'
|
||||
@@ -14,9 +14,9 @@ csharp_style_expression_bodied_accessors = when_on_single_line:silent
|
||||
csharp_style_expression_bodied_lambdas = when_on_single_line:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
csharp_style_var_for_built_in_types = false:silent
|
||||
csharp_style_var_when_type_is_apparent = false:silent
|
||||
csharp_style_var_elsewhere = false:silent
|
||||
csharp_style_var_for_built_in_types = false:warning
|
||||
csharp_style_var_when_type_is_apparent = false:warning
|
||||
csharp_style_var_elsewhere = false:warning
|
||||
csharp_prefer_simple_using_statement = false:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
@@ -24,11 +24,11 @@ csharp_prefer_static_local_function = false:suggestion
|
||||
|
||||
[*.{cs,vb}]
|
||||
end_of_line = crlf
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
dotnet_code_quality_unused_parameters = non_public:suggestion
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
indent_size = 4
|
||||
@@ -165,6 +165,7 @@ dotnet_diagnostic.CA1805.severity = suggestion
|
||||
dotnet_diagnostic.VSTHRD111.severity = suggestion
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_prefer_readonly_struct = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Installer;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx";
|
||||
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock";
|
||||
private const string ValueName = "AllowDevelopmentWithoutDevLicense";
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:启用分析器发布跟踪", Justification = "<挂起>", Scope = "member", Target = "~F:Snap.Hutao.SourceGeneration.TodoAnalyzer.Descriptor")]
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// 高亮TODO
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal class TodoAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor Descriptor =
|
||||
new("SH0001", "TODO 项尚未实现", "此 TODO 项需要实现", "Standard", DiagnosticSeverity.Info, true);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get => ImmutableArray.Create(Descriptor); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterSyntaxTreeAction(HandleSyntaxTree);
|
||||
}
|
||||
|
||||
private static void HandleSyntaxTree(SyntaxTreeAnalysisContext context)
|
||||
{
|
||||
SyntaxNode root = context.Tree.GetCompilationUnitRoot(context.CancellationToken);
|
||||
foreach (SyntaxTrivia node in root.DescendantTrivia(descendIntoTrivia: true))
|
||||
{
|
||||
switch (node.Kind())
|
||||
{
|
||||
case SyntaxKind.SingleLineCommentTrivia:
|
||||
case SyntaxKind.MultiLineCommentTrivia:
|
||||
string text = node.ToString().ToLowerInvariant();
|
||||
if (text.Contains("todo:"))
|
||||
{
|
||||
string hint = node.ToString().Substring(text.IndexOf("todo:") + 6);
|
||||
DiagnosticDescriptor descriptor = new("SH0001", "TODO 项尚未实现", hint, "Standard", DiagnosticSeverity.Info, true);
|
||||
context.ReportDiagnostic(Diagnostic.Create(descriptor, node.GetLocation()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,24 +11,24 @@
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<muxc:XamlControlsResources/>
|
||||
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/>
|
||||
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<Color x:Key="AvatarPropertyAddValueColor">#FF74BF00</Color>
|
||||
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<Color x:Key="AvatarPropertyAddValueColor">#FF90E800</Color>
|
||||
<Color x:Key="CompatBackgroundColor">#FF242424</Color>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- Modify Window title bar color -->
|
||||
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/>
|
||||
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
|
||||
<!-- Page Transparent Background -->
|
||||
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
|
||||
<!-- IconFont -->
|
||||
<FontFamily x:Key="SymbolThemeFontFamily">ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
|
||||
<!-- InfoBar Resource -->
|
||||
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
|
||||
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
|
||||
@@ -42,6 +42,25 @@
|
||||
<CornerRadius x:Key="CompatCornerRadiusRight">0,6,6,0</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusBottom">0,0,6,6</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
|
||||
<!-- OpenPaneLength -->
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength2">268</x:Double>
|
||||
<GridLength x:Key="CompatGridLength2">268</GridLength>
|
||||
<!-- Brushes -->
|
||||
<SolidColorBrush x:Key="AvatarPropertyAddValueBrush" Color="{ThemeResource AvatarPropertyAddValueColor}"/>
|
||||
<!-- Uris -->
|
||||
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie</x:String>
|
||||
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
|
||||
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
|
||||
|
||||
<x:String x:Key="UI_ItemIcon_None">https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png</x:String>
|
||||
<x:String x:Key="UI_ImgSign_ItemIcon">https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png</x:String>
|
||||
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon25">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon25.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon71">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon250">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon272">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon293">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||
<!-- Converters -->
|
||||
<cwuc:BoolNegationConverter x:Key="BoolNegationConverter"/>
|
||||
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
@@ -62,15 +81,14 @@
|
||||
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
|
||||
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
|
||||
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
|
||||
<shvc:EmptyCollectionToBoolConverter x:Key="EmptyCollectionToBoolConverter"/>
|
||||
<shvc:EmptyCollectionToBoolRevertConverter x:Key="EmptyCollectionToBoolRevertConverter"/>
|
||||
<shvc:EmptyCollectionToVisibilityConverter x:Key="EmptyCollectionToVisibilityConverter"/>
|
||||
<shvc:EmptyCollectionToVisibilityRevertConverter x:Key="EmptyCollectionToVisibilityRevertConverter"/>
|
||||
<shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/>
|
||||
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
|
||||
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
|
||||
<shvc:EmptyObjectToVisibilityRevertConverter x:Key="EmptyObjectToVisibilityRevertConverter"/>
|
||||
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
|
||||
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
|
||||
<!-- Styles -->
|
||||
|
||||
<Style
|
||||
x:Key="LargeGridViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
|
||||
@@ -5,7 +5,7 @@ using CommunityToolkit.WinUI.Notifications;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Exception;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using System.Diagnostics;
|
||||
@@ -25,7 +25,6 @@ public partial class App : Application
|
||||
/// Initializes the singleton application object.
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="appCenter">App Center</param>
|
||||
public App(ILogger<App> logger)
|
||||
{
|
||||
// load app resource
|
||||
@@ -51,7 +50,7 @@ public partial class App : Application
|
||||
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
|
||||
|
||||
logger.LogInformation(EventIds.CommonLog, "Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
|
||||
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
|
||||
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path);
|
||||
|
||||
JumpListHelper.ConfigureAsync().SafeForget(logger);
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Context.FileSystem.Location;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Context.FileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 文件系统上下文
|
||||
/// </summary>
|
||||
/// <typeparam name="TLocation">路径位置类型</typeparam>
|
||||
internal abstract class FileSystemContext
|
||||
{
|
||||
private readonly IFileSystemLocation location;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化文件系统上下文
|
||||
/// </summary>
|
||||
/// <param name="location">指定的文件系统位置</param>
|
||||
public FileSystemContext(IFileSystemLocation location)
|
||||
{
|
||||
this.location = location;
|
||||
EnsureDirectory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建文件,若已存在文件,则不会创建
|
||||
/// </summary>
|
||||
/// <param name="file">文件</param>
|
||||
public void CreateFileOrIgnore(string file)
|
||||
{
|
||||
file = Locate(file);
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
File.Create(file).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建文件夹,若已存在文件,则不会创建
|
||||
/// </summary>
|
||||
/// <param name="folder">文件夹</param>
|
||||
public void CreateFolderOrIgnore(string folder)
|
||||
{
|
||||
folder = Locate(folder);
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试删除文件夹
|
||||
/// </summary>
|
||||
/// <param name="folder">文件夹</param>
|
||||
public void DeleteFolderOrIgnore(string folder)
|
||||
{
|
||||
folder = Locate(folder);
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
Directory.Delete(folder, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文件是否存在
|
||||
/// </summary>
|
||||
/// <param name="file">文件名称</param>
|
||||
/// <returns>是否存在</returns>
|
||||
public bool FileExists(string file)
|
||||
{
|
||||
return File.Exists(Locate(file));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文件是否存在
|
||||
/// </summary>
|
||||
/// <param name="folder">文件夹名称</param>
|
||||
/// <param name="file">文件名称</param>
|
||||
/// <returns>是否存在</returns>
|
||||
public bool FileExists(string folder, string file)
|
||||
{
|
||||
return File.Exists(Locate(folder, file));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文件是否存在
|
||||
/// </summary>
|
||||
/// <param name="folder">文件夹名称</param>
|
||||
/// <returns>是否存在</returns>
|
||||
public bool FolderExists(string folder)
|
||||
{
|
||||
return Directory.Exists(Locate(folder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定位根目录中的文件或文件夹
|
||||
/// </summary>
|
||||
/// <param name="fileOrFolder">文件或文件夹</param>
|
||||
/// <returns>绝对路径</returns>
|
||||
public string Locate(string fileOrFolder)
|
||||
{
|
||||
return Path.GetFullPath(fileOrFolder, location.GetPath());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定位根目录下子文件夹中的文件
|
||||
/// </summary>
|
||||
/// <param name="folder">文件夹</param>
|
||||
/// <param name="file">文件</param>
|
||||
/// <returns>绝对路径</returns>
|
||||
public string Locate(string folder, string file)
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(folder, file), location.GetPath());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将文件移动到指定的子目录
|
||||
/// </summary>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="folder">文件夹</param>
|
||||
/// <param name="overwrite">是否覆盖</param>
|
||||
/// <returns>是否成功 当文件不存在时会失败</returns>
|
||||
public bool MoveToFolderOrIgnore(string file, string folder, bool overwrite = true)
|
||||
{
|
||||
string target = Locate(folder, file);
|
||||
file = Locate(file);
|
||||
|
||||
if (File.Exists(file))
|
||||
{
|
||||
File.Move(file, target, overwrite);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等效于 <see cref="File.OpenRead(string)"/> ,但路径经过解析
|
||||
/// </summary>
|
||||
/// <param name="file">文件名</param>
|
||||
/// <returns>文件流</returns>
|
||||
public FileStream OpenRead(string file)
|
||||
{
|
||||
return File.OpenRead(Locate(file));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等效于 <see cref="File.Create(string)"/> ,但路径经过解析
|
||||
/// </summary>
|
||||
/// <param name="file">文件名</param>
|
||||
/// <returns>文件流</returns>
|
||||
public FileStream Create(string file)
|
||||
{
|
||||
return File.Create(Locate(file));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查根目录
|
||||
/// </summary>
|
||||
/// <returns>是否创建了路径</returns>
|
||||
private bool EnsureDirectory()
|
||||
{
|
||||
string folder = location.GetPath();
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Context.FileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 我的文档上下文
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class HutaoContext : FileSystemContext
|
||||
{
|
||||
/// <inheritdoc cref="FileSystemContext"/>
|
||||
public HutaoContext(Location.HutaoLocation myDocument)
|
||||
: base(myDocument)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Snap.Hutao.Context.FileSystem.Location;
|
||||
|
||||
/// <summary>
|
||||
/// 我的文档位置
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class HutaoLocation : IFileSystemLocation
|
||||
{
|
||||
private string? path;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetPath()
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
path = Path.GetFullPath(Path.Combine(myDocument, folderName));
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Context.FileSystem.Location;
|
||||
|
||||
/// <summary>
|
||||
/// 文件系统位置
|
||||
/// </summary>
|
||||
public interface IFileSystemLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取路径
|
||||
/// </summary>
|
||||
/// <returns>路径</returns>
|
||||
string GetPath();
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Context.FileSystem.Location;
|
||||
|
||||
/// <summary>
|
||||
/// 我的文档位置
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class Metadata : IFileSystemLocation
|
||||
{
|
||||
private string? path;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetPath()
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
path = Path.GetFullPath(Path.Combine(myDocument, "Hutao", "Metadata"));
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Context.FileSystem.Location;
|
||||
|
||||
namespace Snap.Hutao.Context.FileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 元数据上下文
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class MetadataContext : FileSystemContext
|
||||
{
|
||||
/// <inheritdoc cref="FileSystemContext"/>
|
||||
public MetadataContext(Metadata metadata)
|
||||
: base(metadata)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.Xaml.Interactivity;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 打开附着的浮出控件操作
|
||||
/// </summary>
|
||||
internal class OpenAttachedFlyoutAction : DependencyObject, IAction
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object Execute(object sender, object parameter)
|
||||
{
|
||||
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
@@ -11,19 +10,6 @@ namespace Snap.Hutao.Control.Extension;
|
||||
/// </summary>
|
||||
internal static class ContentDialogExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 针对窗口进行初始化
|
||||
/// </summary>
|
||||
/// <param name="contentDialog">对话框</param>
|
||||
/// <param name="window">窗口</param>
|
||||
/// <returns>初始化完成的对话框</returns>
|
||||
public static ContentDialog InitializeWithWindow(this ContentDialog contentDialog, Window window)
|
||||
{
|
||||
contentDialog.XamlRoot = window.Content.XamlRoot;
|
||||
|
||||
return contentDialog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阻止用户交互
|
||||
/// </summary>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此类支持取消任务
|
||||
/// </summary>
|
||||
public interface ISupportCancellation
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于通知事件取消的取消令牌
|
||||
/// </summary>
|
||||
CancellationToken CancellationToken { get; set; }
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Extension;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
@@ -23,6 +22,7 @@ public class CachedImage : ImageEx
|
||||
{
|
||||
IsCacheEnabled = true;
|
||||
EnableLazyLoading = true;
|
||||
LazyLoadingThreshold = 500;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -33,18 +33,18 @@ public class CachedImage : ImageEx
|
||||
try
|
||||
{
|
||||
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
|
||||
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
|
||||
|
||||
// check token state to determine whether the operation should be canceled.
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
return new BitmapImage(new(file.Path));
|
||||
return new BitmapImage(new(file));
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
|
||||
imageCache.Remove(imageUri.Enumerate());
|
||||
return null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
||||
@@ -152,19 +152,17 @@ internal static class CompositionExtensions
|
||||
/// 创建一个线性渐变画刷
|
||||
/// </summary>
|
||||
/// <param name="compositor">合成器</param>
|
||||
/// <param name="start">起点</param>
|
||||
/// <param name="end">终点</param>
|
||||
/// <param name="direction">方向</param>
|
||||
/// <param name="stops">锚点</param>
|
||||
/// <returns>线性渐变画刷</returns>
|
||||
public static CompositionLinearGradientBrush CompositeLinearGradientBrush(
|
||||
this Compositor compositor,
|
||||
Vector2 start,
|
||||
Vector2 end,
|
||||
GradientDirection direction,
|
||||
params GradientStop[] stops)
|
||||
{
|
||||
CompositionLinearGradientBrush brush = compositor.CreateLinearGradientBrush();
|
||||
brush.StartPoint = start;
|
||||
brush.EndPoint = end;
|
||||
brush.StartPoint = GetStartPointOfDirection(direction);
|
||||
brush.EndPoint = GetEndPointOfDirection(direction);
|
||||
|
||||
foreach (GradientStop stop in stops)
|
||||
{
|
||||
@@ -193,5 +191,31 @@ internal static class CompositionExtensions
|
||||
return brush;
|
||||
}
|
||||
|
||||
private static Vector2 GetStartPointOfDirection(GradientDirection direction)
|
||||
{
|
||||
return direction switch
|
||||
{
|
||||
GradientDirection.BottomToTop => Vector2.UnitY,
|
||||
GradientDirection.LeftBottomToRightTop => Vector2.UnitY,
|
||||
GradientDirection.RightBottomToLeftTop => Vector2.One,
|
||||
GradientDirection.RightToLeft => Vector2.UnitX,
|
||||
GradientDirection.RightTopToLeftBottom => Vector2.UnitX,
|
||||
_ => Vector2.Zero,
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector2 GetEndPointOfDirection(GradientDirection direction)
|
||||
{
|
||||
return direction switch
|
||||
{
|
||||
GradientDirection.LeftBottomToRightTop => Vector2.UnitX,
|
||||
GradientDirection.LeftToRight => Vector2.UnitX,
|
||||
GradientDirection.LeftTopToRightBottom => Vector2.One,
|
||||
GradientDirection.RightTopToLeftBottom => Vector2.UnitY,
|
||||
GradientDirection.TopToBottom => Vector2.UnitY,
|
||||
_ => Vector2.Zero,
|
||||
};
|
||||
}
|
||||
|
||||
public record struct GradientStop(float Offset, Windows.UI.Color Color);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
@@ -60,15 +59,16 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
/// <summary>
|
||||
/// 异步加载图像表面
|
||||
/// </summary>
|
||||
/// <param name="storageFile">文件</param>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>加载的图像表面</returns>
|
||||
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
|
||||
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
{
|
||||
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token).ConfigureAwait(true))
|
||||
{
|
||||
return LoadedImageSurface.StartLoadFromStream(imageStream);
|
||||
}
|
||||
TaskCompletionSource loadCompleteTaskSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
|
||||
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
|
||||
await loadCompleteTaskSource.Task.ConfigureAwait(true);
|
||||
return surface;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -130,7 +130,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
else
|
||||
{
|
||||
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
|
||||
string storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -138,7 +138,11 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
await imageCache.RemoveAsync(uri.Enumerate()).ConfigureAwait(true);
|
||||
imageCache.Remove(uri.Enumerate());
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
imageCache.Remove(uri.Enumerate());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System.Numerics;
|
||||
using System.IO;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
@@ -16,8 +16,29 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// </summary>
|
||||
public class Gradient : CompositionImage
|
||||
{
|
||||
private static readonly DependencyProperty BackgroundDirectionProperty = Property<Gradient>.Depend(nameof(BackgroundDirection), GradientDirection.TopToBottom);
|
||||
private static readonly DependencyProperty ForegroundDirectionProperty = Property<Gradient>.Depend(nameof(ForegroundDirection), GradientDirection.TopToBottom);
|
||||
|
||||
private double imageAspectRatio;
|
||||
|
||||
/// <summary>
|
||||
/// 背景方向
|
||||
/// </summary>
|
||||
public GradientDirection BackgroundDirection
|
||||
{
|
||||
get => (GradientDirection)GetValue(BackgroundDirectionProperty);
|
||||
set => SetValue(BackgroundDirectionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 前景方向
|
||||
/// </summary>
|
||||
public GradientDirection ForegroundDirection
|
||||
{
|
||||
get => (GradientDirection)GetValue(ForegroundDirectionProperty);
|
||||
set => SetValue(ForegroundDirectionProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnUpdateVisual(SpriteVisual spriteVisual)
|
||||
{
|
||||
@@ -29,15 +50,22 @@ public class Gradient : CompositionImage
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
|
||||
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
{
|
||||
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token).ConfigureAwait(true))
|
||||
using (FileStream fileStream = new(file, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token).ConfigureAwait(true);
|
||||
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
|
||||
|
||||
return LoadedImageSurface.StartLoadFromStream(imageStream);
|
||||
using (IRandomAccessStream imageStream = fileStream.AsRandomAccessStream())
|
||||
{
|
||||
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream);
|
||||
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
|
||||
}
|
||||
}
|
||||
|
||||
TaskCompletionSource loadCompleteTaskSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
|
||||
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
|
||||
await loadCompleteTaskSource.Task.ConfigureAwait(true);
|
||||
return surface;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -45,8 +73,8 @@ public class Gradient : CompositionImage
|
||||
{
|
||||
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.UniformToFill, vRatio: 0f);
|
||||
|
||||
CompositionLinearGradientBrush backgroundBrush = compositor.CompositeLinearGradientBrush(new(1f, 0), Vector2.UnitY, new(0, Colors.White), new(1, Colors.Black));
|
||||
CompositionLinearGradientBrush foregroundBrush = compositor.CompositeLinearGradientBrush(Vector2.Zero, Vector2.UnitY, new(0, Colors.White), new(0.95f, Colors.Black));
|
||||
CompositionLinearGradientBrush backgroundBrush = compositor.CompositeLinearGradientBrush(BackgroundDirection, new(0, Colors.White), new(1, Colors.Black));
|
||||
CompositionLinearGradientBrush foregroundBrush = compositor.CompositeLinearGradientBrush(ForegroundDirection, new(0, Colors.White), new(1, Colors.Black));
|
||||
|
||||
CompositionEffectBrush gradientEffectBrush = compositor.CompositeBlendEffectBrush(backgroundBrush, foregroundBrush);
|
||||
CompositionEffectBrush opacityMaskEffectBrush = compositor.CompositeLuminanceToAlphaEffectBrush(gradientEffectBrush);
|
||||
|
||||
50
src/Snap.Hutao/Snap.Hutao/Control/Image/GradientDirection.cs
Normal file
50
src/Snap.Hutao/Snap.Hutao/Control/Image/GradientDirection.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
/// <summary>
|
||||
/// 渐变方向
|
||||
/// </summary>
|
||||
public enum GradientDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// 下到上
|
||||
/// </summary>
|
||||
BottomToTop,
|
||||
|
||||
/// <summary>
|
||||
/// 左下到右上
|
||||
/// </summary>
|
||||
LeftBottomToRightTop,
|
||||
|
||||
/// <summary>
|
||||
/// 左到右
|
||||
/// </summary>
|
||||
LeftToRight,
|
||||
|
||||
/// <summary>
|
||||
/// 左上到右下
|
||||
/// </summary>
|
||||
LeftTopToRightBottom,
|
||||
|
||||
/// <summary>
|
||||
/// 右下到左上
|
||||
/// </summary>
|
||||
RightBottomToLeftTop,
|
||||
|
||||
/// <summary>
|
||||
/// 右到左
|
||||
/// </summary>
|
||||
RightToLeft,
|
||||
|
||||
/// <summary>
|
||||
/// 右上到左下
|
||||
/// </summary>
|
||||
RightTopToLeftBottom,
|
||||
|
||||
/// <summary>
|
||||
/// 上到下
|
||||
/// </summary>
|
||||
TopToBottom,
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Snap.Hutao.Control.Markup;
|
||||
|
||||
/// <summary>
|
||||
/// Uri扩展
|
||||
/// </summary>
|
||||
[MarkupExtensionReturnType(ReturnType = typeof(Uri))]
|
||||
public sealed class UriExtension : MarkupExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的Uri扩展
|
||||
/// </summary>
|
||||
public UriExtension()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 地址
|
||||
/// </summary>
|
||||
public string? Value { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
return new Uri(Value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
57
src/Snap.Hutao/Snap.Hutao/Control/Panel/AspectRatio.cs
Normal file
57
src/Snap.Hutao/Snap.Hutao/Control/Panel/AspectRatio.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
/// <summary>
|
||||
/// 纵横比控件
|
||||
/// </summary>
|
||||
internal class AspectRatio : Microsoft.UI.Xaml.Controls.ContentControl
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AspectRatio>.Depend(nameof(TargetWidth), 1D);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AspectRatio>.Depend(nameof(TargetHeight), 1D);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
double ratio = TargetWidth / TargetHeight;
|
||||
double ratioAvailable = availableSize.Width / availableSize.Height;
|
||||
|
||||
// 更宽
|
||||
if (ratioAvailable > ratio)
|
||||
{
|
||||
double newWidth = ratio * availableSize.Height;
|
||||
return new Size(newWidth, availableSize.Height);
|
||||
}
|
||||
|
||||
// 更高
|
||||
else if (ratioAvailable < ratio)
|
||||
{
|
||||
double newHeight = availableSize.Width / ratio;
|
||||
return new Size(availableSize.Width, newHeight);
|
||||
}
|
||||
|
||||
return availableSize;
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,13 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
Loaded="OnRootControlLoaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<SplitButton
|
||||
Name="RootSplitButton"
|
||||
Padding="0,6"
|
||||
Click="SplitButtonClick"
|
||||
Loaded="SplitButtonLoaded">
|
||||
Click="SplitButtonClick">
|
||||
<SplitButton.Content>
|
||||
<FontIcon Name="IconPresenter" Glyph=""/>
|
||||
</SplitButton.Content>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Control.Panel;
|
||||
/// </summary>
|
||||
public sealed partial class PanelSelector : UserControl
|
||||
{
|
||||
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List");
|
||||
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List", OnCurrentChanged);
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的面板选择器
|
||||
@@ -30,51 +30,60 @@ public sealed partial class PanelSelector : UserControl
|
||||
set => SetValue(CurrentProperty, value);
|
||||
}
|
||||
|
||||
private void SplitButtonLoaded(object sender, RoutedEventArgs e)
|
||||
private static void OnCurrentChanged(PanelSelector sender, string current)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)((SplitButton)sender).Flyout;
|
||||
((RadioMenuFlyoutItem)menuFlyout.Items[0]).IsChecked = true;
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout;
|
||||
RadioMenuFlyoutItem targetItem = menuFlyout.Items
|
||||
.Cast<RadioMenuFlyoutItem>()
|
||||
.Single(i => (string)i.Tag == current);
|
||||
targetItem.IsChecked = true;
|
||||
sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph;
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
|
||||
}
|
||||
|
||||
private void OnRootControlLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// because the GroupName shares in global
|
||||
// we have to impl a control scoped GroupName.
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout;
|
||||
int hash = GetHashCode();
|
||||
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
|
||||
{
|
||||
item.GroupName = $"PanelSelector{hash}Group";
|
||||
}
|
||||
|
||||
OnCurrentChanged(selector, Current);
|
||||
}
|
||||
|
||||
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
|
||||
|
||||
int i = 0;
|
||||
for (; i < menuFlyout.Items.Count; i++)
|
||||
{
|
||||
RadioMenuFlyoutItem current = (RadioMenuFlyoutItem)menuFlyout.Items[i];
|
||||
if (current.IsChecked)
|
||||
if ((string)menuFlyout.Items[i].Tag == Current)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
if (i > menuFlyout.Items.Count)
|
||||
{
|
||||
i = 1;
|
||||
}
|
||||
|
||||
if (i == menuFlyout.Items.Count)
|
||||
{
|
||||
i = 0;
|
||||
}
|
||||
++i;
|
||||
i %= menuFlyout.Items.Count; // move the count index to 0
|
||||
|
||||
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i];
|
||||
item.IsChecked = true;
|
||||
UpdateState(item);
|
||||
Current = (string)item.Tag;
|
||||
}
|
||||
|
||||
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
|
||||
UpdateState(item);
|
||||
}
|
||||
|
||||
private void UpdateState(RadioMenuFlyoutItem item)
|
||||
{
|
||||
Current = (string)item.Tag;
|
||||
IconPresenter.Glyph = ((FontIcon)item.Icon).Glyph;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.ViewModel.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
@@ -15,7 +16,7 @@ namespace Snap.Hutao.Control;
|
||||
[SuppressMessage("", "CA1001")]
|
||||
public class ScopedPage : Page
|
||||
{
|
||||
private readonly CancellationTokenSource viewLoadingCancellationTokenSource = new();
|
||||
private readonly CancellationTokenSource viewCancellationTokenSource = new();
|
||||
private readonly IServiceScope serviceScope;
|
||||
|
||||
/// <summary>
|
||||
@@ -33,10 +34,10 @@ public class ScopedPage : Page
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">视图模型类型</typeparam>
|
||||
public void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, ISupportCancellation
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
|
||||
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
@@ -59,22 +60,32 @@ public class ScopedPage : Page
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
base.OnNavigatingFrom(e);
|
||||
viewLoadingCancellationTokenSource.Cancel();
|
||||
using (viewCancellationTokenSource)
|
||||
{
|
||||
// Cancel all tasks executed by the view model
|
||||
viewCancellationTokenSource.Cancel();
|
||||
IViewModel viewModel = (IViewModel)DataContext;
|
||||
|
||||
// Try dispose scope when page is not presented
|
||||
serviceScope.Dispose();
|
||||
viewLoadingCancellationTokenSource.Dispose();
|
||||
using (SemaphoreSlim locker = viewModel.DisposeLock)
|
||||
{
|
||||
// Wait to ensure viewmodel operation is completed
|
||||
locker.Wait();
|
||||
viewModel.IsViewDisposed = true;
|
||||
|
||||
// Dispose the scope
|
||||
serviceScope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
protected override async void OnNavigatedTo(NavigationEventArgs e)
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
if (e.Parameter is INavigationData extra)
|
||||
{
|
||||
await NotifyRecipentAsync(extra).ConfigureAwait(false);
|
||||
NotifyRecipentAsync(extra).SafeForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
210
src/Snap.Hutao/Snap.Hutao/Control/Theme/FontStyle.xaml
Normal file
210
src/Snap.Hutao/Snap.Hutao/Control/Theme/FontStyle.xaml
Normal file
@@ -0,0 +1,210 @@
|
||||
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls">
|
||||
<FontFamily x:Key="MiSans">ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
|
||||
<FontFamily x:Key="CascadiaMonoAndMiSans">ms-appx:///Resource/Font/CascadiaMono.ttf#Cascadia Mono, ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
|
||||
|
||||
<StaticResource x:Key="PivotHeaderItemFontFamily" ResourceKey="MiSans"/>
|
||||
<StaticResource x:Key="ContentControlThemeFontFamily" ResourceKey="MiSans"/>
|
||||
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock"/>
|
||||
<Style x:Key="BaseTextBlockStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
<Setter Property="FontSize" Value="{StaticResource BodyTextBlockFontSize}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="LineStackingStrategy" Value="MaxHeight"/>
|
||||
<Setter Property="TextLineBounds" Value="Full"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="HeaderTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="46"/>
|
||||
<Setter Property="FontWeight" Value="Light"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SubheaderTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="34"/>
|
||||
<Setter Property="FontWeight" Value="Light"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="TitleTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource TitleTextBlockFontSize}"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SubtitleTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource SubtitleTextBlockFontSize}"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="BodyTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontWeight" Value="Normal"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="CaptionTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource CaptionTextBlockFontSize}"/>
|
||||
<Setter Property="FontWeight" Value="Normal"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="BodyStrongTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource BodyStrongTextBlockFontSize}"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="TitleLargeTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource TitleLargeTextBlockFontSize}"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="DisplayTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource DisplayTextBlockFontSize}"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}" TargetType="MenuFlyoutItem">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultMenuFlyoutSubItemStyle}" TargetType="MenuFlyoutSubItem">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="InfoBar">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultSettingStyle}" TargetType="wsc:Setting"/>
|
||||
|
||||
<Style x:Key="DefaultSettingStyle" TargetType="wsc:Setting">
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
<Setter Property="Margin" Value="0,0,0,0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="wsc:Setting">
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
MinHeight="48"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Icon -->
|
||||
<ColumnDefinition Width="*"/>
|
||||
<!-- Header and subtitle -->
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Action control -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter
|
||||
x:Name="IconPresenter"
|
||||
MaxWidth="20"
|
||||
Margin="2,0,18,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Content="{TemplateBinding Icon}"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
|
||||
IsTextScaleFactorEnabled="False"/>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="0,0,16,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
x:Name="HeaderPresenter"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource MiSans}"
|
||||
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
|
||||
Text="{TemplateBinding Header}"/>
|
||||
|
||||
<ContentPresenter
|
||||
x:Name="DescriptionPresenter"
|
||||
Content="{TemplateBinding Description}"
|
||||
FontFamily="{StaticResource MiSans}"
|
||||
FontSize="{StaticResource SecondaryTextFontSize}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="WrapWholeWords">
|
||||
<ContentPresenter.Resources>
|
||||
<Style BasedOn="{StaticResource CaptionTextBlockStyle}" TargetType="TextBlock">
|
||||
<Style.Setters>
|
||||
<Setter Property="TextWrapping" Value="WrapWholeWords"/>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
<Style BasedOn="{StaticResource TextButtonStyle}" TargetType="HyperlinkButton">
|
||||
<Style.Setters>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Padding" Value="0,0,0,0"/>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ContentPresenter.Resources>
|
||||
</ContentPresenter>
|
||||
</StackPanel>
|
||||
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding ActionContent}"/>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal"/>
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="HeaderPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
|
||||
<Setter Target="DescriptionPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
|
||||
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
|
||||
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Control;
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">源类型</typeparam>
|
||||
/// <typeparam name="TTo">目标类型</typeparam>
|
||||
public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
|
||||
public abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
@@ -23,7 +23,7 @@ public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverterBase<TFrom, TTo>>>()
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public abstract class DisposableObject : IDisposable
|
||||
{
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(isDisposing: true);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool isDisposing)
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
protected void VerifyNotDisposed()
|
||||
{
|
||||
if (IsDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 可异步初始化
|
||||
/// </summary>
|
||||
internal interface ISupportAsyncInitialization
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否已经初始化完成
|
||||
/// </summary>
|
||||
public bool IsInitialized { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 异步初始化
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>初始化任务</returns>
|
||||
ValueTask<bool> InitializeAsync();
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 表示支持验证
|
||||
/// </summary>
|
||||
internal interface ISupportValidation
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证
|
||||
/// </summary>
|
||||
/// <returns>当前数据是否有效</returns>
|
||||
public bool Validate();
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
/// <summary>
|
||||
@@ -12,23 +10,20 @@ namespace Snap.Hutao.Core.Caching;
|
||||
internal interface IImageCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the StorageFile containing cached item for given Uri
|
||||
/// Gets the file path containing cached item for given Uri
|
||||
/// </summary>
|
||||
/// <param name="uri">Uri of the item.</param>
|
||||
/// <returns>a StorageFile</returns>
|
||||
Task<StorageFile> GetFileFromCacheAsync(Uri uri);
|
||||
/// <returns>a string path</returns>
|
||||
Task<string> GetFileFromCacheAsync(Uri uri);
|
||||
|
||||
/// <summary>
|
||||
/// Removed items based on uri list passed
|
||||
/// </summary>
|
||||
/// <param name="uriForCachedItems">Enumerable uri list</param>
|
||||
/// <returns>awaitable Task</returns>
|
||||
Task RemoveAsync(IEnumerable<Uri> uriForCachedItems);
|
||||
void Remove(IEnumerable<Uri> uriForCachedItems);
|
||||
|
||||
/// <summary>
|
||||
/// Removes cached files that have expired
|
||||
/// Removes invalid cached files
|
||||
/// </summary>
|
||||
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
|
||||
/// <returns>awaitable task</returns>
|
||||
Task RemoveExpiredAsync(TimeSpan? duration = null);
|
||||
void RemoveInvalid();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// 图像缓存 文件路径操作
|
||||
/// </summary>
|
||||
internal interface IImageCacheFilePathOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// 从分类与文件名获取文件路径
|
||||
/// </summary>
|
||||
/// <param name="category">分类</param>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <returns>文件路径</returns>
|
||||
string GetFilePathFromCategoryAndFileName(string category, string fileName);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
@@ -10,7 +11,6 @@ using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.FileProperties;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
@@ -20,13 +20,12 @@ namespace Snap.Hutao.Core.Caching;
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton, typeof(IImageCache))]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 16)]
|
||||
[SuppressMessage("", "CA1001")]
|
||||
public class ImageCache : IImageCache
|
||||
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
|
||||
public class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string DateAccessedProperty = "System.DateAccessed";
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
|
||||
private static readonly ImmutableDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
|
||||
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
|
||||
{
|
||||
[0] = TimeSpan.FromSeconds(4),
|
||||
[1] = TimeSpan.FromSeconds(16),
|
||||
@@ -34,19 +33,15 @@ public class ImageCache : IImageCache
|
||||
[3] = TimeSpan.FromSeconds(4),
|
||||
[4] = TimeSpan.FromSeconds(16),
|
||||
[5] = TimeSpan.FromSeconds(64),
|
||||
}.ToImmutableDictionary();
|
||||
};
|
||||
|
||||
private readonly List<string> extendedPropertyNames = new() { DateAccessedProperty };
|
||||
|
||||
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
|
||||
private readonly ILogger logger;
|
||||
|
||||
// violate di rule
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
private StorageFolder? baseFolder;
|
||||
private string? cacheFolderName;
|
||||
private StorageFolder? cacheFolder;
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
private string? baseFolder;
|
||||
private string? cacheFolder;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageCache"/> class.
|
||||
@@ -57,115 +52,103 @@ public class ImageCache : IImageCache
|
||||
{
|
||||
this.logger = logger;
|
||||
httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
|
||||
CacheDuration = TimeSpan.FromDays(30);
|
||||
RetryCount = 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the life duration of every cache entry.
|
||||
/// </summary>
|
||||
public TimeSpan CacheDuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of retries trying to ensure the file is cached.
|
||||
/// </summary>
|
||||
public uint RetryCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Clears all files in the cache
|
||||
/// </summary>
|
||||
/// <returns>awaitable task</returns>
|
||||
public async Task ClearAsync()
|
||||
/// <inheritdoc/>
|
||||
public void RemoveInvalid()
|
||||
{
|
||||
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
||||
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
|
||||
string folder = GetCacheFolder();
|
||||
string[] files = Directory.GetFiles(folder);
|
||||
|
||||
await RemoveAsync(files).ConfigureAwait(false);
|
||||
}
|
||||
List<string> filesToDelete = new();
|
||||
|
||||
/// <summary>
|
||||
/// Removes cached files that have expired
|
||||
/// </summary>
|
||||
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
|
||||
/// <returns>awaitable task</returns>
|
||||
public async Task RemoveExpiredAsync(TimeSpan? duration = null)
|
||||
{
|
||||
TimeSpan expiryDuration = duration ?? CacheDuration;
|
||||
|
||||
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
||||
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
|
||||
|
||||
List<StorageFile> filesToDelete = new();
|
||||
|
||||
foreach (StorageFile file in files)
|
||||
foreach (string file in files)
|
||||
{
|
||||
if (file == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
|
||||
if (IsFileInvalid(file, false))
|
||||
{
|
||||
filesToDelete.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
await RemoveAsync(filesToDelete).ConfigureAwait(false);
|
||||
RemoveInternal(filesToDelete);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removed items based on uri list passed
|
||||
/// </summary>
|
||||
/// <param name="uriForCachedItems">Enumerable uri list</param>
|
||||
/// <returns>awaitable Task</returns>
|
||||
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems)
|
||||
/// <inheritdoc/>
|
||||
public void Remove(IEnumerable<Uri> uriForCachedItems)
|
||||
{
|
||||
if (uriForCachedItems == null || !uriForCachedItems.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
||||
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
|
||||
string folder = GetCacheFolder();
|
||||
string[] files = Directory.GetFiles(folder);
|
||||
|
||||
List<StorageFile> filesToDelete = new();
|
||||
|
||||
Dictionary<string, StorageFile> cachedFiles = files.ToDictionary(file => file.Name);
|
||||
List<string> filesToDelete = new();
|
||||
|
||||
foreach (Uri uri in uriForCachedItems)
|
||||
{
|
||||
string fileName = GetCacheFileName(uri);
|
||||
if (cachedFiles.TryGetValue(fileName, out StorageFile? file))
|
||||
string filePath = Path.Combine(folder, GetCacheFileName(uri));
|
||||
if (files.Contains(filePath))
|
||||
{
|
||||
filesToDelete.Add(file);
|
||||
filesToDelete.Add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
await RemoveAsync(filesToDelete).ConfigureAwait(false);
|
||||
RemoveInternal(filesToDelete);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the StorageFile containing cached item for given Uri
|
||||
/// </summary>
|
||||
/// <param name="uri">Uri of the item.</param>
|
||||
/// <returns>a StorageFile</returns>
|
||||
public async Task<StorageFile> GetFileFromCacheAsync(Uri uri)
|
||||
/// <inheritdoc/>
|
||||
public async Task<string> GetFileFromCacheAsync(Uri uri)
|
||||
{
|
||||
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
||||
|
||||
string fileName = GetCacheFileName(uri);
|
||||
string filePath = Path.Combine(GetCacheFolder(), fileName);
|
||||
|
||||
IStorageItem? item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
|
||||
|
||||
if (item == null || (await item.GetBasicPropertiesAsync()).Size == 0)
|
||||
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
|
||||
{
|
||||
StorageFile baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
|
||||
await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
|
||||
item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
try
|
||||
{
|
||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||
{
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
taskCompletionSource.TrySetResult();
|
||||
}
|
||||
}
|
||||
|
||||
return Must.NotNull((item as StorageFile)!);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetFilePathFromCategoryAndFileName(string category, string fileName)
|
||||
{
|
||||
Uri dummyUri = new(Web.HutaoEndpoints.StaticFile(category, fileName));
|
||||
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
|
||||
}
|
||||
|
||||
private static void RemoveInternal(IEnumerable<string> filePaths)
|
||||
{
|
||||
foreach (string filePath in filePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCacheFileName(Uri uri)
|
||||
@@ -176,48 +159,19 @@ public class ImageCache : IImageCache
|
||||
return System.Convert.ToHexString(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override-able method that checks whether file is valid or not.
|
||||
/// </summary>
|
||||
/// <param name="file">storage file</param>
|
||||
/// <param name="duration">cache duration</param>
|
||||
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
|
||||
/// <returns>bool indicate whether file has expired or not</returns>
|
||||
private async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
|
||||
private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = true)
|
||||
{
|
||||
if (file == null)
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
return treatNullFileAsOutOfDate;
|
||||
return treatNullFileAsInvalid;
|
||||
}
|
||||
|
||||
// Get extended properties.
|
||||
IDictionary<string, object> extraProperties = await file.Properties
|
||||
.RetrievePropertiesAsync(extendedPropertyNames)
|
||||
.AsTask()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Get date-accessed property.
|
||||
object? propValue = extraProperties[DateAccessedProperty];
|
||||
|
||||
if (propValue != null)
|
||||
{
|
||||
DateTimeOffset? lastAccess = propValue as DateTimeOffset?;
|
||||
|
||||
if (lastAccess.HasValue)
|
||||
{
|
||||
return DateTime.Now.Subtract(lastAccess.Value.DateTime) > duration;
|
||||
}
|
||||
}
|
||||
|
||||
BasicProperties properties = await file
|
||||
.GetBasicPropertiesAsync()
|
||||
.AsTask()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return properties.Size == 0 || DateTime.Now.Subtract(properties.DateModified.DateTime) > duration;
|
||||
FileInfo fileInfo = new(file);
|
||||
return fileInfo.Length == 0;
|
||||
}
|
||||
|
||||
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
|
||||
private async Task DownloadFileAsync(Uri uri, string baseFile)
|
||||
{
|
||||
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri);
|
||||
|
||||
@@ -230,18 +184,23 @@ public class ImageCache : IImageCache
|
||||
{
|
||||
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
using (FileStream fileStream = File.Create(baseFile.Path))
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (message.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
// directly goto https://static.hut.ao
|
||||
retryCount = 3;
|
||||
}
|
||||
else if (message.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry after {delay}.", delay);
|
||||
logger.LogInformation("Retry {uri} after {delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -252,61 +211,20 @@ public class ImageCache : IImageCache
|
||||
|
||||
if (retryCount == 3)
|
||||
{
|
||||
uri = new UriBuilder(uri) { Host = "static.hut.ao", }.Uri;
|
||||
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao }.Uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes with default values if user has not initialized explicitly
|
||||
/// </summary>
|
||||
/// <returns>awaitable task</returns>
|
||||
private async Task InitializeInternalAsync()
|
||||
{
|
||||
if (cacheFolder != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (await cacheFolderSemaphore.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
baseFolder ??= ApplicationData.Current.TemporaryFolder;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFolderName))
|
||||
{
|
||||
cacheFolderName = GetType().Name;
|
||||
}
|
||||
|
||||
cacheFolder = await baseFolder
|
||||
.CreateFolderAsync(cacheFolderName, CreationCollisionOption.OpenIfExists)
|
||||
.AsTask()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<StorageFolder> GetCacheFolderAsync()
|
||||
private string GetCacheFolder()
|
||||
{
|
||||
if (cacheFolder == null)
|
||||
{
|
||||
await InitializeInternalAsync().ConfigureAwait(false);
|
||||
baseFolder ??= ApplicationData.Current.LocalCacheFolder.Path;
|
||||
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
|
||||
cacheFolder = info.FullName;
|
||||
}
|
||||
|
||||
return Must.NotNull(cacheFolder!);
|
||||
}
|
||||
|
||||
private async Task RemoveAsync(IEnumerable<StorageFile> files)
|
||||
{
|
||||
foreach (StorageFile file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation(EventIds.CacheRemoveFile, "Removing file {file}", file.Path);
|
||||
await file.DeleteAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.LogError(EventIds.CacheException, "Failed to delete file: {file}", file.Path);
|
||||
}
|
||||
}
|
||||
return cacheFolder!;
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,19 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core.Convert;
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 支持Md5转换
|
||||
/// </summary>
|
||||
internal abstract class Md5Convert
|
||||
internal static class Convert
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取字符串的MD5计算结果
|
||||
/// </summary>
|
||||
/// <param name="source">源字符串</param>
|
||||
/// <returns>计算的结果</returns>
|
||||
public static string ToHexString(string source)
|
||||
public static string ToMd5HexString(string source)
|
||||
{
|
||||
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
|
||||
return System.Convert.ToHexString(hash);
|
||||
@@ -2,11 +2,11 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.Convert;
|
||||
using Snap.Hutao.Core.Json;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
@@ -30,7 +30,7 @@ internal static class CoreEnvironment
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabXrpcVersion = "2.43.1";
|
||||
public const string HoyolabXrpcVersion = "2.44.1";
|
||||
|
||||
/// <summary>
|
||||
/// 盐
|
||||
@@ -38,8 +38,8 @@ internal static class CoreEnvironment
|
||||
// https://github.com/UIGF-org/Hoyolab.Salt
|
||||
public static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
|
||||
{
|
||||
[nameof(SaltType.K2)] = "ODzG1Jrn6zebX19VRmaJwjFI2CDvBUGq",
|
||||
[nameof(SaltType.LK2)] = "V1PYbXKQY7ysdx3MNCcNbsE1LtY2QZpW",
|
||||
[nameof(SaltType.K2)] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
|
||||
[nameof(SaltType.LK2)] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
|
||||
[nameof(SaltType.X4)] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
[nameof(SaltType.X6)] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
[nameof(SaltType.PROD)] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
@@ -70,6 +70,11 @@ internal static class CoreEnvironment
|
||||
/// </summary>
|
||||
public static readonly string FamilyName;
|
||||
|
||||
/// <summary>
|
||||
/// 数据文件夹
|
||||
/// </summary>
|
||||
public static readonly string DataFolder;
|
||||
|
||||
/// <summary>
|
||||
/// 默认的Json序列化选项
|
||||
/// </summary>
|
||||
@@ -93,6 +98,7 @@ internal static class CoreEnvironment
|
||||
|
||||
static CoreEnvironment()
|
||||
{
|
||||
DataFolder = GetDocumentsHutaoPath();
|
||||
Version = Package.Current.Id.Version.ToVersion();
|
||||
FamilyName = Package.Current.Id.FamilyName;
|
||||
CommonUA = $"Snap Hutao/{Version}";
|
||||
@@ -106,6 +112,21 @@ internal static class CoreEnvironment
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
|
||||
return Md5Convert.ToHexString($"{userName}{machineGuid}");
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
}
|
||||
|
||||
private static string GetDocumentsHutaoPath()
|
||||
{
|
||||
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库集合上下文
|
||||
/// 数据库集合扩展
|
||||
/// </summary>
|
||||
public static class DbSetExtension
|
||||
{
|
||||
@@ -134,4 +134,4 @@ public static class DbSetExtension
|
||||
dbSet.Update(entity);
|
||||
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 可查询扩展
|
||||
/// </summary>
|
||||
public static class QueryableExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// source.Where(predicate).ExecuteDeleteAsync(token)
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <param name="predicate">条件</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>SQL返回个数</returns>
|
||||
public static Task<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
|
||||
{
|
||||
return source.Where(predicate).ExecuteDeleteAsync(token);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Context.FileSystem;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -31,9 +30,7 @@ internal static class IocConfiguration
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddDatebase(this IServiceCollection services)
|
||||
{
|
||||
HutaoContext myDocument = new(new());
|
||||
|
||||
string dbFile = myDocument.Locate("Userdata.db");
|
||||
string dbFile = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
// temporarily create a context
|
||||
@@ -41,11 +38,20 @@ internal static class IocConfiguration
|
||||
{
|
||||
if (context.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[Debug] Performing AppDbContext Migrations");
|
||||
#endif
|
||||
context.Database.Migrate();
|
||||
}
|
||||
}
|
||||
|
||||
return services.AddDbContext<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
|
||||
return services.AddDbContext<AppDbContext>(builder =>
|
||||
{
|
||||
builder
|
||||
#if DEBUG
|
||||
.EnableSensitiveDataLogging()
|
||||
#endif
|
||||
.UseSqlite(sqlConnectionString);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ namespace Snap.Hutao.Core.DependencyInjection;
|
||||
/// </summary>
|
||||
public static class ServiceScopeExtension
|
||||
{
|
||||
private static IServiceScope? scopeReference;
|
||||
// Allow GC to Collect the IServiceScope
|
||||
private static readonly WeakReference<IServiceScope> ScopeReference = new(null!);
|
||||
|
||||
/// <summary>
|
||||
/// 追踪服务范围
|
||||
@@ -19,7 +20,7 @@ public static class ServiceScopeExtension
|
||||
public static void Track(this IServiceScope scope)
|
||||
{
|
||||
DisposeLast();
|
||||
scopeReference = scope;
|
||||
ScopeReference.SetTarget(scope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -27,6 +28,9 @@ public static class ServiceScopeExtension
|
||||
/// </summary>
|
||||
public static void DisposeLast()
|
||||
{
|
||||
scopeReference?.Dispose();
|
||||
if (ScopeReference.TryGetTarget(out IServiceScope? scope))
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
|
||||
namespace Snap.Hutao.Core.Exception;
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 异常记录器
|
||||
@@ -27,10 +26,13 @@ internal class ExceptionRecorder
|
||||
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
|
||||
#if RELEASE
|
||||
#pragma warning disable VSTHRD002
|
||||
Ioc.Default.GetRequiredService<Web.Hutao.HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002
|
||||
#endif
|
||||
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
|
||||
|
||||
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
|
||||
@@ -43,4 +45,4 @@ internal class ExceptionRecorder
|
||||
{
|
||||
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 用户数据损坏异常
|
||||
/// </summary>
|
||||
internal class UserdataCorruptedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的用户数据损坏异常
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public UserdataCorruptedException(string message, Exception innerException)
|
||||
: base($"用户数据已损坏: {message}", innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Snap.Hutao.Core.ExpressionService;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举帮助类
|
||||
/// </summary>
|
||||
public static class EnumExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断枚举是否有对应的Flag
|
||||
/// </summary>
|
||||
/// <typeparam name="T">枚举类型</typeparam>
|
||||
/// <param name="enum">待检查的枚举</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>是否有对应的Flag</returns>
|
||||
public static bool HasOption<T>(this T @enum, T value)
|
||||
where T : struct, Enum
|
||||
{
|
||||
return ExpressionCache<T>.Entry(@enum, value);
|
||||
}
|
||||
|
||||
private static class ExpressionCache<T>
|
||||
{
|
||||
public static readonly Func<T, T, bool> Entry = Get();
|
||||
|
||||
private static Func<T, T, bool> Get()
|
||||
{
|
||||
ParameterExpression paramSource = Expression.Parameter(typeof(T));
|
||||
ParameterExpression paramValue = Expression.Parameter(typeof(T));
|
||||
|
||||
BinaryExpression logicalAnd = Expression.AndAssign(paramSource, paramValue);
|
||||
BinaryExpression equal = Expression.Equal(logicalAnd, paramValue);
|
||||
|
||||
// 生成一个源类型入,目标类型出的 lamdba
|
||||
return Expression.Lambda<Func<T, T, bool>>(equal, paramSource, paramValue).Compile();
|
||||
}
|
||||
}
|
||||
}
|
||||
318
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsJob.cs
Normal file
318
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsJob.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Bits;
|
||||
|
||||
/// <summary>
|
||||
/// BITS Job
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1600")]
|
||||
internal class BitsJob : DisposableObject, IBackgroundCopyCallback
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务名称前缀
|
||||
/// </summary>
|
||||
public const string JobNamePrefix = "SnapHutaoBitsJob";
|
||||
|
||||
private const uint BitsEngineNoProgressTimeout = 120;
|
||||
private const int MaxResumeAttempts = 10;
|
||||
|
||||
private readonly string displayName;
|
||||
private readonly ILogger<BitsJob> log;
|
||||
private readonly object lockObj = new();
|
||||
|
||||
private IBackgroundCopyJob? nativeJob;
|
||||
private System.Exception? jobException;
|
||||
private BG_JOB_PROGRESS progress;
|
||||
private BG_JOB_STATE state;
|
||||
private bool isJobComplete;
|
||||
private int resumeAttempts;
|
||||
|
||||
private BitsJob(IServiceProvider serviceProvider, string displayName, IBackgroundCopyJob job)
|
||||
{
|
||||
this.displayName = displayName;
|
||||
nativeJob = job;
|
||||
log = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
|
||||
}
|
||||
|
||||
public HRESULT ErrorCode { get; private set; }
|
||||
|
||||
public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath)
|
||||
{
|
||||
ILogger<BitsJob> service = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
|
||||
string text = $"{JobNamePrefix} - {uri}";
|
||||
IBackgroundCopyJob ppJob;
|
||||
try
|
||||
{
|
||||
backgroundCopyManager.CreateJob(text, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out ppJob);
|
||||
|
||||
// BG_NOTIFY_JOB_TRANSFERRED & BG_NOTIFY_JOB_ERROR & BG_NOTIFY_JOB_MODIFICATION
|
||||
ppJob.SetNotifyFlags(0b1011);
|
||||
ppJob.SetNoProgressTimeout(BitsEngineNoProgressTimeout);
|
||||
ppJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND);
|
||||
ppJob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, null, null);
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
service.LogInformation("Failed to create job. {message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
|
||||
BitsJob bitsJob = new(serviceProvider, text, ppJob);
|
||||
bitsJob.InitJob(uri.AbsoluteUri, filePath);
|
||||
return bitsJob;
|
||||
}
|
||||
|
||||
public void JobTransferred(IBackgroundCopyJob job)
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateProgress();
|
||||
UpdateJobState();
|
||||
CompleteOrCancel();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
log.LogInformation("Failed to job transfer: {message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void JobError(IBackgroundCopyJob job, IBackgroundCopyError error)
|
||||
{
|
||||
IBackgroundCopyError error2 = error;
|
||||
try
|
||||
{
|
||||
log.LogInformation("Failed job: {message}", displayName);
|
||||
UpdateJobState();
|
||||
BG_ERROR_CONTEXT errorContext = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_NONE;
|
||||
HRESULT returnCode = new(0);
|
||||
|
||||
Invoke(() => error2.GetError(out errorContext, out returnCode), "GetError", throwOnFailure: false);
|
||||
ErrorCode = returnCode;
|
||||
jobException = new IOException(string.Format("Error context: {0}, Error code: {1}", errorContext, returnCode));
|
||||
CompleteOrCancel();
|
||||
log.LogInformation(jobException, "Job Exception:");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
log?.LogInformation("Failed to handle job error: {message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void JobModification(IBackgroundCopyJob job, uint reserved)
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateJobState();
|
||||
if (state == BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR)
|
||||
{
|
||||
HRESULT errorCode = GetErrorCode(job);
|
||||
if (errorCode == -2145844944)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
CompleteOrCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
resumeAttempts++;
|
||||
if (resumeAttempts <= MaxResumeAttempts)
|
||||
{
|
||||
Resume();
|
||||
return;
|
||||
}
|
||||
|
||||
log.LogInformation("Max resume attempts for job '{name}' exceeded. Canceling.", displayName);
|
||||
CompleteOrCancel();
|
||||
}
|
||||
else if (IsProgressingState(state))
|
||||
{
|
||||
UpdateProgress();
|
||||
}
|
||||
else if (state == BG_JOB_STATE.BG_JOB_STATE_CANCELLED || state == BG_JOB_STATE.BG_JOB_STATE_ERROR)
|
||||
{
|
||||
CompleteOrCancel();
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
log.LogInformation(ex, "message");
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
log.LogInformation("Canceling job {name}", displayName);
|
||||
lock (lockObj)
|
||||
{
|
||||
if (!isJobComplete)
|
||||
{
|
||||
Invoke(() => nativeJob?.Cancel(), "Bits Cancel");
|
||||
jobException = new OperationCanceledException();
|
||||
isJobComplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WaitForCompletion(Action<ProgressUpdateStatus> callback, CancellationToken cancellationToken)
|
||||
{
|
||||
CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(Cancel);
|
||||
int noProgressSeconds = 0;
|
||||
try
|
||||
{
|
||||
UpdateJobState();
|
||||
while (IsProgressingState(state) || state == BG_JOB_STATE.BG_JOB_STATE_QUEUED)
|
||||
{
|
||||
if (noProgressSeconds > BitsEngineNoProgressTimeout)
|
||||
{
|
||||
jobException = new TimeoutException($"Timeout reached for job {displayName} whilst in state {state}");
|
||||
break;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
UpdateJobState();
|
||||
UpdateProgress();
|
||||
|
||||
if (state is BG_JOB_STATE.BG_JOB_STATE_TRANSFERRING or BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED or BG_JOB_STATE.BG_JOB_STATE_ACKNOWLEDGED)
|
||||
{
|
||||
noProgressSeconds = 0;
|
||||
callback(new ProgressUpdateStatus((long)progress.BytesTransferred, (long)progress.BytesTotal));
|
||||
}
|
||||
|
||||
// Refresh every seconds.
|
||||
Thread.Sleep(1000);
|
||||
++noProgressSeconds;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancellationTokenRegistration.Dispose();
|
||||
CompleteOrCancel();
|
||||
}
|
||||
|
||||
if (jobException != null)
|
||||
{
|
||||
throw jobException;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
UpdateJobState();
|
||||
CompleteOrCancel();
|
||||
nativeJob = null;
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
private static bool IsProgressingState(BG_JOB_STATE state)
|
||||
{
|
||||
if (state != BG_JOB_STATE.BG_JOB_STATE_CONNECTING && state != BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR)
|
||||
{
|
||||
return state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRING;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CompleteOrCancel()
|
||||
{
|
||||
if (isJobComplete)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (lockObj)
|
||||
{
|
||||
if (isJobComplete)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED)
|
||||
{
|
||||
log.LogInformation("Completing job '{name}'.", displayName);
|
||||
Invoke(() => nativeJob?.Complete(), "Bits Complete");
|
||||
while (state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED)
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
UpdateJobState();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.LogInformation("Canceling job '{name}'.", displayName);
|
||||
Invoke(() => nativeJob?.Cancel(), "Bits Cancel");
|
||||
}
|
||||
|
||||
isJobComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateJobState()
|
||||
{
|
||||
if (nativeJob is IBackgroundCopyJob job)
|
||||
{
|
||||
Invoke(() => job.GetState(out state), "GetState");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateProgress()
|
||||
{
|
||||
if (!isJobComplete)
|
||||
{
|
||||
Invoke(() => nativeJob?.GetProgress(out progress), "GetProgress");
|
||||
}
|
||||
}
|
||||
|
||||
private void Resume()
|
||||
{
|
||||
Invoke(() => nativeJob?.Resume(), "Bits Resume");
|
||||
}
|
||||
|
||||
private void Invoke(Action action, string displayName, bool throwOnFailure = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
log.LogInformation("{name} failed. {exception}", displayName, ex);
|
||||
if (throwOnFailure)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitJob(string remoteUrl, string filePath)
|
||||
{
|
||||
nativeJob?.AddFile(remoteUrl, filePath);
|
||||
nativeJob?.SetNotifyInterface(this);
|
||||
Resume();
|
||||
}
|
||||
|
||||
private HRESULT GetErrorCode(IBackgroundCopyJob job)
|
||||
{
|
||||
IBackgroundCopyJob job2 = job;
|
||||
IBackgroundCopyError? error = null;
|
||||
|
||||
Invoke(() => job2.GetError(out error), "GetError", false);
|
||||
if (error != null)
|
||||
{
|
||||
HRESULT returnCode = new(0);
|
||||
Invoke(() => error.GetError(out _, out returnCode), "GetError", false);
|
||||
return returnCode;
|
||||
}
|
||||
|
||||
return new(0);
|
||||
}
|
||||
}
|
||||
123
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
Normal file
123
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Bits;
|
||||
|
||||
/// <summary>
|
||||
/// BITS 管理器
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal class BitsManager
|
||||
{
|
||||
private readonly Lazy<IBackgroundCopyManager> lazyBackgroundCopyManager = new(() => (IBackgroundCopyManager)new BackgroundCopyManager());
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<BitsManager> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 BITS 管理器
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public BitsManager(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
logger = serviceProvider.GetRequiredService<ILogger<BitsManager>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步下载文件
|
||||
/// </summary>
|
||||
/// <param name="uri">文件uri</param>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否下载成功,以及创建的文件</returns>
|
||||
public async Task<ValueResult<bool, TempFile>> DownloadAsync(Uri uri, IProgress<ProgressUpdateStatus> progress, CancellationToken token = default)
|
||||
{
|
||||
TempFile tempFile = new(true);
|
||||
bool result = await Task.Run(() => DownloadCore(uri, tempFile.Path, progress.Report, token), token).ConfigureAwait(false);
|
||||
return new(result, tempFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消所有先前创建的任务
|
||||
/// </summary>
|
||||
public void CancelAllJobs()
|
||||
{
|
||||
IBackgroundCopyManager value;
|
||||
try
|
||||
{
|
||||
value = lazyBackgroundCopyManager.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
value.EnumJobs(0, out IEnumBackgroundCopyJobs pJobs);
|
||||
pJobs.GetCount(out uint count);
|
||||
|
||||
List<IBackgroundCopyJob> jobsToCancel = new();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
uint actualFetched = 0;
|
||||
pJobs.Next(1, out IBackgroundCopyJob pJob, ref actualFetched);
|
||||
pJob.GetDisplayName(out PWSTR name);
|
||||
if (name.AsSpan().StartsWith(BitsJob.JobNamePrefix))
|
||||
{
|
||||
jobsToCancel.Add(pJob);
|
||||
}
|
||||
}
|
||||
|
||||
jobsToCancel.ForEach(job => job.Cancel());
|
||||
}
|
||||
|
||||
private bool DownloadCore(Uri uri, string tempFile, Action<ProgressUpdateStatus> progress, CancellationToken token)
|
||||
{
|
||||
IBackgroundCopyManager value;
|
||||
|
||||
try
|
||||
{
|
||||
value = lazyBackgroundCopyManager.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
bitsJob.WaitForCompletion(progress, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "BITS download failed:");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bitsJob.ErrorCode != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// BITS job creation failed
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Bits;
|
||||
|
||||
/// <summary>
|
||||
/// 进度更新状态
|
||||
/// </summary>
|
||||
public class ProgressUpdateStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的进度更新状态
|
||||
/// </summary>
|
||||
/// <param name="bytesRead">接收字节数</param>
|
||||
/// <param name="totalBytes">总字节数</param>
|
||||
public ProgressUpdateStatus(long bytesRead, long totalBytes)
|
||||
{
|
||||
BytesRead = bytesRead;
|
||||
TotalBytes = totalBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接收字节数
|
||||
/// </summary>
|
||||
public long BytesRead { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总字节数
|
||||
/// </summary>
|
||||
public long TotalBytes { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{BytesRead}/{TotalBytes}";
|
||||
}
|
||||
}
|
||||
42
src/Snap.Hutao/Snap.Hutao/Core/IO/Digest.cs
Normal file
42
src/Snap.Hutao/Snap.Hutao/Core/IO/Digest.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 摘要
|
||||
/// </summary>
|
||||
internal static class Digest
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步获取文件 Md5 摘要
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>文件 Md5 摘要</returns>
|
||||
public static async Task<string> GetFileMd5Async(string filePath, CancellationToken token = default)
|
||||
{
|
||||
using (FileStream stream = File.OpenRead(filePath))
|
||||
{
|
||||
return await GetStreamMd5Async(stream, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取流的 Md5 摘要
|
||||
/// </summary>
|
||||
/// <param name="stream">流</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>流 Md5 摘要</returns>
|
||||
public static async Task<string> GetStreamMd5Async(Stream stream, CancellationToken token = default)
|
||||
{
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
byte[] bytes = await md5.ComputeHashAsync(stream, token).ConfigureAwait(false);
|
||||
return System.Convert.ToHexString(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Snap.Hutao/Snap.Hutao/Core/IO/FileOperation.cs
Normal file
41
src/Snap.Hutao/Snap.Hutao/Core/IO/FileOperation.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 文件操作
|
||||
/// </summary>
|
||||
internal static class FileOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// 将指定文件移动到新位置,提供指定新文件名和覆盖目标文件(如果它已存在)的选项。
|
||||
/// </summary>
|
||||
/// <param name="sourceFileName">要移动的文件的名称。 可以包括相对或绝对路径。</param>
|
||||
/// <param name="destFileName">文件的新路径和名称。</param>
|
||||
/// <param name="overwrite">如果要覆盖目标文件</param>
|
||||
/// <returns>是否发生了移动操作</returns>
|
||||
public static bool Move(string sourceFileName, string destFileName, bool overwrite)
|
||||
{
|
||||
if (File.Exists(sourceFileName))
|
||||
{
|
||||
if (overwrite)
|
||||
{
|
||||
File.Move(sourceFileName, destFileName, overwrite);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!File.Exists(destFileName))
|
||||
{
|
||||
File.Move(sourceFileName, destFileName, overwrite);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
112
src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs
Normal file
112
src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 文件路径
|
||||
/// </summary>
|
||||
internal readonly struct FilePath : IEquatable<FilePath>
|
||||
{
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public readonly string Value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FilePath"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">value</param>
|
||||
public FilePath(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator string(FilePath value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static implicit operator FilePath(string value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static bool operator ==(FilePath left, FilePath right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(FilePath left, FilePath right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步反序列化文件中的内容
|
||||
/// </summary>
|
||||
/// <typeparam name="T">内容的类型</typeparam>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>操作是否成功,反序列化后的内容</returns>
|
||||
public async Task<ValueResult<bool, T?>> DeserializeFromJsonAsync<T>(JsonSerializerOptions options)
|
||||
where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream stream = File.OpenRead(Value))
|
||||
{
|
||||
T? t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
|
||||
return new(true, t);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
return new(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象异步序列化入文件
|
||||
/// </summary>
|
||||
/// <typeparam name="T">对象的类型</typeparam>
|
||||
/// <param name="obj">对象</param>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>操作是否成功</returns>
|
||||
public async Task<bool> SerializeToJsonAsync<T>(T obj, JsonSerializerOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream stream = File.Create(Value))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, obj, options).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(FilePath other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is FilePath other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
77
src/Snap.Hutao/Snap.Hutao/Core/IO/PickerExtension.cs
Normal file
77
src/Snap.Hutao/Snap.Hutao/Core/IO/PickerExtension.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Pickers;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 选择器拓展
|
||||
/// </summary>
|
||||
internal static class PickerExtension
|
||||
{
|
||||
/// <inheritdoc cref="FileOpenPicker.PickSingleFileAsync"/>
|
||||
public static async Task<ValueResult<bool, FilePath>> TryPickSingleFileAsync(this FileOpenPicker picker)
|
||||
{
|
||||
StorageFile? file;
|
||||
Exception? exception = null;
|
||||
try
|
||||
{
|
||||
file = await picker.PickSingleFileAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
file = null;
|
||||
}
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
return new(true, file.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (exception != null)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<Service.Abstraction.IInfoBarService>()
|
||||
.Warning("无法打开文件选择器", $"请勿在管理员模式下使用此功能 {exception.Message}");
|
||||
}
|
||||
|
||||
return new(false, null!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="FileSavePicker.PickSaveFileAsync"/>
|
||||
public static async Task<ValueResult<bool, FilePath>> TryPickSaveFileAsync(this FileSavePicker picker)
|
||||
{
|
||||
StorageFile? file;
|
||||
Exception? exception = null;
|
||||
try
|
||||
{
|
||||
file = await picker.PickSaveFileAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
file = null;
|
||||
}
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
return new(true, file.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (exception != null)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<Service.Abstraction.IInfoBarService>()
|
||||
.Warning("无法打开文件选择器", $"请勿在管理员模式下使用此功能 {exception.Message}");
|
||||
}
|
||||
|
||||
return new(false, null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 文件拓展
|
||||
/// </summary>
|
||||
public static class StorageFileExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步反序列化文件中的内容
|
||||
/// </summary>
|
||||
/// <typeparam name="T">内容的类型</typeparam>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>操作是否成功,反序列化后的内容</returns>
|
||||
public static async Task<ValueResult<bool, T?>> DeserializeFromJsonAsync<T>(this StorageFile file, JsonSerializerOptions options)
|
||||
where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream stream = File.OpenRead(file.Path))
|
||||
{
|
||||
T? t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
|
||||
return new(true, t);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
return new(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象异步序列化入文件
|
||||
/// </summary>
|
||||
/// <typeparam name="T">对象的类型</typeparam>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="obj">对象</param>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>操作是否成功</returns>
|
||||
public static async Task<bool> SerializeToJsonAsync<T>(this StorageFile file, T obj, JsonSerializerOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream stream = File.Create(file.Path))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, obj, options).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,20 @@ namespace Snap.Hutao.Core.IO;
|
||||
/// <summary>
|
||||
/// 封装一个临时文件
|
||||
/// </summary>
|
||||
internal sealed class TemporaryFile : IDisposable
|
||||
internal sealed class TempFile : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的临时文件
|
||||
/// </summary>
|
||||
public TemporaryFile()
|
||||
/// <param name="delete">是否在创建时删除文件</param>
|
||||
public TempFile(bool delete = false)
|
||||
{
|
||||
Path = System.IO.Path.GetTempFileName();
|
||||
|
||||
if (delete)
|
||||
{
|
||||
File.Delete(Path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -28,15 +34,15 @@ internal sealed class TemporaryFile : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="file">源文件</param>
|
||||
/// <returns>临时文件</returns>
|
||||
public static TemporaryFile? CreateFromFileCopy(string file)
|
||||
public static TempFile? CreateFromFileCopy(string file)
|
||||
{
|
||||
TemporaryFile temporaryFile = new();
|
||||
TempFile temporaryFile = new();
|
||||
try
|
||||
{
|
||||
File.Copy(file, temporaryFile.Path, true);
|
||||
return temporaryFile;
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -47,6 +53,12 @@ internal sealed class TemporaryFile : IDisposable
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
File.Delete(Path);
|
||||
try
|
||||
{
|
||||
File.Delete(Path);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ internal class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerabl
|
||||
/// <inheritdoc/>
|
||||
public override IEnumerable<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
string? team = reader.GetString();
|
||||
IEnumerable<int>? ids = team?.Split(',').Select(x => int.Parse(x));
|
||||
string? source = reader.GetString();
|
||||
IEnumerable<int>? ids = source?.Split(',').Select(int.Parse);
|
||||
return ids ?? Enumerable.Empty<int>();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeToConvert.GetGenericTypeDefinition() != typeof(IDictionary<,>))
|
||||
if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -27,8 +27,9 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
|
||||
/// <inheritdoc/>
|
||||
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
|
||||
{
|
||||
Type keyType = type.GetGenericArguments()[0];
|
||||
Type valueType = type.GetGenericArguments()[1];
|
||||
Type[] arguments = type.GetGenericArguments();
|
||||
Type keyType = arguments[0];
|
||||
Type valueType = arguments[1];
|
||||
|
||||
Type innerConverterType = typeof(StringEnumDictionaryConverterInner<,>).MakeGenericType(keyType, valueType);
|
||||
JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType)!;
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace Snap.Hutao.Core.Json;
|
||||
/// </summary>
|
||||
internal class JsonTextEncoder : JavaScriptEncoder
|
||||
{
|
||||
private static readonly string BackSlashDoubleQuote = "\\\"";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int MaxOutputCharactersPerInputCharacter { get => 6; }
|
||||
|
||||
@@ -27,7 +29,7 @@ internal class JsonTextEncoder : JavaScriptEncoder
|
||||
if (unicodeScalar == '"')
|
||||
{
|
||||
numberOfCharactersWritten = 2;
|
||||
return "\\\"".AsSpan().TryCopyTo(new Span<char>(buffer, bufferLength));
|
||||
return BackSlashDoubleQuote.AsSpan().TryCopyTo(new Span<char>(buffer, bufferLength));
|
||||
}
|
||||
|
||||
string encoded = $"\\u{(uint)unicodeScalar:x4}";
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.DailyNote;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
#if RELEASE
|
||||
using System.Security.Principal;
|
||||
#endif
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
@@ -36,11 +39,15 @@ internal static class Activation
|
||||
/// <returns>是否提升了权限</returns>
|
||||
public static bool GetElevated()
|
||||
{
|
||||
#if RELEASE
|
||||
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
WindowsPrincipal principal = new(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -123,6 +130,9 @@ internal static class Activation
|
||||
{
|
||||
case "":
|
||||
{
|
||||
// Increase launch times
|
||||
LocalSetting.Set(SettingKeys.LaunchTimes, LocalSetting.Get(SettingKeys.LaunchTimes, 0) + 1);
|
||||
|
||||
await WaitMainWindowAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
@@ -145,7 +155,7 @@ internal static class Activation
|
||||
|
||||
Ioc.Default
|
||||
.GetRequiredService<IMetadataService>()
|
||||
.ImplictAs<IMetadataInitializer>()?
|
||||
.ImplictAs<IMetadataServiceInitialization>()?
|
||||
.InitializeInternalAsync()
|
||||
.SafeForget();
|
||||
}
|
||||
|
||||
@@ -14,27 +14,26 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
/// </summary>
|
||||
internal static class AppInstanceExtension
|
||||
{
|
||||
// Hold the reference here to prevent memory corruption.
|
||||
private static HANDLE redirectEventHandle = HANDLE.Null;
|
||||
|
||||
/// <summary>
|
||||
/// 同步非阻塞重定向
|
||||
/// </summary>
|
||||
/// <param name="appInstance">app实例</param>
|
||||
/// <param name="args">参数</param>
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
[SuppressMessage("", "VSTHRD110")]
|
||||
public static void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
|
||||
public static unsafe void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
|
||||
{
|
||||
HANDLE redirectEventHandle = UnsafeCreateEvent();
|
||||
Task.Run(async () =>
|
||||
redirectEventHandle = CreateEvent(default(SECURITY_ATTRIBUTES*), true, false, null);
|
||||
Task.Run(() =>
|
||||
{
|
||||
await appInstance.RedirectActivationToAsync(args);
|
||||
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
|
||||
SetEvent(redirectEventHandle);
|
||||
});
|
||||
|
||||
ReadOnlySpan<HANDLE> handles = new(in redirectEventHandle);
|
||||
CoWaitForMultipleObjects((uint)CWMO_FLAGS.CWMO_DEFAULT, INFINITE, handles, out uint _);
|
||||
}
|
||||
|
||||
private static unsafe HANDLE UnsafeCreateEvent()
|
||||
{
|
||||
return CreateEvent(default(SECURITY_ATTRIBUTES*), true, false, null);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Context.FileSystem;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
@@ -61,8 +60,8 @@ public sealed class LogEntryQueue : IDisposable
|
||||
|
||||
private static LogDbContext InitializeDbContext()
|
||||
{
|
||||
HutaoContext myDocument = new(new());
|
||||
LogDbContext logDbContext = LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
|
||||
string logDbName = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Log.db");
|
||||
LogDbContext logDbContext = LogDbContext.Create($"Data Source={logDbName}");
|
||||
if (logDbContext.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
Debug.WriteLine("[Debug] Performing LogDbContext Migrations");
|
||||
@@ -70,7 +69,7 @@ public sealed class LogEntryQueue : IDisposable
|
||||
}
|
||||
|
||||
// only raw sql can pass
|
||||
logDbContext.Database.ExecuteSqlRaw("DELETE FROM logs WHERE Exception IS NULL");
|
||||
logDbContext.Logs.Where(log => log.Exception == null).ExecuteDelete();
|
||||
return logDbContext;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ internal static class ScheduleTaskHelper
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: 似乎可以不删除任务,直接注册已经包含了更新功能
|
||||
SchedulerTask? targetTask = TaskService.Instance.GetTask(DailyNoteRefreshTaskName);
|
||||
if (targetTask != null)
|
||||
{
|
||||
@@ -36,12 +37,9 @@ internal static class ScheduleTaskHelper
|
||||
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
|
||||
return true;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (COMException)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,32 @@ internal static class SettingKeys
|
||||
/// 导航侧栏是否展开
|
||||
/// </summary>
|
||||
public const string IsNavPaneOpen = "IsNavPaneOpen";
|
||||
|
||||
/// <summary>
|
||||
/// 启动次数
|
||||
/// </summary>
|
||||
public const string LaunchTimes = "LaunchTimes";
|
||||
|
||||
/// <summary>
|
||||
/// 静态资源合约
|
||||
/// 新增合约时 请注意
|
||||
/// <see cref="StaticResource.FulfillAllContracts"/>
|
||||
/// 与 <see cref="StaticResource.IsAnyUnfulfilledContractPresent"/>
|
||||
/// </summary>
|
||||
public const string StaticResourceV1Contract = "StaticResourceV1Contract";
|
||||
|
||||
/// <summary>
|
||||
/// 静态资源合约V2 成就图标与物品图标
|
||||
/// </summary>
|
||||
public const string StaticResourceV2Contract = "StaticResourceV2Contract";
|
||||
|
||||
/// <summary>
|
||||
/// 静态资源合约V3 刷新 Skill Talent
|
||||
/// </summary>
|
||||
public const string StaticResourceV3Contract = "StaticResourceV3Contract";
|
||||
|
||||
/// <summary>
|
||||
/// 静态资源合约V4 刷新 AvatarIcon
|
||||
/// </summary>
|
||||
public const string StaticResourceV4Contract = "StaticResourceV4Contract";
|
||||
}
|
||||
43
src/Snap.Hutao/Snap.Hutao/Core/Setting/StaticResource.cs
Normal file
43
src/Snap.Hutao/Snap.Hutao/Core/Setting/StaticResource.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Setting;
|
||||
|
||||
/// <summary>
|
||||
/// 静态资源
|
||||
/// </summary>
|
||||
internal static class StaticResource
|
||||
{
|
||||
/// <summary>
|
||||
/// 完成所有合约
|
||||
/// </summary>
|
||||
public static void FulfillAllContracts()
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
|
||||
LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
|
||||
LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true);
|
||||
LocalSetting.Set(SettingKeys.StaticResourceV4Contract, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供的合约是否未完成
|
||||
/// </summary>
|
||||
/// <param name="contractKey">合约的键</param>
|
||||
/// <returns>合约是否未完成</returns>
|
||||
public static bool IsContractUnfulfilled(string contractKey)
|
||||
{
|
||||
return !LocalSetting.Get(contractKey, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否有任何静态资源合约尚未完成
|
||||
/// </summary>
|
||||
/// <returns>静态资源合约尚未完成</returns>
|
||||
public static bool IsAnyUnfulfilledContractPresent()
|
||||
{
|
||||
return !LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false)
|
||||
|| (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false))
|
||||
|| (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false))
|
||||
|| (!LocalSetting.Get(SettingKeys.StaticResourceV4Contract, false));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">包装类型</typeparam>
|
||||
public sealed class CancellationTokenTaskCompletionSource : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The cancellation token registration, if any. This is <c>null</c> if the registration was not necessary.
|
||||
/// </summary>
|
||||
private readonly IDisposable? registration;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a task for the specified cancellation token, registering with the token if necessary.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token to observe.</param>
|
||||
public CancellationTokenTaskCompletionSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Task = Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
TaskCompletionSource tcs = new();
|
||||
registration = cancellationToken.Register(() => tcs.TrySetResult(), useSynchronizationContext: false);
|
||||
Task = tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the task for the source cancellation token.
|
||||
/// </summary>
|
||||
public Task Task { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the cancellation token registration, if any. Note that this may cause <see cref="Task"/> to never complete.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
registration?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 在复杂的异步方法环境下
|
||||
/// 指示方法的线程访问状态
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
internal class ThreadAccessAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 指示方法的进入线程访问状态
|
||||
/// </summary>
|
||||
/// <param name="enter">进入状态</param>
|
||||
public ThreadAccessAttribute(ThreadAccessState enter)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 线程访问情况
|
||||
/// </summary>
|
||||
internal enum ThreadAccessState
|
||||
{
|
||||
/// <summary>
|
||||
/// 任何线程均有可能访问该方法
|
||||
/// </summary>
|
||||
AnyThread,
|
||||
|
||||
/// <summary>
|
||||
/// 仅主线程有机会访问该方法
|
||||
/// 仅允许主线程访问该方法
|
||||
/// </summary>
|
||||
MainThread,
|
||||
}
|
||||
@@ -6,9 +6,30 @@ using System.Collections.Concurrent;
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 并发<see cref="CancellationTokenSource"/>
|
||||
/// 无区分项的并发<see cref="CancellationTokenSource"/>
|
||||
/// </summary>
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal class ConcurrentCancellationTokenSource
|
||||
{
|
||||
private CancellationTokenSource source = new();
|
||||
|
||||
/// <summary>
|
||||
/// 注册取消令牌
|
||||
/// </summary>
|
||||
/// <returns>取消令牌</returns>
|
||||
public CancellationToken Register()
|
||||
{
|
||||
source.Cancel();
|
||||
source = new();
|
||||
return source.Token;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 有区分项的并发<see cref="CancellationTokenSource"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">项类型</typeparam>
|
||||
[SuppressMessage("", "SA1402")]
|
||||
internal class ConcurrentCancellationTokenSource<TItem>
|
||||
where TItem : notnull
|
||||
{
|
||||
@@ -17,7 +38,7 @@ internal class ConcurrentCancellationTokenSource<TItem>
|
||||
/// <summary>
|
||||
/// 为某个项注册取消令牌
|
||||
/// </summary>
|
||||
/// <param name="item">项</param>
|
||||
/// <param name="item">区分项</param>
|
||||
/// <returns>取消令牌</returns>
|
||||
public CancellationToken Register(TItem item)
|
||||
{
|
||||
@@ -28,4 +49,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
|
||||
|
||||
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 调度器队列拓展
|
||||
/// </summary>
|
||||
public static class DispatcherQueueExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 在调度器队列同步调用,直到执行结束,会持续阻塞当前线程
|
||||
/// </summary>
|
||||
/// <param name="dispatcherQueue">调度器队列</param>
|
||||
/// <param name="action">执行的回调</param>
|
||||
public static void Invoke(this DispatcherQueue dispatcherQueue, Action action)
|
||||
{
|
||||
ManualResetEventSlim blockEvent = new();
|
||||
dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
action();
|
||||
blockEvent.Set();
|
||||
});
|
||||
|
||||
blockEvent.Wait();
|
||||
}
|
||||
}
|
||||
@@ -43,9 +43,6 @@ public readonly struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueue
|
||||
/// <inheritdoc/>
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
dispatherQueue.TryEnqueue(() =>
|
||||
{
|
||||
continuation();
|
||||
});
|
||||
dispatherQueue.TryEnqueue(() => continuation());
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,19 @@ public static class SemaphoreSlimExtensions
|
||||
/// 异步进入信号量
|
||||
/// </summary>
|
||||
/// <param name="semaphoreSlim">信号量</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>可释放的对象,用于释放信号量</returns>
|
||||
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim)
|
||||
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim, CancellationToken token = default)
|
||||
{
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
throw new OperationCanceledException("信号量已经被释放,操作取消", ex);
|
||||
}
|
||||
|
||||
return new SemaphoreSlimReleaser(semaphoreSlim);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,4 +95,4 @@ public static class TaskExtensions
|
||||
onException?.Invoke(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
@@ -10,6 +11,21 @@ namespace Snap.Hutao.Core.Threading;
|
||||
/// </summary>
|
||||
internal static class ThreadHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 主线程队列
|
||||
/// </summary>
|
||||
private static volatile DispatcherQueue? dispatcherQueue;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
DispatcherQueueSynchronizationContext context = new(dispatcherQueue);
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用此静态方法以 异步切换到 后台线程
|
||||
/// </summary>
|
||||
@@ -29,6 +45,23 @@ internal static class ThreadHelper
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static DispatherQueueSwitchOperation SwitchToMainThreadAsync()
|
||||
{
|
||||
return new(Program.DispatcherQueue!);
|
||||
return new(dispatcherQueue!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在主线程上同步等待执行操作
|
||||
/// </summary>
|
||||
/// <param name="action">操作</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeOnMainThread(Action action)
|
||||
{
|
||||
if (dispatcherQueue!.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatcherQueue.Invoke(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public enum BackdropType
|
||||
/// <summary>
|
||||
/// 无
|
||||
/// </summary>
|
||||
None = 0,
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// 亚克力
|
||||
|
||||
@@ -135,6 +135,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
logger.LogInformation(EventIds.WindowState, "Postion: [{pos}], Size: [{size}]", pos, size);
|
||||
|
||||
// appWindow.Show(true);
|
||||
// appWindow.Show can't bring window to top.
|
||||
window.Activate();
|
||||
|
||||
systemBackdrop = new(window);
|
||||
@@ -147,6 +148,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
IMessenger messenger = Ioc.Default.GetRequiredService<IMessenger>();
|
||||
messenger.Register<BackdropTypeChangedMessage>(this);
|
||||
messenger.Register<FlyoutOpenCloseMessage>(this);
|
||||
|
||||
window.Closed += OnWindowClosed;
|
||||
}
|
||||
|
||||
@@ -190,7 +192,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
}
|
||||
else
|
||||
{
|
||||
double scale = Persistence.GetScaleForWindow(handle);
|
||||
double scale = Persistence.GetScaleForWindowHandle(handle);
|
||||
|
||||
// 48 is the navigation button leftInset
|
||||
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), titleBar.ActualSize).Scale(scale);
|
||||
|
||||
@@ -8,6 +8,7 @@ using Snap.Hutao.Win32;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
@@ -22,28 +23,24 @@ internal static class Persistence
|
||||
/// </summary>
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
/// <param name="persistSize">持久化尺寸</param>
|
||||
/// <param name="size">初始尺寸</param>
|
||||
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
|
||||
/// <param name="initialSize">初始尺寸</param>
|
||||
public static unsafe void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 initialSize)
|
||||
{
|
||||
// Set first launch size.
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd);
|
||||
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
||||
|
||||
if (persistSize)
|
||||
{
|
||||
RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedSize.Width * persistedSize.Height > 848 * 524)
|
||||
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedRect.Size() >= initialSize.Size())
|
||||
{
|
||||
rect = persistedSize;
|
||||
rect = persistedRect;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
TransformToCenterScreen(&rect);
|
||||
}
|
||||
|
||||
TransformToCenterScreen(&rect);
|
||||
appWindow.MoveAndResize(rect);
|
||||
}
|
||||
|
||||
@@ -53,7 +50,15 @@ internal static class Persistence
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
public static void Save(AppWindow appWindow)
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
|
||||
GetWindowPlacement(hwnd, ref windowPlacement);
|
||||
|
||||
// prevent save value when we are maximized.
|
||||
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,7 +66,7 @@ internal static class Persistence
|
||||
/// </summary>
|
||||
/// <param name="hwnd">窗体句柄</param>
|
||||
/// <returns>缩放比</returns>
|
||||
public static double GetScaleForWindow(HWND hwnd)
|
||||
public static double GetScaleForWindowHandle(HWND hwnd)
|
||||
{
|
||||
uint dpi = GetDpiForWindow(hwnd);
|
||||
return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero);
|
||||
@@ -69,7 +74,7 @@ internal static class Persistence
|
||||
|
||||
private static SizeInt32 TransformSizeForWindow(SizeInt32 size, HWND hwnd)
|
||||
{
|
||||
double scale = GetScaleForWindow(hwnd);
|
||||
double scale = GetScaleForWindowHandle(hwnd);
|
||||
return new((int)(size.Width * scale), (int)(size.Height * scale));
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
|
||||
{
|
||||
case WM_GETMINMAXINFO:
|
||||
{
|
||||
double scalingFactor = Persistence.GetScaleForWindow(hwnd);
|
||||
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
|
||||
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="BinaryReader"/> 扩展
|
||||
/// </summary>
|
||||
public static class BinaryReaderExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断是否处于流的结尾
|
||||
/// </summary>
|
||||
/// <param name="reader">读取器</param>
|
||||
/// <returns>是否处于流的结尾</returns>
|
||||
public static bool EndOfStream(this BinaryReader reader)
|
||||
{
|
||||
return reader.BaseStream.Position >= reader.BaseStream.Length;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public static class DateTimeOffsetExtension
|
||||
{
|
||||
if (keepTicks)
|
||||
{
|
||||
dateTimeOffset += TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now).Negate();
|
||||
dateTimeOffset -= TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
return dateTimeOffset.ToLocalTime();
|
||||
|
||||
@@ -77,4 +77,32 @@ public static partial class EnumerableExtension
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
|
||||
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
where TKey : notnull
|
||||
{
|
||||
Dictionary<TKey, TSource> dictionary = new();
|
||||
|
||||
foreach (TSource value in source)
|
||||
{
|
||||
dictionary[keySelector(value)] = value;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
|
||||
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
|
||||
where TKey : notnull
|
||||
{
|
||||
Dictionary<TKey, TValue> dictionary = new();
|
||||
|
||||
foreach (TSource value in source)
|
||||
{
|
||||
dictionary[keySelector(value)] = valueSelector(value);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
@@ -37,6 +38,7 @@ public static partial class EnumerableExtension
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <returns>源列表或空列表</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static List<TSource> EmptyIfNull<TSource>(this List<TSource>? source)
|
||||
{
|
||||
return source ?? new();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
/// <summary>
|
||||
@@ -8,26 +10,6 @@ namespace Snap.Hutao.Extension;
|
||||
/// </summary>
|
||||
public static partial class EnumerableExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <typeparam name="TKey">计数的键类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <param name="keySelector">键选择器</param>
|
||||
/// <returns>计数表</returns>
|
||||
public static IEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
where TKey : notnull, IEquatable<TKey>
|
||||
{
|
||||
CounterInt32<TKey> counter = new();
|
||||
foreach (TSource item in source)
|
||||
{
|
||||
counter.Increase(keySelector(item));
|
||||
}
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 如果传入集合不为空则原路返回,
|
||||
/// 如果传入集合为空返回一个集合的空集
|
||||
@@ -64,56 +46,14 @@ public static partial class EnumerableExtension
|
||||
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
|
||||
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
where TKey : notnull
|
||||
{
|
||||
Dictionary<TKey, TSource> dictionary = new();
|
||||
|
||||
foreach (TSource value in source)
|
||||
{
|
||||
dictionary[keySelector(value)] = value;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
|
||||
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
|
||||
where TKey : notnull
|
||||
{
|
||||
Dictionary<TKey, TValue> dictionary = new();
|
||||
|
||||
foreach (TSource value in source)
|
||||
{
|
||||
dictionary[keySelector(value)] = valueSelector(value);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个对 <see cref="TItem"/> 类型的计数器
|
||||
/// 转换到 <see cref="ObservableCollection{T}"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">待计数的类型</typeparam>
|
||||
private class CounterInt32<TItem> : Dictionary<TItem, int>
|
||||
where TItem : notnull, IEquatable<TItem>
|
||||
/// <typeparam name="T">类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <returns><see cref="ObservableCollection{T}"/></returns>
|
||||
public static ObservableCollection<T> ToObservableCollection<T>(this IEnumerable<T> source)
|
||||
{
|
||||
/// <summary>
|
||||
/// 增加计数器
|
||||
/// </summary>
|
||||
/// <param name="item">物品</param>
|
||||
public void Increase(TItem? item)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
if (!ContainsKey(item))
|
||||
{
|
||||
this[item] = 0;
|
||||
}
|
||||
|
||||
this[item] += 1;
|
||||
}
|
||||
}
|
||||
return new ObservableCollection<T>(source);
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,6 @@ namespace Snap.Hutao.Extension;
|
||||
[SuppressMessage("", "CA2254")]
|
||||
public static class LoggerExtension
|
||||
{
|
||||
/// <inheritdoc cref="LoggerExtensions.LogInformation(ILogger, string?, object?[])"/>
|
||||
public static T LogInformation<T>(this ILogger logger, string message, params object?[] param)
|
||||
{
|
||||
logger.LogInformation(message, param);
|
||||
return default!;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="LoggerExtensions.LogWarning(ILogger, string?, object?[])"/>
|
||||
public static T LogWarning<T>(this ILogger logger, string message, params object?[] param)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
/// <summary>
|
||||
@@ -8,36 +10,15 @@ namespace Snap.Hutao.Extension;
|
||||
/// </summary>
|
||||
public static class NumberExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取从右向左某位上的数字
|
||||
/// </summary>
|
||||
/// <param name="x">源</param>
|
||||
/// <param name="place">位</param>
|
||||
/// <returns>数字</returns>
|
||||
public static int AtPlace(this int x, int place)
|
||||
{
|
||||
return (int)(x / Math.Pow(10, place - 1)) % 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算给定整数的位数
|
||||
/// </summary>
|
||||
/// <param name="x">给定的整数</param>
|
||||
/// <returns>位数</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Place(this int x)
|
||||
{
|
||||
// Benchmarked and compared as a most optimized solution
|
||||
return (int)(MathF.Log10(x) + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算给定整数的位数
|
||||
/// </summary>
|
||||
/// <param name="x">给定的整数</param>
|
||||
/// <returns>位数</returns>
|
||||
public static int Place(this long x)
|
||||
{
|
||||
// Benchmarked and compared as a most optimized solution
|
||||
return (int)(MathF.Log10(x) + 1);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 对象扩展
|
||||
/// </summary>
|
||||
public static class ObjectExtension
|
||||
internal static class ObjectExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="as"/> 的链式调用扩展
|
||||
@@ -19,4 +19,4 @@ public static class ObjectExtension
|
||||
{
|
||||
return obj as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Snap.Hutao.Factory.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="AsyncRelayCommand"/> with additional processing.
|
||||
/// </summary>
|
||||
public interface IAsyncRelayCommandFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand Create(Func<Task> execute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute);
|
||||
|
||||
/// <summary>
|
||||
/// Create a reference to AsyncRelayCommand.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <returns>AsyncRelayCommand.</returns>
|
||||
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Factory.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 内容对话框工厂
|
||||
/// </summary>
|
||||
internal interface IContentDialogFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步确认
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="content">内容</param>
|
||||
/// <returns>结果</returns>
|
||||
ValueTask<ContentDialogResult> ConfirmAsync(string title, string content);
|
||||
|
||||
/// <summary>
|
||||
/// 异步确认或取消
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="content">内容</param>
|
||||
/// <param name="defaultButton">默认按钮</param>
|
||||
/// <returns>结果</returns>
|
||||
ValueTask<ContentDialogResult> ConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close);
|
||||
|
||||
/// <summary>
|
||||
/// 异步创建一个新的内容对话框,用于提示未知的进度
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <returns>内容对话框</returns>
|
||||
ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
|
||||
namespace Snap.Hutao.Factory;
|
||||
|
||||
/// <inheritdoc cref="IAsyncRelayCommandFactory"/>
|
||||
[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
|
||||
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
|
||||
{
|
||||
private readonly ILogger<AsyncRelayCommandFactory> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的异步命令工厂
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand<T>(execute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand<T>(cancelableExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand<T>(execute, canExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand<T>(cancelableExecute, canExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand Create(Func<Task> execute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand(execute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand(cancelableExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand(execute, canExecute));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute)
|
||||
{
|
||||
return Register(new AsyncRelayCommand(cancelableExecute, canExecute));
|
||||
}
|
||||
|
||||
private AsyncRelayCommand Register(AsyncRelayCommand command)
|
||||
{
|
||||
ReportException(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
private AsyncRelayCommand<T> Register<T>(AsyncRelayCommand<T> command)
|
||||
{
|
||||
ReportException(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
private void ReportException(IAsyncRelayCommand command)
|
||||
{
|
||||
command.PropertyChanged += (sender, args) =>
|
||||
{
|
||||
if (sender is IAsyncRelayCommand asyncRelayCommand)
|
||||
{
|
||||
if (args.PropertyName == nameof(AsyncRelayCommand.ExecutionTask))
|
||||
{
|
||||
if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
|
||||
{
|
||||
Exception baseException = exception.GetBaseException();
|
||||
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
|
||||
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(baseException).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
84
src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs
Normal file
84
src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Factory;
|
||||
|
||||
/// <inheritdoc cref="IContentDialogFactory"/>
|
||||
[Injection(InjectAs.Transient, typeof(IContentDialogFactory))]
|
||||
internal class ContentDialogFactory : IContentDialogFactory
|
||||
{
|
||||
private readonly MainWindow mainWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的内容对话框工厂
|
||||
/// </summary>
|
||||
/// <param name="mainWindow">主窗体</param>
|
||||
public ContentDialogFactory(MainWindow mainWindow)
|
||||
{
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ContentDialogResult> ConfirmAsync(string title, string content)
|
||||
{
|
||||
ContentDialog dialog = await CreateForConfirmAsync(title, content).ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ContentDialogResult> ConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
|
||||
{
|
||||
ContentDialog dialog = await CreateForConfirmCancelAsync(title, content, defaultButton).ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
Title = title,
|
||||
Content = new ProgressBar() { IsIndeterminate = true },
|
||||
};
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private async ValueTask<ContentDialog> CreateForConfirmAsync(string title, string content)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
Title = title,
|
||||
Content = content,
|
||||
DefaultButton = ContentDialogButton.Primary,
|
||||
PrimaryButtonText = "确认",
|
||||
};
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private async ValueTask<ContentDialog> CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
Title = title,
|
||||
Content = content,
|
||||
DefaultButton = defaultButton,
|
||||
PrimaryButtonText = "确认",
|
||||
CloseButtonText = "取消",
|
||||
};
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 翻译
|
||||
/// </summary>
|
||||
internal interface ITranslation
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取对应键的值
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>对应的值</returns>
|
||||
string this[string key] { get; }
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 中文翻译 zh-CN
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal class LanguagezhCN : ITranslation
|
||||
{
|
||||
private readonly Dictionary<string, string> translations = new()
|
||||
{
|
||||
["AppName"] = "胡桃",
|
||||
|
||||
["NavigationViewItem_Activity"] = "活动",
|
||||
["NavigationViewItem_Achievement"] = "成就",
|
||||
["NavigationViewItem_Wiki_Avatar"] = "角色",
|
||||
["NavigationViewItem_GachaLog"] = "祈愿记录",
|
||||
|
||||
["UserPanel_Account"] = "账号",
|
||||
["UserPanel_Add_Account"] = "添加新账号",
|
||||
["UserPanel_GameRole"] = "角色",
|
||||
|
||||
["Achievement_Search_PlaceHolder"] = "搜索成就名称,描述或编号",
|
||||
["Achievement_Create_Archive"] = "创建新存档",
|
||||
["Achievement_Delete_Archive"] = "删除当前存档",
|
||||
["Achievement_Import"] = "导入",
|
||||
["Achievement_Import_From_Clipboard"] = "从剪贴板导入",
|
||||
["Achievement_Import_From_File"] = "从 UIAF 文件导入",
|
||||
["Achievement_IncompleteItemFirst"] = "优先未完成",
|
||||
|
||||
["Wiki_Avatar_Filter"] = "筛选",
|
||||
["Wiki_Avatar_Filter_Element"] = "元素",
|
||||
["Wiki_Avatar_Filter_Association"] = "所属",
|
||||
["Wiki_Avatar_Filter_Weapon"] = "武器",
|
||||
["Wiki_Avatar_Filter_Quality"] = "星级",
|
||||
["Wiki_Avatar_Filter_Body"] = "体型",
|
||||
["Wiki_Avatar_Fetter_Native"] = "所属",
|
||||
["Wiki_Avatar_Fetter_Constellation"] = "命之座",
|
||||
["Wiki_Avatar_Fetter_Birth"] = "生日",
|
||||
["Wiki_Avatar_Fetter_CvChinese"] = "汉语 CV",
|
||||
["Wiki_Avatar_Fetter_CvJapanese"] = "日语 CV",
|
||||
["Wiki_Avatar_Fetter_CvEnglish"] = "英语 CV",
|
||||
["Wiki_Avatar_Fetter_CvKorean"] = "韩语 CV",
|
||||
["Wiki_Avatar_Subtitle_Skill"] = "天赋",
|
||||
["Wiki_Avatar_Subtitle_Talent"] = "命之座",
|
||||
["Wiki_Avatar_Subtitle_Other"] = "其他",
|
||||
["Wiki_Avatar_Expander_Costumes"] = "衣装",
|
||||
["Wiki_Avatar_Expander_Fetters"] = "资料",
|
||||
["Wiki_Avatar_Expander_FetterStories"] = "故事",
|
||||
|
||||
["DescParamComboBox_Level"] = "等级",
|
||||
|
||||
["GachaLog_Refresh"] = "刷新",
|
||||
["GachaLog_Refresh_WebCache"] = "从缓存刷新",
|
||||
["GachaLog_Refresh_ManualInput"] = "手动输入Url",
|
||||
["GachaLog_Refresh_Aggressive"] = "全量刷新",
|
||||
["GachaLog_Import"] = "导入",
|
||||
["GachaLog_Import_UIGFJ"] = "从 UIGF Json 文件导入",
|
||||
["GachaLog_Import_UIGFW"] = "从 UIGF Excel 文件导入",
|
||||
["GachaLog_Export"] = "导出",
|
||||
["GachaLog_Export_UIGFJ"] = "导出到 UIGF Json 文件",
|
||||
["GachaLog_Export_UIGFW"] = "导出到 UIGF Excel 文件",
|
||||
["GachaLog_PivotItem_Summary"] = "总览",
|
||||
["GachaLog_PivotItem_History"] = "历史",
|
||||
["GachaLog_PivotItem_Avatar"] = "角色",
|
||||
["GachaLog_PivotItem_Weapon"] = "武器",
|
||||
|
||||
["StatisticsCard_Guarantee"] = "保底",
|
||||
["StatisticsCard_Up"] = "保底",
|
||||
["StatisticsCard_Pull"] = "抽",
|
||||
["StatisticsCard_Orange"] = "五星",
|
||||
["StatisticsCard_Purple"] = "四星",
|
||||
["StatisticsCard_Blue"] = "三星",
|
||||
["StatisticsCard_OrangeAverage"] = "五星平均抽数",
|
||||
["StatisticsCard_UpOrangeAverage"] = "UP 平均抽数",
|
||||
|
||||
["Setting_Group_AboutHutao"] = "关于 胡桃",
|
||||
["Setting_HutaoIcon_Description_Part1"] = "胡桃 图标由 ",
|
||||
["Setting_HutaoIcon_Description_Part2"] = "纸绘,并由 ",
|
||||
["Setting_HutaoIcon_Description_Part3"] = " 后期处理后,授权使用。",
|
||||
["Setting_Feedback_Header"] = "反馈",
|
||||
["Setting_Feedback_Description"] = "只处理在 Github 上反馈的问题",
|
||||
["Setting_Feedback_Hyperlink"] = "只处理在 Github 上反馈的问题",
|
||||
["Setting_UpdateCheck_Header"] = "检查更新",
|
||||
["Setting_UpdateCheck_Description"] = "根本没有检查更新选项",
|
||||
["Setting_UpdateCheck_Info"] = "都说了没有了",
|
||||
["Setting_Group_Experimental"] = "测试功能",
|
||||
["Setting_DataFolder_Header"] = "打开 数据 文件夹",
|
||||
["Setting_DataFolder_Description"] = "用户数据/日志/元数据在此处存放",
|
||||
["Setting_DataFolder_Action"] = "打开",
|
||||
["Setting_CacheFolder_Header"] = "打开 缓存 文件夹",
|
||||
["Setting_CacheFolder_Description"] = "图片缓存在此处存放",
|
||||
["Setting_CacheFolder_Action"] = "打开",
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (translations.TryGetValue(key, out string? result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
x:Class="Snap.Hutao.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shv="using:Snap.Hutao.View"
|
||||
@@ -13,6 +14,19 @@
|
||||
Height="44"
|
||||
Margin="48,0,0,0"/>
|
||||
|
||||
<shv:MainView/>
|
||||
<cwuc:SwitchPresenter x:Name="ContentSwitchPresenter">
|
||||
<cwuc:Case>
|
||||
<cwuc:Case.Value>
|
||||
<x:Boolean>False</x:Boolean>
|
||||
</cwuc:Case.Value>
|
||||
<shv:MainView/>
|
||||
</cwuc:Case>
|
||||
<cwuc:Case>
|
||||
<cwuc:Case.Value>
|
||||
<x:Boolean>True</x:Boolean>
|
||||
</cwuc:Case.Value>
|
||||
<shv:WelcomeView/>
|
||||
</cwuc:Case>
|
||||
</cwuc:SwitchPresenter>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.Message;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
@@ -13,7 +16,7 @@ namespace Snap.Hutao;
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
[SuppressMessage("", "CA1001")]
|
||||
public sealed partial class MainWindow : Window, IExtendedWindowSource
|
||||
public sealed partial class MainWindow : Window, IExtendedWindowSource, IRecipient<WelcomeStateCompleteMessage>
|
||||
{
|
||||
private const int MinWidth = 848;
|
||||
private const int MinHeight = 524;
|
||||
@@ -27,6 +30,11 @@ public sealed partial class MainWindow : Window, IExtendedWindowSource
|
||||
ExtendedWindow<MainWindow>.Initialize(this);
|
||||
IsPresent = true;
|
||||
Closed += (s, e) => IsPresent = false;
|
||||
|
||||
Ioc.Default.GetRequiredService<IMessenger>().Register(this);
|
||||
|
||||
// If not complete we should present the welcome view.
|
||||
ContentSwitchPresenter.Value = StaticResource.IsAnyUnfulfilledContractPresent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,4 +57,10 @@ public sealed partial class MainWindow : Window, IExtendedWindowSource
|
||||
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
|
||||
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Receive(WelcomeStateCompleteMessage message)
|
||||
{
|
||||
ContentSwitchPresenter.Value = false;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ namespace Snap.Hutao.Message;
|
||||
/// <summary>
|
||||
/// 用户切换消息
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal class UserChangedMessage : ValueChangedMessage<User>
|
||||
{
|
||||
}
|
||||
@@ -7,7 +7,6 @@ namespace Snap.Hutao.Message;
|
||||
/// 值变化消息
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">值的类型</typeparam>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal abstract class ValueChangedMessage<TValue>
|
||||
where TValue : class
|
||||
{
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Message;
|
||||
|
||||
/// <summary>
|
||||
/// 欢迎状态完成消息
|
||||
/// </summary>
|
||||
public class WelcomeStateCompleteMessage
|
||||
{
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class Avatar : ICalculableSource<ICalculableAvatar>
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public Weapon Weapon { get; set; } = default!;
|
||||
public Weapon? Weapon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物列表
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Cultivation;
|
||||
|
||||
@@ -25,7 +24,7 @@ public class CultivateItem : ObservableObject
|
||||
Inner = inner;
|
||||
Entity = entity;
|
||||
isFinished = Entity.IsFinished;
|
||||
IsToday = CultivateItemHelper.IsTodaysMaterial(inner.Id, DateTimeOffset.Now);
|
||||
IsToday = inner.IsTodaysItem();
|
||||
|
||||
FinishStateCommand = new RelayCommand(FlipIsFinished);
|
||||
}
|
||||
@@ -55,7 +54,6 @@ public class CultivateItem : ObservableObject
|
||||
if (SetProperty(ref isFinished, value))
|
||||
{
|
||||
Entity.IsFinished = value;
|
||||
Ioc.Default.GetRequiredService<ICultivationService>().SaveCultivateItem(Entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Cultivation;
|
||||
|
||||
/// <summary>
|
||||
/// 养成物品帮助类
|
||||
/// </summary>
|
||||
public static class CultivateItemHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断是否为当日物品
|
||||
/// </summary>
|
||||
/// <param name="itemId">材料Id</param>
|
||||
/// <param name="now">时间</param>
|
||||
/// <returns>是否为当日物品</returns>
|
||||
public static bool IsTodaysMaterial(int itemId, DateTimeOffset now)
|
||||
{
|
||||
DateTimeOffset utcNow = now.ToUniversalTime();
|
||||
utcNow = utcNow.AddHours(4);
|
||||
DayOfWeek dayOfWeek = utcNow.DayOfWeek;
|
||||
|
||||
return dayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Monday or DayOfWeek.Thursday => itemId switch
|
||||
{
|
||||
104301 or 104302 or 104303 => true, // 「自由」
|
||||
104310 or 104311 or 104312 => true, // 「繁荣」
|
||||
104320 or 104321 or 104322 => true, // 「浮世」
|
||||
104329 or 104330 or 104331 => true, // 「诤言」
|
||||
114001 or 114002 or 114003 or 114004 => true, // 高塔孤王
|
||||
114013 or 114014 or 114015 or 114016 => true, // 孤云寒林
|
||||
114025 or 114026 or 114027 or 114028 => true, // 远海夷地
|
||||
114037 or 114038 or 114039 or 114040 => true, // 谧林涓露
|
||||
_ => false,
|
||||
},
|
||||
DayOfWeek.Tuesday or DayOfWeek.Friday => itemId switch
|
||||
{
|
||||
104304 or 104305 or 104306 => true, // 「抗争」
|
||||
104313 or 104314 or 104315 => true, // 「勤劳」
|
||||
104323 or 104324 or 104325 => true, // 「风雅」
|
||||
104332 or 104333 or 104334 => true, // 「巧思」
|
||||
114005 or 114006 or 114007 or 114008 => true, // 凛风奔狼
|
||||
114017 or 114018 or 114019 or 114020 => true, // 雾海云间
|
||||
114029 or 114030 or 114031 or 114032 => true, // 鸣神御灵
|
||||
114041 or 114042 or 114043 or 114044 => true, // 绿洲花园
|
||||
_ => false,
|
||||
},
|
||||
DayOfWeek.Wednesday or DayOfWeek.Saturday => itemId switch
|
||||
{
|
||||
104307 or 104308 or 104309 => true, // 「诗文」
|
||||
104316 or 104317 or 104318 => true, // 「黄金」
|
||||
104326 or 104327 or 104328 => true, // 「天光」
|
||||
104335 or 104336 or 104337 => true, // 「笃行」
|
||||
114009 or 114010 or 114011 or 114012 => true, // 狮牙斗士
|
||||
114021 or 114022 or 114023 or 114024 => true, // 漆黑陨铁
|
||||
114033 or 114034 or 114035 or 114036 => true, // 今昔剧画
|
||||
114045 or 114046 or 114047 or 114048 => true, // 谧林涓露
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user