Compare commits

..

100 Commits

Author SHA1 Message Date
DismissedLight
8c52921b3e refactor codebase 2023-02-14 14:30:59 +08:00
DismissedLight
605aecb216 remove strings 2023-02-13 14:51:18 +08:00
DismissedLight
e3e124d52f Merge pull request #500 from DGP-Studio/l10n_main
New Crowdin updates
2023-02-13 14:48:40 +08:00
Masterain
0866e1947b New translations SH.resx (English) 2023-02-12 22:47:53 -08:00
Masterain
dbcb7dd879 New translations SH.resx (Chinese Traditional) 2023-02-12 22:47:52 -08:00
Masterain
44687dd87b New translations SH.resx (English) 2023-02-12 21:59:02 -08:00
Masterain
1a209f6c8d New translations SH.resx (Chinese Traditional) 2023-02-12 21:59:01 -08:00
Masterain
8633b78725 New translations SH.resx (Russian) 2023-02-12 21:59:00 -08:00
Masterain
2e20701c6c New translations SH.resx (Japanese) 2023-02-12 21:58:59 -08:00
DismissedLight
9c4d4cda1e fix localization 2023-02-13 13:51:15 +08:00
DismissedLight
a32481980b code style [skip ci] 2023-02-12 16:18:17 +08:00
DismissedLight
b5577e76a5 add hint for #493 2023-02-12 16:15:30 +08:00
DismissedLight
6c2ff9b3c9 fix process name detection 2023-02-11 18:32:00 +08:00
DismissedLight
818365b816 fix #486 2023-02-11 12:32:00 +08:00
DismissedLight
d7dd8c6f0d code style 2023-02-10 16:07:01 +08:00
DismissedLight
faad104e0e fix import thread issue 2023-02-10 11:57:55 +08:00
DismissedLight
2f6ee75f80 Merge pull request #479 from DGP-Studio/l10n_main
New Crowdin updates
2023-02-09 20:32:35 +08:00
Masterain
34f319bdac New translations SH.resx (English) 2023-02-09 04:32:02 -08:00
Masterain
f242808768 New translations SH.resx (Chinese Traditional) 2023-02-09 04:32:01 -08:00
DismissedLight
98f18f91d8 use WScript.Shell to run scheduled tasks 2023-02-09 19:23:24 +08:00
Masterain
0fd1f6959a New translations SH.resx (English) 2023-02-08 20:27:42 -08:00
Masterain
ba46ed64db New translations SH.resx (Chinese Traditional) 2023-02-08 20:27:41 -08:00
Masterain
0fb8312605 New translations SH.resx (Russian) 2023-02-08 20:27:40 -08:00
Masterain
b722554950 New translations SH.resx (Japanese) 2023-02-08 20:27:39 -08:00
DismissedLight
165c33ef2c fix translation 2023-02-09 12:26:42 +08:00
Masterain
54bb3d634b New translations SH.resx (English) 2023-02-08 20:26:02 -08:00
Masterain
629975480a New translations SH.resx (Chinese Traditional) 2023-02-08 20:26:01 -08:00
Masterain
5a36448c23 New translations SH.resx (Russian) 2023-02-08 20:26:00 -08:00
Masterain
80a6aaab46 New translations SH.resx (Japanese) 2023-02-08 20:25:58 -08:00
Masterain
6c83cd3da5 Update azure-pipelines.yml for Azure Pipelines 2023-02-07 21:54:23 -08:00
DismissedLight
e60a04a2bc impl #117 2023-02-08 12:28:31 +08:00
DismissedLight
aec483510f fix #460 2023-02-08 10:07:10 +08:00
DismissedLight
c245fe654e add gacha import validation 2023-02-07 16:57:53 +08:00
DismissedLight
898d95bb1d add more globalization strings 2023-02-07 15:36:50 +08:00
DismissedLight
1df22e5b75 fix L10n issues 2023-02-07 14:44:36 +08:00
DismissedLight
332e09fef0 fix #439 2023-02-07 13:41:53 +08:00
DismissedLight
2a77daf2ca Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-07 10:21:51 +08:00
DismissedLight
8a47ea8727 fix enka api 2023-02-07 10:21:36 +08:00
DismissedLight
b3937ac810 Merge pull request #453 from DGP-Studio/l10n_main
New Crowdin updates
2023-02-06 16:49:06 +08:00
Masterain
ed5c52dc63 New translations SH.resx (English) 2023-02-06 00:44:03 -08:00
Masterain
461d139602 New translations SH.resx (English) 2023-02-05 22:49:55 -08:00
Masterain
164ec2af33 New translations SH.resx (Chinese Traditional) 2023-02-05 22:49:54 -08:00
Masterain
e30523c621 New translations SH.resx (Russian) 2023-02-05 22:49:53 -08:00
Masterain
11d0405102 New translations SH.resx (Japanese) 2023-02-05 22:49:52 -08:00
DismissedLight
a1c0b4f830 fix zh-cn showcase 2 [skip ci] 2023-02-06 14:46:04 +08:00
Masterain
e476ed5960 New translations SH.resx (English) 2023-02-05 22:21:50 -08:00
Masterain
ffc999360d New translations SH.resx (Chinese Traditional) 2023-02-05 22:21:49 -08:00
Masterain
84058011c7 New translations SH.resx (Russian) 2023-02-05 22:21:48 -08:00
Masterain
c18e0c40c5 New translations SH.resx (Japanese) 2023-02-05 22:21:48 -08:00
DismissedLight
ad78515094 fix zh-cn task [skip ci] 2023-02-06 14:20:10 +08:00
Masterain
38367a090d New translations SH.resx (English) 2023-02-05 22:12:29 -08:00
Masterain
ce30f609fb New translations SH.resx (Chinese Traditional) 2023-02-05 22:12:28 -08:00
Masterain
f4b9cc7c48 New translations SH.resx (Russian) 2023-02-05 22:12:27 -08:00
Masterain
7c2212f44c New translations SH.resx (Japanese) 2023-02-05 22:12:26 -08:00
DismissedLight
95eddef457 fix zh-cn showcase 2023-02-06 14:11:43 +08:00
Masterain
02447bc966 New translations SH.resx (English) 2023-02-05 21:49:52 -08:00
Masterain
fb88e33d16 New translations SH.resx (Chinese Traditional) 2023-02-05 21:49:51 -08:00
Masterain
5fa36416ef New translations SH.resx (Russian) 2023-02-05 21:49:50 -08:00
Masterain
7076caaa5d New translations SH.resx (Japanese) 2023-02-05 21:49:49 -08:00
DismissedLight
b7b1155cfc adjust achievement UI 2023-02-06 13:45:41 +08:00
Masterain
6351f2b460 Update SH.resx
- fix typo in based language
2023-02-05 20:09:28 -08:00
DismissedLight
35ac2f33ba Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-05 21:06:45 +08:00
DismissedLight
f8ff1988bb update readme 2023-02-05 21:06:41 +08:00
Masterain
907d70ba71 Update azure-pipelines.yml for Azure Pipelines
[skip ci]
2023-02-05 05:04:47 -08:00
Masterain
a5bdc17712 Update azure-pipelines.yml
[skip ci]
2023-02-05 04:50:00 -08:00
Masterain
f078d92f33 Update Crowdin configuration file 2023-02-05 04:46:19 -08:00
DismissedLight
f2d4f0f1d3 locale start 2023-02-05 19:52:00 +08:00
DismissedLight
fcde9b21ae fix #442 2023-02-03 20:05:32 +08:00
DismissedLight
24f09861fd locale zh-cn phase 2 2023-02-02 20:48:48 +08:00
DismissedLight
47708adc83 update readme 2023-02-02 16:35:48 +08:00
DismissedLight
79a254235a Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-01 20:28:40 +08:00
DismissedLight
d9bcb3b16b locale zh-cn 2023-02-01 20:28:32 +08:00
Masterain
cf7dd548a2 Update network-issue.yml 2023-01-30 17:34:07 -08:00
DismissedLight
04deeb7086 Create FUNDING.yml 2023-01-30 19:26:25 +08:00
DismissedLight
9fb2da41cd store migration 2023-01-30 16:22:54 +08:00
DismissedLight
bb01f3a3cb fix package convert issue 2023-01-30 10:43:05 +08:00
DismissedLight
f7f2d9c867 fix #406 2023-01-28 20:03:37 +08:00
DismissedLight
01b7e58b3e fix convert cache 2023-01-27 16:51:43 +08:00
DismissedLight
2518ae0b90 package convert impl 2023-01-27 11:22:25 +08:00
DismissedLight
7d4a8cdcd9 fix empty statistics [skip ci] 2023-01-23 13:06:56 +08:00
DismissedLight
623893e00e remove visual transition gap in gacha log initialization 2023-01-23 12:58:00 +08:00
Masterain
0d34c81bcf Merge pull request #388 from wordlesswind/patch-1
Update version information and fix broken links
2023-01-22 01:07:49 -08:00
清靈語
5f3d0126b3 Update version information 2023-01-22 12:51:57 +08:00
DismissedLight
5d1fe3f38a move dispatcher queue to thread helper 2023-01-21 13:14:54 +08:00
DismissedLight
c810ffa625 remove unnecessary converters 2023-01-20 17:30:16 +08:00
DismissedLight
ee70205245 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-01-20 15:47:18 +08:00
DismissedLight
06c8b347d3 Announcement Viewer 2023-01-20 15:47:05 +08:00
Masterain
5c6ab1dee9 Update azure-pipelines.yml for Azure Pipelines 2023-01-19 15:23:50 -08:00
DismissedLight
ad440e0561 fix #377 2023-01-19 14:55:53 +08:00
DismissedLight
ca56d8c636 remove async relay command factory 2023-01-18 15:29:22 +08:00
Masterain
da0ee0cca6 Update PublishDistribution.yml
[skip ci]
2023-01-16 13:35:57 -08:00
Masterain
5d00d9cc0d Update azure-pipelines.yml for Azure Pipelines
[force ci]
2023-01-16 13:15:05 -08:00
Masterain
e8b27e6655 Update azure-pipelines.yml for Azure Pipelines 2023-01-16 13:05:55 -08:00
DismissedLight
0ac79012d1 fix #368 2023-01-16 18:12:12 +08:00
DismissedLight
bb2665b75e Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-01-16 14:27:45 +08:00
DismissedLight
d22ac39c1d fix dupe download items [skip ci] 2023-01-16 14:27:31 +08:00
Masterain
a312603d61 Update azure-pipelines.yml for Azure Pipelines 2023-01-15 22:22:34 -08:00
DismissedLight
0732ea0e06 replace font 2023-01-16 14:10:28 +08:00
Masterain
e4d2b3055c Update azure-pipelines.yml for Azure Pipelines
[skip ci]
2023-01-14 17:28:43 -08:00
Masterain
5668931230 Update PublishDistribution.yml
[skip ci]
2023-01-14 17:08:50 -08:00
381 changed files with 18990 additions and 5722 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: https://afdian.net/a/DismissedLight

View File

@@ -15,7 +15,7 @@ body:
description: |- description: |-
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略 请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
options: options:
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/most-frequent-questions.html),并认为我的问题没有在文档中得到解答 - label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/),并认为我的问题没有在文档中得到解答
required: true 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) - label: 我使用的操作系统是[受支持的版本](https://hut.ao/quick-start.html#%E6%9C%80%E4%BD%8E%E7%B3%BB%E7%BB%9F%E8%A6%81%E6%B1%82)
@@ -33,7 +33,7 @@ body:
label: Windows 版本 label: Windows 版本
description: | description: |
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到 `Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
placeholder: 22000.556 placeholder: 22621.1105
validations: validations:
required: true required: true
@@ -42,7 +42,7 @@ body:
attributes: attributes:
label: Snap Hutao 版本 label: Snap Hutao 版本
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到 description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
placeholder: 1.1.0 placeholder: 1.3.13.0
validations: validations:
required: true required: true

View File

@@ -14,7 +14,7 @@ body:
id: back id: back
attributes: attributes:
label: 背景与动机 label: 背景与动机
description: 添加此功能的理由 description: 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的 Issue
validations: validations:
required: true required: true

View File

@@ -21,7 +21,7 @@ body:
**在填写下面的问题之前请先使用我们的网络诊断工具** **在填写下面的问题之前请先使用我们的网络诊断工具**
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交** **这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
- 你可以点击下面的链接以下载网络诊断工具: - 你可以点击下面的链接以下载网络诊断工具:
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-tool.exe) - [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-hutao.exe)
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe) - [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
validations: validations:
required: true required: true

View File

@@ -2,7 +2,7 @@ name: PublishDistribution
on: on:
release: release:
types: [published] types: [released]
workflow_dispatch: workflow_dispatch:
@@ -39,3 +39,13 @@ jobs:
EOF EOF
rclone copy ./release-download/* dgpODCN:/releases/ rclone copy ./release-download/* dgpODCN:/releases/
# Purge Patch System Cache
- name: Purge Patch
env:
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
PURGE_URL: ${{ secrets.PURGE_URL }}
run: |
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL

View File

@@ -1,11 +1,15 @@
# [Snap.Hutao](https://hut.ao) ![](https://repository-images.githubusercontent.com/482734649/5f8cf574-2ef0-43e9-aa8d-6cf094b54dd9)
> 唷,找本堂主有何贵干呀?
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg) ## 下载使用
# 特别感谢 [![](https://get.microsoft.com/images/zh-cn%20light.svg)](https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52)
### 原神组织与个人 ## 贡献
* [向我们提交 PR](https://github.com/DGP-Studio/Snap.Hutao/pulls)
* [在 Crowdin 上进行本地化](https://crowdin.com/project/snap-hutao)
## 特别感谢
* [HolographicHat](https://github.com/HolographicHat) * [HolographicHat](https://github.com/HolographicHat)
* [UIGF organization](https://uigf.org) * [UIGF organization](https://uigf.org)
@@ -29,4 +33,13 @@
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation) * [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK) * [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml) * [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [WinUICommunity/SettingsUI](https://github.com/WinUICommunity/SettingsUI) * [WinUICommunity/SettingsUI](https://github.com/WinUICommunity/SettingsUI)
### 支撑项目
* [Snap.Hutao.Server](https://github.com/DGP-Studio/Snap.Hutao.Server)
* [Snap.Metadata](https://github.com/DGP-Studio/Snap.Metadata)
* [Snap.Data.Mapper](https://github.com/DGP-Studio/Snap.Data.Mapper)
## 近期活跃数据
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)

View File

@@ -17,6 +17,7 @@ trigger:
- azure-pipelines.yml - azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml - .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml - .github/workflows/*.yml
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
pr: pr:
branches: branches:
include: include:
@@ -27,6 +28,7 @@ pr:
- azure-pipelines.yml - azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml - .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml - .github/workflows/*.yml
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
pool: pool:
@@ -91,7 +93,8 @@ steps:
"Package/Identity/@Publisher": "CN=DGP Studio CI", "Package/Identity/@Publisher": "CN=DGP Studio CI",
"Package/Identity/@Version": "$(build_date).$(rev_number)", "Package/Identity/@Version": "$(build_date).$(rev_number)",
"Package/Properties/DisplayName": "胡桃 Alpha", "Package/Properties/DisplayName": "胡桃 Alpha",
"Package/Properties/PublisherDisplayName":"DGP Studio CI" "Package/Properties/PublisherDisplayName":"DGP Studio CI",
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
} }
- task: CmdLine@2 - task: CmdLine@2
@@ -124,18 +127,21 @@ steps:
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix' script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
- task: MsixSigning@1 - task: MsixSigning@1
name: signMsix
displayName: Sign MSIX package displayName: Sign MSIX package
inputs: inputs:
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix' package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
certificate: 'DGP_Studio_CI.pfx' certificate: 'DGP_Studio_CI.pfx'
passwordVariable: 'pw' passwordVariable: 'pw'
condition: succeeded()
- task: PublishPipelineArtifact@1
displayName: 'Upload Output' #- task: PublishPipelineArtifact@1
inputs: # displayName: 'Upload Output'
targetPath: '$(Build.ArtifactStagingDirectory)/' # inputs:
artifact: 'Output' # targetPath: '$(Build.ArtifactStagingDirectory)/'
publishLocation: 'pipeline' # artifact: 'Output'
# publishLocation: 'pipeline'
- task: DownloadSecureFile@1 - task: DownloadSecureFile@1
name: cerFile name: cerFile
@@ -144,7 +150,6 @@ steps:
secureFile: 'Snap.Hutao.CI.cer' secureFile: 'Snap.Hutao.CI.cer'
- task: GitHubRelease@1 - task: GitHubRelease@1
condition: or(eq(variables['Build.Reason'], 'Manual'), eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'))
inputs: inputs:
gitHubConnection: 'github.com_Masterain' gitHubConnection: 'github.com_Masterain'
repositoryName: 'DGP-Studio/Snap.Hutao' repositoryName: 'DGP-Studio/Snap.Hutao'
@@ -155,10 +160,11 @@ steps:
title: '$(build_date).$(rev_number)' title: '$(build_date).$(rev_number)'
releaseNotesSource: 'inline' releaseNotesSource: 'inline'
releaseNotesInline: | releaseNotesInline: |
## 提示 (Hint) ## 普通用户请勿下载
发布版本由 CI 程序自动打包生成,属于 `Alpha` 测试版,仅用于开发调试和内部测试用途。使用该版本可能存在意料之外的风险,请仅在有明确用途的情况下使用该版本。 该版本由 CI 程序自动打包生成 `Alpha` 测试版本,**仅供开发者测试使用**
This release is a Alpha Testing version generated by CI program automatically in a purpose of debugging and interal testing. Using this release may have unexpected risk, please only use it when you know what you are doing. 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
assets: | assets: |
$(Build.ArtifactStagingDirectory)/* $(Build.ArtifactStagingDirectory)/*
$(cerFile.secureFilePath) $(cerFile.secureFilePath)
@@ -167,12 +173,13 @@ steps:
changeLogType: 'commitBased' changeLogType: 'commitBased'
- task: DownloadSecureFile@1 - task: DownloadSecureFile@1
name: cerFile name: RcloneConfigFile
displayName: Download Rclone Config displayName: Download Rclone Config
inputs: inputs:
secureFile: 'rclone.conf' secureFile: 'rclone.conf'
- task: rclone@1 - task: rclone@1
displayName: Upload CI via Rclone
inputs: inputs:
arguments: 'copy $(Build.ArtifactStagingDirectory)/* downloadDGPCN:/releases/Alpha/' arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/Alpha/'
configPath: '$(cerFile.secureFilePath)/rclone.conf' configPath: '$(RcloneConfigFile.secureFilePath)'

3
crowdin.yml Normal file
View File

@@ -0,0 +1,3 @@
files:
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.%locale%.resx

View File

@@ -10,7 +10,7 @@ csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = false:silent csharp_style_expression_bodied_properties = false:silent
csharp_style_expression_bodied_indexers = false:silent csharp_style_expression_bodied_indexers = false:silent
csharp_style_expression_bodied_accessors = when_on_single_line:silent csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
csharp_style_expression_bodied_lambdas = when_on_single_line:silent csharp_style_expression_bodied_lambdas = when_on_single_line:silent
csharp_style_expression_bodied_local_functions = false:silent csharp_style_expression_bodied_local_functions = false:silent
csharp_style_conditional_delegate_call = true:suggestion csharp_style_conditional_delegate_call = true:suggestion

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Installer;
internal class Program internal class Program
{ {
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx"; private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock";
private const string ValueName = "AllowDevelopmentWithoutDevLicense"; private const string ValueName = "AllowDevelopmentWithoutDevLicense";
public static async Task Main(string[] args) public static async Task Main(string[] args)

View File

@@ -25,4 +25,4 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -50,8 +50,8 @@ Global
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86 {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86 {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86 {AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU {8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU {8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64 {8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64
@@ -66,8 +66,8 @@ Global
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64 {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.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = 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.ActiveCfg = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|Any CPU {CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.ActiveCfg = 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|arm64.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.ActiveCfg = Debug|x64 {CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.ActiveCfg = Debug|x64
@@ -78,8 +78,8 @@ Global
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.Build.0 = 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.ActiveCfg = Release|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|Any CPU {CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.ActiveCfg = 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 {CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection

View File

@@ -9,7 +9,7 @@
}, },
"pathSegment": { "pathSegment": {
"add": { "add": {
".*": [ ".cs" ] ".*": [ ".cs", ".resx" ]
} }
}, },
"fileSuffixToExtension": { "fileSuffixToExtension": {
@@ -19,11 +19,12 @@
}, },
"fileToFile": { "fileToFile": {
"add": { "add": {
".filenesting.json": [ "App.xaml.cs" ],
"app.manifest": [ "App.xaml.cs" ], "app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml.cs" ], "Package.appxmanifest": [ "App.xaml" ],
"GlobalUsing.cs": [ "Program.cs" ], "Package.StoreAssociation.xml": [ "App.xaml" ],
".filenesting.json": [ "Program.cs" ], ".editorconfig": [ "Program.cs" ],
".editorconfig": [ "Program.cs" ] "GlobalUsing.cs": [ "Program.cs" ]
} }
} }
} }

View File

@@ -11,24 +11,24 @@
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<muxc:XamlControlsResources/> <muxc:XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/> <ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/>
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries> <ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light"> <ResourceDictionary x:Key="Light">
<Color x:Key="AvatarPropertyAddValueColor">#FF74BF00</Color>
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color> <Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
</ResourceDictionary> </ResourceDictionary>
<ResourceDictionary x:Key="Dark"> <ResourceDictionary x:Key="Dark">
<Color x:Key="AvatarPropertyAddValueColor">#FF90E800</Color>
<Color x:Key="CompatBackgroundColor">#FF242424</Color> <Color x:Key="CompatBackgroundColor">#FF242424</Color>
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </ResourceDictionary.ThemeDictionaries>
<!-- Modify Window title bar color --> <!-- Modify Window title bar color -->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/> <StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/>
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/> <StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- Page Transparent Background --> <!-- Page Transparent Background -->
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/> <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 --> <!-- InfoBar Resource -->
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness> <Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness> <Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
@@ -44,8 +44,10 @@
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius> <CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
<!-- OpenPaneLength --> <!-- OpenPaneLength -->
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double> <x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">252</x:Double> <x:Double x:Key="CompatSplitViewOpenPaneLength2">268</x:Double>
<GridLength x:Key="CompatGridLength2">252</GridLength> <GridLength x:Key="CompatGridLength2">268</GridLength>
<!-- Brushes -->
<SolidColorBrush x:Key="AvatarPropertyAddValueBrush" Color="{ThemeResource AvatarPropertyAddValueColor}"/>
<!-- Uris --> <!-- Uris -->
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie</x:String> <x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie</x:String>
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String> <x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
@@ -79,10 +81,6 @@
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/> <shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/> <shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/> <shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<shvc:EmptyCollectionToBoolConverter x:Key="EmptyCollectionToBoolConverter"/>
<shvc:EmptyCollectionToBoolRevertConverter x:Key="EmptyCollectionToBoolRevertConverter"/>
<shvc:EmptyCollectionToVisibilityConverter x:Key="EmptyCollectionToVisibilityConverter"/>
<shvc:EmptyCollectionToVisibilityRevertConverter x:Key="EmptyCollectionToVisibilityRevertConverter"/>
<shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/> <shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/>
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/> <shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/> <shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
@@ -90,6 +88,7 @@
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/> <shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/> <shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
<!-- Styles --> <!-- Styles -->
<Style <Style
x:Key="LargeGridViewItemStyle" x:Key="LargeGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}" BasedOn="{StaticResource DefaultGridViewItemStyle}"
@@ -112,6 +111,328 @@
<Setter Property="BorderThickness" Value="1"/> <Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{StaticResource CompatCornerRadius}"/> <Setter Property="CornerRadius" Value="{StaticResource CompatCornerRadius}"/>
</Style> </Style>
<Style x:Key="WebView2ContentDialogStyle" TargetType="ContentDialog">
<Setter Property="Foreground" Value="{ThemeResource ContentDialogForeground}"/>
<Setter Property="Background" Value="{ThemeResource ContentDialogBackground}"/>
<Setter Property="BorderThickness" Value="{ThemeResource ContentDialogBorderWidth}"/>
<Setter Property="BorderBrush" Value="{ThemeResource ContentDialogBorderBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentDialog">
<Border x:Name="Container">
<Grid x:Name="LayoutRoot" Visibility="Collapsed">
<Rectangle x:Name="SmokeLayerBackground" Fill="{ThemeResource ContentDialogSmokeFill}"/>
<Border
x:Name="BackgroundElement"
MinWidth="{ThemeResource ContentDialogMinWidth}"
MinHeight="{ThemeResource ContentDialogMinHeight}"
MaxWidth="{ThemeResource ContentDialogMaxWidth}"
MaxHeight="{ThemeResource ContentDialogMaxHeight}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BackgroundSizing="InnerBorderEdge"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
FlowDirection="{TemplateBinding FlowDirection}"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<ScaleTransform x:Name="ScaleTransform"/>
</Border.RenderTransform>
<Grid x:Name="DialogSpace" CornerRadius="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer
x:Name="ContentScrollViewer"
HorizontalScrollBarVisibility="Disabled"
IsTabStop="False"
VerticalScrollBarVisibility="Disabled"
ZoomMode="Disabled">
<Grid
Padding="0"
BorderBrush="{ThemeResource ContentDialogSeparatorBorderBrush}"
BorderThickness="{ThemeResource ContentDialogSeparatorThickness}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ContentControl
x:Name="Title"
Margin="{ThemeResource ContentDialogTitleMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{TemplateBinding Title}"
ContentTemplate="{TemplateBinding TitleTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="20"
FontWeight="SemiBold"
Foreground="{TemplateBinding Foreground}"
IsTabStop="False">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
MaxLines="2"
TextWrapping="Wrap"/>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
<ContentPresenter
x:Name="Content"
Grid.Row="1"
Margin="0,0,0,8"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="{StaticResource ControlContentThemeFontSize}"
Foreground="{TemplateBinding Foreground}"
TextWrapping="Wrap"/>
</Grid>
</ScrollViewer>
<Grid
x:Name="CommandSpace"
Grid.Row="1"
Padding="8,0,8,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
XYFocusKeyboardNavigation="Enabled">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="PrimaryColumn" Width="*"/>
<ColumnDefinition x:Name="FirstSpacer" Width="0"/>
<ColumnDefinition x:Name="SecondaryColumn" Width="0"/>
<ColumnDefinition x:Name="SecondSpacer" Width="{ThemeResource ContentDialogButtonSpacing}"/>
<ColumnDefinition x:Name="CloseColumn" Width="*"/>
</Grid.ColumnDefinitions>
<Button
x:Name="PrimaryButton"
HorizontalAlignment="Stretch"
Content="{TemplateBinding PrimaryButtonText}"
ElementSoundMode="FocusOnly"
IsEnabled="{TemplateBinding IsPrimaryButtonEnabled}"
IsTabStop="False"
Style="{TemplateBinding PrimaryButtonStyle}"/>
<Button
x:Name="SecondaryButton"
HorizontalAlignment="Stretch"
Content="{TemplateBinding SecondaryButtonText}"
ElementSoundMode="FocusOnly"
IsEnabled="{TemplateBinding IsSecondaryButtonEnabled}"
IsTabStop="False"
Style="{TemplateBinding SecondaryButtonStyle}"/>
<Button
x:Name="CloseButton"
Grid.Column="4"
HorizontalAlignment="Stretch"
Content="{TemplateBinding CloseButtonText}"
ElementSoundMode="FocusOnly"
IsTabStop="False"
Style="{TemplateBinding CloseButtonStyle}"/>
</Grid>
</Grid>
</Border>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DialogShowingStates">
<VisualStateGroup.Transitions>
<VisualTransition To="DialogHidden">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="False"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleX">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFastAnimationDuration}"
Value="1.05"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleY">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFastAnimationDuration}"
Value="1.05"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0.0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition To="DialogShowing">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleX">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.05"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleY">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.05"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0.0"/>
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="DialogHidden"/>
<VisualState x:Name="DialogShowing">
<VisualState.Setters>
<Setter Target="PrimaryButton.IsTabStop" Value="True"/>
<Setter Target="SecondaryButton.IsTabStop" Value="True"/>
<Setter Target="CloseButton.IsTabStop" Value="True"/>
<Setter Target="LayoutRoot.Visibility" Value="Visible"/>
<Setter Target="BackgroundElement.TabFocusNavigation" Value="Cycle"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DialogShowingWithoutSmokeLayer">
<VisualState.Setters>
<Setter Target="PrimaryButton.IsTabStop" Value="True"/>
<Setter Target="SecondaryButton.IsTabStop" Value="True"/>
<Setter Target="CloseButton.IsTabStop" Value="True"/>
<Setter Target="LayoutRoot.Visibility" Value="Visible"/>
<Setter Target="LayoutRoot.Background" Value="{x:Null}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DialogSizingStates">
<VisualState x:Name="DefaultDialogSizing"/>
<VisualState x:Name="FullDialogSizing">
<VisualState.Setters>
<Setter Target="BackgroundElement.VerticalAlignment" Value="Stretch"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ButtonsVisibilityStates">
<VisualState x:Name="AllVisible">
<VisualState.Setters>
<Setter Target="FirstSpacer.Width" Value="{ThemeResource ContentDialogButtonSpacing}"/>
<Setter Target="SecondaryColumn.Width" Value="*"/>
<Setter Target="SecondaryButton.(Grid.Column)" Value="2"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NoneVisible">
<VisualState.Setters>
<Setter Target="CommandSpace.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.(Grid.Column)" Value="4"/>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.(Grid.Column)" Value="4"/>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="CloseVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryAndSecondaryVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.(Grid.Column)" Value="4"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryAndCloseVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryAndCloseVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DefaultButtonStates">
<VisualState x:Name="NoDefaultButton"/>
<VisualState x:Name="PrimaryAsDefaultButton">
<VisualState.Setters>
<Setter Target="PrimaryButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryAsDefaultButton">
<VisualState.Setters>
<Setter Target="SecondaryButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="CloseAsDefaultButton">
<VisualState.Setters>
<Setter Target="CloseButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DialogBorderStates">
<VisualState x:Name="NoBorder"/>
<VisualState x:Name="AccentColorBorder">
<VisualState.Setters>
<Setter Target="BackgroundElement.BorderBrush" Value="{ThemeResource SystemControlForegroundAccentBrush}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ItemsPanelTemplate --> <!-- ItemsPanelTemplate -->
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate"> <ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
<ItemsStackPanel/> <ItemsStackPanel/>
@@ -121,4 +442,4 @@
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -7,7 +7,6 @@ using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging;
using System.Diagnostics; using System.Diagnostics;
using Windows.Storage; using Windows.Storage;
@@ -25,7 +24,6 @@ public partial class App : Application
/// Initializes the singleton application object. /// Initializes the singleton application object.
/// </summary> /// </summary>
/// <param name="logger">日志器</param> /// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger) public App(ILogger<App> logger)
{ {
// load app resource // load app resource
@@ -50,8 +48,8 @@ public partial class App : Application
firstInstance.Activated += Activation.Activate; firstInstance.Activated += Activation.Activate;
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate; ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
logger.LogInformation(EventIds.CommonLog, "Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version); logger.LogInformation("Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path); logger.LogInformation("Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path);
JumpListHelper.ConfigureAsync().SafeForget(logger); JumpListHelper.ConfigureAsync().SafeForget(logger);
} }

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Animation;
/// <summary>
/// 动画时长
/// </summary>
[HighQuality]
internal static class AnimationDurations
{
/// <summary>
/// 图片缩放动画
/// </summary>
public static readonly TimeSpan ImageZoom = TimeSpan.FromSeconds(0.5);
}

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.WinUI.UI; using CommunityToolkit.WinUI.UI;
using CommunityToolkit.WinUI.UI.Animations; using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using System.Diagnostics.Contracts;
using System.Numerics; using System.Numerics;
namespace Snap.Hutao.Control.Animation; namespace Snap.Hutao.Control.Animation;
@@ -11,17 +12,18 @@ namespace Snap.Hutao.Control.Animation;
/// <summary> /// <summary>
/// 图片放大动画 /// 图片放大动画
/// </summary> /// </summary>
internal class ImageZoomInAnimation : ImplicitAnimation<string, Vector3> [HighQuality]
internal sealed class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
{ {
/// <summary> /// <summary>
/// 构造一个新的图片放大动画 /// 构造一个新的图片放大动画
/// </summary> /// </summary>
public ImageZoomInAnimation() public ImageZoomInAnimation()
{ {
Duration = AnimationDurations.ImageZoom;
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut; EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.UI.Animations.EasingType.Circle; EasingType = CommunityToolkit.WinUI.UI.Animations.EasingType.Circle;
To = "1.1"; To = Core.StringLiterals.OnePointOne;
Duration = TimeSpan.FromSeconds(0.5);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -35,4 +37,4 @@ internal class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
{ {
return (To?.ToVector3(), From?.ToVector3()); return (To?.ToVector3(), From?.ToVector3());
} }
} }

View File

@@ -11,17 +11,18 @@ namespace Snap.Hutao.Control.Animation;
/// <summary> /// <summary>
/// 图片缩小动画 /// 图片缩小动画
/// </summary> /// </summary>
internal class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3> [HighQuality]
internal sealed class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
{ {
/// <summary> /// <summary>
/// 构造一个新的图片缩小动画 /// 构造一个新的图片缩小动画
/// </summary> /// </summary>
public ImageZoomOutAnimation() public ImageZoomOutAnimation()
{ {
Duration = AnimationDurations.ImageZoom;
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut; EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.UI.Animations.EasingType.Circle; EasingType = CommunityToolkit.WinUI.UI.Animations.EasingType.Circle;
To = "1"; To = Core.StringLiterals.One;
Duration = TimeSpan.FromSeconds(0.5);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -9,10 +9,11 @@ namespace Snap.Hutao.Control.Behavior;
/// <summary> /// <summary>
/// 按给定比例自动调整高度的行为 /// 按给定比例自动调整高度的行为
/// </summary> /// </summary>
internal class AutoHeightBehavior : BehaviorBase<FrameworkElement> [HighQuality]
internal sealed class AutoHeightBehavior : BehaviorBase<FrameworkElement>
{ {
private static readonly DependencyProperty TargetWidthProperty = Property<AutoHeightBehavior>.Depend(nameof(TargetWidth), 1080D); private static readonly DependencyProperty TargetWidthProperty = Property<AutoHeightBehavior>.DependBoxed<double>(nameof(TargetWidth), BoxedValues.DoubleOne);
private static readonly DependencyProperty TargetHeightProperty = Property<AutoHeightBehavior>.Depend(nameof(TargetHeight), 390D); private static readonly DependencyProperty TargetHeightProperty = Property<AutoHeightBehavior>.DependBoxed<double>(nameof(TargetHeight), BoxedValues.DoubleOne);
/// <summary> /// <summary>
/// 目标宽度 /// 目标宽度
@@ -35,7 +36,7 @@ internal class AutoHeightBehavior : BehaviorBase<FrameworkElement>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnAssociatedObjectLoaded() protected override void OnAssociatedObjectLoaded()
{ {
UpdateElementHeight(); UpdateElement();
AssociatedObject.SizeChanged += OnSizeChanged; AssociatedObject.SizeChanged += OnSizeChanged;
} }
@@ -43,15 +44,16 @@ internal class AutoHeightBehavior : BehaviorBase<FrameworkElement>
protected override void OnDetaching() protected override void OnDetaching()
{ {
AssociatedObject.SizeChanged -= OnSizeChanged; AssociatedObject.SizeChanged -= OnSizeChanged;
base.OnDetaching();
} }
private void OnSizeChanged(object sender, SizeChangedEventArgs e) private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{ {
UpdateElementHeight(); UpdateElement();
} }
private void UpdateElementHeight() private void UpdateElement()
{ {
AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth); AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
} }
} }

View File

@@ -9,7 +9,8 @@ namespace Snap.Hutao.Control.Behavior;
/// <summary> /// <summary>
/// 按给定比例自动调整高度的行为 /// 按给定比例自动调整高度的行为
/// </summary> /// </summary>
internal class AutoWidthBehavior : BehaviorBase<FrameworkElement> [HighQuality]
internal sealed class AutoWidthBehavior : BehaviorBase<FrameworkElement>
{ {
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetWidth), 320D); private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetWidth), 320D);
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetHeight), 1024D); private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetHeight), 1024D);
@@ -35,7 +36,7 @@ internal class AutoWidthBehavior : BehaviorBase<FrameworkElement>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnAssociatedObjectLoaded() protected override void OnAssociatedObjectLoaded()
{ {
UpdateElementWidth(); UpdateElement();
AssociatedObject.SizeChanged += OnSizeChanged; AssociatedObject.SizeChanged += OnSizeChanged;
} }
@@ -43,14 +44,15 @@ internal class AutoWidthBehavior : BehaviorBase<FrameworkElement>
protected override void OnDetaching() protected override void OnDetaching()
{ {
AssociatedObject.SizeChanged -= OnSizeChanged; AssociatedObject.SizeChanged -= OnSizeChanged;
base.OnDetaching();
} }
private void OnSizeChanged(object sender, SizeChangedEventArgs e) private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{ {
UpdateElementWidth(); UpdateElement();
} }
private void UpdateElementWidth() private void UpdateElement()
{ {
AssociatedObject.Width = (double)AssociatedObject.Height * (TargetWidth / TargetHeight); AssociatedObject.Width = (double)AssociatedObject.Height * (TargetWidth / TargetHeight);
} }

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Control.Behavior;
/// AppTitleBar Workaround /// AppTitleBar Workaround
/// https://github.com/microsoft/microsoft-ui-xaml/issues/7756 /// https://github.com/microsoft/microsoft-ui-xaml/issues/7756
/// </summary> /// </summary>
internal class ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior : BehaviorBase<ComboBox> internal sealed class ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior : BehaviorBase<ComboBox>
{ {
private readonly IMessenger messenger; private readonly IMessenger messenger;
@@ -30,6 +30,15 @@ internal class ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior : BehaviorBa
AssociatedObject.DropDownClosed += OnDropDownClosed; AssociatedObject.DropDownClosed += OnDropDownClosed;
} }
/// <inheritdoc/>
protected override void OnDetaching()
{
AssociatedObject.DropDownOpened -= OnDropDownOpened;
AssociatedObject.DropDownClosed -= OnDropDownClosed;
base.OnDetaching();
}
private void OnDropDownOpened(object? sender, object e) private void OnDropDownOpened(object? sender, object e)
{ {
messenger.Send(new Message.FlyoutOpenCloseMessage(true)); messenger.Send(new Message.FlyoutOpenCloseMessage(true));

View File

@@ -9,7 +9,8 @@ namespace Snap.Hutao.Control.Behavior;
/// <summary> /// <summary>
/// 在元素加载完成后执行命令的行为 /// 在元素加载完成后执行命令的行为
/// </summary> /// </summary>
internal class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement> [HighQuality]
internal sealed class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement>
{ {
private static readonly DependencyProperty CommandProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<ICommand>(nameof(Command)); private static readonly DependencyProperty CommandProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<ICommand>(nameof(Command));
private static readonly DependencyProperty CommandParameterProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<object>(nameof(CommandParameter)); private static readonly DependencyProperty CommandParameterProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<object>(nameof(CommandParameter));
@@ -38,9 +39,7 @@ internal class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement>
{ {
if (Command != null && Command.CanExecute(CommandParameter)) if (Command != null && Command.CanExecute(CommandParameter))
{ {
Command?.Execute(CommandParameter); Command.Execute(CommandParameter);
} }
base.OnAssociatedObjectLoaded();
} }
} }

View File

@@ -1,36 +0,0 @@
// 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 InvokeCommandOnUnloadedBehavior : BehaviorBase<UIElement>
{
private static readonly DependencyProperty CommandProperty = Property<InvokeCommandOnUnloadedBehavior>.Depend<ICommand>(nameof(Command));
/// <summary>
/// 待执行的命令
/// </summary>
public ICommand Command
{
get => (ICommand)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
/// <inheritdoc/>
protected override void OnDetaching()
{
// 由于卸载顺序问题,必须重写此方法才能正确触发命令
if (Command != null && Command.CanExecute(null))
{
Command.Execute(null);
}
base.OnDetaching();
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.Xaml.Interactivity;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// 打开附着的浮出控件操作
/// </summary>
[HighQuality]
internal sealed class OpenAttachedFlyoutAction : DependencyObject, IAction
{
/// <inheritdoc/>
public object Execute(object sender, object parameter)
{
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
return null!;
}
}

View File

@@ -8,7 +8,10 @@ namespace Snap.Hutao.Control;
/// <summary> /// <summary>
/// 绑定探针 /// 绑定探针
/// 用于处理特定情况下需要穿透数据上下文的工作 /// 用于处理特定情况下需要穿透数据上下文的工作
/// DependencyObject will dispose inner ReferenceTracker in any time
/// when object is not used anymore.
/// </summary> /// </summary>
[HighQuality]
public class BindingProxy : DependencyObject public class BindingProxy : DependencyObject
{ {
private static readonly DependencyProperty DataContextProperty = Property<BindingProxy>.Depend<object>(nameof(DataContext)); private static readonly DependencyProperty DataContextProperty = Property<BindingProxy>.Depend<object>(nameof(DataContext));

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
/// <summary>
/// 封装的值
/// </summary>
[HighQuality]
internal static class BoxedValues
{
/// <summary>
/// <see cref="double"/> 0
/// </summary>
public static readonly object DoubleZero = 0D;
/// <summary>
/// <see cref="double"/> 0
/// </summary>
public static readonly object DoubleOne = 1D;
/// <summary>
/// <see cref="true"/>
/// </summary>
public static readonly object True = true;
}

View File

@@ -8,23 +8,25 @@ namespace Snap.Hutao.Control.Extension;
/// <summary> /// <summary>
/// 对话框扩展 /// 对话框扩展
/// </summary> /// </summary>
internal static class ContentDialogExtensions [HighQuality]
internal static class ContentDialogExtension
{ {
/// <summary> /// <summary>
/// 阻止用户交互 /// 阻止用户交互
/// </summary> /// </summary>
/// <param name="contentDialog">对话框</param> /// <param name="contentDialog">对话框</param>
/// <returns>用于恢复用户交互</returns> /// <returns>用于恢复用户交互</returns>
public static async ValueTask<IAsyncDisposable> BlockAsync(this ContentDialog contentDialog) public static async ValueTask<IDisposable> BlockAsync(this ContentDialog contentDialog)
{ {
await ThreadHelper.SwitchToMainThreadAsync(); await ThreadHelper.SwitchToMainThreadAsync();
contentDialog.ShowAsync().AsTask().SafeForget(); contentDialog.ShowAsync().AsTask().SafeForget();
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019 // E_ASYNC_OPERATION_NOT_STARTED 0x80000019
// Only a single ContentDialog can be open at any time.
return new ContentDialogHider(contentDialog); return new ContentDialogHider(contentDialog);
} }
private readonly struct ContentDialogHider : IAsyncDisposable private class ContentDialogHider : IDisposable
{ {
private readonly ContentDialog contentDialog; private readonly ContentDialog contentDialog;
@@ -33,12 +35,10 @@ internal static class ContentDialogExtensions
this.contentDialog = contentDialog; this.contentDialog = contentDialog;
} }
public async ValueTask DisposeAsync() public void Dispose()
{ {
await ThreadHelper.SwitchToMainThreadAsync();
// Hide() must be called on main thread. // Hide() must be called on main thread.
contentDialog.Hide(); ThreadHelper.InvokeOnMainThread(contentDialog.Hide);
} }
} }
} }

View File

@@ -13,7 +13,8 @@ namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 缓存图像 /// 缓存图像
/// </summary> /// </summary>
public class CachedImage : ImageEx [HighQuality]
internal sealed class CachedImage : ImageEx
{ {
/// <summary> /// <summary>
/// 构造一个新的缓存图像 /// 构造一个新的缓存图像
@@ -22,17 +23,20 @@ public class CachedImage : ImageEx
{ {
IsCacheEnabled = true; IsCacheEnabled = true;
EnableLazyLoading = true; EnableLazyLoading = true;
LazyLoadingThreshold = 500;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token) protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{ {
// We can only use Ioc to retrive IImageCache,
// no IServiceProvider is available.
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>(); IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
try try
{ {
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri"); Verify.Operation(imageUri.Host != string.Empty, SH.ControlImageCachedImageInvalidResourceUri);
// BitmapImage need to be created by main thread.
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
// check token state to determine whether the operation should be canceled. // check token state to determine whether the operation should be canceled.

View File

@@ -10,8 +10,14 @@ namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 合成扩展 /// 合成扩展
/// </summary> /// </summary>
internal static class CompositionExtensions [HighQuality]
internal static class CompositionExtension
{ {
private const string Background = nameof(Background);
private const string Foreground = nameof(Foreground);
private const string Source = nameof(Source);
private const string AlphaMask = nameof(AlphaMask);
/// <summary> /// <summary>
/// 创建拼合图视觉对象 /// 创建拼合图视觉对象
/// </summary> /// </summary>
@@ -41,15 +47,15 @@ internal static class CompositionExtensions
{ {
BlendEffect effect = new() BlendEffect effect = new()
{ {
Background = new CompositionEffectSourceParameter("Background"), Background = new CompositionEffectSourceParameter(Background),
Foreground = new CompositionEffectSourceParameter("Foreground"), Foreground = new CompositionEffectSourceParameter(Foreground),
Mode = blendEffectMode, Mode = blendEffectMode,
}; };
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter("Background", background); brush.SetSourceParameter(Background, background);
brush.SetSourceParameter("Foreground", foreground); brush.SetSourceParameter(Foreground, foreground);
return brush; return brush;
} }
@@ -66,12 +72,12 @@ internal static class CompositionExtensions
{ {
GrayscaleEffect effect = new() GrayscaleEffect effect = new()
{ {
Source = new CompositionEffectSourceParameter("Source"), Source = new CompositionEffectSourceParameter(Source),
}; };
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter("Source", source); brush.SetSourceParameter(Source, source);
return brush; return brush;
} }
@@ -88,12 +94,12 @@ internal static class CompositionExtensions
{ {
LuminanceToAlphaEffect effect = new() LuminanceToAlphaEffect effect = new()
{ {
Source = new CompositionEffectSourceParameter("Source"), Source = new CompositionEffectSourceParameter(Source),
}; };
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter("Source", sourceBrush); brush.SetSourceParameter(Source, sourceBrush);
return brush; return brush;
} }
@@ -112,14 +118,14 @@ internal static class CompositionExtensions
{ {
AlphaMaskEffect maskEffect = new() AlphaMaskEffect maskEffect = new()
{ {
AlphaMask = new CompositionEffectSourceParameter("AlphaMask"), AlphaMask = new CompositionEffectSourceParameter(AlphaMask),
Source = new CompositionEffectSourceParameter("Source"), Source = new CompositionEffectSourceParameter(Source),
}; };
CompositionEffectBrush brush = compositor.CreateEffectFactory(maskEffect).CreateBrush(); CompositionEffectBrush brush = compositor.CreateEffectFactory(maskEffect).CreateBrush();
brush.SetSourceParameter("AlphaMask", alphaMask); brush.SetSourceParameter(AlphaMask, alphaMask);
brush.SetSourceParameter("Source", sourceBrush); brush.SetSourceParameter(Source, sourceBrush);
return brush; return brush;
} }
@@ -172,25 +178,6 @@ internal static class CompositionExtensions
return brush; return brush;
} }
/// <summary>
/// 创建一个新的蒙版画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="source">源</param>
/// <param name="mask">蒙版</param>
/// <returns>蒙版画刷</returns>
public static CompositionMaskBrush CompositeMaskBrush(
this Compositor compositor,
CompositionBrush source,
CompositionBrush mask)
{
CompositionMaskBrush brush = compositor.CreateMaskBrush();
brush.Source = source;
brush.Mask = mask;
return brush;
}
private static Vector2 GetStartPointOfDirection(GradientDirection direction) private static Vector2 GetStartPointOfDirection(GradientDirection direction)
{ {
return direction switch return direction switch
@@ -216,6 +203,4 @@ internal static class CompositionExtensions
_ => Vector2.Zero, _ => Vector2.Zero,
}; };
} }
}
public record struct GradientStop(float Offset, Windows.UI.Color Color);
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Animations; using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting; using Microsoft.UI.Xaml.Hosting;
@@ -19,12 +20,14 @@ namespace Snap.Hutao.Control.Image;
/// 合成图像控件 /// 合成图像控件
/// 为其他图像类控件提供基类 /// 为其他图像类控件提供基类
/// </summary> /// </summary>
[HighQuality]
public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{ {
private static readonly DependencyProperty SourceProperty = Property<CompositionImage>.Depend(nameof(Source), default(Uri), OnSourceChanged); private static readonly DependencyProperty SourceProperty = Property<CompositionImage>.Depend(nameof(Source), default(Uri), OnSourceChanged);
private static readonly DependencyProperty EnableLazyLoadingProperty = Property<CompositionImage>.DependBoxed<bool>(nameof(EnableLazyLoading), BoxedValues.True);
private static readonly ConcurrentCancellationTokenSource<CompositionImage> LoadingTokenSource = new(); private static readonly ConcurrentCancellationTokenSource<CompositionImage> LoadingTokenSource = new();
private readonly IImageCache imageCache; private readonly IServiceProvider serviceProvider;
private SpriteVisual? spriteVisual; private SpriteVisual? spriteVisual;
private bool isShow = true; private bool isShow = true;
@@ -34,8 +37,15 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
/// </summary> /// </summary>
public CompositionImage() public CompositionImage()
{ {
imageCache = Ioc.Default.GetRequiredService<IImageCache>(); serviceProvider = Ioc.Default.GetRequiredService<IServiceProvider>();
AllowFocusOnInteraction = false;
IsDoubleTapEnabled = false;
IsHitTestVisible = false;
IsHoldingEnabled = false;
IsRightTapEnabled = false;
IsTabStop = false; IsTabStop = false;
SizeChanged += OnSizeChanged; SizeChanged += OnSizeChanged;
} }
@@ -48,6 +58,15 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
set => SetValue(SourceProperty, value); set => SetValue(SourceProperty, value);
} }
/// <summary>
/// 启用延迟加载
/// </summary>
public bool EnableLazyLoading
{
get => (bool)GetValue(EnableLazyLoadingProperty);
set => SetValue(EnableLazyLoadingProperty, value);
}
/// <summary> /// <summary>
/// 合成组合视觉 /// 合成组合视觉
/// </summary> /// </summary>
@@ -80,33 +99,22 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
spriteVisual.Size = ActualSize; spriteVisual.Size = ActualSize;
} }
private static void OnApplyImageFailed(Uri? uri, Exception exception)
{
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
if (exception is HttpRequestException httpRequestException)
{
infoBarService.Error(httpRequestException, $"GET {uri}");
}
else
{
infoBarService.Error(exception, $"应用 {nameof(CompositionImage)} 的源时发生异常");
}
}
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg) private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
{ {
CompositionImage image = (CompositionImage)sender; CompositionImage image = (CompositionImage)sender;
CancellationToken token = LoadingTokenSource.Register(image); CancellationToken token = LoadingTokenSource.Register(image);
ILogger<CompositionImage> logger = Ioc.Default.GetRequiredService<ILogger<CompositionImage>>(); IServiceProvider serviceProvider = image.serviceProvider;
ILogger<CompositionImage> logger = serviceProvider.GetRequiredService<ILogger<CompositionImage>>();
// source is valid // source is valid
if (arg.NewValue is Uri inner && !string.IsNullOrEmpty(inner.Host)) if (arg.NewValue is Uri inner && !string.IsNullOrEmpty(inner.OriginalString))
{ {
// value is different from old one // value is different from old one
if (inner != (arg.OldValue as Uri)) if (inner != (arg.OldValue as Uri))
{ {
image.ApplyImageInternalAsync(inner, token).SafeForget(logger, ex => OnApplyImageFailed(inner, ex)); image
.ApplyImageAsync(inner, token)
.SafeForget(logger, ex => OnApplyImageFailed(serviceProvider, inner, ex));
} }
} }
else else
@@ -115,35 +123,47 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
} }
} }
private async Task ApplyImageInternalAsync(Uri? uri, CancellationToken token) private static void OnApplyImageFailed(IServiceProvider serviceProvider, Uri? uri, Exception exception)
{
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
if (exception is HttpRequestException httpRequestException)
{
infoBarService.Error(httpRequestException, string.Format(SH.ControlImageCompositionImageHttpRequest, uri));
}
else
{
Exception baseException = exception.GetBaseException();
if (baseException is not COMException)
{
infoBarService.Error(baseException, SH.ControlImageCompositionImageSystemException);
}
}
}
private async Task ApplyImageAsync(Uri? uri, CancellationToken token)
{ {
await HideAsync(token).ConfigureAwait(true); await HideAsync(token).ConfigureAwait(true);
LoadedImageSurface? imageSurface = null;
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
if (uri != null) if (uri != null)
{ {
if (uri.Scheme == "ms-appx") LoadedImageSurface? imageSurface = null;
{ Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
imageSurface = LoadedImageSurface.StartLoadFromUri(uri);
}
else
{
string storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
try IImageCache imageCache = serviceProvider.GetRequiredService<IImageCache>();
{ string file = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
imageSurface = await LoadImageSurfaceAsync(storageFile, token).ConfigureAwait(true);
} try
catch (COMException) {
{ imageSurface = await LoadImageSurfaceAsync(file, token).ConfigureAwait(true);
imageCache.Remove(uri.Enumerate()); }
} catch (COMException)
catch (IOException) {
{ imageCache.Remove(uri.Enumerate());
imageCache.Remove(uri.Enumerate()); }
} catch (IOException)
{
imageCache.Remove(uri.Enumerate());
} }
if (imageSurface != null) if (imageSurface != null)
@@ -161,8 +181,16 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{ {
if (!isShow) if (!isShow)
{ {
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token).ConfigureAwait(true);
isShow = true; isShow = true;
if (EnableLazyLoading)
{
await AnimationBuilder.Create().Opacity(1d, 0d).StartAsync(this, token).ConfigureAwait(true);
}
else
{
Opacity = 1;
}
} }
} }
@@ -170,8 +198,16 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{ {
if (isShow) if (isShow)
{ {
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token).ConfigureAwait(true);
isShow = false; isShow = false;
if (EnableLazyLoading)
{
await AnimationBuilder.Create().Opacity(0d, 1d).StartAsync(this, token).ConfigureAwait(true);
}
else
{
Opacity = 0;
}
} }
} }

View File

@@ -5,6 +5,7 @@ using Microsoft.UI;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Win32;
using System.IO; using System.IO;
using Windows.Graphics.Imaging; using Windows.Graphics.Imaging;
using Windows.Storage.Streams; using Windows.Storage.Streams;
@@ -12,9 +13,10 @@ using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image; namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 支持渐变图像 /// 渐变图像
/// </summary> /// </summary>
public class Gradient : CompositionImage [HighQuality]
internal sealed class Gradient : CompositionImage
{ {
private static readonly DependencyProperty BackgroundDirectionProperty = Property<Gradient>.Depend(nameof(BackgroundDirection), GradientDirection.TopToBottom); private static readonly DependencyProperty BackgroundDirectionProperty = Property<Gradient>.Depend(nameof(BackgroundDirection), GradientDirection.TopToBottom);
private static readonly DependencyProperty ForegroundDirectionProperty = Property<Gradient>.Depend(nameof(ForegroundDirection), GradientDirection.TopToBottom); private static readonly DependencyProperty ForegroundDirectionProperty = Property<Gradient>.Depend(nameof(ForegroundDirection), GradientDirection.TopToBottom);
@@ -44,7 +46,7 @@ public class Gradient : CompositionImage
{ {
if (spriteVisual is not null) if (spriteVisual is not null)
{ {
Height = (double)Math.Clamp(ActualWidth / imageAspectRatio, 0, MaxHeight); Height = Math.Clamp(ActualWidth / imageAspectRatio, 0D, MaxHeight);
spriteVisual.Size = ActualSize; spriteVisual.Size = ActualSize;
} }
} }
@@ -52,19 +54,11 @@ public class Gradient : CompositionImage
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token) protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
{ {
using (FileStream fileStream = new(file, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (IRandomAccessStream imageStream = fileStream.AsRandomAccessStream())
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream);
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
}
}
TaskCompletionSource loadCompleteTaskSource = new(); TaskCompletionSource loadCompleteTaskSource = new();
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file)); LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult(); surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
await loadCompleteTaskSource.Task.ConfigureAwait(true); await loadCompleteTaskSource.Task.ConfigureAwait(true);
imageAspectRatio = surface.NaturalSize.AspectRatio();
return surface; return surface;
} }

View File

@@ -6,7 +6,8 @@ namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 渐变方向 /// 渐变方向
/// </summary> /// </summary>
public enum GradientDirection [HighQuality]
internal enum GradientDirection
{ {
/// <summary> /// <summary>
/// 下到上 /// 下到上

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 渐变锚点
/// </summary>
/// <param name="Offset">便宜</param>
/// <param name="Color">颜色</param>
[HighQuality]
internal struct GradientStop
{
/// <summary>
/// 便宜
/// </summary>
public float Offset;
/// <summary>
/// 颜色
/// </summary>
public Windows.UI.Color Color;
/// <summary>
/// 构造一个新的渐变锚点
/// </summary>
/// <param name="offset">偏移</param>
/// <param name="color">颜色</param>
public GradientStop(float offset, Windows.UI.Color color)
{
Offset = offset;
Color = color;
}
}

View File

@@ -6,13 +6,15 @@ using Microsoft.UI;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Theme;
namespace Snap.Hutao.Control.Image; namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 支持单色的图像 /// 支持单色的图像
/// </summary> /// </summary>
public class MonoChrome : CompositionImage [HighQuality]
internal sealed class MonoChrome : CompositionImage
{ {
private CompositionColorBrush? backgroundBrush; private CompositionColorBrush? backgroundBrush;
@@ -49,18 +51,13 @@ public class MonoChrome : CompositionImage
private void SetBackgroundColor(CompositionColorBrush backgroundBrush) private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
{ {
ApplicationTheme theme = ActualTheme switch ApplicationTheme theme = ThemeHelper.ElementToApplication(ActualTheme);
{
ElementTheme.Light => ApplicationTheme.Light,
ElementTheme.Dark => ApplicationTheme.Dark,
_ => Ioc.Default.GetRequiredService<App>().RequestedTheme,
};
backgroundBrush.Color = theme switch backgroundBrush.Color = theme switch
{ {
ApplicationTheme.Light => Colors.Black, ApplicationTheme.Light => Colors.Black,
ApplicationTheme.Dark => Colors.White, ApplicationTheme.Dark => Colors.White,
_ => throw Must.NeverHappen(), _ => Colors.Transparent,
}; };
} }
} }

View File

@@ -9,8 +9,9 @@ namespace Snap.Hutao.Control.Markup;
/// <summary> /// <summary>
/// Custom <see cref="Markup"/> which can provide <see cref="BitmapIcon"/> values. /// Custom <see cref="Markup"/> which can provide <see cref="BitmapIcon"/> values.
/// </summary> /// </summary>
[HighQuality]
[MarkupExtensionReturnType(ReturnType = typeof(BitmapIcon))] [MarkupExtensionReturnType(ReturnType = typeof(BitmapIcon))]
public sealed class BitmapIconExtension : MarkupExtension internal sealed class BitmapIconExtension : MarkupExtension
{ {
/// <summary> /// <summary>
/// Gets or sets the <see cref="Uri"/> representing the image to display. /// Gets or sets the <see cref="Uri"/> representing the image to display.

View File

@@ -9,8 +9,9 @@ namespace Snap.Hutao.Control.Markup;
/// <summary> /// <summary>
/// Custom <see cref="MarkupExtension"/> which can provide <see cref="FontIcon"/> values. /// Custom <see cref="MarkupExtension"/> which can provide <see cref="FontIcon"/> values.
/// </summary> /// </summary>
[HighQuality]
[MarkupExtensionReturnType(ReturnType = typeof(FontIcon))] [MarkupExtensionReturnType(ReturnType = typeof(FontIcon))]
internal class FontIconExtension : MarkupExtension internal sealed class FontIconExtension : MarkupExtension
{ {
/// <summary> /// <summary>
/// Gets or sets the <see cref="string"/> value representing the icon to display. /// Gets or sets the <see cref="string"/> value representing the icon to display.

View File

@@ -1,28 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// Custom <see cref="MarkupExtension"/> which can provide <see cref="FontIcon"/> values.
/// </summary>
[MarkupExtensionReturnType(ReturnType = typeof(FontIcon))]
internal class FontIconSourceExtension : MarkupExtension
{
/// <summary>
/// Gets or sets the <see cref="string"/> value representing the icon to display.
/// </summary>
public string Glyph { get; set; } = default!;
/// <inheritdoc/>
protected override object ProvideValue()
{
return new FontIconSource()
{
Glyph = Glyph,
};
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// Xaml extension to return a <see cref="string"/> value from resource file associated with a resource key
/// </summary>
[HighQuality]
[MarkupExtensionReturnType(ReturnType = typeof(string))]
internal sealed class ResourceStringExtension : MarkupExtension
{
/// <summary>
/// Gets or sets associated ID from resource strings.
/// </summary>
public string? Name { get; set; }
/// <inheritdoc/>
protected override object ProvideValue()
{
return SH.ResourceManager.GetString(Name ?? string.Empty) ?? Name ?? string.Empty;
}
}

View File

@@ -9,8 +9,9 @@ namespace Snap.Hutao.Control.Media;
/// <summary> /// <summary>
/// BGRA8 结构 /// BGRA8 结构
/// </summary> /// </summary>
[HighQuality]
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
public struct Bgra8 internal struct Bgra8
{ {
/// <summary> /// <summary>
/// B /// B

View File

@@ -12,8 +12,9 @@ namespace Snap.Hutao.Control.Media;
/// <summary> /// <summary>
/// RGBA 颜色 /// RGBA 颜色
/// </summary> /// </summary>
[HighQuality]
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
public struct Rgba8 internal struct Rgba8
{ {
/// <summary> /// <summary>
/// R /// R
@@ -48,7 +49,6 @@ public struct Rgba8
/// <param name="hex">色值字符串</param> /// <param name="hex">色值字符串</param>
public Rgba8(ReadOnlySpan<char> hex) public Rgba8(ReadOnlySpan<char> hex)
{ {
Must.Argument(hex.Length == 8, "色值长度不为8");
R = 0; R = 0;
G = 0; G = 0;
B = 0; B = 0;

View File

@@ -12,7 +12,8 @@ namespace Snap.Hutao.Control.Media;
/// <summary> /// <summary>
/// 软件位图拓展 /// 软件位图拓展
/// </summary> /// </summary>
public static class SoftwareBitmapExtension [HighQuality]
internal static class SoftwareBitmapExtension
{ {
/// <summary> /// <summary>
/// 混合模式 正常 /// 混合模式 正常

View File

@@ -0,0 +1,65 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
/// <summary>
/// 纵横比控件
/// </summary>
[HighQuality]
internal class AspectRatio : Microsoft.UI.Xaml.Controls.Control
{
private const double Epsilon = 2.2204460492503131e-016;
private static readonly DependencyProperty TargetWidthProperty = Property<AspectRatio>.DependBoxed<double>(nameof(TargetWidth), BoxedValues.DoubleOne);
private static readonly DependencyProperty TargetHeightProperty = Property<AspectRatio>.DependBoxed<double>(nameof(TargetHeight), BoxedValues.DoubleOne);
/// <summary>
/// 目标宽度
/// </summary>
public double TargetWidth
{
get => (double)GetValue(TargetWidthProperty);
set => SetValue(TargetWidthProperty, value);
}
/// <summary>
/// 目标高度
/// </summary>
public double TargetHeight
{
get => (double)GetValue(TargetHeightProperty);
set => SetValue(TargetHeightProperty, value);
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
double ratio = TargetWidth / TargetHeight;
double ratioAvailable = availableSize.Width / availableSize.Height;
if (Math.Abs(ratioAvailable - ratio) < Epsilon)
{
return availableSize;
}
// 更宽
if (ratioAvailable > ratio)
{
double newWidth = ratio * availableSize.Height;
return new Size(newWidth, availableSize.Height);
}
// 更高
else if (ratioAvailable < ratio)
{
double newHeight = availableSize.Width / ratio;
return new Size(availableSize.Width, newHeight);
}
return availableSize;
}
}

View File

@@ -1,33 +1,31 @@
<UserControl <SplitButton
x:Class="Snap.Hutao.Control.Panel.PanelSelector" x:Class="Snap.Hutao.Control.Panel.PanelSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shcm="using:Snap.Hutao.Control.Markup"
Name="RootSplitButton"
Padding="0,6"
Click="SplitButtonClick"
Loaded="OnRootControlLoaded" Loaded="OnRootControlLoaded"
mc:Ignorable="d"> mc:Ignorable="d">
<SplitButton <SplitButton.Content>
Name="RootSplitButton" <FontIcon Name="IconPresenter" Glyph="&#xE8FD;"/>
Padding="0,6" </SplitButton.Content>
Click="SplitButtonClick"> <SplitButton.Flyout>
<SplitButton.Content> <MenuFlyout>
<FontIcon Name="IconPresenter" Glyph="&#xE8FD;"/> <RadioMenuFlyoutItem
</SplitButton.Content> Click="RadioMenuFlyoutItemClick"
<SplitButton.Flyout> Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
<MenuFlyout> Tag="List"
<RadioMenuFlyoutItem Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
Click="RadioMenuFlyoutItemClick" <RadioMenuFlyoutItem
Icon="{shcm:FontIcon Glyph=&#xE8FD;}" Click="RadioMenuFlyoutItemClick"
Tag="List" Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Text="列表"/> Tag="Grid"
<RadioMenuFlyoutItem Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
Click="RadioMenuFlyoutItemClick" </MenuFlyout>
Icon="{shcm:FontIcon Glyph=&#xF0E2;}" </SplitButton.Flyout>
Tag="Grid" </SplitButton>
Text="网格"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</UserControl>

View File

@@ -9,9 +9,12 @@ namespace Snap.Hutao.Control.Panel;
/// <summary> /// <summary>
/// 面板选择器 /// 面板选择器
/// </summary> /// </summary>
public sealed partial class PanelSelector : UserControl [HighQuality]
internal sealed partial class PanelSelector : SplitButton
{ {
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List", OnCurrentChanged); private const string List = nameof(List);
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), List, OnCurrentChanged);
/// <summary> /// <summary>
/// 构造一个新的面板选择器 /// 构造一个新的面板选择器

View File

@@ -9,6 +9,7 @@ namespace Snap.Hutao.Control;
/// 快速创建 <see cref="TOwner"/> 的 <see cref="DependencyProperty"/> /// 快速创建 <see cref="TOwner"/> 的 <see cref="DependencyProperty"/>
/// </summary> /// </summary>
/// <typeparam name="TOwner">所有者的类型</typeparam> /// <typeparam name="TOwner">所有者的类型</typeparam>
[HighQuality]
internal static class Property<TOwner> internal static class Property<TOwner>
{ {
/// <summary> /// <summary>
@@ -34,6 +35,18 @@ internal static class Property<TOwner>
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue)); return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue));
} }
/// <summary>
/// 注册依赖属性
/// </summary>
/// <typeparam name="TProperty">属性的类型</typeparam>
/// <param name="name">属性名称</param>
/// <param name="defaultValue">封装的默认值</param>
/// <returns>注册的依赖属性</returns>
public static DependencyProperty DependBoxed<TProperty>(string name, object defaultValue)
{
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue));
}
/// <summary> /// <summary>
/// 注册依赖属性 /// 注册依赖属性
/// </summary> /// </summary>

View File

@@ -13,19 +13,38 @@ namespace Snap.Hutao.Control;
/// 表示支持取消加载的异步页面 /// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知 /// 在被导航到其他页面前触发取消异步通知
/// </summary> /// </summary>
[HighQuality]
[SuppressMessage("", "CA1001")] [SuppressMessage("", "CA1001")]
public class ScopedPage : Page internal class ScopedPage : Page
{ {
private readonly CancellationTokenSource viewLoadingCancellationTokenSource = new(); // Allow GC to Collect the IServiceScope
private readonly IServiceScope serviceScope; private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(null!);
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope currentScope;
/// <summary> /// <summary>
/// 构造一个新的页面 /// 构造一个新的页面
/// </summary> /// </summary>
public ScopedPage() public ScopedPage()
{ {
serviceScope = Ioc.Default.CreateScope(); Unloaded += OnScopedPageUnloaded;
serviceScope.Track(); currentScope = Ioc.Default.CreateScope();
DisposePreviousScope();
// track current
PreviousScopeReference.SetTarget(currentScope);
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposePreviousScope()
{
if (PreviousScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
} }
/// <summary> /// <summary>
@@ -36,8 +55,8 @@ public class ScopedPage : Page
public void InitializeWith<TViewModel>() public void InitializeWith<TViewModel>()
where TViewModel : class, IViewModel where TViewModel : class, IViewModel
{ {
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>(); IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token; viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel; DataContext = viewModel;
} }
@@ -59,11 +78,10 @@ public class ScopedPage : Page
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{ {
base.OnNavigatingFrom(e); using (viewCancellationTokenSource)
using (viewLoadingCancellationTokenSource)
{ {
// Cancel tasks executed by the view model // Cancel all tasks executed by the view model
viewLoadingCancellationTokenSource.Cancel(); viewCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext; IViewModel viewModel = (IViewModel)DataContext;
using (SemaphoreSlim locker = viewModel.DisposeLock) using (SemaphoreSlim locker = viewModel.DisposeLock)
@@ -73,20 +91,23 @@ public class ScopedPage : Page
viewModel.IsViewDisposed = true; viewModel.IsViewDisposed = true;
// Dispose the scope // Dispose the scope
serviceScope.Dispose(); currentScope.Dispose();
} }
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
[SuppressMessage("", "VSTHRD100")] protected override void OnNavigatedTo(NavigationEventArgs e)
protected override async void OnNavigatedTo(NavigationEventArgs e)
{ {
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra) if (e.Parameter is INavigationData extra)
{ {
await NotifyRecipentAsync(extra).ConfigureAwait(false); NotifyRecipentAsync(extra).SafeForget();
} }
} }
private void OnScopedPageUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
DataContext = null;
Unloaded -= OnScopedPageUnloaded;
}
} }

View File

@@ -1,7 +1,5 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
// Some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -9,15 +7,18 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents; using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Media; using Snap.Hutao.Control.Media;
using Snap.Hutao.Core; using Snap.Hutao.Control.Theme;
using Windows.UI; using Windows.UI;
namespace Snap.Hutao.Control.Text; namespace Snap.Hutao.Control.Text;
/// <summary> /// <summary>
/// 专用于呈现描述文本的文本块 /// 专用于呈现描述文本的文本块
/// Some part of this file came from:
/// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
/// </summary> /// </summary>
public class DescriptionTextBlock : ContentControl [HighQuality]
internal sealed class DescriptionTextBlock : ContentControl
{ {
private static readonly DependencyProperty DescriptionProperty = Property<DescriptionTextBlock>.Depend(nameof(Description), string.Empty, OnDescriptionChanged); private static readonly DependencyProperty DescriptionProperty = Property<DescriptionTextBlock>.Depend(nameof(Description), string.Empty, OnDescriptionChanged);
@@ -33,6 +34,7 @@ public class DescriptionTextBlock : ContentControl
public DescriptionTextBlock() public DescriptionTextBlock()
{ {
IsTabStop = false; IsTabStop = false;
Content = new TextBlock() Content = new TextBlock()
{ {
TextWrapping = TextWrapping.Wrap, TextWrapping = TextWrapping.Wrap,

View File

@@ -0,0 +1,210 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls">
<FontFamily x:Key="MiSans">ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
<FontFamily x:Key="CascadiaMonoAndMiSans">ms-appx:///Resource/Font/CascadiaMono.ttf#Cascadia Mono, ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
<StaticResource x:Key="PivotHeaderItemFontFamily" ResourceKey="MiSans"/>
<StaticResource x:Key="ContentControlThemeFontFamily" ResourceKey="MiSans"/>
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock"/>
<Style x:Key="BaseTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
<Setter Property="FontSize" Value="{StaticResource BodyTextBlockFontSize}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="LineStackingStrategy" Value="MaxHeight"/>
<Setter Property="TextLineBounds" Value="Full"/>
</Style>
<Style
x:Key="HeaderTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="46"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="SubheaderTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="34"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="TitleTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource TitleTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="SubtitleTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource SubtitleTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="BodyTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style
x:Key="CaptionTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource CaptionTextBlockFontSize}"/>
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style
x:Key="BodyStrongTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource BodyStrongTextBlockFontSize}"/>
</Style>
<Style
x:Key="TitleLargeTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource TitleLargeTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="DisplayTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource DisplayTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}" TargetType="MenuFlyoutItem">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultMenuFlyoutSubItemStyle}" TargetType="MenuFlyoutSubItem">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style TargetType="InfoBar">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultSettingStyle}" TargetType="wsc:Setting"/>
<Style x:Key="DefaultSettingStyle" TargetType="wsc:Setting">
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="0,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="wsc:Setting">
<Grid
x:Name="RootGrid"
MinHeight="48"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- Icon -->
<ColumnDefinition Width="*"/>
<!-- Header and subtitle -->
<ColumnDefinition Width="Auto"/>
<!-- Action control -->
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="IconPresenter"
MaxWidth="20"
Margin="2,0,18,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Icon}"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="20"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
IsTextScaleFactorEnabled="False"/>
<StackPanel
Grid.Column="1"
Margin="0,0,16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<TextBlock
x:Name="HeaderPresenter"
VerticalAlignment="Center"
FontFamily="{StaticResource MiSans}"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
Text="{TemplateBinding Header}"/>
<ContentPresenter
x:Name="DescriptionPresenter"
Content="{TemplateBinding Description}"
FontFamily="{StaticResource MiSans}"
FontSize="{StaticResource SecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="WrapWholeWords">
<ContentPresenter.Resources>
<Style BasedOn="{StaticResource CaptionTextBlockStyle}" TargetType="TextBlock">
<Style.Setters>
<Setter Property="TextWrapping" Value="WrapWholeWords"/>
</Style.Setters>
</Style>
<Style BasedOn="{StaticResource TextButtonStyle}" TargetType="HyperlinkButton">
<Style.Setters>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Padding" Value="0,0,0,0"/>
</Style.Setters>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</StackPanel>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding ActionContent}"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="HeaderPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="DescriptionPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -4,11 +4,12 @@
using Microsoft.UI.Composition.SystemBackdrops; using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Control.Theme;
/// <summary> /// <summary>
/// 主题帮助工具类 /// 主题帮助工具类
/// </summary> /// </summary>
[HighQuality]
public static class ThemeHelper public static class ThemeHelper
{ {
/// <summary> /// <summary>
@@ -42,6 +43,21 @@ public static class ThemeHelper
}; };
} }
/// <summary>
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="ApplicationTheme"/>
/// </summary>
/// <param name="applicationTheme">元素主题</param>
/// <returns>应用主题</returns>
public static ApplicationTheme ElementToApplication(ElementTheme applicationTheme)
{
return applicationTheme switch
{
ElementTheme.Light => ApplicationTheme.Light,
ElementTheme.Dark => ApplicationTheme.Dark,
_ => Ioc.Default.GetRequiredService<App>().RequestedTheme,
};
}
/// <summary> /// <summary>
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="SystemBackdropTheme"/> /// 从 <see cref="ElementTheme"/> 转换到 <see cref="SystemBackdropTheme"/>
/// </summary> /// </summary>

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Control;
/// </summary> /// </summary>
/// <typeparam name="TFrom">源类型</typeparam> /// <typeparam name="TFrom">源类型</typeparam>
/// <typeparam name="TTo">目标类型</typeparam> /// <typeparam name="TTo">目标类型</typeparam>
public abstract class ValueConverter<TFrom, TTo> : IValueConverter internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
{ {
/// <inheritdoc/> /// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language) public object? Convert(object value, Type targetType, object parameter, string language)

View File

@@ -3,8 +3,9 @@
namespace Snap.Hutao.Core.Abstraction; namespace Snap.Hutao.Core.Abstraction;
[HighQuality]
[SuppressMessage("", "SA1600")] [SuppressMessage("", "SA1600")]
public abstract class DisposableObject : IDisposable internal abstract class DisposableObject : IDisposable
{ {
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }

View File

@@ -8,6 +8,7 @@ namespace Snap.Hutao.Core.Abstraction;
/// </summary> /// </summary>
/// <typeparam name="T1">元组的第一个类型</typeparam> /// <typeparam name="T1">元组的第一个类型</typeparam>
/// <typeparam name="T2">元组的第二个类型</typeparam> /// <typeparam name="T2">元组的第二个类型</typeparam>
[HighQuality]
internal interface IDeconstructable<T1, T2> internal interface IDeconstructable<T1, T2>
{ {
/// <summary> /// <summary>

View File

@@ -7,7 +7,8 @@ namespace Snap.Hutao.Core.Abstraction;
/// 有名称的对象 /// 有名称的对象
/// 指示该对象可通过名称区分 /// 指示该对象可通过名称区分
/// </summary> /// </summary>
internal interface INamed [HighQuality]
internal interface INamedService
{ {
/// <summary> /// <summary>
/// 名称 /// 名称

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 可异步初始化
/// </summary>
internal interface ISupportAsyncInitialization
{
/// <summary>
/// 是否已经初始化完成
/// </summary>
public bool IsInitialized { get; }
/// <summary>
/// 异步初始化
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>初始化任务</returns>
ValueTask<bool> InitializeAsync();
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 表示支持验证
/// </summary>
internal interface ISupportValidation
{
/// <summary>
/// 验证
/// </summary>
/// <returns>当前数据是否有效</returns>
public bool Validate();
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Annotation;
/// <summary>
/// 高质量代码
/// </summary>
[AttributeUsage(AttributeTargets.All)]
internal class HighQualityAttribute : Attribute
{
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Annotation;
/// <summary>
/// 本地化键
/// </summary>
[HighQuality]
[AttributeUsage(AttributeTargets.Field)]
internal class LocalizationKeyAttribute : Attribute
{
/// <summary>
/// 指定本地化键
/// </summary>
/// <param name="key">键</param>
public LocalizationKeyAttribute(string key)
{
Key = key;
}
/// <summary>
/// 键
/// </summary>
public string Key { get; }
}

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Core.Caching;
/// 为图像缓存提供抽象 /// 为图像缓存提供抽象
/// </summary> /// </summary>
/// <typeparam name="T">缓存类型</typeparam> /// <typeparam name="T">缓存类型</typeparam>
[HighQuality]
internal interface IImageCache internal interface IImageCache
{ {
/// <summary> /// <summary>

View File

@@ -6,6 +6,7 @@ namespace Snap.Hutao.Core.Caching;
/// <summary> /// <summary>
/// 图像缓存 文件路径操作 /// 图像缓存 文件路径操作
/// </summary> /// </summary>
[HighQuality]
internal interface IImageCacheFilePathOperation internal interface IImageCacheFilePathOperation
{ {
/// <summary> /// <summary>

View File

@@ -2,8 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Logging; using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -17,14 +16,15 @@ namespace Snap.Hutao.Core.Caching;
/// Provides methods and tools to cache files in a folder /// Provides methods and tools to cache files in a folder
/// The class's name will become the cache folder's name /// The class's name will become the cache folder's name
/// </summary> /// </summary>
[HighQuality]
[Injection(InjectAs.Singleton, typeof(IImageCache))] [Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)] [HttpClient(HttpClientConfigration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)] [PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
public class ImageCache : IImageCache, IImageCacheFilePathOperation public sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
{ {
private const string CacheFolderName = nameof(ImageCache); private const string CacheFolderName = nameof(ImageCache);
private static readonly ImmutableDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>() private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
{ {
[0] = TimeSpan.FromSeconds(4), [0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16), [1] = TimeSpan.FromSeconds(16),
@@ -32,13 +32,13 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
[3] = TimeSpan.FromSeconds(4), [3] = TimeSpan.FromSeconds(4),
[4] = TimeSpan.FromSeconds(16), [4] = TimeSpan.FromSeconds(16),
[5] = TimeSpan.FromSeconds(64), [5] = TimeSpan.FromSeconds(64),
}.ToImmutableDictionary(); };
private readonly ILogger logger; private readonly ILogger logger;
// violate di rule
private readonly HttpClient httpClient; private readonly HttpClient httpClient;
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private string? baseFolder; private string? baseFolder;
private string? cacheFolder; private string? cacheFolder;
@@ -88,7 +88,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
foreach (Uri uri in uriForCachedItems) foreach (Uri uri in uriForCachedItems)
{ {
string filePath = Path.Combine(folder, GetCacheFileName(uri)); string filePath = Path.Combine(folder, GetCacheFileName(uri));
if (files.Contains(filePath)) if (Array.IndexOf(files, filePath) >= 0)
{ {
filesToDelete.Add(filePath); filesToDelete.Add(filePath);
} }
@@ -100,11 +100,29 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
/// <inheritdoc/> /// <inheritdoc/>
public async Task<string> GetFileFromCacheAsync(Uri uri) public async Task<string> GetFileFromCacheAsync(Uri uri)
{ {
string filePath = Path.Combine(GetCacheFolder(), GetCacheFileName(uri)); string fileName = GetCacheFileName(uri);
string filePath = Path.Combine(GetCacheFolder(), fileName);
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0) if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
{ {
await DownloadFileAsync(uri, filePath).ConfigureAwait(false); TaskCompletionSource taskCompletionSource = new();
try
{
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
{
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
}
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
{
await task.ConfigureAwait(false);
}
concurrentTasks.TryRemove(fileName, out _);
}
finally
{
taskCompletionSource.TrySetResult();
}
} }
return filePath; return filePath;
@@ -153,7 +171,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
private async Task DownloadFileAsync(Uri uri, string baseFile) private async Task DownloadFileAsync(Uri uri, string baseFile)
{ {
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri); logger.LogInformation("Begin downloading for {uri}", uri);
int retryCount = 0; int retryCount = 0;
while (retryCount < 6) while (retryCount < 6)
@@ -191,7 +209,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
if (retryCount == 3) if (retryCount == 3)
{ {
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao, }.Uri; uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao }.Uri;
} }
} }
} }

View File

@@ -8,7 +8,8 @@ namespace Snap.Hutao.Core;
/// <summary> /// <summary>
/// 命令行建造器 /// 命令行建造器
/// </summary> /// </summary>
public class CommandLineBuilder [HighQuality]
internal sealed class CommandLineBuilder
{ {
private const char WhiteSpace = ' '; private const char WhiteSpace = ' ';
private readonly Dictionary<string, string?> options = new(); private readonly Dictionary<string, string?> options = new();
@@ -56,6 +57,7 @@ public class CommandLineBuilder
{ {
s.Append(WhiteSpace); s.Append(WhiteSpace);
s.Append(key); s.Append(key);
if (!string.IsNullOrEmpty(value)) if (!string.IsNullOrEmpty(value))
{ {
s.Append(WhiteSpace); s.Append(WhiteSpace);

View File

@@ -4,19 +4,20 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
namespace Snap.Hutao.Core.Convert; namespace Snap.Hutao.Core;
/// <summary> /// <summary>
/// 支持Md5转换 /// 支持Md5转换
/// </summary> /// </summary>
internal abstract class Md5Convert [HighQuality]
internal static class Convert
{ {
/// <summary> /// <summary>
/// 获取字符串的MD5计算结果 /// 获取字符串的MD5计算结果
/// </summary> /// </summary>
/// <param name="source">源字符串</param> /// <param name="source">源字符串</param>
/// <returns>计算的结果</returns> /// <returns>计算的结果</returns>
public static string ToHexString(string source) public static string ToMd5HexString(string source)
{ {
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source)); byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
return System.Convert.ToHexString(hash); return System.Convert.ToHexString(hash);

View File

@@ -2,12 +2,13 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.Win32; using Microsoft.Win32;
using Snap.Hutao.Core.Convert;
using Snap.Hutao.Core.Json; using Snap.Hutao.Core.Json;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.DynamicSecret; using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO; using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json.Serialization.Metadata; using System.Text.Json.Serialization.Metadata;
using Windows.ApplicationModel; using Windows.ApplicationModel;
@@ -16,6 +17,7 @@ namespace Snap.Hutao.Core;
/// <summary> /// <summary>
/// 核心环境参数 /// 核心环境参数
/// </summary> /// </summary>
[HighQuality]
internal static class CoreEnvironment internal static class CoreEnvironment
{ {
/// <summary> /// <summary>
@@ -26,35 +28,63 @@ internal static class CoreEnvironment
/// <summary> /// <summary>
/// 米游社移动端请求UA /// 米游社移动端请求UA
/// </summary> /// </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}"; public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary> /// <summary>
/// 米游社 Rpc 版本 /// 米游社 Rpc 版本
/// </summary> /// </summary>
public const string HoyolabXrpcVersion = "2.43.1"; public const string HoyolabXrpcVersion = "2.44.1";
/// <summary> /// <summary>
/// 盐 /// 盐
/// </summary> /// </summary>
// https://github.com/UIGF-org/Hoyolab.Salt // https://github.com/UIGF-org/Hoyolab.Salt
public static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>() public static readonly ImmutableDictionary<SaltType, string> DynamicSecretSalts = new Dictionary<SaltType, string>()
{ {
[nameof(SaltType.K2)] = "ODzG1Jrn6zebX19VRmaJwjFI2CDvBUGq", [SaltType.K2] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
[nameof(SaltType.LK2)] = "V1PYbXKQY7ysdx3MNCcNbsE1LtY2QZpW", [SaltType.LK2] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
[nameof(SaltType.X4)] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs", [SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
[nameof(SaltType.X6)] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v", [SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
[nameof(SaltType.PROD)] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS", [SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
}.ToImmutableDictionary(); }.ToImmutableDictionary();
/// <summary>
/// 默认的Json序列化选项
/// </summary>
public static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true,
WriteIndented = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers =
{
JsonTypeInfoResolvers.ResolveEnumType,
},
},
};
/// <summary>
/// 当前版本
/// </summary>
public static readonly Version Version;
/// <summary> /// <summary>
/// 标准UA /// 标准UA
/// </summary> /// </summary>
public static readonly string CommonUA; public static readonly string CommonUA;
/// <summary> /// <summary>
/// 当前版本 /// 数据文件夹
/// </summary> /// </summary>
public static readonly Version Version; public static readonly string DataFolder;
/// <summary>
/// 包家族名称
/// </summary>
public static readonly string FamilyName;
/// <summary> /// <summary>
/// 米游社设备Id /// 米游社设备Id
@@ -67,41 +97,19 @@ internal static class CoreEnvironment
public static readonly string HutaoDeviceId; public static readonly string HutaoDeviceId;
/// <summary> /// <summary>
/// 包家族名称 /// 安装位置
/// </summary> /// </summary>
public static readonly string FamilyName; public static readonly string InstalledLocation;
/// <summary>
/// 数据文件夹
/// </summary>
public static readonly string DataFolder;
/// <summary>
/// 默认的Json序列化选项
/// </summary>
public static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
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 CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid"; private const string MachineGuidValue = "MachineGuid";
static CoreEnvironment() static CoreEnvironment()
{ {
DataFolder = GetDocumentsHutaoPath(); DataFolder = GetDatafolderPath();
Version = Package.Current.Id.Version.ToVersion(); Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName; FamilyName = Package.Current.Id.FamilyName;
InstalledLocation = Package.Current.InstalledLocation.Path;
CommonUA = $"Snap Hutao/{Version}"; CommonUA = $"Snap Hutao/{Version}";
// simply assign a random guid // simply assign a random guid
@@ -113,21 +121,30 @@ internal static class CoreEnvironment
{ {
string userName = Environment.UserName; string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName); object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
return Md5Convert.ToHexString($"{userName}{machineGuid}"); return Convert.ToMd5HexString($"{userName}{machineGuid}");
} }
private static string GetDocumentsHutaoPath() private static string GetDatafolderPath()
{ {
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
if (string.IsNullOrEmpty(preferredPath))
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE #if RELEASE
// 将测试版与正式版的文件目录分离 // 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao"; string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else #else
// 使得迁移能正常生成 // 使得迁移能正常生成
string folderName = "Hutao"; string folderName = "Hutao";
#endif #endif
string path = Path.GetFullPath(Path.Combine(myDocument, folderName)); string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path); Directory.CreateDirectory(path);
return path; return path;
}
else
{
return preferredPath;
}
} }
} }

View File

@@ -12,7 +12,8 @@ namespace Snap.Hutao.Core.Database;
/// </summary> /// </summary>
/// <typeparam name="TEntity">实体的类型</typeparam> /// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam> /// <typeparam name="TMessage">消息的类型</typeparam>
internal class DbCurrent<TEntity, TMessage> [HighQuality]
internal sealed class DbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>, new() where TMessage : Message.ValueChangedMessage<TEntity>, new()
{ {

View File

@@ -3,12 +3,14 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Database; namespace Snap.Hutao.Core.Database;
/// <summary> /// <summary>
/// 数据库集合上下文 /// 数据库集合扩展
/// </summary> /// </summary>
[HighQuality]
public static class DbSetExtension public static class DbSetExtension
{ {
/// <summary> /// <summary>
@@ -17,6 +19,7 @@ public static class DbSetExtension
/// <typeparam name="TEntity">实体类型</typeparam> /// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param> /// <param name="dbSet">数据库集</param>
/// <returns>对应的数据库上下文</returns> /// <returns>对应的数据库上下文</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DbContext Context<TEntity>(this DbSet<TEntity> dbSet) public static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class where TEntity : class
{ {
@@ -134,4 +137,4 @@ public static class DbSetExtension
dbSet.Update(entity); dbSet.Update(entity);
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false); return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
} }
} }

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 可枚举扩展
/// </summary>
[HighQuality]
public static class EnumerableExtension
{
/// <summary>
/// 获取选中的值或默认值
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>选中的值或默认值</returns>
public static TSource? SelectedOrDefault<TSource>(this IEnumerable<TSource> source)
where TSource : ISelectable
{
return source.SingleOrDefault(i => i.IsSelected);
}
}

View File

@@ -5,8 +5,11 @@ namespace Snap.Hutao.Core.Database;
/// <summary> /// <summary>
/// 可选择的项 /// 可选择的项
/// 若要使用 <see cref="DbCurrent{TEntity, TMessage}"/> 必须实现该接口 /// 若要使用 <see cref="DbCurrent{TEntity, TMessage}"/>
/// 或 <see cref="ScopedDbCurrent{TEntity, TMessage}"/>
/// 必须实现该接口
/// </summary> /// </summary>
[HighQuality]
public interface ISelectable public interface ISelectable
{ {
/// <summary> /// <summary>

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 可查询扩展
/// </summary>
[HighQuality]
public static class QueryableExtension
{
/// <summary>
/// source.Where(predicate).ExecuteDeleteAsync(token)
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <param name="predicate">条件</param>
/// <param name="token">取消令牌</param>
/// <returns>SQL返回个数</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Task<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
{
return source.Where(predicate).ExecuteDeleteAsync(token);
}
}

View File

@@ -13,12 +13,12 @@ namespace Snap.Hutao.Core.Database;
/// </summary> /// </summary>
/// <typeparam name="TEntity">实体的类型</typeparam> /// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam> /// <typeparam name="TMessage">消息的类型</typeparam>
internal class ScopedDbCurrent<TEntity, TMessage> internal sealed class ScopedDbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>, new() where TMessage : Message.ValueChangedMessage<TEntity>, new()
{ {
private readonly IServiceScopeFactory scopeFactory; private readonly IServiceScopeFactory scopeFactory;
private readonly Func<IServiceProvider, DbSet<TEntity>> dbSetFunc; private readonly Func<IServiceProvider, DbSet<TEntity>> dbSetSelector;
private readonly IMessenger messenger; private readonly IMessenger messenger;
private TEntity? current; private TEntity? current;
@@ -27,12 +27,12 @@ internal class ScopedDbCurrent<TEntity, TMessage>
/// 构造一个新的数据库当前项 /// 构造一个新的数据库当前项
/// </summary> /// </summary>
/// <param name="scopeFactory">范围工厂</param> /// <param name="scopeFactory">范围工厂</param>
/// <param name="dbSetFunc">数据集</param> /// <param name="dbSetSelector">数据集选择器</param>
/// <param name="messenger">消息器</param> /// <param name="messenger">消息器</param>
public ScopedDbCurrent(IServiceScopeFactory scopeFactory, Func<IServiceProvider, DbSet<TEntity>> dbSetFunc, IMessenger messenger) public ScopedDbCurrent(IServiceScopeFactory scopeFactory, Func<IServiceProvider, DbSet<TEntity>> dbSetSelector, IMessenger messenger)
{ {
this.scopeFactory = scopeFactory; this.scopeFactory = scopeFactory;
this.dbSetFunc = dbSetFunc; this.dbSetSelector = dbSetSelector;
this.messenger = messenger; this.messenger = messenger;
} }
@@ -52,7 +52,7 @@ internal class ScopedDbCurrent<TEntity, TMessage>
using (IServiceScope scope = scopeFactory.CreateScope()) using (IServiceScope scope = scopeFactory.CreateScope())
{ {
DbSet<TEntity> dbSet = dbSetFunc(scope.ServiceProvider); DbSet<TEntity> dbSet = dbSetSelector(scope.ServiceProvider);
// only update when not processing a deletion // only update when not processing a deletion
if (value != null) if (value != null)

View File

@@ -9,18 +9,9 @@ namespace Snap.Hutao.Core.Database;
/// <summary> /// <summary>
/// 设置帮助类 /// 设置帮助类
/// </summary> /// </summary>
public static class SettingEntryHelper [HighQuality]
public static class SettingEntryExtension
{ {
/// <summary>
/// "True"
/// </summary>
public static readonly string TrueString = true.ToString();
/// <summary>
/// "False"
/// </summary>
public static readonly string FalseString = false.ToString();
/// <summary> /// <summary>
/// 获取或添加一个对应的设置 /// 获取或添加一个对应的设置
/// </summary> /// </summary>
@@ -35,8 +26,7 @@ public static class SettingEntryHelper
if (entry == null) if (entry == null)
{ {
entry = new(key, value); entry = new(key, value);
dbSet.Add(entry); dbSet.AddAndSave(entry);
dbSet.Context().SaveChanges();
} }
return entry; return entry;

View File

@@ -11,6 +11,7 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// <summary> /// <summary>
/// <see cref="Ioc"/> 配置 /// <see cref="Ioc"/> 配置
/// </summary> /// </summary>
[HighQuality]
internal static class IocConfiguration internal static class IocConfiguration
{ {
/// <summary> /// <summary>
@@ -18,7 +19,7 @@ internal static class IocConfiguration
/// </summary> /// </summary>
/// <param name="services">集合</param> /// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns> /// <returns>可继续操作的集合</returns>
public static IServiceCollection AddJsonSerializerOptions(this IServiceCollection services) public static IServiceCollection AddJsonOptions(this IServiceCollection services)
{ {
return services.AddSingleton(CoreEnvironment.JsonOptions); return services.AddSingleton(CoreEnvironment.JsonOptions);
} }
@@ -38,7 +39,9 @@ internal static class IocConfiguration
{ {
if (context.Database.GetPendingMigrations().Any()) if (context.Database.GetPendingMigrations().Any())
{ {
#if DEBUG
Debug.WriteLine("[Debug] Performing AppDbContext Migrations"); Debug.WriteLine("[Debug] Performing AppDbContext Migrations");
#endif
context.Database.Migrate(); context.Database.Migrate();
} }
} }

View File

@@ -9,6 +9,7 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// <summary> /// <summary>
/// <see cref="Ioc"/> 与 <see cref="HttpClient"/> 配置 /// <see cref="Ioc"/> 与 <see cref="HttpClient"/> 配置
/// </summary> /// </summary>
[HighQuality]
internal static partial class IocHttpClientConfiguration internal static partial class IocHttpClientConfiguration
{ {
/// <summary> /// <summary>

View File

@@ -9,6 +9,7 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// 服务管理器 /// 服务管理器
/// 依赖注入的核心管理类 /// 依赖注入的核心管理类
/// </summary> /// </summary>
[HighQuality]
internal static partial class ServiceCollectionExtension internal static partial class ServiceCollectionExtension
{ {
/// <summary> /// <summary>

View File

@@ -1,32 +0,0 @@
// 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

@@ -2,14 +2,16 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging; using System.Collections;
using System.Text;
namespace Snap.Hutao.Core.ExceptionService; namespace Snap.Hutao.Core.ExceptionService;
/// <summary> /// <summary>
/// 异常记录器 /// 异常记录器
/// </summary> /// </summary>
internal class ExceptionRecorder [HighQuality]
internal sealed class ExceptionRecorder
{ {
private readonly ILogger logger; private readonly ILogger logger;
@@ -33,16 +35,20 @@ internal class ExceptionRecorder
Ioc.Default.GetRequiredService<Web.Hutao.HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult(); Ioc.Default.GetRequiredService<Web.Hutao.HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
#pragma warning restore VSTHRD002 #pragma warning restore VSTHRD002
#endif #endif
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常"); StringBuilder dataDetailBuilder = new();
foreach (DictionaryEntry entry in e.Exception.Data)
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
{ {
provider.Dispose(); string key = $"{entry.Key}";
string value = $"{entry.Value}";
dataDetailBuilder.Append(key).Append(':').Append(value).Append("\r\n");
} }
logger.LogError(e.Exception, "未经处理的异常\r\n{detail}", dataDetailBuilder.ToString());
} }
private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e) private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)
{ {
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message); logger.LogCritical("XAML绑定失败: {message}", e.Message);
} }
} }

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 运行环境异常
/// 用户的计算机中的某些设置不符合要求
/// </summary>
[HighQuality]
internal sealed class RuntimeEnvironmentException : Exception
{
/// <summary>
/// 构造一个新的运行环境异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public RuntimeEnvironmentException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Package;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 帮助更好的抛出异常
/// </summary>
[HighQuality]
[System.Diagnostics.StackTraceHidden]
internal static class ThrowHelper
{
/// <summary>
/// 操作取消
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <returns>nothing</returns>
/// <exception cref="OperationCanceledException">操作取消异常</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? inner)
{
throw new OperationCanceledException(message, inner);
}
/// <summary>
/// 游戏文件操作失败
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <returns>nothing</returns>
/// <exception cref="GameFileOperationException">文件操作失败</exception>
public static GameFileOperationException GameFileOperation(string message, Exception inner)
{
throw new GameFileOperationException(message, inner);
}
/// <summary>
/// 包转换错误
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <returns>nothing</returns>
/// <exception cref="PackageConvertException">包转换错误异常</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static PackageConvertException PackageConvert(string message, Exception inner)
{
throw new PackageConvertException(message, inner);
}
/// <summary>
/// 用户数据损坏
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <returns>nothing</returns>
/// <exception cref="UserdataCorruptedException">数据损坏</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static UserdataCorruptedException UserdataCorrupted(string message, Exception inner)
{
throw new UserdataCorruptedException(message, inner);
}
/// <summary>
/// 运行环境异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <returns>nothing</returns>
/// <exception cref="RuntimeEnvironmentException">环境异常</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static RuntimeEnvironmentException RuntimeEnvironment(string message, Exception inner)
{
throw new RuntimeEnvironmentException(message, inner);
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 用户数据损坏异常
/// </summary>
[HighQuality]
internal sealed class UserdataCorruptedException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public UserdataCorruptedException(string message, Exception innerException)
: base(string.Format(SH.CoreExceptionServiceUserdataCorruptedMessage, message), innerException)
{
}
}

View File

@@ -9,6 +9,7 @@ namespace Snap.Hutao.Core.ExpressionService;
/// Class to cast to type <see cref="TTo"/> /// Class to cast to type <see cref="TTo"/>
/// </summary> /// </summary>
/// <typeparam name="TTo">Target type</typeparam> /// <typeparam name="TTo">Target type</typeparam>
[HighQuality]
public static class CastTo<TTo> public static class CastTo<TTo>
{ {
/// <summary> /// <summary>

View File

@@ -14,24 +14,25 @@ namespace Snap.Hutao.Core.IO.Bits;
/// <summary> /// <summary>
/// BITS Job /// BITS Job
/// </summary> /// </summary>
[HighQuality]
[SuppressMessage("", "SA1600")] [SuppressMessage("", "SA1600")]
internal class BitsJob : DisposableObject, IBackgroundCopyCallback internal sealed class BitsJob : DisposableObject, IBackgroundCopyCallback
{ {
/// <summary> /// <summary>
/// 任务名称前缀 /// 任务名称前缀
/// </summary> /// </summary>
public const string JobNamePrefix = "SnapHutaoBitsJob"; public const string JobNamePrefix = "SnapHutaoBitsJob";
private const uint BitsEngineNoProgressTimeout = 120; private const uint Timeout = 29;
private const int MaxResumeAttempts = 10; private const int MaxResumeAttempts = 3;
private readonly string displayName; private readonly string displayName;
private readonly ILogger<BitsJob> log; private readonly ILogger<BitsJob> logger;
private readonly object lockObj = new(); private readonly object syncRoot = new();
private IBackgroundCopyJob? nativeJob; private IBackgroundCopyJob? nativeJob;
private System.Exception? jobException; private Exception? jobException;
private BG_JOB_PROGRESS progress; private BG_JOB_PROGRESS jobProgress;
private BG_JOB_STATE state; private BG_JOB_STATE state;
private bool isJobComplete; private bool isJobComplete;
private int resumeAttempts; private int resumeAttempts;
@@ -40,33 +41,33 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{ {
this.displayName = displayName; this.displayName = displayName;
nativeJob = job; nativeJob = job;
log = serviceProvider.GetRequiredService<ILogger<BitsJob>>(); logger = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
} }
public HRESULT ErrorCode { get; private set; } public HRESULT ErrorCode { get; private set; }
public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath) public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath)
{ {
ILogger<BitsJob> service = serviceProvider.GetRequiredService<ILogger<BitsJob>>(); ILogger<BitsJob> logger = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
string text = $"{JobNamePrefix} - {uri}"; string jobName = $"{JobNamePrefix} - {uri}";
IBackgroundCopyJob ppJob; IBackgroundCopyJob job;
try try
{ {
backgroundCopyManager.CreateJob(text, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out ppJob); backgroundCopyManager.CreateJob(jobName, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out job);
// BG_NOTIFY_JOB_TRANSFERRED & BG_NOTIFY_JOB_ERROR & BG_NOTIFY_JOB_MODIFICATION // BG_NOTIFY_JOB_TRANSFERRED & BG_NOTIFY_JOB_ERROR & BG_NOTIFY_JOB_MODIFICATION
ppJob.SetNotifyFlags(0b1011); job.SetNotifyFlags(0B1011);
ppJob.SetNoProgressTimeout(BitsEngineNoProgressTimeout); job.SetNoProgressTimeout(Timeout);
ppJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND); job.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND);
ppJob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, null, null); job.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, default, default);
} }
catch (COMException ex) catch (COMException ex)
{ {
service.LogInformation("Failed to create job. {message}", ex.Message); logger.LogInformation("Failed to create job. {message}", ex.Message);
throw; throw;
} }
BitsJob bitsJob = new(serviceProvider, text, ppJob); BitsJob bitsJob = new(serviceProvider, jobName, job);
bitsJob.InitJob(uri.AbsoluteUri, filePath); bitsJob.InitJob(uri.AbsoluteUri, filePath);
return bitsJob; return bitsJob;
} }
@@ -79,9 +80,9 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
UpdateJobState(); UpdateJobState();
CompleteOrCancel(); CompleteOrCancel();
} }
catch (System.Exception ex) catch (Exception ex)
{ {
log.LogInformation("Failed to job transfer: {message}", ex.Message); logger.LogInformation("Failed to job transfer: {message}", ex.Message);
} }
} }
@@ -90,7 +91,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
IBackgroundCopyError error2 = error; IBackgroundCopyError error2 = error;
try try
{ {
log.LogInformation("Failed job: {message}", displayName); logger.LogInformation("Failed job: {message}", displayName);
UpdateJobState(); UpdateJobState();
BG_ERROR_CONTEXT errorContext = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_NONE; BG_ERROR_CONTEXT errorContext = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_NONE;
HRESULT returnCode = new(0); HRESULT returnCode = new(0);
@@ -99,11 +100,11 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
ErrorCode = returnCode; ErrorCode = returnCode;
jobException = new IOException(string.Format("Error context: {0}, Error code: {1}", errorContext, returnCode)); jobException = new IOException(string.Format("Error context: {0}, Error code: {1}", errorContext, returnCode));
CompleteOrCancel(); CompleteOrCancel();
log.LogInformation(jobException, "Job Exception:"); logger.LogInformation(jobException, "Job Exception:");
} }
catch (System.Exception ex) catch (Exception ex)
{ {
log?.LogInformation("Failed to handle job error: {message}", ex.Message); logger?.LogInformation("Failed to handle job error: {message}", ex.Message);
} }
} }
@@ -115,7 +116,9 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
if (state == BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR) if (state == BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR)
{ {
HRESULT errorCode = GetErrorCode(job); HRESULT errorCode = GetErrorCode(job);
if (errorCode == -2145844944)
// BG_E_HTTP_ERROR_304
if (errorCode == 0x80190130)
{ {
ErrorCode = errorCode; ErrorCode = errorCode;
CompleteOrCancel(); CompleteOrCancel();
@@ -129,7 +132,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
return; return;
} }
log.LogInformation("Max resume attempts for job '{name}' exceeded. Canceling.", displayName); logger.LogInformation("Max resume attempts for job '{name}' exceeded. Canceling.", displayName);
CompleteOrCancel(); CompleteOrCancel();
} }
else if (IsProgressingState(state)) else if (IsProgressingState(state))
@@ -141,16 +144,16 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
CompleteOrCancel(); CompleteOrCancel();
} }
} }
catch (System.Exception ex) catch (Exception ex)
{ {
log.LogInformation(ex, "message"); logger.LogInformation(ex, "message");
} }
} }
public void Cancel() public void Cancel()
{ {
log.LogInformation("Canceling job {name}", displayName); logger.LogInformation("Canceling job {name}", displayName);
lock (lockObj) lock (syncRoot)
{ {
if (!isJobComplete) if (!isJobComplete)
{ {
@@ -161,16 +164,18 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
} }
} }
public void WaitForCompletion(Action<ProgressUpdateStatus> callback, CancellationToken cancellationToken) public void WaitForCompletion(IProgress<ProgressUpdateStatus> progress, CancellationToken cancellationToken)
{ {
CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(Cancel); CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(Cancel);
int noProgressSeconds = 0; int noProgressSeconds = 0;
int noTransferSeconds = 0;
ulong previousTransferred = 0;
try try
{ {
UpdateJobState(); UpdateJobState();
while (IsProgressingState(state) || state == BG_JOB_STATE.BG_JOB_STATE_QUEUED) while (IsProgressingState(state) || state == BG_JOB_STATE.BG_JOB_STATE_QUEUED)
{ {
if (noProgressSeconds > BitsEngineNoProgressTimeout) if (noProgressSeconds > Timeout || noTransferSeconds > Timeout)
{ {
jobException = new TimeoutException($"Timeout reached for job {displayName} whilst in state {state}"); jobException = new TimeoutException($"Timeout reached for job {displayName} whilst in state {state}");
break; break;
@@ -183,7 +188,18 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
if (state is BG_JOB_STATE.BG_JOB_STATE_TRANSFERRING or BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED or BG_JOB_STATE.BG_JOB_STATE_ACKNOWLEDGED) if (state is BG_JOB_STATE.BG_JOB_STATE_TRANSFERRING or BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED or BG_JOB_STATE.BG_JOB_STATE_ACKNOWLEDGED)
{ {
noProgressSeconds = 0; noProgressSeconds = 0;
callback(new ProgressUpdateStatus((long)progress.BytesTransferred, (long)progress.BytesTotal)); if (jobProgress.BytesTransferred > previousTransferred)
{
previousTransferred = jobProgress.BytesTransferred;
noTransferSeconds = 0;
logger.LogInformation("{job}: {read} / {total}", displayName, jobProgress.BytesTransferred, jobProgress.BytesTotal);
progress.Report(new ProgressUpdateStatus((long)jobProgress.BytesTransferred, (long)jobProgress.BytesTotal));
}
else
{
++noTransferSeconds;
logger.LogInformation("{job} no transfer for {x} seconds", displayName, noTransferSeconds);
}
} }
// Refresh every seconds. // Refresh every seconds.
@@ -229,7 +245,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
return; return;
} }
lock (lockObj) lock (syncRoot)
{ {
if (isJobComplete) if (isJobComplete)
{ {
@@ -238,7 +254,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
if (state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED) if (state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED)
{ {
log.LogInformation("Completing job '{name}'.", displayName); logger.LogInformation("Completing job '{name}'.", displayName);
Invoke(() => nativeJob?.Complete(), "Bits Complete"); Invoke(() => nativeJob?.Complete(), "Bits Complete");
while (state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED) while (state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED)
{ {
@@ -248,7 +264,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
} }
else else
{ {
log.LogInformation("Canceling job '{name}'.", displayName); logger.LogInformation("Canceling job '{name}'.", displayName);
Invoke(() => nativeJob?.Cancel(), "Bits Cancel"); Invoke(() => nativeJob?.Cancel(), "Bits Cancel");
} }
@@ -268,7 +284,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{ {
if (!isJobComplete) if (!isJobComplete)
{ {
Invoke(() => nativeJob?.GetProgress(out progress), "GetProgress"); Invoke(() => nativeJob?.GetProgress(out jobProgress), "GetProgress");
} }
} }
@@ -283,9 +299,9 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{ {
action(); action();
} }
catch (System.Exception ex) catch (Exception ex)
{ {
log.LogInformation("{name} failed. {exception}", displayName, ex); logger.LogInformation("{name} failed. {exception}", displayName, ex);
if (throwOnFailure) if (throwOnFailure)
{ {
throw; throw;

View File

@@ -12,6 +12,7 @@ namespace Snap.Hutao.Core.IO.Bits;
/// <summary> /// <summary>
/// BITS 管理器 /// BITS 管理器
/// </summary> /// </summary>
[HighQuality]
[Injection(InjectAs.Singleton)] [Injection(InjectAs.Singleton)]
internal class BitsManager internal class BitsManager
{ {
@@ -25,8 +26,8 @@ internal class BitsManager
/// <param name="serviceProvider">服务提供器</param> /// <param name="serviceProvider">服务提供器</param>
public BitsManager(IServiceProvider serviceProvider) public BitsManager(IServiceProvider serviceProvider)
{ {
this.serviceProvider = serviceProvider;
logger = serviceProvider.GetRequiredService<ILogger<BitsManager>>(); logger = serviceProvider.GetRequiredService<ILogger<BitsManager>>();
this.serviceProvider = serviceProvider;
} }
/// <summary> /// <summary>
@@ -39,7 +40,8 @@ internal class BitsManager
public async Task<ValueResult<bool, TempFile>> DownloadAsync(Uri uri, IProgress<ProgressUpdateStatus> progress, CancellationToken token = default) public async Task<ValueResult<bool, TempFile>> DownloadAsync(Uri uri, IProgress<ProgressUpdateStatus> progress, CancellationToken token = default)
{ {
TempFile tempFile = new(true); TempFile tempFile = new(true);
bool result = await Task.Run(() => DownloadCore(uri, tempFile.Path, progress.Report, token), token).ConfigureAwait(false); await ThreadHelper.SwitchToBackgroundAsync();
bool result = DownloadCore(uri, tempFile.Path, progress, token);
return new(result, tempFile); return new(result, tempFile);
} }
@@ -68,17 +70,21 @@ internal class BitsManager
{ {
uint actualFetched = 0; uint actualFetched = 0;
pJobs.Next(1, out IBackgroundCopyJob pJob, ref actualFetched); pJobs.Next(1, out IBackgroundCopyJob pJob, ref actualFetched);
pJob.GetDisplayName(out PWSTR name);
if (name.AsSpan().StartsWith(BitsJob.JobNamePrefix)) if (actualFetched != 0)
{ {
jobsToCancel.Add(pJob); pJob.GetDisplayName(out PWSTR name);
if (name.AsSpan().StartsWith(BitsJob.JobNamePrefix))
{
jobsToCancel.Add(pJob);
}
} }
} }
jobsToCancel.ForEach(job => job.Cancel()); jobsToCancel.ForEach(job => job.Cancel());
} }
private bool DownloadCore(Uri uri, string tempFile, Action<ProgressUpdateStatus> progress, CancellationToken token) private bool DownloadCore(Uri uri, string tempFile, IProgress<ProgressUpdateStatus> progress, CancellationToken token)
{ {
IBackgroundCopyManager value; IBackgroundCopyManager value;
@@ -88,7 +94,7 @@ internal class BitsManager
} }
catch (Exception ex) catch (Exception ex)
{ {
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message); logger.LogWarning("BITS download engine not supported: {message}", ex.Message);
return false; return false;
} }
@@ -102,7 +108,7 @@ internal class BitsManager
} }
catch (Exception ex) catch (Exception ex)
{ {
logger?.LogWarning(ex, "BITS download failed:"); logger.LogWarning(ex, "BITS download failed:");
return false; return false;
} }

View File

@@ -1,11 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Core.IO.Bits; namespace Snap.Hutao.Core.IO.Bits;
/// <summary> /// <summary>
/// 进度更新状态 /// 进度更新状态
/// </summary> /// </summary>
[HighQuality]
[DebuggerDisplay("{BytesRead}/{TotalBytes}")]
public class ProgressUpdateStatus public class ProgressUpdateStatus
{ {
/// <summary> /// <summary>

View File

@@ -7,8 +7,9 @@ using Windows.Storage.Streams;
namespace Snap.Hutao.Core.IO.DataTransfer; namespace Snap.Hutao.Core.IO.DataTransfer;
/// <summary> /// <summary>
/// 剪贴板 /// 剪贴板 在主线程使用
/// </summary> /// </summary>
[HighQuality]
internal static class Clipboard internal static class Clipboard
{ {
/// <summary> /// <summary>
@@ -23,6 +24,8 @@ internal static class Clipboard
await ThreadHelper.SwitchToMainThreadAsync(); await ThreadHelper.SwitchToMainThreadAsync();
DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent(); DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
string json = await view.GetTextAsync(); string json = await view.GetTextAsync();
await ThreadHelper.SwitchToBackgroundAsync();
return JsonSerializer.Deserialize<T>(json, options); return JsonSerializer.Deserialize<T>(json, options);
} }

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using System.Security.Cryptography;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 摘要
/// </summary>
[HighQuality]
internal static class Digest
{
/// <summary>
/// 异步获取文件 Md5 摘要
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="token">取消令牌</param>
/// <returns>文件 Md5 摘要</returns>
public static async Task<string> GetFileMD5Async(string filePath, CancellationToken token = default)
{
using (FileStream stream = File.OpenRead(filePath))
{
return await GetStreamMD5Async(stream, token).ConfigureAwait(false);
}
}
/// <summary>
/// 获取流的 Md5 摘要
/// </summary>
/// <param name="stream">流</param>
/// <param name="token">取消令牌</param>
/// <returns>流 Md5 摘要</returns>
public static async Task<string> GetStreamMD5Async(Stream stream, CancellationToken token = default)
{
using (MD5 md5 = MD5.Create())
{
byte[] bytes = await md5.ComputeHashAsync(stream, token).ConfigureAwait(false);
return System.Convert.ToHexString(bytes);
}
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 文件操作
/// </summary>
[HighQuality]
internal static class FileOperation
{
/// <summary>
/// 将指定文件移动到新位置,提供指定新文件名和覆盖目标文件(如果它已存在)的选项。
/// </summary>
/// <param name="sourceFileName">要移动的文件的名称。 可以包括相对或绝对路径。</param>
/// <param name="destFileName">文件的新路径和名称。</param>
/// <param name="overwrite">如果要覆盖目标文件</param>
/// <returns>是否发生了移动操作</returns>
public static bool Move(string sourceFileName, string destFileName, bool overwrite)
{
if (File.Exists(sourceFileName))
{
if (overwrite)
{
File.Move(sourceFileName, destFileName, overwrite);
return true;
}
else
{
if (!File.Exists(destFileName))
{
File.Move(sourceFileName, destFileName, overwrite);
return true;
}
}
}
return false;
}
}

View File

@@ -109,4 +109,4 @@ internal readonly struct FilePath : IEquatable<FilePath>
{ {
return Value.GetHashCode(); return Value.GetHashCode();
} }
} }

View File

@@ -6,7 +6,8 @@ namespace Snap.Hutao.Core.IO.Ini;
/// <summary> /// <summary>
/// Ini 注释 /// Ini 注释
/// </summary> /// </summary>
internal class IniComment : IniElement [HighQuality]
internal sealed class IniComment : IniElement
{ {
/// <summary> /// <summary>
/// 构造一个新的 Ini 注释 /// 构造一个新的 Ini 注释

View File

@@ -6,6 +6,7 @@ namespace Snap.Hutao.Core.IO.Ini;
/// <summary> /// <summary>
/// Ini 元素 /// Ini 元素
/// </summary> /// </summary>
[HighQuality]
internal abstract class IniElement internal abstract class IniElement
{ {
/// <summary> /// <summary>

View File

@@ -6,7 +6,8 @@ namespace Snap.Hutao.Core.IO.Ini;
/// <summary> /// <summary>
/// Ini 参数 /// Ini 参数
/// </summary> /// </summary>
internal class IniParameter : IniElement [HighQuality]
internal sealed class IniParameter : IniElement
{ {
/// <summary> /// <summary>
/// Ini 参数 /// Ini 参数

View File

@@ -6,7 +6,8 @@ namespace Snap.Hutao.Core.IO.Ini;
/// <summary> /// <summary>
/// Ini 节 /// Ini 节
/// </summary> /// </summary>
internal class IniSection : IniElement [HighQuality]
internal sealed class IniSection : IniElement
{ {
/// <summary> /// <summary>
/// 构造一个新的Ini 节 /// 构造一个新的Ini 节

View File

@@ -8,6 +8,7 @@ namespace Snap.Hutao.Core.IO.Ini;
/// <summary> /// <summary>
/// Ini 序列化器 /// Ini 序列化器
/// </summary> /// </summary>
[HighQuality]
internal static class IniSerializer internal static class IniSerializer
{ {
/// <summary> /// <summary>
@@ -17,7 +18,7 @@ internal static class IniSerializer
/// <returns>Ini 元素集合</returns> /// <returns>Ini 元素集合</returns>
public static IEnumerable<IniElement> Deserialize(FileStream fileStream) public static IEnumerable<IniElement> Deserialize(FileStream fileStream)
{ {
using (TextReader reader = new StreamReader(fileStream)) using (StreamReader reader = new(fileStream))
{ {
while (reader.ReadLine() is string line) while (reader.ReadLine() is string line)
{ {
@@ -52,7 +53,7 @@ internal static class IniSerializer
/// <param name="elements">元素</param> /// <param name="elements">元素</param>
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements) public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
{ {
using (TextWriter writer = new StreamWriter(fileStream)) using (StreamWriter writer = new(fileStream))
{ {
foreach (IniElement element in elements) foreach (IniElement element in elements)
{ {

View File

@@ -32,13 +32,7 @@ internal static class PickerExtension
} }
else else
{ {
if (exception != null) InfoBarWaringPickerException(exception);
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning($"无法打开文件选择器 {exception.Message}");
}
return new(false, null!); return new(false, null!);
} }
} }
@@ -64,14 +58,46 @@ internal static class PickerExtension
} }
else else
{ {
if (exception != null) InfoBarWaringPickerException(exception);
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning($"无法打开文件选择器 {exception.Message}");
}
return new(false, null!); return new(false, null!);
} }
} }
/// <inheritdoc cref="FolderPicker.PickSingleFolderAsync"/>
public static async Task<ValueResult<bool, string>> TryPickSingleFolderAsync(this FolderPicker picker)
{
StorageFolder? folder;
Exception? exception = null;
try
{
folder = await picker.PickSingleFolderAsync().AsTask().ConfigureAwait(false);
}
catch (Exception ex)
{
exception = ex;
folder = null;
}
if (folder != null)
{
return new(true, folder.Path);
}
else
{
InfoBarWaringPickerException(exception);
return new(false, null!);
}
}
private static void InfoBarWaringPickerException(Exception? exception)
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning(
SH.CoreIOPickerExtensionPickerExceptionInfoBarTitle,
string.Format(SH.CoreIOPickerExtensionPickerExceptionInfoBarMessage, exception.Message));
}
}
} }

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using System.IO; using System.IO;
namespace Snap.Hutao.Core.IO; namespace Snap.Hutao.Core.IO;
@@ -8,6 +9,7 @@ namespace Snap.Hutao.Core.IO;
/// <summary> /// <summary>
/// 封装一个临时文件 /// 封装一个临时文件
/// </summary> /// </summary>
[HighQuality]
internal sealed class TempFile : IDisposable internal sealed class TempFile : IDisposable
{ {
/// <summary> /// <summary>
@@ -16,7 +18,14 @@ internal sealed class TempFile : IDisposable
/// <param name="delete">是否在创建时删除文件</param> /// <param name="delete">是否在创建时删除文件</param>
public TempFile(bool delete = false) public TempFile(bool delete = false)
{ {
Path = System.IO.Path.GetTempFileName(); try
{
Path = System.IO.Path.GetTempFileName();
}
catch (UnauthorizedAccessException ex)
{
ThrowHelper.RuntimeEnvironment(SH.CoreIOTempFileCreateFail, ex);
}
if (delete) if (delete)
{ {
@@ -34,7 +43,7 @@ internal sealed class TempFile : IDisposable
/// </summary> /// </summary>
/// <param name="file">源文件</param> /// <param name="file">源文件</param>
/// <returns>临时文件</returns> /// <returns>临时文件</returns>
public static TempFile? CreateFromFileCopy(string file) public static TempFile? CreateCopyFrom(string file)
{ {
TempFile temporaryFile = new(); TempFile temporaryFile = new();
try try
@@ -53,6 +62,12 @@ internal sealed class TempFile : IDisposable
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
File.Delete(Path); try
{
File.Delete(Path);
}
catch (IOException)
{
}
} }
} }

View File

@@ -10,7 +10,8 @@ namespace Snap.Hutao.Core.Json.Converter;
/// 枚举转换器 /// 枚举转换器
/// </summary> /// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam> /// <typeparam name="TEnum">枚举的类型</typeparam>
internal class ConfigurableEnumConverter<TEnum> : JsonConverter<TEnum> [HighQuality]
internal sealed class ConfigurableEnumConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum where TEnum : struct, Enum
{ {
private readonly JsonSerializeType readAs; private readonly JsonSerializeType readAs;

View File

@@ -6,6 +6,7 @@ namespace Snap.Hutao.Core.Json.Converter;
/// <summary> /// <summary>
/// 实现日期的转换 /// 实现日期的转换
/// </summary> /// </summary>
[HighQuality]
internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset> internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{ {
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -1,24 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.Extensions.Primitives;
using Snap.Hutao.Extension;
namespace Snap.Hutao.Core.Json.Converter; namespace Snap.Hutao.Core.Json.Converter;
/// <summary> /// <summary>
/// 逗号分隔列表转换器 /// 逗号分隔列表转换器
/// </summary> /// </summary>
internal class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerable<int>> [HighQuality]
internal sealed class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerable<int>>
{ {
private const char Comma = ',';
/// <inheritdoc/> /// <inheritdoc/>
public override IEnumerable<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override IEnumerable<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
string? team = reader.GetString(); if (reader.GetString() is string source)
IEnumerable<int>? ids = team?.Split(',').Select(x => int.Parse(x)); {
return ids ?? Enumerable.Empty<int>(); return EnumerateNumbers(source);
}
return Enumerable.Empty<int>();
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, IEnumerable<int> value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, IEnumerable<int> value, JsonSerializerOptions options)
{ {
writer.WriteStringValue(string.Join(',', value)); writer.WriteStringValue(string.Join(Comma, value));
}
private static IEnumerable<int> EnumerateNumbers(string source)
{
foreach (StringSegment id in new StringTokenizer(source, Comma.Enumerate().ToArray()))
{
yield return int.Parse(id.AsSpan());
}
} }
} }

View File

@@ -4,10 +4,13 @@
namespace Snap.Hutao.Core.Json.Converter; namespace Snap.Hutao.Core.Json.Converter;
/// <summary> /// <summary>
/// Json字典转换器 /// Json枚举键字典转换器
/// </summary> /// </summary>
public class StringEnumKeyDictionaryConverter : JsonConverterFactory [HighQuality]
internal sealed class StringEnumKeyDictionaryConverter : JsonConverterFactory
{ {
private readonly Type converterType = typeof(StringEnumDictionaryConverterInner<,>);
/// <inheritdoc/> /// <inheritdoc/>
public override bool CanConvert(Type typeToConvert) public override bool CanConvert(Type typeToConvert)
{ {
@@ -16,7 +19,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
return false; return false;
} }
if (typeToConvert.GetGenericTypeDefinition() != typeof(IDictionary<,>)) if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
{ {
return false; return false;
} }
@@ -27,10 +30,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
/// <inheritdoc/> /// <inheritdoc/>
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{ {
Type keyType = type.GetGenericArguments()[0]; Type innerConverterType = converterType.MakeGenericType(type.GetGenericArguments());
Type valueType = type.GetGenericArguments()[1];
Type innerConverterType = typeof(StringEnumDictionaryConverterInner<,>).MakeGenericType(keyType, valueType);
JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType)!; JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType)!;
return converter; return converter;
} }
@@ -70,7 +70,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
string? propertyName = reader.GetString(); string? propertyName = reader.GetString();
if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) && !Enum.TryParse(propertyName, ignoreCase: true, out key)) if (!Enum.TryParse(propertyName, false, out TKey key) && !Enum.TryParse(propertyName, true, out key))
{ {
throw new JsonException($"Unable to convert \"{propertyName}\" to Enum \"{keyType}\"."); throw new JsonException($"Unable to convert \"{propertyName}\" to Enum \"{keyType}\".");
} }

View File

@@ -1,43 +0,0 @@
// 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

@@ -9,6 +9,7 @@ namespace Snap.Hutao.Core.Json;
/// <summary> /// <summary>
/// Json 类型信息解析器 /// Json 类型信息解析器
/// </summary> /// </summary>
[HighQuality]
internal static class JsonTypeInfoResolvers internal static class JsonTypeInfoResolvers
{ {
private static readonly Type JsonEnumAttributeType = typeof(JsonEnumAttribute); private static readonly Type JsonEnumAttributeType = typeof(JsonEnumAttribute);
@@ -16,22 +17,28 @@ internal static class JsonTypeInfoResolvers
/// <summary> /// <summary>
/// 解析枚举类型 /// 解析枚举类型
/// </summary> /// </summary>
/// <param name="ti">Json 类型信息</param> /// <param name="typeInfo">Json 类型信息</param>
public static void ResolveEnumType(JsonTypeInfo ti) public static void ResolveEnumType(JsonTypeInfo typeInfo)
{ {
if (ti.Kind != JsonTypeInfoKind.Object) if (typeInfo.Kind != JsonTypeInfoKind.Object)
{ {
return; return;
} }
IEnumerable<JsonPropertyInfo> enumProperties = ti.Properties foreach (JsonPropertyInfo property in typeInfo.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(); if (property.PropertyType.IsEnum)
{
enumProperty.CustomConverter = attr.CreateConverter(enumProperty); if (property.AttributeProvider is System.Reflection.ICustomAttributeProvider provider)
{
object[] attributes = provider.GetCustomAttributes(JsonEnumAttributeType, false);
if (attributes.Length == 1)
{
JsonEnumAttribute attr = (JsonEnumAttribute)attributes[0];
property.CustomConverter = attr.CreateConverter(property);
}
}
}
} }
} }
} }

View File

@@ -9,7 +9,8 @@ namespace Snap.Hutao.Core;
/// <summary> /// <summary>
/// 跳转列表帮助类 /// 跳转列表帮助类
/// </summary> /// </summary>
public static class JumpListHelper [HighQuality]
internal static class JumpListHelper
{ {
/// <summary> /// <summary>
/// 异步配置跳转列表 /// 异步配置跳转列表
@@ -23,8 +24,7 @@ public static class JumpListHelper
list.Items.Clear(); list.Items.Clear();
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, "启动游戏"); JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, SH.CoreJumpListHelperLaunchGameItemDisplayName);
launchGameItem.GroupName = "快捷操作";
launchGameItem.Logo = new("ms-appx:///Resource/Icon/UI_GuideIcon_PlayMethod.png"); launchGameItem.Logo = new("ms-appx:///Resource/Icon/UI_GuideIcon_PlayMethod.png");
list.Items.Add(launchGameItem); list.Items.Add(launchGameItem);

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