Compare commits

...

48 Commits

Author SHA1 Message Date
Masterain
7a5f1ded35 Update PublishDistribution.yml 2022-12-12 17:00:31 -08:00
DismissedLight
844c8b7810 Thread pool optimization 2022-12-10 13:50:42 +08:00
DismissedLight
267b34f571 xaml styles 2022-12-05 18:35:45 +08:00
Masterain
0f27ebc12c Update issue templates 2022-12-04 22:48:24 -08:00
DismissedLight
e051787584 add wiki weapon page 2022-11-29 18:16:59 +08:00
DismissedLight
8f273e69b5 support sign in bbs 2022-11-27 16:00:15 +08:00
Masterain
88037049f3 Create feature-request.yml
#265
2022-11-26 22:53:22 -08:00
DismissedLight
8357d3b971 check pooint 2 2022-11-25 16:16:52 +08:00
DismissedLight
d41acc0a77 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-11-25 14:52:11 +08:00
DismissedLight
0dd79d4206 web bridge check point 2022-11-25 14:51:30 +08:00
Masterain
86061e404f Update Issue Templates 2022-11-24 02:00:26 -08:00
DismissedLight
ee4197a18a fix code style 2022-11-24 13:40:14 +08:00
DismissedLight
c90e1ab8b8 obtain v2 stoken by old token #207 2022-11-24 13:19:46 +08:00
Masterain
de40947a7f Update PublishDistribution.yml
- add timeout field
2022-11-23 14:01:43 -08:00
DismissedLight
542a0a4622 fix dailynote refresh time 2022-11-20 15:13:24 +08:00
DismissedLight
a718ba16e2 introduce cache download retry and host replace 2022-11-20 14:54:04 +08:00
DismissedLight
c0d670c5b6 fix avatarInfo def value 2022-11-20 13:28:54 +08:00
DismissedLight
efcf0620d7 support ltoken requests #207 2022-11-18 21:39:45 +08:00
DismissedLight
a8bc0cf9b2 fixup ds algorithm call 2022-11-18 16:20:07 +08:00
DismissedLight
f1dda029e8 login by password 2022-11-16 19:48:07 +08:00
DismissedLight
d8b77369aa support rsa encrypt for passport api #207
Co-Authored-By: HolographicHat <58809250+HolographicHat@users.noreply.github.com>
2022-11-14 20:46:32 +08:00
DismissedLight
0be84a2585 support PROD salt for #207 2022-11-14 16:37:17 +08:00
DismissedLight
e29e12c9fe support login api for #207 2022-11-13 21:40:51 +08:00
DismissedLight
283df388bb fix game records for #207 2022-11-13 20:36:12 +08:00
DismissedLight
971f319b76 support Action Ticket for #207 2022-11-13 19:01:57 +08:00
DismissedLight
58b34ea60a support verifyLtoken for #207 2022-11-13 18:04:08 +08:00
DismissedLight
a150c4a04c support getWidgetData for #207 2022-11-13 15:38:02 +08:00
DismissedLight
9205d51cd5 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-11-11 18:55:08 +08:00
DismissedLight
a66a0b8a23 reduce using statements 2022-11-11 18:55:01 +08:00
Masterain
17c53dce4c Optimize Issue Template 2022-11-11 01:33:22 -08:00
Masterain
5049aa9cb6 Create artifact-rating-rules.yml
Inspired by #198
2022-11-11 01:32:47 -08:00
DismissedLight
aac1e62fd2 was 1.2.0 2022-11-11 17:20:36 +08:00
DismissedLight
5c1f861956 dailynote notification logic 2022-11-10 23:36:00 +08:00
DismissedLight
9667917559 file nesting 2022-11-09 12:08:59 +08:00
DismissedLight
9a3183e917 add launcher resource 2022-11-06 18:25:41 +08:00
DismissedLight
77db918178 update bug report template 2022-11-05 21:35:57 +08:00
DismissedLight
77158fc708 add installer 2022-11-05 20:45:38 +08:00
DismissedLight
f2d63e69ea bug fixes 2022-11-04 17:45:17 +08:00
DismissedLight
9e344f56e0 LaunchGame 2022-11-03 18:19:36 +08:00
DismissedLight
848392f8d4 prevent user service capture scoped app db context 2022-10-30 15:19:21 +08:00
DismissedLight
62d0fb5d05 launch game phase 1 2022-10-28 14:59:31 +08:00
DismissedLight
7a99c44b29 collocation for wiki avatar 2022-10-25 15:47:12 +08:00
DismissedLight
792a701183 fix view model scope 2022-10-25 13:12:50 +08:00
DismissedLight
fa19f7e817 add api cache 2022-10-24 16:12:30 +08:00
DismissedLight
bf5fcb70f8 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-10-21 16:40:45 +08:00
DismissedLight
fda642b72f hutao api v2 page 2022-10-21 16:40:10 +08:00
Masterain
fa650a95c5 Update PublishDistribution.yml 2022-10-18 12:23:53 -07:00
Masterain
02fae69d1e Create PublishDistribution.yml 2022-10-18 12:05:43 -07:00
508 changed files with 21722 additions and 5130 deletions

View File

@@ -0,0 +1,65 @@
name: 圣遗物评分细则建议
description: 为圣遗物评分规则提供你的想法
title: "[Artifact Rating] 请在这里填写角色名称"
labels: area-AvatarInfo
assignees: Lightczx
body:
- type: markdown
attributes:
value: |
请按下方的要求填写完整的问题表单
- type: textarea
id: your-suggested-rule
attributes:
label: 评分细则
description: |
请修改下方表格中的**角色名称**和**各属性权重**,并在表格后添加合适的说明
你可以点击预览按钮preview来查看表格最终会显示出的内容
value: |
|项目|评分权重(0-100)|
|-----|-----|
|角色名称| 旅行者 |
|生命值| 10 |
|攻击力| 10 |
|防御力| 10 |
|暴击率| 10 |
|暴击伤害| 10 |
|元素精通| 10 |
|充能效率| 10 |
|治疗加成| 10 |
|元素伤害| 10 |
validations:
required: true
- type: dropdown
id: no-duplicated-dropdown
attributes:
label: 我确认当前没有其它的该角色的圣遗物评分细则建议
description: 如果有,你应该在已有的工单内回复以提出你的建议
options:
-
-
validations:
required: true
- type: dropdown
id: title-filled-dropdown
attributes:
label: 我确认已设置合适的标题
options:
-
-
validations:
required: true
- type: dropdown
id: all-filled-dropdown
attributes:
label: 我确认已完整填写表格
options:
-
-
validations:
required: true

View File

@@ -1,14 +1,31 @@
name: 问题反馈
description: 告诉我们你的问题
title: "[Bug]: 在这里填写一个合适的标题"
labels: ["BUG"]
assignees:
- Lightczx
body:
- type: markdown
attributes:
value: |
请按下方的要求填写完整的问题表单,以便我们更快的定位问题。
> **请在上方设置一个合适的工单标题**
> 请按下方的要求填写完整的问题表单,以便我们更快的定位问题。
- type: checkboxes
id: checklist
attributes:
label: 检查清单
description: |-
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
options:
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/most-frequent-questions.html),并认为我的问题没有在文档中得到解答
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)
required: true
- label: 我确认没有其他人已经提出了相同或类似的问题
required: true
- label: 我会在下方的表单中附上充足的信息以帮助开发人员确定问题
required: true
- type: input
id: winver
@@ -26,6 +43,28 @@ body:
label: Snap Hutao 版本
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
placeholder: 1.1.0
validations:
required: true
- type: dropdown
id: user-set-category
attributes:
label: 问题分类
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
options:
- 安装和环境
- 成就管理
- 角色信息面板
- 游戏启动
- 实时便笺
- 养成计算
- 文件缓存
- 祈愿记录
- 玩家查询
- 胡桃数据库
- 用户界面
- 公告
- 其它
validations:
required: true
@@ -48,42 +87,10 @@ body:
- type: textarea
id: logs
attributes:
label: 相关的崩溃日志 位于 `%HOMEPATH%/Documents/Hutao/Log.db`
label: 相关的崩溃日志
description: |
在资源管理器中直接输入`%HOMEPATH%/Documents/Hutao`即可进入文件夹
在资源管理器中直接输入`%userprofile%/Documents/Hutao`即可进入文件夹
如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传
如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件
**务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**
render: shell
- type: checkboxes
id: confirm-issue
attributes:
label: 我确认已在表单中附上了充足的补充说明以帮助开发人员确定问题
description: 补充说明包括但不限于:日志文件、抛出的错误信息、截图和录屏
options:
- label:
required: true
- type: checkboxes
id: confirm-no-duplicated-issue
attributes:
label: 我确认没有他人提出相同或类似的问题
description: |
请先通过 Issue 搜索功能确认这不是相同的问题;
[BUG Issues](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aissue+is%3Aopen+label%3ABUG)
你应该在原始 Issue 中通过回复添加有助于解决问题的信息,而不是创建重复的问题;
**没有帮助的重复问题可能会被直接关闭**
options:
- label:
required: true
- type: checkboxes
id: confirm-docs
attributes:
label: 我确认该问题没有在文档中解释
description: Snap Hutao 官方文档:[https://hut.ao](https://hut.ao)
options:
- label:
required: true
> **务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**

View File

@@ -1,7 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 问题与操作指南文档
url: https://www.snapgenshin.com/documents/
about: |
[暂不可用]在发起 Issue 前 请查阅此文档确认文档中尚未包含你的问题的解决方案
- name: 胡桃工具箱 - 官方文档
url: https://hut.ao
about: 请在提出问题前阅读文档

View File

@@ -16,4 +16,4 @@ body:
label: 你想要实现或优化的功能?
description: 详细的描述一下你想要的功能
validations:
required: true
required: true

View File

@@ -0,0 +1,76 @@
name: 网络问题
description: 当网络问题影响到你的程序使用时
title: "[Network]: 在这里填写一个合适的标题"
labels: ["area-Network"]
assignees:
- Lightczx
- Masterain98
body:
- type: markdown
attributes:
value: |
**请先在上方为工单设置一个合适的标题**
**请按下方的要求填写完整的问题表单,以便我们更快的定位问题。**
- type: textarea
id: network-diagnosis-report
attributes:
label: 提交你的网络诊断报告
description: |
停下!
**在填写下面的问题之前请先使用我们的网络诊断工具**
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
- 你可以点击下面的链接以下载网络诊断工具:
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-tool.exe)
- [GitHub](https://github.com/DGP-Studio/Snap.Hutao/files/10081999/network-diagnosis-hutao.zip)
validations:
required: true
- type: input
id: user-geo-location
attributes:
label: 你的地理位置
description: |
中国用户请精确到省级行政区
海外用户请精确到国家
placeholder: 北京
validations:
required: true
- type: dropdown
id: user-isp
attributes:
label: 你的运营商
description: 中国用户请精确到省级行政区,海外地区请精确到国家
options:
- 中国电信
- 中国联通
- 中国移动
- 中国广电
- 其它
validations:
required: true
- type: dropdown
id: user-issue-category
attributes:
label: 你的问题
description: 选择一个问题类别
options:
- 完全无法连接服务器
- 连接速度慢
- 获取到了不正确的页面或数据
- 图片下载错误429 Error
- 其它
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 你的问题(补充)
description: 如果你在上一项中选择了`其它`或者你有更多信息需要提供,请在这里写下来
validations:
required: false

View File

@@ -0,0 +1,41 @@
name: PublishDistribution
on:
release:
types: [published]
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
Publish:
runs-on: ubuntu-latest
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout Repo
uses: actions/checkout@v3
# Download Publish.zip
- name: Download Release
timeout-minutes: 5
uses: robinraju/release-downloader@v1.5
with:
repository: "DGP-Studio/Snap.Hutao"
latest: true
fileName: "*.zip"
out-file-path: ./release-download
# Upload to Drive
- name: Upload Drive
timeout-minutes: 15
env:
RCCONF: ${{ secrets.RCCONF }}
run: |
curl https://rclone.org/install.sh | sudo bash
mkdir -p ~/.config/rclone/
cat << EOF > ~/.config/rclone/rclone.conf
$RCCONF
EOF
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/

4
.gitignore vendored
View File

@@ -11,6 +11,10 @@ src/Snap.Hutao/Snap.Hutao/bin/
src/Snap.Hutao/Snap.Hutao/obj/
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
src/Snap.Hutao/Snap.Hutao.Installer/bin/
src/Snap.Hutao/Snap.Hutao.Installer/obj/
src/Snap.Hutao/Snap.Hutao.Installer/Properties/PublishProfiles/FolderProfile.pubxml.user
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/

View File

@@ -22,6 +22,7 @@
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
* [HolographicHat/YaeAchievement](https://github.com/HolographicHat/YaeAchievement)
* [HolographicHat/MiHoYoWebBridge](https://github.com/HolographicHat/MiHoYoWebBridge)
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
@@ -36,5 +37,4 @@
* [microsoft/vs-threading](https://github.com/microsoft/vs-threading)
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [MiniExcel/MiniExcel](https://github.com/MiniExcel/MiniExcel)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)

6
desktop.ini Normal file
View File

@@ -0,0 +1,6 @@
[.ShellClassInfo]
IconResource=D:\Develop\Projects\Snap.Hutao\src\Snap.Hutao\Snap.Hutao\Assets\Logo.ico,0
[ViewState]
Mode=
Vid=
FolderType=Generic

View File

@@ -1,4 +1,6 @@
[*.cs]
charset = utf-8-bom
[*.cs]
# SA1101: Prefix local calls with this
dotnet_diagnostic.SA1101.severity = none
@@ -76,34 +78,36 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.non_field_members.required_modifiers =
# 命名样式
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_diagnostic.SA1629.severity = silent
dotnet_diagnostic.SA1642.severity = silent
dotnet_diagnostic.SA1629.severity = none
dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0060.severity = none
# SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1208.severity = none
@@ -160,6 +164,7 @@ dotnet_diagnostic.CA1805.severity = suggestion
# VSTHRD111: Use ConfigureAwait(bool)
dotnet_diagnostic.VSTHRD111.severity = suggestion
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_readonly_struct = true:suggestion
[*.vb]
#### 命名样式 ####
@@ -182,29 +187,29 @@ dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.类型.required_modifiers =
dotnet_naming_symbols.类型.required_modifiers =
dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method
dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.非字段成员.required_modifiers =
dotnet_naming_symbols.非字段成员.required_modifiers =
# 命名样式
dotnet_naming_style.以_i_开始.required_prefix = I
dotnet_naming_style.以_i_开始.required_suffix =
dotnet_naming_style.以_i_开始.word_separator =
dotnet_naming_style.以_i_开始.required_suffix =
dotnet_naming_style.以_i_开始.word_separator =
dotnet_naming_style.以_i_开始.capitalization = pascal_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-windows10.0.17763.0</TargetFrameworks>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<TargetFrameworks>net7.0-windows10.0.18362.0</TargetFrameworks>
<TargetPlatformMinVersion>10.0.18362.0</TargetPlatformMinVersion>
<RootNamespace>SettingsUI</RootNamespace>
<Platforms>x64</Platforms>
<RuntimeIdentifiers>win10-x64</RuntimeIdentifiers>
@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.4" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221116.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,50 +4,57 @@
xmlns:contract7NotPresent="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractNotPresent(Windows.Foundation.UniversalApiContract,7)"
xmlns:contract7Present="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,7)">
<Style x:Key="SettingButtonStyle" TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}" >
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Padding" Value="16,5,16,6" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Style
x:Key="SettingButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Padding" Value="16,5,16,6"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
<Style x:Key="HyperlinkButtonStyle" TargetType="HyperlinkButton" >
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Style x:Key="HyperlinkButtonStyle" TargetType="HyperlinkButton">
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
</Style>
<Style x:Key="TextButtonStyle" TargetType="ButtonBase">
<Setter Property="Background" Value="{ThemeResource HyperlinkButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource HyperlinkButtonForeground}" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="Background" Value="{ThemeResource HyperlinkButtonBackground}"/>
<Setter Property="Foreground" Value="{ThemeResource HyperlinkButtonForeground}"/>
<Setter Property="MinWidth" Value="0"/>
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ButtonBase">
<Grid Margin="{TemplateBinding Padding}" CornerRadius="4" Background="{TemplateBinding Background}">
<ContentPresenter x:Name="Text"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
FontWeight="SemiBold"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<Grid
Margin="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="4">
<ContentPresenter
x:Name="Text"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
FontWeight="SemiBold"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundPointerOver}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundPointerOver}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushPointerOver}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
@@ -56,13 +63,13 @@
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundPressed}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundPressed}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushPressed}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
@@ -71,13 +78,13 @@
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushDisabled}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
@@ -85,7 +92,6 @@
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
@@ -93,192 +99,434 @@
</Setter>
</Style>
<!-- This style overrides the default style so that all ToggleSwitches are right aligned, with the label on the left -->
<!-- This style overrides the default style so that all ToggleSwitches are right aligned, with the label on the left -->
<Style x:Key="ToggleSwitchSettingStyle" TargetType="ToggleSwitch">
<Setter Property="Foreground" Value="{ThemeResource ToggleSwitchContentForeground}" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="System,TranslateX" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="Foreground" Value="{ThemeResource ToggleSwitchContentForeground}"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalContentAlignment" Value="Right"/>
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/>
<Setter Property="ManipulationMode" Value="System,TranslateX"/>
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}"/>
<Setter Property="FocusVisualMargin" Value="-7,-3,-7,-3" />
<Setter Property="FocusVisualMargin" Value="-7,-3,-7,-3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleSwitch">
<Grid
contract7Present:CornerRadius="{TemplateBinding CornerRadius}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
contract7Present:CornerRadius="{TemplateBinding CornerRadius}">
BorderThickness="{TemplateBinding BorderThickness}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource ToggleSwitchTopHeaderMargin}"
VerticalAlignment="Top"
x:DeferLoadStrategy="Lazy"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Foreground="{ThemeResource ToggleSwitchHeaderForeground}"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="Collapsed"/>
<Grid
Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource ToggleSwitchPreContentMargin}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{ThemeResource ToggleSwitchPostContentMargin}"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="12" MaxWidth="12"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid
x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
Margin="0,5"
contract7NotPresent:CornerRadius="{StaticResource ControlCornerRadius}"
contract7Present:CornerRadius="{TemplateBinding CornerRadius}"
Background="{ThemeResource ToggleSwitchContainerBackground}"
Control.IsTemplateFocusTarget="True"/>
<ContentPresenter
x:Name="OffContentPresenter"
Grid.RowSpan="3"
Grid.Column="0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding OffContent}"
ContentTemplate="{TemplateBinding OffContentTemplate}"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Opacity="0"/>
<ContentPresenter
x:Name="OnContentPresenter"
Grid.RowSpan="3"
Grid.Column="0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding OnContent}"
ContentTemplate="{TemplateBinding OnContentTemplate}"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Opacity="0"/>
<Rectangle
x:Name="OuterBorder"
Grid.Row="1"
Grid.Column="2"
Width="40"
Height="20"
Fill="{ThemeResource ToggleSwitchFillOff}"
RadiusX="10"
RadiusY="10"
Stroke="{ThemeResource ToggleSwitchStrokeOff}"
StrokeThickness="{ThemeResource ToggleSwitchOuterBorderStrokeThickness}"/>
<Rectangle
x:Name="SwitchKnobBounds"
Grid.Row="1"
Grid.Column="2"
Width="40"
Height="20"
Fill="{ThemeResource ToggleSwitchFillOn}"
Opacity="0"
RadiusX="10"
RadiusY="10"
Stroke="{ThemeResource ToggleSwitchStrokeOn}"
StrokeThickness="{ThemeResource ToggleSwitchOnStrokeThickness}"/>
<Grid
x:Name="SwitchKnob"
Grid.Row="1"
Grid.Column="2"
Width="20"
Height="20"
HorizontalAlignment="Left">
<Border
x:Name="SwitchKnobOn"
Grid.Column="2"
Width="12"
Height="12"
Margin="0,0,1,0"
HorizontalAlignment="Center"
contract7Present:BackgroundSizing="OuterBorderEdge"
Background="{ThemeResource ToggleSwitchKnobFillOn}"
BorderBrush="{ThemeResource ToggleSwitchKnobStrokeOn}"
CornerRadius="7"
Opacity="0"
RenderTransformOrigin="0.5, 0.5">
<Border.RenderTransform>
<CompositeTransform/>
</Border.RenderTransform>
</Border>
<Rectangle
x:Name="SwitchKnobOff"
Grid.Column="2"
Width="12"
Height="12"
Margin="-1,0,0,0"
HorizontalAlignment="Center"
Fill="{ThemeResource ToggleSwitchKnobFillOff}"
RadiusX="7"
RadiusY="7"
RenderTransformOrigin="0.5, 0.5">
<Rectangle.RenderTransform>
<CompositeTransform/>
</Rectangle.RenderTransform>
</Rectangle>
<Grid.RenderTransform>
<TranslateTransform x:Name="KnobTranslateTransform"/>
</Grid.RenderTransform>
</Grid>
<Thumb
x:Name="SwitchThumb"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
AutomationProperties.AccessibilityView="Raw">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Rectangle Fill="Transparent"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOff}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOff}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOff}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOff}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOff}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOff}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOn}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOn}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOn}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOn}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOn}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOn}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContainerBackground}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContainerBackground}"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Width" EnableDependentAnimation="True" >
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Width" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffPointerOver}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffPointerOver}"/>
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffPointerOver}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffPointerOver}"/>
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffPointerOver}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnPointerOver}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnPointerOver}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnPointerOver}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundPointerOver}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundPointerOver}"/>
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Width" EnableDependentAnimation="True" >
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Width" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="SwitchKnobOn.HorizontalAlignment" Value="Right" />
<Setter Target="SwitchKnobOn.Margin" Value="0,0,3,0" />
<Setter Target="SwitchKnobOff.HorizontalAlignment" Value="Left" />
<Setter Target="SwitchKnobOff.Margin" Value="3,0,0,0" />
<Setter Target="SwitchKnobOn.HorizontalAlignment" Value="Right"/>
<Setter Target="SwitchKnobOn.Margin" Value="0,0,3,0"/>
<Setter Target="SwitchKnobOff.HorizontalAlignment" Value="Left"/>
<Setter Target="SwitchKnobOff.Margin" Value="3,0,0,0"/>
</VisualState.Setters>
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffPressed}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffPressed}"/>
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffPressed}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffPressed}"/>
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnPressed}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnPressed}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffPressed}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnPressed}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundPressed}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundPressed}"/>
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Width" EnableDependentAnimation="True" >
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="17" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="17"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Width" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="17" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="17"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchHeaderForegroundDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchHeaderForegroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContentForegroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContentForegroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffDisabled}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffDisabled}"/>
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffDisabled}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffDisabled}"/>
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnDisabled}" />
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundDisabled}" />
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundDisabled}"/>
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Width" EnableDependentAnimation="True" >
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlNormalAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlNormalAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Width" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlNormalAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlNormalAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
@@ -287,111 +535,117 @@
<VisualStateGroup x:Name="ToggleStates">
<VisualStateGroup.Transitions>
<VisualTransition x:Name="DraggingToOnTransition"
<VisualTransition
x:Name="DraggingToOnTransition"
GeneratedDuration="0"
From="Dragging"
To="On"
GeneratedDuration="0">
To="On">
<Storyboard>
<RepositionThemeAnimation TargetName="SwitchKnob" FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOnOffset}" />
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOnOffset}" TargetName="SwitchKnob"/>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OnToDraggingTransition"
<VisualTransition
x:Name="OnToDraggingTransition"
GeneratedDuration="0"
From="On"
To="Dragging"
GeneratedDuration="0">
To="Dragging">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="1" />
<LinearDoubleKeyFrame KeyTime="0" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="1" />
<LinearDoubleKeyFrame KeyTime="0" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="0" />
<LinearDoubleKeyFrame KeyTime="0" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="DraggingToOffTransition"
<VisualTransition
x:Name="DraggingToOffTransition"
GeneratedDuration="0"
From="Dragging"
To="Off"
GeneratedDuration="0">
To="Off">
<Storyboard>
<RepositionThemeAnimation TargetName="SwitchKnob" FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOffOffset}" />
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOffOffset}" TargetName="SwitchKnob"/>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OnToOffTransition"
<VisualTransition
x:Name="OnToOffTransition"
GeneratedDuration="0"
From="On"
To="Off"
GeneratedDuration="0">
To="Off">
<Storyboard>
<RepositionThemeAnimation TargetName="SwitchKnob" FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOnToOffOffset}" />
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOnToOffOffset}" TargetName="SwitchKnob"/>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OffToOnTransition"
<VisualTransition
x:Name="OffToOnTransition"
GeneratedDuration="0"
From="Off"
To="On"
GeneratedDuration="0">
To="On">
<Storyboard>
<RepositionThemeAnimation TargetName="SwitchKnob" FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOffToOnOffset}"/>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOffToOnOffset}" TargetName="SwitchKnob"/>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Dragging" />
<VisualState x:Name="Off" />
<VisualState x:Name="Dragging"/>
<VisualState x:Name="Off"/>
<VisualState x:Name="On">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="KnobTranslateTransform"
<DoubleAnimation
Storyboard.TargetName="KnobTranslateTransform"
Storyboard.TargetProperty="X"
To="20"
Duration="0" />
Duration="0"/>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
@@ -401,11 +655,12 @@
<VisualState x:Name="OffContent">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OffContentPresenter"
<DoubleAnimation
Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="IsHitTestVisible" Storyboard.TargetName="OffContentPresenter">
Duration="0"/>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter" Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
@@ -417,11 +672,12 @@
<VisualState x:Name="OnContent">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OnContentPresenter"
<DoubleAnimation
Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="IsHitTestVisible" Storyboard.TargetName="OnContentPresenter">
Duration="0"/>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter" Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
@@ -434,149 +690,11 @@
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter x:Name="HeaderContentPresenter"
x:DeferLoadStrategy="Lazy"
Grid.Row="0"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Foreground="{ThemeResource ToggleSwitchHeaderForeground}"
IsHitTestVisible="False"
Margin="{ThemeResource ToggleSwitchTopHeaderMargin}"
TextWrapping="Wrap"
VerticalAlignment="Top"
Visibility="Collapsed"
AutomationProperties.AccessibilityView="Raw" />
<Grid
Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource ToggleSwitchPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource ToggleSwitchPostContentMargin}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="12" MaxWidth="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
Margin="0,5"
contract7Present:CornerRadius="{TemplateBinding CornerRadius}"
contract7NotPresent:CornerRadius="{StaticResource ControlCornerRadius}"
Control.IsTemplateFocusTarget="True"
Background="{ThemeResource ToggleSwitchContainerBackground}" />
<ContentPresenter x:Name="OffContentPresenter"
Grid.RowSpan="3"
Grid.Column="0"
Opacity="0"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Content="{TemplateBinding OffContent}"
ContentTemplate="{TemplateBinding OffContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw" />
<ContentPresenter x:Name="OnContentPresenter"
Grid.RowSpan="3"
Grid.Column="0"
Opacity="0"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Content="{TemplateBinding OnContent}"
ContentTemplate="{TemplateBinding OnContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw" />
<Rectangle x:Name="OuterBorder"
Grid.Row="1"
Grid.Column="2"
Height="20"
Width="40"
RadiusX="10"
RadiusY="10"
Fill="{ThemeResource ToggleSwitchFillOff}"
Stroke="{ThemeResource ToggleSwitchStrokeOff}"
StrokeThickness="{ThemeResource ToggleSwitchOuterBorderStrokeThickness}" />
<Rectangle x:Name="SwitchKnobBounds"
Grid.Row="1"
Height="20"
Width="40"
RadiusX="10"
RadiusY="10"
Grid.Column="2"
Fill="{ThemeResource ToggleSwitchFillOn}"
Stroke="{ThemeResource ToggleSwitchStrokeOn}"
StrokeThickness="{ThemeResource ToggleSwitchOnStrokeThickness}"
Opacity="0" />
<Grid x:Name="SwitchKnob"
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Left"
Width="20"
Height="20">
<Border x:Name="SwitchKnobOn"
Background="{ThemeResource ToggleSwitchKnobFillOn}"
BorderBrush="{ThemeResource ToggleSwitchKnobStrokeOn}"
contract7Present:BackgroundSizing="OuterBorderEdge"
Width="12"
Height="12"
CornerRadius="7"
Grid.Column="2"
Opacity="0"
HorizontalAlignment="Center"
Margin="0,0,1,0"
RenderTransformOrigin="0.5, 0.5">
<Border.RenderTransform>
<CompositeTransform/>
</Border.RenderTransform>
</Border>
<Rectangle x:Name="SwitchKnobOff"
Fill="{ThemeResource ToggleSwitchKnobFillOff}"
Width="12"
Height="12"
RadiusX="7"
Grid.Column="2"
RadiusY="7"
HorizontalAlignment="Center"
Margin="-1,0,0,0"
RenderTransformOrigin="0.5, 0.5">
<Rectangle.RenderTransform>
<CompositeTransform/>
</Rectangle.RenderTransform>
</Rectangle>
<Grid.RenderTransform>
<TranslateTransform x:Name="KnobTranslateTransform" />
</Grid.RenderTransform>
</Grid>
<Thumb x:Name="SwitchThumb"
AutomationProperties.AccessibilityView="Raw"
Grid.RowSpan="3"
Grid.ColumnSpan="3">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Rectangle Fill="Transparent" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,15 +1,13 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:controls="using:SettingsUI.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:SettingsUI.Controls">
<Style x:Key="ListViewItemSettingStyle" TargetType="ListViewItem">
<Setter Property="Margin" Value="0,0,0,2" />
<Setter Property="Padding" Value="0,0,0,0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0,0,0,2"/>
<Setter Property="Padding" Value="0,0,0,0"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
<Style
TargetType="controls:CheckBoxWithDescriptionControl"
BasedOn="{StaticResource DefaultCheckBoxStyle}" />
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="controls:CheckBoxWithDescriptionControl"/>
</ResourceDictionary>

View File

@@ -1,17 +1,15 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="OobeSubtitleStyle" TargetType="TextBlock">
<Setter Property="Margin" Value="0,16,0,0" />
<Setter Property="Foreground" Value="{ThemeResource DefaultTextForegroundThemeBrush}" />
<Setter Property="AutomationProperties.HeadingLevel" Value="Level3" />
<Setter Property="FontFamily" Value="XamlAutoFontFamily" />
<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" />
<Setter Property="Margin" Value="0,16,0,0"/>
<Setter Property="Foreground" Value="{ThemeResource DefaultTextForegroundThemeBrush}"/>
<Setter Property="AutomationProperties.HeadingLevel" Value="Level3"/>
<Setter Property="FontFamily" Value="XamlAutoFontFamily"/>
<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>
<x:Double x:Key="SecondaryTextFontSize">12</x:Double>

View File

@@ -1,29 +1,27 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="CardBackgroundFillColorDefaultBrush" />
<StaticResource x:Key="CardBorderBrush" ResourceKey="CardStrokeColorDefaultBrush" />
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="CardBackgroundFillColorDefaultBrush"/>
<StaticResource x:Key="CardBorderBrush" ResourceKey="CardStrokeColorDefaultBrush"/>
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="TextFillColorPrimaryBrush"/>
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FF34424d"/>
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF5fb2f2</Color>
<Thickness x:Key="CardBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="CardBackgroundFillColorDefaultBrush" />
<StaticResource x:Key="CardBorderBrush" ResourceKey="CardStrokeColorDefaultBrush" />
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="CardBackgroundFillColorDefaultBrush"/>
<StaticResource x:Key="CardBorderBrush" ResourceKey="CardStrokeColorDefaultBrush"/>
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="TextFillColorPrimaryBrush"/>
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FFd3e7f7"/>
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF0063b1</Color>
<Thickness x:Key="CardBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="SystemColorButtonFaceColorBrush" />
<StaticResource x:Key="CardBorderBrush" ResourceKey="SystemColorButtonTextColorBrush" />
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="SystemColorButtonTextColorBrush" />
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="SystemColorButtonFaceColorBrush"/>
<StaticResource x:Key="CardBorderBrush" ResourceKey="SystemColorButtonTextColorBrush"/>
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="SystemColorButtonTextColorBrush"/>
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FF34424d"/>
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF5fb2f2</Color>
<Thickness x:Key="CardBorderThickness">2</Thickness>

View File

@@ -11,42 +11,38 @@
<!-- Styles -->
<!-- Setting used in a Expander header -->
<Style x:Key="ExpanderHeaderSettingStyle"
TargetType="controls:Setting">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Padding" Value="0,14,0,14" />
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Style x:Key="ExpanderHeaderSettingStyle" TargetType="controls:Setting">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Padding" Value="0,14,0,14"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
<Thickness x:Key="ExpanderChevronMargin">0,0,8,0</Thickness>
<!-- Setting used in a Expander header -->
<Style x:Key="ExpanderContentSettingStyle"
TargetType="controls:Setting">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0,1,0,0" />
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Padding" Value="{StaticResource ExpanderSettingMargin}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Style x:Key="ExpanderContentSettingStyle" TargetType="controls:Setting">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0,1,0,0"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="Padding" Value="{StaticResource ExpanderSettingMargin}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
<!-- Setting expander style -->
<Style x:Key="SettingExpanderStyle"
TargetType="Expander">
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Style x:Key="SettingExpanderStyle" TargetType="Expander">
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
<Style x:Key="ExpanderSeparatorStyle"
TargetType="Rectangle">
<Setter Property="Height" Value="1" />
<Setter Property="Stroke" Value="{ThemeResource CardBorderBrush}" />
<Style x:Key="ExpanderSeparatorStyle" TargetType="Rectangle">
<Setter Property="Height" Value="1"/>
<Setter Property="Stroke" Value="{ThemeResource CardBorderBrush}"/>
</Style>
</ResourceDictionary>

View File

@@ -1,7 +1,5 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Styles/Common.xaml"/>
<ResourceDictionary Source="../Styles/TextBlock.xaml"/>
@@ -9,6 +7,6 @@
<ResourceDictionary Source="../Themes/Colors.xaml"/>
<ResourceDictionary Source="../Themes/SettingsExpanderStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
</ResourceDictionary>

View File

@@ -0,0 +1,73 @@
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Snap.Hutao.Installer;
internal class Program
{
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx";
private const string ValueName = "AllowDevelopmentWithoutDevLicense";
public static async Task Main(string[] args)
{
_ = args;
string ps1File = Path.Combine(AppContext.BaseDirectory, "Install.ps1");
if (!File.Exists(ps1File))
{
Console.WriteLine("未检测到 Install.ps1 文件");
Console.WriteLine("请勿移动该安装程序,按下任意键退出...");
Console.ReadKey();
return;
}
try
{
//以管理策略打开开发者模式
Registry.SetValue(AppxKey, ValueName, 1, RegistryValueKind.DWord);
}
catch (Exception)
{
Console.WriteLine("开发者模式未开启,请手动开启,参阅下方链接");
Console.WriteLine("https://learn.microsoft.com/zh-CN/windows/apps/get-started/developer-mode-features-and-debugging");
}
await InstallAsync(ps1File).ConfigureAwait(false);
Console.WriteLine();
Console.WriteLine("官方文档与使用教程");
Console.WriteLine("https://hut.ao");
Console.WriteLine();
Console.WriteLine("在开始菜单中启动 Snap.Hutao ,按下任意键退出...");
Console.ReadKey();
}
private static async Task InstallAsync(string ps1File)
{
Console.WriteLine("请注意 PowerShell 中的提示");
Process ps = new()
{
StartInfo = new ProcessStartInfo()
{
FileName = "powershell.exe",
Arguments = $"-ExecutionPolicy Unrestricted \"{ps1File}\"",
UseShellExecute = true,
}
};
try
{
ps.Start();
await ps.WaitForExitAsync();
Console.WriteLine("安装脚本运行完成");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishTrimmed>true</PublishTrimmed>
<ApplicationManifest>app.manifest</ApplicationManifest>
<DebugType>embedded</DebugType>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC 清单选项
如果想要更改 Windows 用户帐户控制级别,请使用
以下节点之一替换 requestedExecutionLevel 节点。
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
元素。
-->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
Windows 版本的列表。取消评论适当的元素,
Windows 将自动选择最兼容的环境。 -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI无需
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
<!--
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
-->
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
<!--
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
-->
</assembly>

View File

@@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
@@ -22,8 +23,10 @@ public class HttpClientGenerator : ISourceGenerator
{
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.Default";
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc";
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc2";
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
@@ -44,12 +47,14 @@ public class HttpClientGenerator : ISourceGenerator
string toolName = this.GetGeneratorType().FullName;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Net.Http;
namespace Snap.Hutao.Core.DependencyInjection;
@@ -77,10 +82,7 @@ internal static partial class IocHttpClientConfiguration
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
lineBuilder
.Clear()
.Append("\r\n");
lineBuilder.Clear().Append("\r\n");
lineBuilder.Append(@" services.AddHttpClient<");
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
@@ -100,6 +102,9 @@ internal static partial class IocHttpClientConfiguration
case XRpcName:
lineBuilder.Append(@"XRpcConfiguration)");
break;
case XRpc2Name:
lineBuilder.Append(@"XRpc2Configuration)");
break;
default:
throw new InvalidOperationException($"非法的HttpClientConfigration值: [{injectAsName}]");
}
@@ -125,6 +130,11 @@ internal static partial class IocHttpClientConfiguration
lineBuilder.Append(" })");
}
if (classSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == DynamicSecretAttributeName))
{
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
}
lineBuilder.Append(";");
lines.Add(lineBuilder.ToString());
@@ -135,4 +145,34 @@ internal static partial class IocHttpClientConfiguration
sourceCodeBuilder.Append(line);
}
}
private class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
/// <summary>
/// Created on demand before each generation pass
/// </summary>
public class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}

View File

@@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
@@ -22,6 +23,7 @@ public class InjectionGenerator : ISourceGenerator
{
private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
@@ -51,7 +53,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class ServiceCollectionExtensions
internal static partial class ServiceCollectionExtension
{{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
@@ -64,7 +66,7 @@ internal static partial class ServiceCollectionExtensions
}
}");
context.AddSource("ServiceCollectionExtensions.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
context.AddSource("ServiceCollectionExtension.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
private static void FillWithInjectionServices(InjectionSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
@@ -97,8 +99,11 @@ internal static partial class ServiceCollectionExtensions
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient(");
break;
case InjectAsScopedName:
lineBuilder.Append(@" services.AddScoped(");
break;
default:
throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]");
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
}
if (arguments.Length == 2)
@@ -118,4 +123,34 @@ internal static partial class ServiceCollectionExtensions
sourceCodeBuilder.Append(line);
}
}
private class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
/// <summary>
/// Created on demand before each generation pass
/// </summary>
public class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
// 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")]

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@@ -6,6 +6,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
@@ -17,15 +18,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4-beta1.22518.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,52 @@
// 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;
}
}
}
}

View File

@@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SettingsUI", "SettingsUI\Se
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Installer", "Snap.Hutao.Installer\Snap.Hutao.Installer.csproj", "{CEC01691-F65E-4874-9AE2-F571369A7631}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -82,6 +84,22 @@ Global
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.ActiveCfg = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.Build.0 = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,31 @@
{
"help": "https://go.microsoft.com/fwlink/?linkid=866610",
"dependentFileProviders": {
"add": {
"extensionToExtension": {
"add": {
".json": [ ".txt" ]
}
},
"pathSegment": {
"add": {
".*": [ ".cs" ]
}
},
"fileSuffixToExtension": {
"add": {
"DesignTimeFactory.cs": [".cs"]
}
},
"fileToFile": {
"add": {
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml.cs" ],
"GlobalUsing.cs": [ "Program.cs" ],
".filenesting.json": [ "Program.cs" ],
".editorconfig": [ "Program.cs" ]
}
}
}
}
}

View File

@@ -2,7 +2,10 @@
x:Class="Snap.Hutao.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
xmlns:shvc="using:Snap.Hutao.View.Converter">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@@ -10,24 +13,68 @@
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/SettingsUI.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!--Modify Window title bar color-->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="CompatBackgroundColor">#FF272727</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>
<!-- Pivot Resource -->
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
<!-- CornerRadius -->
<CornerRadius x:Key="CompatCornerRadius">6</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusTop">6,6,0,0</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusRight">0,6,6,0</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusBottom">0,0,6,6</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
<!-- Converters -->
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:DescParamDescriptor x:Key="DescParamDescriptor"/>
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
<shmmc:EquipIconConverter x:Key="EquipIconConverter"/>
<shmmc:GachaAvatarImgConverter x:Key="GachaAvatarImgConverter"/>
<shmmc:GachaAvatarIconConverter x:Key="GachaAvatarIconConverter"/>
<shmmc:GachaEquipIconConverter x:Key="GachaEquipIconConverter"/>
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
<shmmc:PropertyInfoDescriptor x:Key="PropertyDescriptor"/>
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<!-- Styles -->
<Style
x:Key="LargeGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}"
TargetType="GridViewItem">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
<!-- ItemsPanelTemplate -->
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
<ItemsStackPanel/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,15 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
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.LifeCycle;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Metadata;
using System.Diagnostics;
using Windows.Storage;
@@ -27,6 +25,7 @@ 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
@@ -48,15 +47,12 @@ public partial class App : Application
// manually invoke
Activation.Activate(firstInstance, activatedEventArgs);
firstInstance.Activated += Activation.Activate;
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
logger.LogInformation(EventIds.CommonLog, "Snap Hutao : {version}", CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
Ioc.Default
.GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget(logger);
JumpListHelper.ConfigAsync().SafeForget(logger);
}
else
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -10,8 +10,11 @@ namespace Snap.Hutao.Context.Database;
/// <summary>
/// 应用程序数据库上下文
/// </summary>
public class AppDbContext : DbContext
public sealed class AppDbContext : DbContext
{
private readonly Guid contextId;
private readonly ILogger<AppDbContext>? logger;
/// <summary>
/// 构造一个新的应用程序数据库上下文
/// </summary>
@@ -21,6 +24,19 @@ public class AppDbContext : DbContext
{
}
/// <summary>
/// 构造一个新的应用程序数据库上下文
/// </summary>
/// <param name="options">选项</param>
/// <param name="logger">日志器</param>
public AppDbContext(DbContextOptions<AppDbContext> options, ILogger<AppDbContext> logger)
: this(options)
{
contextId = Guid.NewGuid();
this.logger = logger;
logger.LogInformation("AppDbContext[{id}] created.", contextId);
}
/// <summary>
/// 设置
/// </summary>
@@ -56,6 +72,36 @@ public class AppDbContext : DbContext
/// </summary>
public DbSet<AvatarInfo> AvatarInfos { get; set; } = default!;
/// <summary>
/// 游戏内账号
/// </summary>
public DbSet<GameAccount> GameAccounts { get; set; } = default!;
/// <summary>
/// 实时便笺
/// </summary>
public DbSet<DailyNoteEntry> DailyNotes { get; set; } = default!;
/// <summary>
/// 对象缓存
/// </summary>
public DbSet<ObjectCacheEntry> ObjectCache { get; set; } = default!;
/// <summary>
/// 培养计划
/// </summary>
public DbSet<CultivateProject> CultivateProjects { get; set; } = default!;
/// <summary>
/// 培养入口点
/// </summary>
public DbSet<CultivateEntry> CultivateEntries { get; set; } = default!;
/// <summary>
/// 培养消耗物品
/// </summary>
public DbSet<CultivateItem> CultivateItems { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
@@ -66,11 +112,19 @@ public class AppDbContext : DbContext
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
}
/// <inheritdoc/>
public override void Dispose()
{
base.Dispose();
logger?.LogInformation("AppDbContext[{id}] disposed.", contextId);
}
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.ApplyConfiguration(new AvatarInfoConfiguration())
.ApplyConfiguration(new UserConfiguration());
.ApplyConfiguration(new UserConfiguration())
.ApplyConfiguration(new DailyNoteEntryConfiguration());
}
}

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Behavior;
@@ -55,4 +54,4 @@ internal class AutoHeightBehavior : BehaviorBase<FrameworkElement>
{
AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
}
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// 按给定比例自动调整高度的行为
/// </summary>
internal class AutoWidthBehavior : BehaviorBase<FrameworkElement>
{
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetWidth), 320D);
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetHeight), 1024D);
/// <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 void OnAssociatedObjectLoaded()
{
UpdateElementWidth();
AssociatedObject.SizeChanged += OnSizeChanged;
}
/// <inheritdoc/>
protected override void OnDetaching()
{
AssociatedObject.SizeChanged -= OnSizeChanged;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateElementWidth();
}
private void UpdateElementWidth()
{
AssociatedObject.Width = (double)AssociatedObject.Height * (TargetWidth / TargetHeight);
}
}

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Behavior;

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Behavior;
@@ -33,4 +32,4 @@ internal class InvokeCommandOnUnloadedBehavior : BehaviorBase<UIElement>
base.OnDetaching();
}
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control;

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Threading;
namespace Snap.Hutao.Control.Extension;
@@ -34,10 +33,12 @@ internal static class ContentDialogExtensions
{
await ThreadHelper.SwitchToMainThreadAsync();
contentDialog.ShowAsync().AsTask().SafeForget();
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019
return new ContentDialogHider(contentDialog);
}
private struct ContentDialogHider : IAsyncDisposable
private readonly struct ContentDialogHider : IAsyncDisposable
{
private readonly ContentDialog contentDialog;

View File

@@ -0,0 +1,95 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
using Windows.UI;
namespace Snap.Hutao.Control.Image;
/// <summary>
/// BGRA8 结构
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct Bgra8
{
/// <summary>
/// B
/// </summary>
[FieldOffset(0)]
public byte B;
/// <summary>
/// G
/// </summary>
[FieldOffset(1)]
public byte G;
/// <summary>
/// R
/// </summary>
[FieldOffset(2)]
public byte R;
/// <summary>
/// A
/// </summary>
[FieldOffset(3)]
public byte A;
[FieldOffset(0)]
private readonly uint data;
/// <summary>
/// 构造一个新的 BGRA8 结构
/// </summary>
/// <param name="b">B</param>
/// <param name="g">G</param>
/// <param name="r">R</param>
/// <param name="a">A</param>
public Bgra8(byte b, byte g, byte r, byte a)
{
B = b;
G = g;
R = r;
A = a;
}
/// <summary>
/// 从Color值转换
/// </summary>
/// <param name="color">颜色</param>
/// <returns>新的 BGRA8 结构</returns>
public static Bgra8 FromColor(Color color)
{
return new(color.B, color.G, color.R, color.A);
}
/// <summary>
/// 从RGB值转换
/// </summary>
/// <param name="r">R</param>
/// <param name="g">G</param>
/// <param name="b">B</param>
/// <returns>新的 BGRA8 结构</returns>
public static Bgra8 FromRgb(byte r, byte g, byte b)
{
return new(b, g, r, 0xFF);
}
/// <summary>
/// 加入噪声
/// </summary>
/// <param name="source">源</param>
/// <returns>噪声</returns>
public static Bgra8 Noise(Bgra8 source)
{
Bgra8 target = new(0, 0, 0, 0)
{
B = (byte)Math.Max(0, Math.Min(255, source.B + Random.Shared.Next(-20, 20))),
G = (byte)Math.Max(0, Math.Min(255, source.G + Random.Shared.Next(-20, 20))),
R = (byte)Math.Max(0, Math.Min(255, source.R + Random.Shared.Next(-20, 20))),
A = source.A,
};
return target;
}
}

View File

@@ -6,9 +6,7 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.Net.Http;
@@ -67,7 +65,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
/// <returns>加载的图像表面</returns>
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token).ConfigureAwait(true))
{
return LoadedImageSurface.StartLoadFromStream(imageStream);
}
@@ -119,7 +117,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
private async Task ApplyImageInternalAsync(Uri? uri, CancellationToken token)
{
await HideAsync(token);
await HideAsync(token).ConfigureAwait(true);
LoadedImageSurface? imageSurface = null;
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
@@ -132,15 +130,15 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
}
else
{
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri);
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
try
{
imageSurface = await LoadImageSurfaceAsync(storageFile, token);
imageSurface = await LoadImageSurfaceAsync(storageFile, token).ConfigureAwait(true);
}
catch (COMException)
{
await imageCache.RemoveAsync(uri.Enumerate());
await imageCache.RemoveAsync(uri.Enumerate()).ConfigureAwait(true);
}
}
@@ -150,7 +148,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
OnUpdateVisual(spriteVisual);
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
await ShowAsync(token);
await ShowAsync(token).ConfigureAwait(true);
}
}
}
@@ -159,7 +157,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
if (!isShow)
{
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token);
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token).ConfigureAwait(true);
isShow = true;
}
}
@@ -168,7 +166,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
if (isShow)
{
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token);
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token).ConfigureAwait(true);
isShow = false;
}
}

View File

@@ -31,9 +31,9 @@ public class Gradient : CompositionImage
/// <inheritdoc/>
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token).ConfigureAwait(true))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token);
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token).ConfigureAwait(true);
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
return LoadedImageSurface.StartLoadFromStream(imageStream);

View File

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

View File

@@ -1,27 +1,30 @@
<UserControl
x:Class="Snap.Hutao.Control.Panel.PanelSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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"
mc:Ignorable="d">
<SplitButton Padding="0,6" Click="SplitButtonClick" Loaded="SplitButtonLoaded">
<SplitButton
Padding="0,6"
Click="SplitButtonClick"
Loaded="SplitButtonLoaded">
<SplitButton.Content>
<FontIcon Name="IconPresenter" Glyph="&#xE8FD;"/>
</SplitButton.Content>
<SplitButton.Flyout>
<MenuFlyout>
<RadioMenuFlyoutItem
Tag="List"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Tag="List"
Text="列表"/>
<RadioMenuFlyoutItem
Tag="Grid"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Tag="Grid"
Text="网格"/>
</MenuFlyout>
</SplitButton.Flyout>

View File

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

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core;
namespace Snap.Hutao.Control;
/// <summary>
/// 快速创建 <see cref="TOwner"/> 的 <see cref="DependencyProperty"/>

View File

@@ -12,6 +12,7 @@ namespace Snap.Hutao.Control;
/// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知
/// </summary>
[SuppressMessage("", "CA1001")]
public class ScopedPage : Page
{
private readonly CancellationTokenSource viewLoadingCancellationTokenSource = new();
@@ -23,19 +24,18 @@ public class ScopedPage : Page
public ScopedPage()
{
serviceScope = Ioc.Default.CreateScope();
serviceScope.Track();
}
/// <inheritdoc cref="IServiceScope.ServiceProvider"/>
public IServiceProvider ServiceProvider { get => serviceScope.ServiceProvider; }
/// <summary>
/// 初始化
/// 应当在 InitializeComponent() 前调用
/// </summary>
/// <typeparam name="TViewModel">视图模型类型</typeparam>
public void InitializeWith<TViewModel>()
where TViewModel : class, ISupportCancellation
{
ISupportCancellation viewModel = ServiceProvider.GetRequiredService<TViewModel>();
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
DataContext = viewModel;
}
@@ -63,6 +63,7 @@ public class ScopedPage : Page
// Try dispose scope when page is not presented
serviceScope.Dispose();
viewLoadingCancellationTokenSource.Dispose();
}
/// <inheritdoc/>

View File

@@ -34,7 +34,10 @@ public class DescriptionTextBlock : ContentControl
public DescriptionTextBlock()
{
IsTabStop = false;
Content = new TextBlock();
Content = new TextBlock()
{
TextWrapping = TextWrapping.Wrap,
};
ActualThemeChanged += OnActualThemeChanged;
}

View File

@@ -18,5 +18,5 @@ internal interface ISupportAsyncInitialization
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>初始化任务</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
ValueTask<bool> InitializeAsync();
}

View File

@@ -1,244 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace Snap.Hutao.Core.Caching;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// 经过简化
/// </summary>
/// <typeparam name="T">Generic type as supplied by consumer of the class</typeparam>
public abstract class CacheBase<T>
where T : class
{
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
private readonly ILogger logger;
private readonly HttpClient httpClient;
private StorageFolder? baseFolder;
private string? cacheFolderName;
private StorageFolder? cacheFolder;
/// <summary>
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class.
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="httpClient">http客户端</param>
protected CacheBase(ILogger logger, HttpClient httpClient)
{
this.logger = logger;
this.httpClient = httpClient;
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()
{
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
await RemoveAsync(files).ConfigureAwait(false);
}
/// <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)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
{
filesToDelete.Add(file);
}
}
await RemoveAsync(filesToDelete).ConfigureAwait(false);
}
/// <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)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
List<StorageFile> filesToDelete = new();
Dictionary<string, StorageFile> cachedFiles = files.ToDictionary(file => file.Name);
foreach (Uri uri in uriForCachedItems)
{
string fileName = GetCacheFileName(uri);
if (cachedFiles.TryGetValue(fileName, out StorageFile? file))
{
filesToDelete.Add(file);
}
}
await RemoveAsync(filesToDelete).ConfigureAwait(false);
}
/// <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)
{
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
string fileName = GetCacheFileName(uri);
IStorageItem? item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
if (item == null || (await item.GetBasicPropertiesAsync()).Size == 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);
}
return Must.NotNull((item as StorageFile)!);
}
/// <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>
protected virtual async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
BasicProperties? properties = await file.GetBasicPropertiesAsync().AsTask().ConfigureAwait(false);
return properties.Size == 0 || DateTime.Now.Subtract(properties.DateModified.DateTime) > duration;
}
private static string GetCacheFileName(Uri uri)
{
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
}
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
{
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri);
using (Stream httpStream = await httpClient.GetStreamAsync(uri).ConfigureAwait(false))
{
using (FileStream fileStream = File.Create(baseFile.Path))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
}
/// <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()
{
if (cacheFolder == null)
{
await InitializeInternalAsync().ConfigureAwait(false);
}
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);
}
}
}
}

View File

@@ -1,9 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Logging;
using System.Collections.Immutable;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Windows.Storage;
using Windows.Storage.FileProperties;
@@ -16,20 +21,159 @@ namespace Snap.Hutao.Core.Caching;
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 16)]
public class ImageCache : CacheBase<BitmapImage>, IImageCache
[SuppressMessage("", "CA1001")]
public class ImageCache : IImageCache
{
private const string DateAccessedProperty = "System.DateAccessed";
private static readonly ImmutableDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
[2] = TimeSpan.FromSeconds(64),
[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;
/// <summary>
/// Initializes a new instance of the <see cref="ImageCache"/> class.
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="httpClientFactory">http客户端工厂</param>
public ImageCache(ILogger<ImageCache> logger, IHttpClientFactory httpClientFactory)
: base(logger, httpClientFactory.CreateClient(nameof(ImageCache)))
{
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()
{
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
await RemoveAsync(files).ConfigureAwait(false);
}
/// <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)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
{
filesToDelete.Add(file);
}
}
await RemoveAsync(filesToDelete).ConfigureAwait(false);
}
/// <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)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
List<StorageFile> filesToDelete = new();
Dictionary<string, StorageFile> cachedFiles = files.ToDictionary(file => file.Name);
foreach (Uri uri in uriForCachedItems)
{
string fileName = GetCacheFileName(uri);
if (cachedFiles.TryGetValue(fileName, out StorageFile? file))
{
filesToDelete.Add(file);
}
}
await RemoveAsync(filesToDelete).ConfigureAwait(false);
}
/// <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)
{
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
string fileName = GetCacheFileName(uri);
IStorageItem? item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
if (item == null || (await item.GetBasicPropertiesAsync()).Size == 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);
}
return Must.NotNull((item as StorageFile)!);
}
private static string GetCacheFileName(Uri uri)
{
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
}
/// <summary>
@@ -39,7 +183,7 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
/// <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>
protected override async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
private async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
{
if (file == null)
{
@@ -72,4 +216,93 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
return properties.Size == 0 || DateTime.Now.Subtract(properties.DateModified.DateTime) > duration;
}
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
{
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri);
int retryCount = 0;
while (retryCount < 6)
{
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if (message.IsSuccessStatusCode)
{
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (FileStream fileStream = File.Create(baseFile.Path))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
else if (message.StatusCode == HttpStatusCode.TooManyRequests)
{
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
logger.LogInformation("Retry after {delay}.", delay);
await Task.Delay(delay).ConfigureAwait(false);
}
}
if (retryCount == 3)
{
uri = new UriBuilder(uri) { Host = "static.hut.ao", }.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()
{
if (cacheFolder == null)
{
await InitializeInternalAsync().ConfigureAwait(false);
}
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);
}
}
}
}

View File

@@ -0,0 +1,63 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text;
namespace Snap.Hutao.Core;
/// <summary>
/// 命令行建造器
/// </summary>
public class CommandLineBuilder
{
private const char WhiteSpace = ' ';
private readonly Dictionary<string, string?> options = new();
/// <summary>
/// 当符合条件时添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="condition">条件</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
{
return condition ? Append(name, value) : this;
}
/// <summary>
/// 添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder Append(string name, object? value = null)
{
options.Add(name, value?.ToString());
return this;
}
/// <inheritdoc cref="ToString"/>
public string Build()
{
return ToString();
}
/// <inheritdoc/>
public override string ToString()
{
StringBuilder s = new();
foreach ((string key, string? value) in options)
{
s.Append(WhiteSpace);
s.Append(key);
if (!string.IsNullOrEmpty(value))
{
s.Append(WhiteSpace);
s.Append(value);
}
}
return s.ToString();
}
}

View File

@@ -21,4 +21,4 @@ internal abstract class Md5Convert
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
return System.Convert.ToHexString(hash);
}
}
}

View File

@@ -1,8 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.Convert;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Extension;
using System.Text.Encodings.Web;
using System.Text.Json.Serialization.Metadata;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core;
@@ -12,27 +15,20 @@ namespace Snap.Hutao.Core;
/// </summary>
internal static class CoreEnvironment
{
// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// <summary>
/// 动态密钥1的盐
/// </summary>
public const string DynamicSecret1Salt = "yUZ3s0Sna1IrSNfk29Vo6vRapdOyqyhB";
/// <summary>
/// 动态密钥2的盐
/// </summary>
public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
/// <summary>
/// 米游社请求UA
/// </summary>
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary>
/// 米游社移动端请求UA
/// </summary>
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/106.0.5249.126 Mobile Safari/537.36 miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.38.1";
public const string HoyolabXrpcVersion = "2.41.0";
/// <summary>
/// 标准UA
@@ -49,23 +45,52 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string HoyolabDeviceId;
/// <summary>
/// 胡桃设备Id
/// </summary>
public static readonly string HutaoDeviceId;
/// <summary>
/// 包家族名称
/// </summary>
public static readonly string FamilyName;
/// <summary>
/// 默认的Json序列化选项
/// </summary>
public static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Encoder = new JsonTextEncoder(),
PropertyNameCaseInsensitive = true,
WriteIndented = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers =
{
JsonTypeInfoResolvers.ResolveEnumType,
},
},
};
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid";
static CoreEnvironment()
{
Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName;
CommonUA = $"Snap Hutao/{Version}";
// simply assign a random guid
HoyolabDeviceId = Guid.NewGuid().ToString();
HutaoDeviceId = GetUniqueUserID();
}
private static string GetUniqueUserID()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
return Md5Convert.ToHexString($"{userName}{machineGuid}");
}
}

View File

@@ -14,9 +14,8 @@ namespace Snap.Hutao.Core.Database;
/// <typeparam name="TMessage">消息的类型</typeparam>
internal class DbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>
where TMessage : Message.ValueChangedMessage<TEntity>, new()
{
private readonly DbContext dbContext;
private readonly DbSet<TEntity> dbSet;
private readonly IMessenger messenger;
@@ -25,12 +24,10 @@ internal class DbCurrent<TEntity, TMessage>
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="dbSet">数据集</param>
/// <param name="messenger">消息器</param>
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, IMessenger messenger)
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
{
this.dbContext = dbContext;
this.dbSet = dbSet;
this.messenger = messenger;
}
@@ -55,96 +52,18 @@ internal class DbCurrent<TEntity, TMessage>
if (current != null)
{
current.IsSelected = false;
dbSet.Update(current);
dbContext.SaveChanges();
dbSet.UpdateAndSave(current);
}
}
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current != null)
{
current.IsSelected = true;
dbSet.Update(current);
dbContext.SaveChanges();
}
messenger.Send(message);
}
}
}
/// <summary>
/// 数据库当前项
/// 简化对数据库中选中项的管理
/// </summary>
/// <typeparam name="TObservable">绑定类型</typeparam>
/// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam>
[SuppressMessage("", "SA1402")]
internal class DbCurrent<TObservable, TEntity, TMessage>
where TObservable : class
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TObservable>
{
private readonly DbContext dbContext;
private readonly DbSet<TEntity> dbSet;
private readonly IMessenger messenger;
private readonly Func<TObservable, TEntity> selector;
private TObservable? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="dbSet">数据集</param>
/// <param name="selector">选择器</param>
/// <param name="messenger">消息器</param>
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, Func<TObservable, TEntity> selector, IMessenger messenger)
{
this.dbContext = dbContext;
this.dbSet = dbSet;
this.selector = selector;
this.messenger = messenger;
}
/// <summary>
/// 当前选中的项
/// </summary>
public TObservable? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
// only update when not processing a deletion
if (value != null)
{
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = false;
dbSet.Update(entity);
dbContext.SaveChanges();
}
}
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
current = value;
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = true;
dbSet.Update(entity);
dbContext.SaveChanges();
dbSet.UpdateAndSave(current);
}
messenger.Send(message);

View File

@@ -0,0 +1,164 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 数据库集合上下文
/// </summary>
public static class DbSetExtension
{
/// <summary>
/// 获取对应的数据库上下文
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <returns>对应的数据库上下文</returns>
public static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
{
return dbSet.GetService<ICurrentDbContext>().Context;
}
/// <summary>
/// 获取或添加一个对应的实体
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="predicate">谓词</param>
/// <param name="entityFactory">实体工厂</param>
/// <param name="added">是否添加</param>
/// <returns>实体</returns>
public static TEntity SingleOrAdd<TEntity>(this DbSet<TEntity> dbSet, Func<TEntity, bool> predicate, Func<TEntity> entityFactory, out bool added)
where TEntity : class
{
added = false;
TEntity? entry = dbSet.SingleOrDefault(predicate);
if (entry == null)
{
entry = entityFactory();
dbSet.Add(entry);
dbSet.Context().SaveChanges();
added = true;
}
return entry;
}
/// <summary>
/// 添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Add(entity);
return dbSet.Context().SaveChanges();
}
/// <summary>
/// 异步添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static Task<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Add(entity);
return dbSet.Context().SaveChangesAsync();
}
/// <summary>
/// 添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.Context().SaveChanges();
}
/// <summary>
/// 异步添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static Task<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.Context().SaveChangesAsync();
}
/// <summary>
/// 移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Remove(entity);
return dbSet.Context().SaveChanges();
}
/// <summary>
/// 异步移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static Task<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Remove(entity);
return dbSet.Context().SaveChangesAsync();
}
/// <summary>
/// 更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.Context().SaveChanges();
}
/// <summary>
/// 异步更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static Task<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.Context().SaveChangesAsync();
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 范围化的数据库当前项
/// 简化对数据库中选中项的管理
/// </summary>
/// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam>
internal class ScopedDbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>, new()
{
private readonly IServiceScopeFactory scopeFactory;
private readonly Func<IServiceProvider, DbSet<TEntity>> dbSetFunc;
private readonly IMessenger messenger;
private TEntity? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="scopeFactory">范围工厂</param>
/// <param name="dbSetFunc">数据集</param>
/// <param name="messenger">消息器</param>
public ScopedDbCurrent(IServiceScopeFactory scopeFactory, Func<IServiceProvider, DbSet<TEntity>> dbSetFunc, IMessenger messenger)
{
this.scopeFactory = scopeFactory;
this.dbSetFunc = dbSetFunc;
this.messenger = messenger;
}
/// <summary>
/// 当前选中的项
/// </summary>
public TEntity? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
using (IServiceScope scope = scopeFactory.CreateScope())
{
DbSet<TEntity> dbSet = dbSetFunc(scope.ServiceProvider);
// only update when not processing a deletion
if (value != null)
{
if (current != null)
{
current.IsSelected = false;
dbSet.UpdateAndSave(current);
}
}
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current != null)
{
current.IsSelected = true;
dbSet.UpdateAndSave(current);
}
messenger.Send(message);
}
}
}
}

View File

@@ -0,0 +1,104 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 设置帮助类
/// </summary>
public static class SettingEntryHelper
{
/// <summary>
/// "True"
/// </summary>
public static readonly string TrueString = true.ToString();
/// <summary>
/// "False"
/// </summary>
public static readonly string FalseString = false.ToString();
/// <summary>
/// 获取或添加一个对应的设置
/// </summary>
/// <param name="dbSet">设置集</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns>设置</returns>
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, string value)
{
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
if (entry == null)
{
entry = new(key, value);
dbSet.Add(entry);
dbSet.Context().SaveChanges();
}
return entry;
}
/// <summary>
/// 获取或添加一个对应的设置
/// </summary>
/// <param name="dbSet">设置集</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns>设置</returns>
public static async Task<SettingEntry> SingleOrAddAsync(this DbSet<SettingEntry> dbSet, string key, string value)
{
SettingEntry? entry = await dbSet.SingleOrDefaultAsync(entry => key == entry.Key).ConfigureAwait(false);
if (entry == null)
{
entry = new(key, value);
await dbSet.AddAndSaveAsync(entry).ConfigureAwait(false);
}
return entry;
}
/// <summary>
/// 获取 Boolean 值
/// </summary>
/// <param name="entry">设置</param>
/// <returns>值</returns>
public static bool GetBoolean(this SettingEntry entry)
{
return bool.Parse(entry.Value!);
}
/// <summary>
/// 设置 Boolean 值
/// </summary>
/// <param name="entry">设置</param>
/// <param name="value">值</param>
public static void SetBoolean(this SettingEntry entry, bool value)
{
entry.Value = value.ToString();
}
/// <summary>
/// 获取 Int32 值
/// </summary>
/// <param name="entry">设置</param>
/// <returns>值</returns>
public static int GetInt32(this SettingEntry entry)
{
return int.Parse(entry.Value!);
}
/// <summary>
/// 设置 Int32 值
/// </summary>
/// <param name="entry">设置</param>
/// <param name="value">值</param>
public static void SetInt32(this SettingEntry entry, int value)
{
entry.Value = value.ToString();
}
}

View File

@@ -17,4 +17,9 @@ public enum HttpClientConfigration
/// 米游社请求配置
/// </summary>
XRpc,
/// <summary>
/// 米游社登录请求配置
/// </summary>
XRpc2,
}

View File

@@ -17,4 +17,9 @@ public enum InjectAs
/// 指示应注册为短期对象
/// </summary>
Transient,
/// <summary>
/// 指示应注册为范围对象
/// </summary>
Scoped,
}

View File

@@ -46,6 +46,6 @@ internal static class IocConfiguration
}
}
return services.AddDbContextPool<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
return services.AddDbContext<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
}
}

View File

@@ -41,4 +41,22 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpc2Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
client.DefaultRequestHeaders.Add("x-rpc-aigis", string.Empty);
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
}
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// 服务管理器
/// 依赖注入的核心管理类
/// </summary>
internal static partial class ServiceCollectionExtensions
internal static partial class ServiceCollectionExtension
{
/// <summary>
/// 向容器注册服务
@@ -17,4 +17,4 @@ internal static partial class ServiceCollectionExtensions
/// <param name="services">容器</param>
/// <returns>可继续操作的服务集合</returns>
public static partial IServiceCollection AddInjections(this IServiceCollection services);
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 服务范围扩展
/// </summary>
public static class ServiceScopeExtension
{
private static IServiceScope? scopeReference;
/// <summary>
/// 追踪服务范围
/// </summary>
/// <param name="scope">范围</param>
public static void Track(this IServiceScope scope)
{
DisposeLast();
scopeReference = scope;
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposeLast()
{
scopeReference?.Dispose();
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Diagnostics;
/// <summary>
/// 值类型的<see cref="Stopwatch"/>
/// </summary>
internal struct ValueStopwatch
internal readonly struct ValueStopwatch
{
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
@@ -40,7 +40,6 @@ internal struct ValueStopwatch
/// 获取经过的时间
/// </summary>
/// <returns>经过的时间</returns>
/// <exception cref="InvalidOperationException">当前的停表未合理的初始化</exception>
public long GetElapsedTimestamp()
{
// Start timestamp can't be zero in an initialized ValueStopwatch.
@@ -59,7 +58,6 @@ internal struct ValueStopwatch
/// 获取经过的时间
/// </summary>
/// <returns>经过的时间</returns>
/// <exception cref="InvalidOperationException">当前的停表未合理的初始化</exception>
public TimeSpan GetElapsedTime()
{
return new TimeSpan(GetElapsedTimestamp());

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Exception;
/// <summary>
/// Error codes used by COM-based APIs.
/// </summary>
public enum COMError : uint
{
/// <summary>
/// could not be found.
/// </summary>
STG_E_FILENOTFOUND = 0x80030002,
/// <summary>
/// The component cannot be found.
/// </summary>
WINCODEC_ERR_COMPONENTNOTFOUND = 0x88982F50,
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.Exception;
/// <summary>
/// COM异常扩展
/// </summary>
internal static class COMExceptionExtensions
{
/// <summary>
/// 比较COM异常是否与某个错误代码等价
/// </summary>
/// <param name="exception">异常</param>
/// <param name="code">错误代码</param>
/// <returns>是否为该错误</returns>
public static bool Is(this COMException exception, COMError code)
{
return exception.HResult == unchecked((int)code);
}
}

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.Core.Exception;
@@ -26,11 +27,10 @@ internal class ExceptionRecorder
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
}
[SuppressMessage("", "VSTHRD002")]
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
// string fileName = $"ex-{DateTimeOffset.Now:yyyyMMddHHmmssffff}.txt";
// File.WriteAllText(Path.Combine(path, fileName), $"{e.Exception}\r\n{e.Exception.StackTrace}");
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Linq.Expressions;
namespace Snap.Hutao.Core.ExpressionService;
/// <summary>
/// Class to cast to type <see cref="TTo"/>
/// </summary>
/// <typeparam name="TTo">Target type</typeparam>
public static class CastTo<TTo>
{
/// <summary>
/// Casts <see cref="s"/> to <see cref="TTo"/>.
/// This does not cause boxing for value types.
/// Useful in generic methods.
/// </summary>
/// <typeparam name="TFrom">Source type to cast from. Usually a generic type.</typeparam>
/// <param name="from">from value</param>
/// <returns>target value</returns>
public static TTo From<TFrom>(TFrom from)
{
return Cache<TFrom>.Caster(from);
}
private static class Cache<TCachedFrom>
{
public static readonly Func<TCachedFrom, TTo> Caster = Get();
private static Func<TCachedFrom, TTo> Get()
{
// 参数表达式,表示 传入源类型
ParameterExpression param = Expression.Parameter(typeof(TCachedFrom));
// 一元转换 调用 相关类的显式或隐式转换运算符
UnaryExpression convert = Expression.ConvertChecked(param, typeof(TTo));
// 生成一个源类型入,目标类型出的 lamdba
return Expression.Lambda<Func<TCachedFrom, TTo>>(convert, param).Compile();
}
}
}

View File

@@ -0,0 +1,42 @@
// 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();
}
}
}

View File

@@ -1,8 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
namespace Snap.Hutao.Core.IO.DataTransfer;
@@ -32,9 +32,22 @@ internal static class Clipboard
/// <param name="text">文本</param>
public static void SetText(string text)
{
DataPackage content = new();
DataPackage content = new() { RequestedOperation = DataPackageOperation.Copy };
content.SetText(text);
Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(content);
Windows.ApplicationModel.DataTransfer.Clipboard.Flush();
}
/// <summary>
/// 设置位图
/// </summary>
/// <param name="stream">位图流</param>
public static void SetBitmapStream(IRandomAccessStream stream)
{
RandomAccessStreamReference reference = RandomAccessStreamReference.CreateFromStream(stream);
DataPackage content = new() { RequestedOperation = DataPackageOperation.Copy };
content.SetBitmap(reference);
Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(content);
Windows.ApplicationModel.DataTransfer.Clipboard.Flush();
}
}

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.IO.Ini;
internal static class IniSerializer
{
/// <summary>
/// 异步反序列化
/// 反序列化
/// </summary>
/// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns>
@@ -44,4 +44,20 @@ internal static class IniSerializer
}
}
}
/// <summary>
/// 序列化
/// </summary>
/// <param name="fileStream">写入的流</param>
/// <param name="elements">元素</param>
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
{
using (TextWriter writer = new StreamWriter(fileStream))
{
foreach (IniElement element in elements)
{
writer.WriteLine(element.ToString());
}
}
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using System.IO;
using Windows.Storage;

View File

@@ -28,12 +28,18 @@ internal sealed class TemporaryFile : IDisposable
/// </summary>
/// <param name="file">源文件</param>
/// <returns>临时文件</returns>
public static TemporaryFile CreateFromFileCopy(string file)
public static TemporaryFile? CreateFromFileCopy(string file)
{
TemporaryFile temporaryFile = new();
File.Copy(file, temporaryFile.Path, true);
return temporaryFile;
try
{
File.Copy(file, temporaryFile.Path, true);
return temporaryFile;
}
catch (System.Exception)
{
return null;
}
}
/// <summary>

View File

@@ -0,0 +1,58 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Json.Converter;
using System.Text.Json.Serialization.Metadata;
namespace Snap.Hutao.Core.Json.Annotation;
/// <summary>
/// Json 枚举类型
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
internal class JsonEnumAttribute : Attribute
{
private static readonly Type ConfigurableEnumConverterType = typeof(ConfigurableEnumConverter<>);
/// <summary>
/// 构造一个新的Json枚举声明
/// </summary>
/// <param name="readAndWriteAs">读取与写入</param>
public JsonEnumAttribute(JsonSerializeType readAndWriteAs)
{
ReadAs = readAndWriteAs;
WriteAs = readAndWriteAs;
}
/// <summary>
/// 构造一个新的Json枚举声明
/// </summary>
/// <param name="readAs">读取</param>
/// <param name="writeAs">写入</param>
public JsonEnumAttribute(JsonSerializeType readAs, JsonSerializeType writeAs)
{
ReadAs = readAs;
WriteAs = writeAs;
}
/// <summary>
/// 读取形式
/// </summary>
public JsonSerializeType ReadAs { get; init; }
/// <summary>
/// 写入形式
/// </summary>
public JsonSerializeType WriteAs { get; init; }
/// <summary>
/// 创建一个新的转换器
/// </summary>
/// <param name="info">属性信息</param>
/// <returns>Json转换器</returns>
internal JsonConverter CreateConverter(JsonPropertyInfo info)
{
Type converterType = ConfigurableEnumConverterType.MakeGenericType(info.PropertyType);
return (JsonConverter)Activator.CreateInstance(converterType, ReadAs, WriteAs)!;
}
}

View File

@@ -1,25 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Metadata.Achievement;
namespace Snap.Hutao.Core.Json.Annotation;
/// <summary>
/// 成就触发器类型
/// Json 序列化类型
/// </summary>
public enum AchievementTriggerType
public enum JsonSerializeType
{
/// <summary>
/// 任务
/// Int32
/// </summary>
Quest = 1,
Int32,
/// <summary>
/// 子任务
/// 字符串包裹的数字
/// </summary>
SubQuest = 2,
Int32AsString,
/// <summary>
/// 日常任务
/// 名称字符串
/// </summary>
DailyTask = 3,
String,
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExpressionService;
using Snap.Hutao.Core.Json.Annotation;
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 枚举转换器
/// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam>
internal class ConfigurableEnumConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum
{
private readonly JsonSerializeType readAs;
private readonly JsonSerializeType writeAs;
/// <summary>
/// 构造一个新的枚举转换器
/// </summary>
/// <param name="readAs">读取</param>
/// <param name="writeAs">写入</param>
public ConfigurableEnumConverter(JsonSerializeType readAs, JsonSerializeType writeAs)
{
this.readAs = readAs;
this.writeAs = writeAs;
}
/// <inheritdoc/>
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (readAs == JsonSerializeType.Int32)
{
return CastTo<TEnum>.From(reader.GetInt32());
}
if (reader.GetString() is string str)
{
return Enum.Parse<TEnum>(str);
}
throw Must.NeverHappen();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
switch (writeAs)
{
case JsonSerializeType.Int32:
writer.WriteNumberValue(CastTo<int>.From(value));
break;
case JsonSerializeType.Int32AsString:
writer.WriteStringValue(value.ToString("D"));
break;
default:
writer.WriteStringValue(value.ToString());
break;
}
}
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 枚举 - 字符串数字 转换器
/// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam>
internal class EnumStringValueConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum
{
/// <inheritdoc/>
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is string str)
{
return Enum.Parse<TEnum>(str);
}
throw Must.NeverHappen();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("D"));
}
}

View File

@@ -21,4 +21,4 @@ internal class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerabl
{
writer.WriteStringValue(string.Join(',', value));
}
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Encodings.Web;
namespace Snap.Hutao.Core.Json;
/// <summary>
/// 替换 =
/// </summary>
internal class JsonTextEncoder : JavaScriptEncoder
{
/// <inheritdoc/>
public override int MaxOutputCharactersPerInputCharacter { get => 6; }
/// <inheritdoc/>
public override unsafe int FindFirstCharacterToEncode(char* text, int textLength)
{
Span<char> textSpan = new(text, textLength);
return textSpan.IndexOf('=');
}
/// <inheritdoc/>
public override unsafe bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten)
{
// " => \"
if (unicodeScalar == '"')
{
numberOfCharactersWritten = 2;
return "\\\"".AsSpan().TryCopyTo(new Span<char>(buffer, bufferLength));
}
string encoded = $"\\u{(uint)unicodeScalar:x4}";
numberOfCharactersWritten = (encoded.Length <= (uint)bufferLength) ? encoded.Length : 0;
return encoded.AsSpan().TryCopyTo(new Span<char>(buffer, bufferLength));
}
/// <inheritdoc/>
public override bool WillEncode(int unicodeScalar)
{
return unicodeScalar == '=';
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Json.Annotation;
using Snap.Hutao.Core.Json.Converter;
using System.Reflection;
using System.Text.Json.Serialization.Metadata;
namespace Snap.Hutao.Core.Json;
/// <summary>
/// Json 类型信息解析器
/// </summary>
internal static class JsonTypeInfoResolvers
{
private static readonly Type JsonEnumAttributeType = typeof(JsonEnumAttribute);
/// <summary>
/// 解析枚举类型
/// </summary>
/// <param name="ti">Json 类型信息</param>
public static void ResolveEnumType(JsonTypeInfo ti)
{
if (ti.Kind != JsonTypeInfoKind.Object)
{
return;
}
IEnumerable<JsonPropertyInfo> enumProperties = ti.Properties
.Where(p => p.PropertyType.IsEnum && (p.AttributeProvider?.IsDefined(JsonEnumAttributeType, false) ?? false));
foreach (JsonPropertyInfo enumProperty in enumProperties)
{
JsonEnumAttribute attr = enumProperty.AttributeProvider!.GetCustomAttributes(false).OfType<JsonEnumAttribute>().Single();
enumProperty.CustomConverter = attr.CreateConverter(enumProperty);
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.LifeCycle;
using Windows.UI.StartScreen;
namespace Snap.Hutao.Core;
/// <summary>
/// 跳转列表帮助类
/// </summary>
public static class JumpListHelper
{
/// <summary>
/// 异步配置跳转列表
/// </summary>
/// <returns>任务</returns>
public static async Task ConfigAsync()
{
if (JumpList.IsSupported())
{
JumpList list = await JumpList.LoadCurrentAsync();
list.Items.Clear();
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, "启动游戏");
launchGameItem.GroupName = "快捷操作";
launchGameItem.Logo = new("ms-appx:///Resource/Icon/UI_GuideIcon_PlayMethod.png");
list.Items.Add(launchGameItem);
await list.SaveAsync();
}
}
}

View File

@@ -1,10 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Navigation;
using System.Security.Principal;
namespace Snap.Hutao.Core.LifeCycle;
@@ -13,8 +17,26 @@ namespace Snap.Hutao.Core.LifeCycle;
/// </summary>
internal static class Activation
{
/// <summary>
/// 启动游戏启动参数
/// </summary>
public const string LaunchGame = "LaunchGame";
private static readonly SemaphoreSlim ActivateSemaphore = new(1);
/// <summary>
/// 获取是否提升了权限
/// </summary>
/// <returns>是否提升了权限</returns>
public static bool GetElevated()
{
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
/// <summary>
/// 响应激活事件
/// 激活事件一般不会在UI线程上触发
@@ -24,7 +46,29 @@ internal static class Activation
public static void Activate(object? sender, AppActivationArguments args)
{
_ = sender;
HandleActivationAsync(args).SafeForget();
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
HandleActivationAsync(args).SafeForget();
}
}
/// <summary>
/// 响应通知激活事件
/// </summary>
/// <param name="args">参数</param>
public static void NotificationActivate(ToastNotificationActivatedEventArgsCompat args)
{
ToastArguments toastArgs = ToastArguments.Parse(args.Argument);
_ = toastArgs;
if (toastArgs.TryGetValue("Action", out string? action))
{
if (action == LaunchGame)
{
_ = toastArgs.TryGetValue("Uid", out string? uid);
HandleLaunchGameActionAsync(uid).SafeForget();
}
}
}
/// <summary>
@@ -44,35 +88,68 @@ internal static class Activation
private static async Task HandleActivationCoreAsync(AppActivationArguments args)
{
_ = Ioc.Default.GetRequiredService<MainWindow>();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
await infoBarService.WaitInitializationAsync().ConfigureAwait(false);
if (args.Kind == ExtendedActivationKind.Protocol)
{
if (args.TryGetProtocolActivatedUri(out Uri? uri))
{
infoBarService.Information(uri.ToString());
await HandleUrlActivationAsync(uri).ConfigureAwait(false);
}
}
else if (args.Kind == ExtendedActivationKind.Launch)
{
if (args.TryGetLaunchActivatedArgument(out string? arguments))
{
switch (arguments)
{
case "":
{
await WaitMainWindowAsync().ConfigureAwait(false);
break;
}
case LaunchGame:
{
await HandleLaunchGameActionAsync().ConfigureAwait(false);
break;
}
}
}
}
}
private static async Task WaitMainWindowAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
_ = Ioc.Default.GetRequiredService<MainWindow>();
await Ioc.Default.GetRequiredService<IInfoBarService>().WaitInitializationAsync().ConfigureAwait(false);
Ioc.Default
.GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget();
}
private static async Task HandleUrlActivationAsync(Uri uri)
{
UriBuilder builder = new(uri);
Must.Argument(builder.Scheme == "hutao", "uri 的协议不正确");
string category = builder.Host.ToLowerInvariant();
string action = builder.Path.ToLowerInvariant();
string rawParameter = builder.Query.ToLowerInvariant();
string parameter = builder.Query.ToLowerInvariant();
switch (category)
{
case "achievement":
{
await HandleAchievementActionAsync(action, rawParameter).ConfigureAwait(false);
await WaitMainWindowAsync().ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter).ConfigureAwait(false);
break;
}
case "dailynote":
{
await HandleDailyNoteActionAsync(action, parameter).ConfigureAwait(false);
break;
}
}
@@ -80,6 +157,7 @@ internal static class Activation
private static async Task HandleAchievementActionAsync(string action, string parameter)
{
_ = parameter;
switch (action)
{
case "/import":
@@ -95,4 +173,37 @@ internal static class Activation
}
}
}
private static async Task HandleDailyNoteActionAsync(string action, string parameter)
{
_ = parameter;
switch (action)
{
case "/refresh":
{
await Ioc.Default
.GetRequiredService<IDailyNoteService>()
.RefreshDailyNotesAsync(true)
.ConfigureAwait(false);
break;
}
}
}
private static async Task HandleLaunchGameActionAsync(string? uid = null)
{
await ThreadHelper.SwitchToMainThreadAsync();
// TODO auto switch to account
if (!MainWindow.IsPresent)
{
_ = Ioc.Default.GetRequiredService<LaunchGameWindow>();
}
else
{
await Ioc.Default
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true).ConfigureAwait(false);
}
}
}

View File

@@ -28,4 +28,22 @@ public static class AppActivationArgumentsExtensions
return false;
}
/// <summary>
/// 尝试获取启动的参数
/// </summary>
/// <param name="activatedEventArgs">应用程序激活参数</param>
/// <param name="arguments">参数</param>
/// <returns>是否存在参数</returns>
public static bool TryGetLaunchActivatedArgument(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
{
arguments = null;
if (activatedEventArgs.Data is ILaunchActivatedEventArgs launchArgs)
{
arguments = launchArgs.Arguments;
return true;
}
return false;
}
}

View File

@@ -24,6 +24,7 @@ internal sealed partial class DatebaseLogger : ILogger
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return new NullScope();
}

View File

@@ -4,7 +4,6 @@
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Core.Threading;
using System.Collections.Concurrent;
using System.Diagnostics;

View File

@@ -1,44 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Core;
/// <summary>
/// 进程帮助类
/// </summary>
public static class ProcessHelper
{
/// <summary>
/// 启动进程
/// </summary>
/// <param name="path">路径</param>
/// <param name="useShellExecute">使用shell</param>
/// <returns>进程</returns>
public static Process? Start(string path, bool useShellExecute = true)
{
ProcessStartInfo processInfo = new(path)
{
UseShellExecute = useShellExecute,
};
return Process.Start(processInfo);
}
/// <summary>
/// 启动进程
/// </summary>
/// <param name="path">路径</param>
/// <param name="arguments">命令行参数</param>
/// <param name="useShellExecute">使用shell</param>
/// <returns>进程</returns>
public static Process? Start(string path, string arguments, bool useShellExecute = true)
{
ProcessStartInfo processInfo = new(path)
{
UseShellExecute = useShellExecute,
Arguments = arguments,
};
return Process.Start(processInfo);
}
}

View File

@@ -10,10 +10,6 @@ namespace Snap.Hutao.Core.Setting;
/// </summary>
internal static class LocalSetting
{
/// <summary>
/// 由于 <see cref="Windows.Foundation.Collections.IPropertySet"/> 没有 nullable context,
/// 在处理引用类型时需要格外小心
/// </summary>
private static readonly ApplicationDataContainer Container;
static LocalSetting()
@@ -21,6 +17,198 @@ internal static class LocalSetting
Container = ApplicationData.Current.LocalSettings;
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static byte Get(string key, byte defaultValue)
{
return Get<byte>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static short Get(string key, short defaultValue)
{
return Get<short>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static ushort Get(string key, ushort defaultValue)
{
return Get<ushort>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static int Get(string key, int defaultValue)
{
return Get<int>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static uint Get(string key, uint defaultValue)
{
return Get<uint>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static ulong Get(string key, ulong defaultValue)
{
return Get<ulong>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static float Get(string key, float defaultValue)
{
return Get<float>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static double Get(string key, double defaultValue)
{
return Get<double>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static bool Get(string key, bool defaultValue)
{
return Get<bool>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static char Get(string key, char defaultValue)
{
return Get<char>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static DateTimeOffset Get(string key, DateTimeOffset defaultValue)
{
return Get<DateTimeOffset>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static TimeSpan Get(string key, TimeSpan defaultValue)
{
return Get<TimeSpan>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static Guid Get(string key, Guid defaultValue)
{
return Get<Guid>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static Windows.Foundation.Point Get(string key, Windows.Foundation.Point defaultValue)
{
return Get<Windows.Foundation.Point>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static Windows.Foundation.Size Get(string key, Windows.Foundation.Size defaultValue)
{
return Get<Windows.Foundation.Size>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static Windows.Foundation.Rect Get(string key, Windows.Foundation.Rect defaultValue)
{
return Get<Windows.Foundation.Rect>(key, defaultValue);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, byte value)
{
Set<byte>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, short value)
{
Set<short>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, ushort value)
{
Set<ushort>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, int value)
{
Set<int>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, uint value)
{
Set<uint>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, ulong value)
{
Set<ulong>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, float value)
{
Set<float>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, double value)
{
Set<double>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, bool value)
{
Set<bool>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, char value)
{
Set<char>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, DateTimeOffset value)
{
Set<DateTimeOffset>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, TimeSpan value)
{
Set<TimeSpan>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, Guid value)
{
Set<Guid>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, Windows.Foundation.Point value)
{
Set<Windows.Foundation.Point>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, Windows.Foundation.Size value)
{
Set<Windows.Foundation.Size>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, Windows.Foundation.Rect value)
{
Set<Windows.Foundation.Rect>(key, value);
}
/// <summary>
/// 获取设置项的值
/// </summary>
@@ -28,8 +216,8 @@ internal static class LocalSetting
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>获取的值</returns>
[return: MaybeNull]
public static T Get<T>(string key, [AllowNull] T defaultValue = default)
private static T Get<T>(string key, T defaultValue = default)
where T : struct
{
if (Container.Values.TryGetValue(key, out object? value))
{
@@ -49,9 +237,9 @@ internal static class LocalSetting
/// <typeparam name="T">设置项的类型</typeparam>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns>设置的值</returns>
public static object? Set<T>(string key, T value)
private static void Set<T>(string key, T value)
where T : struct
{
return Container.Values[key] = value;
Container.Values[key] = value;
}
}

View File

@@ -8,11 +8,6 @@ namespace Snap.Hutao.Core.Setting;
/// </summary>
internal static class SettingKeys
{
/// <summary>
/// 上次打开时App的版本
/// </summary>
public const string LastAppVersion = "LastAppVersion";
/// <summary>
/// 窗体左侧
/// </summary>

View File

@@ -0,0 +1,48 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32.TaskScheduler;
using System.Runtime.InteropServices;
using SchedulerTask = Microsoft.Win32.TaskScheduler.Task;
namespace Snap.Hutao.Core;
/// <summary>
/// 任务计划器服务
/// </summary>
internal static class TaskSchedulerHelper
{
private const string DailyNoteRefreshTaskName = "SnapHutaoDailyNoteRefreshTask";
/// <summary>
/// 注册实时便笺刷新任务
/// </summary>
/// <param name="interval">间隔(秒)</param>
/// <returns>是否注册或修改成功</returns>
public static bool RegisterForDailyNoteRefresh(int interval)
{
try
{
SchedulerTask? targetTask = TaskService.Instance.GetTask(DailyNoteRefreshTaskName);
if (targetTask != null)
{
TaskService.Instance.RootFolder.DeleteTask(DailyNoteRefreshTaskName);
}
TaskDefinition task = TaskService.Instance.NewTask();
task.RegistrationInfo.Description = "胡桃实时便笺刷新任务 | 请勿编辑或删除。";
task.Triggers.Add(new TimeTrigger() { Repetition = new(TimeSpan.FromSeconds(interval), TimeSpan.Zero), });
task.Actions.Add("explorer", "hutao://DailyNote/Refresh");
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
return true;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (COMException)
{
return false;
}
}
}

View File

@@ -0,0 +1,46 @@
// 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();
}
}

View File

@@ -17,13 +17,4 @@ internal class ThreadAccessAttribute : Attribute
public ThreadAccessAttribute(ThreadAccessState enter)
{
}
/// <summary>
/// 指示方法的进入退出线程访问状态
/// </summary>
/// <param name="enter">进入状态</param>
/// <param name="leave">离开状态</param>
public ThreadAccessAttribute(ThreadAccessState enter, ThreadAccessState leave)
{
}
}

View File

@@ -28,4 +28,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
}
}
}

View File

@@ -8,8 +8,9 @@ namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 调度器队列切换操作
/// 等待此类型对象后上下文会被切换至主线程
/// </summary>
public struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueueSwitchOperation>, IAwaiter
public readonly struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueueSwitchOperation>, IAwaiter
{
private readonly DispatcherQueue dispatherQueue;
@@ -29,9 +30,9 @@ public struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueueSwitchOpe
}
/// <inheritdoc/>
public void OnCompleted(Action continuation)
public DispatherQueueSwitchOperation GetAwaiter()
{
dispatherQueue.TryEnqueue(() => { continuation(); });
return this;
}
/// <inheritdoc/>
@@ -40,8 +41,8 @@ public struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueueSwitchOpe
}
/// <inheritdoc/>
public DispatherQueueSwitchOperation GetAwaiter()
public void OnCompleted(Action continuation)
{
return this;
dispatherQueue.TryEnqueue(() => { continuation(); });
}
}

View File

@@ -19,7 +19,7 @@ public static class SemaphoreSlimExtensions
return new SemaphoreSlimReleaser(semaphoreSlim);
}
private struct SemaphoreSlimReleaser : IDisposable
private readonly struct SemaphoreSlimReleaser : IDisposable
{
private readonly SemaphoreSlim semaphoreSlim;

View File

@@ -20,7 +20,7 @@ public static class TaskExtensions
{
try
{
await task;
await task.ConfigureAwait(false);
}
catch (System.Exception ex)
{
@@ -37,7 +37,7 @@ public static class TaskExtensions
{
try
{
await task;
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
{
@@ -59,7 +59,7 @@ public static class TaskExtensions
{
try
{
await task;
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
{
@@ -83,7 +83,7 @@ public static class TaskExtensions
{
try
{
await task;
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
{

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Threading;
/// <summary>
@@ -9,11 +11,24 @@ namespace Snap.Hutao.Core.Threading;
internal static class ThreadHelper
{
/// <summary>
/// 异步切换到线程
/// 使用此静态方法以 异步切换到 后台线程
/// </summary>
/// <remarks>使用 <see cref="SwitchToMainThreadAsync"/> 异步切换到 主线程</remarks>
/// <returns>等待体</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ThreadPoolSwitchOperation SwitchToBackgroundAsync()
{
return default;
}
/// <summary>
/// 使用此静态方法以 异步切换到 主线程
/// </summary>
/// <remarks>使用 <see cref="SwitchToBackgroundAsync"/> 异步切换到 后台线程</remarks>
/// <returns>等待体</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DispatherQueueSwitchOperation SwitchToMainThreadAsync()
{
return new(Program.DispatcherQueue!);
}
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading.Abstraction;
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 线程池切换操作
/// 等待此类型对象后上下文会被切换至线程池线程
/// </summary>
public readonly struct ThreadPoolSwitchOperation : IAwaitable<ThreadPoolSwitchOperation>, IAwaiter, ICriticalAwaiter
{
private static readonly WaitCallback WaitCallbackRunAction = RunAction;
/// <inheritdoc/>
public bool IsCompleted { get => false; }
/// <inheritdoc/>
public ThreadPoolSwitchOperation GetAwaiter()
{
return this;
}
/// <inheritdoc/>
public void GetResult()
{
}
/// <inheritdoc/>
public void OnCompleted(Action continuation)
{
QueueContinuation(continuation, flowContext: true);
}
/// <inheritdoc/>
public void UnsafeOnCompleted(Action continuation)
{
QueueContinuation(continuation, flowContext: false);
}
private static void QueueContinuation(Action continuation, bool flowContext)
{
if (flowContext)
{
ThreadPool.QueueUserWorkItem(WaitCallbackRunAction, continuation);
}
else
{
ThreadPool.UnsafeQueueUserWorkItem(WaitCallbackRunAction, continuation);
}
}
private static void RunAction(object? state)
{
((Action)state!)();
}
}

View File

@@ -38,39 +38,15 @@ public static class Must
}
}
/// <summary>
/// 任务异常
/// </summary>
/// <param name="message">异常消息</param>
/// <returns>异常的任务</returns>
[SuppressMessage("", "VSTHRD200")]
public static Task Fault(string message)
{
InvalidOperationException exception = new(message);
return Task.FromException(exception);
}
/// <summary>
/// 任务异常
/// </summary>
/// <typeparam name="T">任务结果类型</typeparam>
/// <param name="message">异常消息</param>
/// <returns>异常的任务</returns>
[SuppressMessage("", "VSTHRD200")]
public static Task<T> Fault<T>(string message)
{
InvalidOperationException exception = new(message);
return Task.FromException<T>(exception);
}
/// <summary>
/// Unconditionally throws an <see cref="NotSupportedException"/>.
/// </summary>
/// <param name="context">上下文</param>
/// <returns>Nothing. This method always throws.</returns>
[DoesNotReturn]
public static System.Exception NeverHappen()
public static System.Exception NeverHappen(string? context = null)
{
throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认");
throw new NotSupportedException(context);
}
/// <summary>

View File

@@ -14,8 +14,8 @@ namespace Snap.Hutao.Core;
/// </summary>
internal abstract class WebView2Helper
{
private static bool hasEverDetected = false;
private static bool isSupported = false;
private static bool hasEverDetected;
private static bool isSupported;
private static string version = "未检测到 WebView2 运行时";
/// <summary>

Some files were not shown because too many files have changed in this diff Show More