Compare commits

...

77 Commits

Author SHA1 Message Date
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
DismissedLight
5126337138 code style [skip ci] 2023-01-12 19:42:45 +08:00
DismissedLight
4d634d3264 improve concurrent execution 2023-01-12 19:38:06 +08:00
Masterain
15a69fd0de Delete PrereleaseDistribution.yml 2023-01-11 20:42:42 -08:00
Masterain
c232891fe7 Update azure-pipelines.yml for Azure Pipelines 2023-01-11 20:41:03 -08:00
Masterain
c35c2a5700 Update workflows 2023-01-11 19:36:23 -08:00
Masterain
42305094f8 Update network-issue.yml 2023-01-11 16:12:16 -08:00
310 changed files with 16291 additions and 3984 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: |-
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
options:
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/most-frequent-questions.html),并认为我的问题没有在文档中得到解答
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/),并认为我的问题没有在文档中得到解答
required: true
- label: 我使用的操作系统是[受支持的版本](https://hut.ao/quick-start.html#%E6%9C%80%E4%BD%8E%E7%B3%BB%E7%BB%9F%E8%A6%81%E6%B1%82)
@@ -33,7 +33,7 @@ body:
label: Windows 版本
description: |
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
placeholder: 22000.556
placeholder: 22621.1105
validations:
required: true
@@ -42,7 +42,7 @@ body:
attributes:
label: Snap Hutao 版本
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
placeholder: 1.1.0
placeholder: 1.3.13.0
validations:
required: true

View File

@@ -21,8 +21,8 @@ body:
**在填写下面的问题之前请先使用我们的网络诊断工具**
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
- 你可以点击下面的链接以下载网络诊断工具:
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-tool.exe)
- [GitHub](https://github.com/DGP-Studio/Snap.Hutao/files/10081999/network-diagnosis-hutao.zip)
- [胡桃资源站](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)
validations:
required: true
@@ -41,7 +41,7 @@ body:
id: user-isp
attributes:
label: 你的运营商
description: 中国用户请精确到省级行政区,海外地区请精确到国家
description: 海外用户请选其它
options:
- 中国电信
- 中国联通
@@ -60,7 +60,9 @@ body:
- 完全无法连接服务器
- 连接速度慢
- 获取到了不正确的页面或数据
- 图片下载错误(429 Error
- 客户端提示 429 Error
- 客户端图片下载错误
- 客户端图片预下载错误
- 其它
validations:
required: true

View File

@@ -2,7 +2,7 @@ name: PublishDistribution
on:
release:
types: [published]
types: [released]
workflow_dispatch:
@@ -16,10 +16,10 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v3
# Download Publish.zip
# Download Assets
- name: Download Release
timeout-minutes: 5
uses: robinraju/release-downloader@v1.5
uses: robinraju/release-downloader@v1.7
with:
repository: "DGP-Studio/Snap.Hutao"
latest: true
@@ -38,4 +38,14 @@ jobs:
$RCCONF
EOF
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/
rclone copy ./release-download/* dgpODCN:/releases/
# Purge Patch System Cache
- name: Purge Patch
env:
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
PURGE_URL: ${{ secrets.PURGE_URL }}
run: |
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL

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

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

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

View File

@@ -25,4 +25,4 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</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.Build.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.Build.0 = 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|x64
{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|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|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|x64
{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.Build.0 = Debug|Any CPU
{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|arm64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|x64
{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.Build.0 = Release|Any CPU
EndGlobalSection

View File

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

View File

@@ -11,24 +11,24 @@
<ResourceDictionary.MergedDictionaries>
<muxc:XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/>
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<Color x:Key="AvatarPropertyAddValueColor">#FF74BF00</Color>
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="AvatarPropertyAddValueColor">#FF90E800</Color>
<Color x:Key="CompatBackgroundColor">#FF242424</Color>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Modify Window title bar color -->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/>
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- Page Transparent Background -->
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- IconFont -->
<FontFamily x:Key="SymbolThemeFontFamily">ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
<!-- InfoBar Resource -->
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
@@ -44,8 +44,10 @@
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
<!-- OpenPaneLength -->
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">252</x:Double>
<GridLength x:Key="CompatGridLength2">252</GridLength>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">268</x:Double>
<GridLength x:Key="CompatGridLength2">268</GridLength>
<!-- Brushes -->
<SolidColorBrush x:Key="AvatarPropertyAddValueBrush" Color="{ThemeResource AvatarPropertyAddValueColor}"/>
<!-- Uris -->
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie</x:String>
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
@@ -79,10 +81,6 @@
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<shvc:EmptyCollectionToBoolConverter x:Key="EmptyCollectionToBoolConverter"/>
<shvc:EmptyCollectionToBoolRevertConverter x:Key="EmptyCollectionToBoolRevertConverter"/>
<shvc:EmptyCollectionToVisibilityConverter x:Key="EmptyCollectionToVisibilityConverter"/>
<shvc:EmptyCollectionToVisibilityRevertConverter x:Key="EmptyCollectionToVisibilityRevertConverter"/>
<shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/>
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
@@ -90,6 +88,7 @@
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
<!-- Styles -->
<Style
x:Key="LargeGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}"
@@ -112,6 +111,328 @@
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{StaticResource CompatCornerRadius}"/>
</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 x:Key="ItemsStackPanelTemplate">
<ItemsStackPanel/>
@@ -121,4 +442,4 @@
</ItemsPanelTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>
</Application>

View File

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

View File

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

View File

@@ -8,6 +8,8 @@ namespace Snap.Hutao.Control;
/// <summary>
/// 绑定探针
/// 用于处理特定情况下需要穿透数据上下文的工作
/// DependencyObject will dispose inner ReferenceTracker in any time
/// when object is not used anymore.
/// </summary>
public class BindingProxy : DependencyObject
{

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Control.Extension;
/// <summary>
/// 对话框扩展
/// </summary>
internal static class ContentDialogExtensions
internal static class ContentDialogExtension
{
/// <summary>
/// 阻止用户交互

View File

@@ -1,15 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
/// <summary>
/// 指示此类支持取消任务
/// </summary>
public interface ISupportCancellation
{
/// <summary>
/// 用于通知事件取消的取消令牌
/// </summary>
CancellationToken CancellationToken { get; set; }
}

View File

@@ -32,7 +32,7 @@ public class CachedImage : ImageEx
try
{
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
Verify.Operation(imageUri.Host != string.Empty, SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
// check token state to determine whether the operation should be canceled.

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
@@ -11,6 +12,7 @@ using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;
@@ -24,7 +26,7 @@ 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 ConcurrentCancellationTokenSource<CompositionImage> LoadingTokenSource = new();
private readonly IImageCache imageCache;
private readonly IServiceProvider serviceProvider;
private SpriteVisual? spriteVisual;
private bool isShow = true;
@@ -34,8 +36,15 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
/// </summary>
public CompositionImage()
{
imageCache = Ioc.Default.GetRequiredService<IImageCache>();
serviceProvider = Ioc.Default.GetRequiredService<IServiceProvider>();
AllowFocusOnInteraction = false;
IsDoubleTapEnabled = false;
IsHitTestVisible = false;
IsHoldingEnabled = false;
IsRightTapEnabled = false;
IsTabStop = false;
SizeChanged += OnSizeChanged;
}
@@ -86,11 +95,11 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
if (exception is HttpRequestException httpRequestException)
{
infoBarService.Error(httpRequestException, $"GET {uri}");
infoBarService.Error(httpRequestException, string.Format(SH.ControlImageCompositionImageHttpRequest, uri));
}
else
{
infoBarService.Error(exception, $"应用 {nameof(CompositionImage)} 的源时发生异常");
infoBarService.Error(exception.GetBaseException(), SH.ControlImageCompositionImageSystemException);
}
}
@@ -124,26 +133,20 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
if (uri != null)
{
if (uri.Scheme == "ms-appx")
{
imageSurface = LoadedImageSurface.StartLoadFromUri(uri);
}
else
{
string storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
IImageCache imageCache = serviceProvider.GetRequiredService<IImageCache>();
string file = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
try
{
imageSurface = await LoadImageSurfaceAsync(storageFile, token).ConfigureAwait(true);
}
catch (COMException)
{
imageCache.Remove(uri.Enumerate());
}
catch (IOException)
{
imageCache.Remove(uri.Enumerate());
}
try
{
imageSurface = await LoadImageSurfaceAsync(file, token).ConfigureAwait(true);
}
catch (COMException)
{
imageCache.Remove(uri.Enumerate());
}
catch (IOException)
{
imageCache.Remove(uri.Enumerate());
}
if (imageSurface != null)

View File

@@ -6,6 +6,7 @@ using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Theme;
namespace Snap.Hutao.Control.Image;
@@ -49,12 +50,7 @@ public class MonoChrome : CompositionImage
private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
{
ApplicationTheme theme = ActualTheme switch
{
ElementTheme.Light => ApplicationTheme.Light,
ElementTheme.Dark => ApplicationTheme.Dark,
_ => Ioc.Default.GetRequiredService<App>().RequestedTheme,
};
ApplicationTheme theme = ThemeHelper.ElementToApplication(ActualTheme);
backgroundBrush.Color = theme switch
{

View File

@@ -0,0 +1,36 @@
// 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>
[MarkupExtensionReturnType(ReturnType = typeof(string))]
public sealed class ResourceStringExtension : MarkupExtension
{
/// <summary>
/// Gets or sets associated ID from resource strings.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets a string value from resource file associated with a resource key.
/// </summary>
/// <param name="name">Resource key name.</param>
/// <returns>A string value from resource file associated with a resource key.</returns>
public static string GetValue(string name)
{
// This function is needed to accomodate compiled function usage without second paramater,
// which doesn't work with optional values.
return SH.ResourceManager.GetString(name)!;
}
/// <inheritdoc/>
protected override object ProvideValue()
{
return GetValue(Name ?? string.Empty);
}
}

View File

@@ -0,0 +1,64 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
/// <summary>
/// 纵横比控件
/// </summary>
internal class AspectRatio : Microsoft.UI.Xaml.Controls.Control
{
private const double Epsilon = 2.2204460492503131e-016;
private static readonly DependencyProperty TargetWidthProperty = Property<AspectRatio>.Depend(nameof(TargetWidth), 1D);
private static readonly DependencyProperty TargetHeightProperty = Property<AspectRatio>.Depend(nameof(TargetHeight), 1D);
/// <summary>
/// 目标宽度
/// </summary>
public double TargetWidth
{
get => (double)GetValue(TargetWidthProperty);
set => SetValue(TargetWidthProperty, value);
}
/// <summary>
/// 目标高度
/// </summary>
public double TargetHeight
{
get => (double)GetValue(TargetHeightProperty);
set => SetValue(TargetHeightProperty, value);
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
double ratio = TargetWidth / TargetHeight;
double ratioAvailable = availableSize.Width / availableSize.Height;
if (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

@@ -21,12 +21,12 @@
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Tag="List"
Text="列表"/>
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
<RadioMenuFlyoutItem
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Tag="Grid"
Text="网格"/>
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.Control;
@@ -15,16 +16,34 @@ namespace Snap.Hutao.Control;
[SuppressMessage("", "CA1001")]
public class ScopedPage : Page
{
private readonly CancellationTokenSource viewLoadingCancellationTokenSource = new();
private readonly IServiceScope serviceScope;
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(null!);
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope currentScope;
/// <summary>
/// 构造一个新的页面
/// </summary>
public ScopedPage()
{
serviceScope = Ioc.Default.CreateScope();
serviceScope.Track();
Unloaded += OnScopedPageUnloaded;
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>
@@ -33,10 +52,10 @@ public class ScopedPage : Page
/// </summary>
/// <typeparam name="TViewModel">视图模型类型</typeparam>
public void InitializeWith<TViewModel>()
where TViewModel : class, ISupportCancellation
where TViewModel : class, IViewModel
{
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
@@ -58,23 +77,36 @@ public class ScopedPage : Page
/// <inheritdoc/>
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
viewLoadingCancellationTokenSource.Cancel();
using (viewCancellationTokenSource)
{
// Cancel all tasks executed by the view model
viewCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext;
// Try dispose scope when page is not presented
serviceScope.Dispose();
viewLoadingCancellationTokenSource.Dispose();
using (SemaphoreSlim locker = viewModel.DisposeLock)
{
// Wait to ensure viewmodel operation is completed
locker.Wait();
viewModel.IsViewDisposed = true;
// Dispose the scope
currentScope.Dispose();
}
}
}
/// <inheritdoc/>
[SuppressMessage("", "VSTHRD100")]
protected override async void OnNavigatedTo(NavigationEventArgs e)
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra)
{
await NotifyRecipentAsync(extra).ConfigureAwait(false);
NotifyRecipentAsync(extra).SafeForget();
}
}
private void OnScopedPageUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
DataContext = null;
Unloaded -= OnScopedPageUnloaded;
}
}

View File

@@ -9,7 +9,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Core;
using Snap.Hutao.Control.Theme;
using Windows.UI;
namespace Snap.Hutao.Control.Text;

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,7 +4,7 @@
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core;
namespace Snap.Hutao.Control.Theme;
/// <summary>
/// 主题帮助工具类
@@ -42,6 +42,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>
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="SystemBackdropTheme"/>
/// </summary>

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Control;
/// </summary>
/// <typeparam name="TFrom">源类型</typeparam>
/// <typeparam name="TTo">目标类型</typeparam>
public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
public abstract class ValueConverter<TFrom, TTo> : IValueConverter
{
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language)
@@ -23,7 +23,7 @@ public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
catch (Exception ex)
{
Ioc.Default
.GetRequiredService<ILogger<ValueConverterBase<TFrom, TTo>>>()
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
.LogError(ex, "值转换器异常");
}

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,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Core.Annotation;
/// <summary>
/// 本地化键
/// </summary>
internal class LocalizationKeyAttribute : Attribute
{
/// <summary>
/// 指定本地化键
/// </summary>
/// <param name="key">键</param>
public LocalizationKeyAttribute(string key)
{
Key = key;
}
/// <summary>
/// 键
/// </summary>
public string Key { get; }
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Net;
@@ -24,7 +25,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
private static readonly ImmutableDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
@@ -32,13 +33,13 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
[3] = TimeSpan.FromSeconds(4),
[4] = TimeSpan.FromSeconds(16),
[5] = TimeSpan.FromSeconds(64),
}.ToImmutableDictionary();
};
private readonly ILogger logger;
// violate di rule
private readonly HttpClient httpClient;
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private string? baseFolder;
private string? cacheFolder;
@@ -100,11 +101,30 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
/// <inheritdoc/>
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)
{
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);
}
}
}
finally
{
taskCompletionSource.TrySetResult();
}
}
return filePath;
@@ -191,7 +211,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
if (retryCount == 3)
{
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao, }.Uri;
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao }.Uri;
}
}
}

View File

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

View File

@@ -2,8 +2,8 @@
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.Convert;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Collections.Immutable;
@@ -31,19 +31,19 @@ internal static class CoreEnvironment
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.43.1";
public const string HoyolabXrpcVersion = "2.44.1";
/// <summary>
/// 盐
/// </summary>
// https://github.com/UIGF-org/Hoyolab.Salt
public static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
public static readonly ImmutableDictionary<SaltType, string> DynamicSecrets = new Dictionary<SaltType, string>()
{
[nameof(SaltType.K2)] = "ODzG1Jrn6zebX19VRmaJwjFI2CDvBUGq",
[nameof(SaltType.LK2)] = "V1PYbXKQY7ysdx3MNCcNbsE1LtY2QZpW",
[nameof(SaltType.X4)] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
[nameof(SaltType.X6)] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
[nameof(SaltType.PROD)] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
[SaltType.K2] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
[SaltType.LK2] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
}.ToImmutableDictionary();
/// <summary>
@@ -71,6 +71,11 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string FamilyName;
/// <summary>
/// 安装位置
/// </summary>
public static readonly string InstalledLocation;
/// <summary>
/// 数据文件夹
/// </summary>
@@ -99,9 +104,10 @@ internal static class CoreEnvironment
static CoreEnvironment()
{
DataFolder = GetDocumentsHutaoPath();
DataFolder = GetDatafolderPath();
Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName;
InstalledLocation = Package.Current.InstalledLocation.Path;
CommonUA = $"Snap Hutao/{Version}";
// simply assign a random guid
@@ -113,21 +119,30 @@ internal static class CoreEnvironment
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
return Md5Convert.ToHexString($"{userName}{machineGuid}");
return Convert.ToMd5HexString($"{userName}{machineGuid}");
}
private static string GetDocumentsHutaoPath()
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
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path);
return path;
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path);
return path;
}
else
{
return preferredPath;
}
}
}

View File

@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 数据库集合上下文
/// 数据库集合扩展
/// </summary>
public static class DbSetExtension
{
@@ -134,4 +134,4 @@ public static class DbSetExtension
dbSet.Update(entity);
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 可枚举扩展
/// </summary>
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

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

View File

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

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

@@ -3,6 +3,8 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using System.Collections;
using System.Text;
namespace Snap.Hutao.Core.ExceptionService;
@@ -33,16 +35,20 @@ internal class ExceptionRecorder
Ioc.Default.GetRequiredService<Web.Hutao.HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
#pragma warning restore VSTHRD002
#endif
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
StringBuilder dataDetailBuilder = new();
foreach (DictionaryEntry entry in e.Exception.Data)
{
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)
{
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
}
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Package;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 帮助更好的抛出异常
/// </summary>
[System.Diagnostics.StackTraceHidden]
internal static class ThrowHelper
{
/// <summary>
/// 操作取消
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <exception cref="OperationCanceledException">操作取消异常</exception>
/// <returns>nothing</returns>
[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="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>
/// <exception cref="UserdataCorruptedException">数据损坏</exception>
/// <returns>nothing</returns>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static UserdataCorruptedException UserdataCorrupted(string message, Exception inner)
{
throw new UserdataCorruptedException(message, inner);
}
}

View File

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

View File

@@ -1,42 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Linq.Expressions;
namespace Snap.Hutao.Core.ExpressionService;
/// <summary>
/// 枚举帮助类
/// </summary>
public static class EnumExtension
{
/// <summary>
/// 判断枚举是否有对应的Flag
/// </summary>
/// <typeparam name="T">枚举类型</typeparam>
/// <param name="enum">待检查的枚举</param>
/// <param name="value">值</param>
/// <returns>是否有对应的Flag</returns>
public static bool HasOption<T>(this T @enum, T value)
where T : struct, Enum
{
return ExpressionCache<T>.Entry(@enum, value);
}
private static class ExpressionCache<T>
{
public static readonly Func<T, T, bool> Entry = Get();
private static Func<T, T, bool> Get()
{
ParameterExpression paramSource = Expression.Parameter(typeof(T));
ParameterExpression paramValue = Expression.Parameter(typeof(T));
BinaryExpression logicalAnd = Expression.AndAssign(paramSource, paramValue);
BinaryExpression equal = Expression.Equal(logicalAnd, paramValue);
// 生成一个源类型入,目标类型出的 lamdba
return Expression.Lambda<Func<T, T, bool>>(equal, paramSource, paramValue).Compile();
}
}
}

View File

@@ -17,7 +17,12 @@ namespace Snap.Hutao.Core.IO.Bits;
[SuppressMessage("", "SA1600")]
internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{
private const uint BitsEngineNoProgressTimeout = 120;
/// <summary>
/// 任务名称前缀
/// </summary>
public const string JobNamePrefix = "SnapHutaoBitsJob";
private const uint BitsEngineNoProgressTimeout = 30;
private const int MaxResumeAttempts = 10;
private readonly string displayName;
@@ -25,7 +30,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
private readonly object lockObj = new();
private IBackgroundCopyJob? nativeJob;
private System.Exception? jobException;
private Exception? jobException;
private BG_JOB_PROGRESS progress;
private BG_JOB_STATE state;
private bool isJobComplete;
@@ -43,12 +48,14 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath)
{
ILogger<BitsJob> service = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
string text = $"BitsDownloadJob - {uri}";
string text = $"{JobNamePrefix} - {uri}";
IBackgroundCopyJob ppJob;
try
{
backgroundCopyManager.CreateJob(text, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out ppJob);
ppJob.SetNotifyFlags(11u);
// BG_NOTIFY_JOB_TRANSFERRED & BG_NOTIFY_JOB_ERROR & BG_NOTIFY_JOB_MODIFICATION
ppJob.SetNotifyFlags(0b1011);
ppJob.SetNoProgressTimeout(BitsEngineNoProgressTimeout);
ppJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND);
ppJob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, null, null);
@@ -72,7 +79,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
UpdateJobState();
CompleteOrCancel();
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation("Failed to job transfer: {message}", ex.Message);
}
@@ -94,7 +101,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
CompleteOrCancel();
log.LogInformation(jobException, "Job Exception:");
}
catch (System.Exception ex)
catch (Exception ex)
{
log?.LogInformation("Failed to handle job error: {message}", ex.Message);
}
@@ -134,7 +141,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
CompleteOrCancel();
}
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation(ex, "message");
}
@@ -276,7 +283,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{
action();
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation("{name} failed. {exception}", displayName, ex);
if (throwOnFailure)

View File

@@ -2,6 +2,9 @@
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
namespace Snap.Hutao.Core.IO.Bits;
@@ -36,10 +39,46 @@ internal class BitsManager
public async Task<ValueResult<bool, TempFile>> DownloadAsync(Uri uri, IProgress<ProgressUpdateStatus> progress, CancellationToken token = default)
{
TempFile tempFile = new(true);
bool result = await Task.Run(() => DownloadCore(uri, tempFile.Path, progress.Report, token), token).ConfigureAwait(false);
await ThreadHelper.SwitchToBackgroundAsync();
bool result = DownloadCore(uri, tempFile.Path, progress.Report, token);
return new(result, tempFile);
}
/// <summary>
/// 取消所有先前创建的任务
/// </summary>
public void CancelAllJobs()
{
IBackgroundCopyManager value;
try
{
value = lazyBackgroundCopyManager.Value;
}
catch (Exception ex)
{
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
return;
}
value.EnumJobs(0, out IEnumBackgroundCopyJobs pJobs);
pJobs.GetCount(out uint count);
List<IBackgroundCopyJob> jobsToCancel = new();
for (int i = 0; i < count; i++)
{
uint actualFetched = 0;
pJobs.Next(1, out IBackgroundCopyJob pJob, ref actualFetched);
pJob.GetDisplayName(out PWSTR name);
if (name.AsSpan().StartsWith(BitsJob.JobNamePrefix))
{
jobsToCancel.Add(pJob);
}
}
jobsToCancel.ForEach(job => job.Cancel());
}
private bool DownloadCore(Uri uri, string tempFile, Action<ProgressUpdateStatus> progress, CancellationToken token)
{
IBackgroundCopyManager value;
@@ -48,29 +87,37 @@ internal class BitsManager
{
value = lazyBackgroundCopyManager.Value;
}
catch (System.Exception ex)
catch (Exception ex)
{
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
return false;
}
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
try
{
try
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
{
bitsJob.WaitForCompletion(progress, token);
}
catch (System.Exception ex)
{
logger?.LogWarning(ex, "BITS download failed:");
return false;
}
try
{
bitsJob.WaitForCompletion(progress, token);
}
catch (Exception ex)
{
logger?.LogWarning(ex, "BITS download failed:");
return false;
}
if (bitsJob.ErrorCode != 0)
{
return false;
if (bitsJob.ErrorCode != 0)
{
return false;
}
}
}
catch (COMException)
{
// BITS job creation failed
return false;
}
return true;
}

View File

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

View File

@@ -20,7 +20,6 @@ internal static class Clipboard
public static async Task<T?> DeserializeTextAsync<T>(JsonSerializerOptions options)
where T : class
{
await ThreadHelper.SwitchToMainThreadAsync();
DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
string json = await view.GetTextAsync();
return JsonSerializer.Deserialize<T>(json, options);

View File

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

View File

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

View File

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

View File

@@ -32,13 +32,7 @@ internal static class PickerExtension
}
else
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning($"无法打开文件选择器 {exception.Message}");
}
InfoBarWaringPickerException(exception);
return new(false, null!);
}
}
@@ -64,14 +58,46 @@ internal static class PickerExtension
}
else
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning($"无法打开文件选择器 {exception.Message}");
}
InfoBarWaringPickerException(exception);
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

@@ -53,6 +53,12 @@ internal sealed class TempFile : IDisposable
/// </summary>
public void Dispose()
{
File.Delete(Path);
try
{
File.Delete(Path);
}
catch (IOException)
{
}
}
}

View File

@@ -11,8 +11,8 @@ internal class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerabl
/// <inheritdoc/>
public override IEnumerable<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? team = reader.GetString();
IEnumerable<int>? ids = team?.Split(',').Select(x => int.Parse(x));
string? source = reader.GetString();
IEnumerable<int>? ids = source?.Split(',').Select(int.Parse);
return ids ?? Enumerable.Empty<int>();
}

View File

@@ -16,7 +16,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
return false;
}
if (typeToConvert.GetGenericTypeDefinition() != typeof(IDictionary<,>))
if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
{
return false;
}
@@ -27,8 +27,9 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
/// <inheritdoc/>
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Type keyType = type.GetGenericArguments()[0];
Type valueType = type.GetGenericArguments()[1];
Type[] arguments = type.GetGenericArguments();
Type keyType = arguments[0];
Type valueType = arguments[1];
Type innerConverterType = typeof(StringEnumDictionaryConverterInner<,>).MakeGenericType(keyType, valueType);
JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType)!;

View File

@@ -10,6 +10,8 @@ namespace Snap.Hutao.Core.Json;
/// </summary>
internal class JsonTextEncoder : JavaScriptEncoder
{
private static readonly string BackSlashDoubleQuote = "\\\"";
/// <inheritdoc/>
public override int MaxOutputCharactersPerInputCharacter { get => 6; }
@@ -27,7 +29,7 @@ internal class JsonTextEncoder : JavaScriptEncoder
if (unicodeScalar == '"')
{
numberOfCharactersWritten = 2;
return "\\\"".AsSpan().TryCopyTo(new Span<char>(buffer, bufferLength));
return BackSlashDoubleQuote.AsSpan().TryCopyTo(new Span<char>(buffer, bufferLength));
}
string encoded = $"\\u{(uint)unicodeScalar:x4}";

View File

@@ -23,8 +23,8 @@ public static class JumpListHelper
list.Items.Clear();
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, "启动游戏");
launchGameItem.GroupName = "快捷操作";
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, SH.CoreJumpListHelperLaunchGameItemDisplayName);
launchGameItem.GroupName = SH.CoreJumpListHelperLaunchGameItemGroupName;
launchGameItem.Logo = new("ms-appx:///Resource/Icon/UI_GuideIcon_PlayMethod.png");
list.Items.Add(launchGameItem);

View File

@@ -10,7 +10,9 @@ using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Navigation;
#if RELEASE
using System.Security.Principal;
#endif
namespace Snap.Hutao.Core.LifeCycle;
@@ -37,11 +39,15 @@ internal static class Activation
/// <returns>是否提升了权限</returns>
public static bool GetElevated()
{
#if RELEASE
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
#else
return true;
#endif
}
/// <summary>
@@ -149,7 +155,7 @@ internal static class Activation
Ioc.Default
.GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()?
.ImplictAs<IMetadataServiceInitialization>()?
.InitializeInternalAsync()
.SafeForget();
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// Extension methods for the <see cref="ILoggerFactory"/> class.
/// </summary>
public static class DatabaseLoggerFactoryExtensions
{
/// <summary>
/// Adds a debug logger named 'Debug' to the factory.
/// </summary>
/// <param name="builder">The extension method argument.</param>
/// <returns>日志构造器</returns>
public static ILoggingBuilder AddDatabase(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DatebaseLoggerProvider>());
return builder;
}
}

View File

@@ -1,80 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// A logger that writes messages in the database table
/// </summary>
internal sealed partial class DatebaseLogger : ILogger
{
private readonly string name;
private readonly LogEntryQueue logEntryQueue;
/// <summary>
/// Initializes a new instance of the <see cref="DatebaseLogger"/> class.
/// </summary>
/// <param name="name">The name of the logger.</param>
/// <param name="logEntryQueue">日志队列</param>
public DatebaseLogger(string name, LogEntryQueue logEntryQueue)
{
this.name = name;
this.logEntryQueue = logEntryQueue;
}
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return new NullScope();
}
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, System.Exception? exception, Func<TState, System.Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
string message = formatter(state, exception);
if (string.IsNullOrEmpty(message))
{
return;
}
LogEntry entry = new()
{
Time = DateTimeOffset.Now,
Category = name,
LogLevel = logLevel,
EventId = eventId.Id,
Message = message,
Exception = exception?.ToString(),
};
logEntryQueue.Enqueue(entry);
}
/// <summary>
/// An empty scope without any logic
/// </summary>
private struct NullScope : IDisposable
{
public NullScope()
{
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// The provider for the <see cref="DatebaseLogger"/>.
/// </summary>
[ProviderAlias("Database")]
public sealed class DatebaseLoggerProvider : ILoggerProvider
{
private readonly LogEntryQueue logEntryQueue = new();
/// <inheritdoc/>
public ILogger CreateLogger(string name)
{
return new DatebaseLogger(name, logEntryQueue);
}
/// <inheritdoc/>
public void Dispose()
{
logEntryQueue.Dispose();
}
}

View File

@@ -1,51 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// 数据库日志入口点
/// </summary>
[Table("logs")]
public class LogEntry
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 日志时间
/// </summary>
public DateTimeOffset Time { get; set; }
/// <summary>
/// 类别
/// </summary>
public string Category { get; set; } = default!;
/// <summary>
/// 日志等级
/// </summary>
public LogLevel LogLevel { get; set; }
/// <summary>
/// 事件Id
/// </summary>
public int EventId { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; } = default!;
/// <summary>
/// 可能的异常
/// </summary>
public string? Exception { get; set; }
}

View File

@@ -1,110 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// 日志队列
/// </summary>
public sealed class LogEntryQueue : IDisposable
{
private readonly ConcurrentQueue<LogEntry> entryQueue = new();
private readonly CancellationTokenSource disposeTokenSource = new();
private readonly TaskCompletionSource writeDbCompletionSource = new();
private readonly LogDbContext logDbContext;
private bool disposed;
/// <summary>
/// 构造一个新的日志队列
/// </summary>
public LogEntryQueue()
{
logDbContext = InitializeDbContext();
Task.Run(() => WritePendingLogsAsync(disposeTokenSource.Token)).SafeForget();
}
/// <summary>
/// 将日志消息存入队列
/// </summary>
/// <param name="logEntry">日志</param>
public void Enqueue(LogEntry logEntry)
{
entryQueue.Enqueue(logEntry);
}
/// <inheritdoc/>
[SuppressMessage("", "VSTHRD002")]
public void Dispose()
{
if (disposed)
{
return;
}
// notify the write task to complete.
disposeTokenSource.Cancel();
// Wait the db operation complete.
writeDbCompletionSource.Task.GetAwaiter().GetResult();
logDbContext.Dispose();
disposed = true;
}
private static LogDbContext InitializeDbContext()
{
string logDbName = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Log.db");
LogDbContext logDbContext = LogDbContext.Create($"Data Source={logDbName}");
if (logDbContext.Database.GetPendingMigrations().Any())
{
Debug.WriteLine("[Debug] Performing LogDbContext Migrations");
logDbContext.Database.Migrate();
}
// only raw sql can pass
logDbContext.Logs.Where(log => log.Exception == null).ExecuteDelete();
return logDbContext;
}
private async Task WritePendingLogsAsync(CancellationToken token)
{
bool hasAdded = false;
while (true)
{
if (entryQueue.TryDequeue(out LogEntry? logEntry))
{
logDbContext.Logs.Add(logEntry);
hasAdded = true;
}
else
{
if (hasAdded)
{
logDbContext.SaveChanges();
hasAdded = false;
}
if (token.IsCancellationRequested)
{
writeDbCompletionSource.TrySetResult();
break;
}
try
{
await Task.Delay(5000, token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
}
}
}
}
}

View File

@@ -23,6 +23,7 @@ internal static class ScheduleTaskHelper
{
try
{
// TODO: 似乎可以不删除任务,直接注册已经包含了更新功能
SchedulerTask? targetTask = TaskService.Instance.GetTask(DailyNoteRefreshTaskName);
if (targetTask != null)
{
@@ -30,18 +31,15 @@ internal static class ScheduleTaskHelper
}
TaskDefinition task = TaskService.Instance.NewTask();
task.RegistrationInfo.Description = "胡桃实时便笺刷新任务 | 请勿编辑或删除。";
task.RegistrationInfo.Description = SH.CoreScheduleTaskHelperDailyNoteRefreshTaskDescription;
task.Triggers.Add(new TimeTrigger() { Repetition = new(TimeSpan.FromSeconds(interval), TimeSpan.Zero), });
task.Actions.Add("explorer", "hutao://DailyNote/Refresh");
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
return true;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (COMException)
catch (Exception ex)
{
_ = ex;
return false;
}
}

View File

@@ -77,6 +77,12 @@ internal static class LocalSetting
return Get<char>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static string Get(string key, string defaultValue)
{
return Get<string>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static DateTimeOffset Get(string key, DateTimeOffset defaultValue)
{
@@ -173,6 +179,12 @@ internal static class LocalSetting
Set<char>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, string value)
{
Set<string>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, DateTimeOffset value)
{
@@ -216,8 +228,7 @@ internal static class LocalSetting
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>获取的值</returns>
private static T Get<T>(string key, T defaultValue = default)
where T : struct
private static T Get<T>(string key, T defaultValue = default!)
{
if (Container.Values.TryGetValue(key, out object? value))
{
@@ -238,7 +249,6 @@ internal static class LocalSetting
/// <param name="key">键</param>
/// <param name="value">值</param>
private static void Set<T>(string key, T value)
where T : struct
{
Container.Values[key] = value;
}

View File

@@ -24,7 +24,15 @@ internal static class SettingKeys
public const string LaunchTimes = "LaunchTimes";
/// <summary>
/// 静态资源合约V1
/// 数据文件夹
/// </summary>
public const string DataFolderPath = "DataFolderPath";
/// <summary>
/// 静态资源合约
/// 新增合约时 请注意
/// <see cref="StaticResource.FulfillAllContracts"/>
/// 与 <see cref="StaticResource.IsAnyUnfulfilledContractPresent"/>
/// </summary>
public const string StaticResourceV1Contract = "StaticResourceV1Contract";
@@ -32,4 +40,14 @@ internal static class SettingKeys
/// 静态资源合约V2 成就图标与物品图标
/// </summary>
public const string StaticResourceV2Contract = "StaticResourceV2Contract";
/// <summary>
/// 静态资源合约V3 刷新 Skill Talent
/// </summary>
public const string StaticResourceV3Contract = "StaticResourceV3Contract";
/// <summary>
/// 静态资源合约V4 刷新 AvatarIcon
/// </summary>
public const string StaticResourceV4Contract = "StaticResourceV4Contract";
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Setting;
/// <summary>
/// 静态资源
/// </summary>
internal static class StaticResource
{
/// <summary>
/// 完成所有合约
/// </summary>
public static void FulfillAllContracts()
{
LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV4Contract, true);
}
/// <summary>
/// 提供的合约是否未完成
/// </summary>
/// <param name="contractKey">合约的键</param>
/// <returns>合约是否未完成</returns>
public static bool IsContractUnfulfilled(string contractKey)
{
return !LocalSetting.Get(contractKey, false);
}
/// <summary>
/// 是否有任何静态资源合约尚未完成
/// </summary>
/// <returns>静态资源合约尚未完成</returns>
public static bool IsAnyUnfulfilledContractPresent()
{
return !LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false)
|| (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false))
|| (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false))
|| (!LocalSetting.Get(SettingKeys.StaticResourceV4Contract, false));
}
}

View File

@@ -1,46 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed.
/// </summary>
/// <typeparam name="T">包装类型</typeparam>
public sealed class CancellationTokenTaskCompletionSource : IDisposable
{
/// <summary>
/// The cancellation token registration, if any. This is <c>null</c> if the registration was not necessary.
/// </summary>
private readonly IDisposable? registration;
/// <summary>
/// Creates a task for the specified cancellation token, registering with the token if necessary.
/// </summary>
/// <param name="cancellationToken">The cancellation token to observe.</param>
public CancellationTokenTaskCompletionSource(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
Task = Task.CompletedTask;
return;
}
TaskCompletionSource tcs = new();
registration = cancellationToken.Register(() => tcs.TrySetResult(), useSynchronizationContext: false);
Task = tcs.Task;
}
/// <summary>
/// Gets the task for the source cancellation token.
/// </summary>
public Task Task { get; private set; }
/// <summary>
/// Disposes the cancellation token registration, if any. Note that this may cause <see cref="Task"/> to never complete.
/// </summary>
public void Dispose()
{
registration?.Dispose();
}
}

View File

@@ -6,9 +6,30 @@ using System.Collections.Concurrent;
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 并发<see cref="CancellationTokenSource"/>
/// 无区分项的并发<see cref="CancellationTokenSource"/>
/// </summary>
[SuppressMessage("", "CA1001")]
internal class ConcurrentCancellationTokenSource
{
private CancellationTokenSource source = new();
/// <summary>
/// 注册取消令牌
/// </summary>
/// <returns>取消令牌</returns>
public CancellationToken Register()
{
source.Cancel();
source = new();
return source.Token;
}
}
/// <summary>
/// 有区分项的并发<see cref="CancellationTokenSource"/>
/// </summary>
/// <typeparam name="TItem">项类型</typeparam>
[SuppressMessage("", "SA1402")]
internal class ConcurrentCancellationTokenSource<TItem>
where TItem : notnull
{
@@ -17,7 +38,7 @@ internal class ConcurrentCancellationTokenSource<TItem>
/// <summary>
/// 为某个项注册取消令牌
/// </summary>
/// <param name="item">项</param>
/// <param name="item">区分项</param>
/// <returns>取消令牌</returns>
public CancellationToken Register(TItem item)
{
@@ -28,4 +49,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
}
}
}

View File

@@ -17,15 +17,13 @@ public static class DispatcherQueueExtension
/// <param name="action">执行的回调</param>
public static void Invoke(this DispatcherQueue dispatcherQueue, Action action)
{
using (ManualResetEventSlim blockEvent = new())
ManualResetEventSlim blockEvent = new();
dispatcherQueue.TryEnqueue(() =>
{
dispatcherQueue.TryEnqueue(() =>
{
action();
blockEvent.Set();
});
action();
blockEvent.Set();
});
blockEvent.Wait();
}
blockEvent.Wait();
}
}

View File

@@ -43,9 +43,6 @@ public readonly struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueue
/// <inheritdoc/>
public void OnCompleted(Action continuation)
{
dispatherQueue.TryEnqueue(() =>
{
continuation();
});
dispatherQueue.TryEnqueue(() => continuation());
}
}

View File

@@ -1,21 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 信号量扩展
/// </summary>
public static class SemaphoreSlimExtensions
public static class SemaphoreSlimExtension
{
/// <summary>
/// 异步进入信号量
/// </summary>
/// <param name="semaphoreSlim">信号量</param>
/// <param name="token">取消令牌</param>
/// <returns>可释放的对象,用于释放信号量</returns>
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim)
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim, CancellationToken token = default)
{
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
await semaphoreSlim.WaitAsync(token).ConfigureAwait(false);
}
catch (ObjectDisposedException ex)
{
ThrowHelper.OperationCanceled(SH.CoreThreadingSemaphoreSlimDisposed, ex);
}
return new SemaphoreSlimReleaser(semaphoreSlim);
}

View File

@@ -22,7 +22,7 @@ public static class TaskExtensions
{
await task.ConfigureAwait(false);
}
catch (System.Exception ex)
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
@@ -39,11 +39,11 @@ public static class TaskExtensions
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
// Do nothing
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
}
@@ -55,17 +55,17 @@ public static class TaskExtensions
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this Task task, ILogger? logger = null, Action<System.Exception>? onException = null)
public static async void SafeForget(this Task task, ILogger? logger = null, Action<Exception>? onException = null)
{
try
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
// Do nothing
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
onException?.Invoke(e);
@@ -79,17 +79,17 @@ public static class TaskExtensions
/// <param name="logger">日志器</param>
/// <param name="onCanceled">任务取消时调用</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this Task task, ILogger? logger = null, Action? onCanceled = null, Action<System.Exception>? onException = null)
public static async void SafeForget(this Task task, ILogger? logger = null, Action? onCanceled = null, Action<Exception>? onException = null)
{
try
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
onCanceled?.Invoke();
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
onException?.Invoke(e);

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Dispatching;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Threading;
@@ -10,6 +11,21 @@ namespace Snap.Hutao.Core.Threading;
/// </summary>
internal static class ThreadHelper
{
/// <summary>
/// 主线程队列
/// </summary>
private static volatile DispatcherQueue? dispatcherQueue;
/// <summary>
/// 初始化
/// </summary>
public static void Initialize()
{
dispatcherQueue = DispatcherQueue.GetForCurrentThread();
DispatcherQueueSynchronizationContext context = new(dispatcherQueue);
SynchronizationContext.SetSynchronizationContext(context);
}
/// <summary>
/// 使用此静态方法以 异步切换到 后台线程
/// </summary>
@@ -29,6 +45,23 @@ internal static class ThreadHelper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DispatherQueueSwitchOperation SwitchToMainThreadAsync()
{
return new(Program.DispatcherQueue!);
return new(dispatcherQueue!);
}
/// <summary>
/// 在主线程上同步等待执行操作
/// </summary>
/// <param name="action">操作</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InvokeOnMainThread(Action action)
{
if (dispatcherQueue!.HasThreadAccess)
{
action();
}
else
{
dispatcherQueue.Invoke(action);
}
}
}

View File

@@ -30,13 +30,13 @@ public readonly struct ThreadPoolSwitchOperation : IAwaitable<ThreadPoolSwitchOp
/// <inheritdoc/>
public void OnCompleted(Action continuation)
{
QueueContinuation(continuation, flowContext: true);
QueueContinuation(continuation, true);
}
/// <inheritdoc/>
public void UnsafeOnCompleted(Action continuation)
{
QueueContinuation(continuation, flowContext: false);
QueueContinuation(continuation, false);
}
private static void QueueContinuation(Action continuation, bool flowContext)

View File

@@ -44,7 +44,7 @@ public static class Must
/// <param name="context">上下文</param>
/// <returns>Nothing. This method always throws.</returns>
[DoesNotReturn]
public static System.Exception NeverHappen(string? context = null)
public static Exception NeverHappen(string? context = null)
{
throw new NotSupportedException(context);
}
@@ -62,21 +62,4 @@ public static class Must
{
return value ?? throw new ArgumentNullException(parameterName);
}
/// <summary>
/// Throws an <see cref="ArgumentNullException"/> if the specified parameter's value is IntPtr.Zero.
/// </summary>
/// <param name="value">The value of the argument.</param>
/// <param name="parameterName">The name of the parameter to include in any thrown exception.</param>
/// <returns>The value of the parameter.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is <see cref="IntPtr.Zero"/>.</exception>
public static Windows.Win32.Foundation.HWND NotNull(Windows.Win32.Foundation.HWND value, [CallerArgumentExpression("value")] string? parameterName = null)
{
if (value == default)
{
throw new ArgumentNullException(parameterName);
}
return value;
}
}

View File

@@ -16,7 +16,7 @@ internal abstract class WebView2Helper
{
private static bool hasEverDetected;
private static bool isSupported;
private static string version = "未检测到 WebView2 运行时";
private static string version = SH.CoreWebView2HelperVersionUndetected;
/// <summary>
/// 检测 WebView2 是否存在
@@ -36,7 +36,7 @@ internal abstract class WebView2Helper
catch (FileNotFoundException ex)
{
ILogger<WebView2Helper> logger = Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>();
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 运行时未安装");
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 Runtime not installed.");
isSupported = false;
}
}

View File

@@ -11,7 +11,7 @@ public enum BackdropType
/// <summary>
/// 无
/// </summary>
None = 0,
None,
/// <summary>
/// 亚克力

View File

@@ -124,8 +124,8 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
private void InitializeWindow()
{
appWindow.Title = "胡桃";
appWindow.SetIcon(Path.Combine(Package.Current.InstalledLocation.Path, "Assets/Logos/Logo.ico"));
appWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version);
appWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico"));
ExtendsContentIntoTitleBar();
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);
@@ -135,6 +135,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
logger.LogInformation(EventIds.WindowState, "Postion: [{pos}], Size: [{size}]", pos, size);
// appWindow.Show(true);
// appWindow.Show can't bring window to top.
window.Activate();
systemBackdrop = new(window);
@@ -147,6 +148,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
IMessenger messenger = Ioc.Default.GetRequiredService<IMessenger>();
messenger.Register<BackdropTypeChangedMessage>(this);
messenger.Register<FlyoutOpenCloseMessage>(this);
window.Closed += OnWindowClosed;
}
@@ -190,7 +192,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
}
else
{
double scale = Persistence.GetScaleForWindow(handle);
double scale = Persistence.GetScaleForWindowHandle(handle);
// 48 is the navigation button leftInset
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), titleBar.ActualSize).Scale(scale);

View File

@@ -8,6 +8,7 @@ using Snap.Hutao.Win32;
using System.Runtime.InteropServices;
using Windows.Graphics;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing;
@@ -22,28 +23,24 @@ internal static class Persistence
/// </summary>
/// <param name="appWindow">应用窗体</param>
/// <param name="persistSize">持久化尺寸</param>
/// <param name="size">初始尺寸</param>
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
/// <param name="initialSize">初始尺寸</param>
public static unsafe void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 initialSize)
{
// Set first launch size.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd);
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
if (persistSize)
{
RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (persistedSize.Width * persistedSize.Height > 848 * 524)
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (persistedRect.Size() >= initialSize.Size())
{
rect = persistedSize;
rect = persistedRect;
}
}
unsafe
{
TransformToCenterScreen(&rect);
}
TransformToCenterScreen(&rect);
appWindow.MoveAndResize(rect);
}
@@ -53,7 +50,15 @@ internal static class Persistence
/// <param name="appWindow">应用窗体</param>
public static void Save(AppWindow appWindow)
{
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
GetWindowPlacement(hwnd, ref windowPlacement);
// prevent save value when we are maximized.
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
{
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
}
}
/// <summary>
@@ -61,7 +66,7 @@ internal static class Persistence
/// </summary>
/// <param name="hwnd">窗体句柄</param>
/// <returns>缩放比</returns>
public static double GetScaleForWindow(HWND hwnd)
public static double GetScaleForWindowHandle(HWND hwnd)
{
uint dpi = GetDpiForWindow(hwnd);
return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero);
@@ -69,7 +74,7 @@ internal static class Persistence
private static SizeInt32 TransformSizeForWindow(SizeInt32 size, HWND hwnd)
{
double scale = GetScaleForWindow(hwnd);
double scale = GetScaleForWindowHandle(hwnd);
return new((int)(size.Width * scale), (int)(size.Height * scale));
}

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Theme;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
@@ -140,7 +141,7 @@ public class SystemBackdrop
/// <summary>
/// 确保系统调度队列控制器存在
/// </summary>
public void Ensure()
public unsafe void Ensure()
{
if (DispatcherQueue.GetForCurrentThread() != null)
{
@@ -152,7 +153,7 @@ public class SystemBackdrop
{
DispatcherQueueOptions options = new()
{
DwSize = Marshal.SizeOf<DispatcherQueueOptions>(),
DwSize = sizeof(DispatcherQueueOptions),
ThreadType = 2, // DQTYPE_THREAD_CURRENT
ApartmentType = 2, // DQTAT_COM_STA
};

View File

@@ -37,7 +37,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
{
this.window = window;
this.hwnd = Must.NotNull(hwnd);
this.hwnd = hwnd;
this.isLegacyDragBar = isLegacyDragBar;
}
@@ -45,7 +45,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
/// 尝试设置窗体子类
/// </summary>
/// <returns>是否设置成功</returns>
public bool TrySetWindowSubclass()
public unsafe bool TrySetWindowSubclass()
{
windowProc = new(OnSubclassProcedure);
bool windowHooked = SetWindowSubclass(hwnd, windowProc, WindowSubclassId, 0);
@@ -87,7 +87,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
{
case WM_GETMINMAXINFO:
{
double scalingFactor = Persistence.GetScaleForWindow(hwnd);
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
break;
}
@@ -115,4 +115,4 @@ internal class WindowSubclassManager<TWindow> : IDisposable
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="BinaryReader"/> 扩展
/// </summary>
public static class BinaryReaderExtension
{
/// <summary>
/// 判断是否处于流的结尾
/// </summary>
/// <param name="reader">读取器</param>
/// <returns>是否处于流的结尾</returns>
public static bool EndOfStream(this BinaryReader reader)
{
return reader.BaseStream.Position >= reader.BaseStream.Length;
}
}

View File

@@ -8,22 +8,6 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static class DateTimeOffsetExtension
{
/// <summary>
/// Converts the current <see cref="DateTimeOffset"/> to a <see cref="DateTimeOffset"/> that represents the local time.
/// </summary>
/// <param name="dateTimeOffset">时间偏移</param>
/// <param name="keepTicks">保留主时间部分</param>
/// <returns>A <see cref="DateTimeOffset"/> that represents the local time.</returns>
public static DateTimeOffset ToLocalTime(this DateTimeOffset dateTimeOffset, bool keepTicks)
{
if (keepTicks)
{
dateTimeOffset += TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now).Negate();
}
return dateTimeOffset.ToLocalTime();
}
/// <summary>
/// 从Unix时间戳转换
/// </summary>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Annotation;
using System.Reflection;
namespace Snap.Hutao.Extension;
@@ -39,4 +40,25 @@ public static class EnumExtension
DescriptionAttribute? attr = field?.GetCustomAttribute<DescriptionAttribute>();
return attr?.Description;
}
/// <summary>
/// 获取本地化的描述
/// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam>
/// <param name="enum">枚举值</param>
/// <returns>本地化的描述</returns>
public static string GetLocalizedDescription<TEnum>(this TEnum @enum)
where TEnum : struct, Enum
{
string enumName = Enum.GetName(@enum)!;
FieldInfo? field = @enum.GetType().GetField(enumName);
LocalizationKeyAttribute? attr = field?.GetCustomAttribute<LocalizationKeyAttribute>();
string? result = null;
if (attr != null)
{
result = SH.ResourceManager.GetString(attr.Key);
}
return result ?? enumName;
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
@@ -37,6 +38,7 @@ public static partial class EnumerableExtension
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>源列表或空列表</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static List<TSource> EmptyIfNull<TSource>(this List<TSource>? source)
{
return source ?? new();

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
/// <summary>
/// 日志器扩展
/// </summary>
[SuppressMessage("", "CA2254")]
public static class LoggerExtension
{
/// <inheritdoc cref="LoggerExtensions.LogInformation(ILogger, string?, object?[])"/>
public static T LogInformation<T>(this ILogger logger, string message, params object?[] param)
{
logger.LogInformation(message, param);
return default!;
}
/// <inheritdoc cref="LoggerExtensions.LogWarning(ILogger, string?, object?[])"/>
public static T LogWarning<T>(this ILogger logger, string message, params object?[] param)
{
logger.LogWarning(message, param);
return default!;
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Extension;
/// <summary>
@@ -8,36 +10,15 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static class NumberExtension
{
/// <summary>
/// 获取从右向左某位上的数字
/// </summary>
/// <param name="x">源</param>
/// <param name="place">位</param>
/// <returns>数字</returns>
public static int AtPlace(this int x, int place)
{
return (int)(x / Math.Pow(10, place - 1)) % 10;
}
/// <summary>
/// 计算给定整数的位数
/// </summary>
/// <param name="x">给定的整数</param>
/// <returns>位数</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Place(this int x)
{
// Benchmarked and compared as a most optimized solution
return (int)(MathF.Log10(x) + 1);
}
/// <summary>
/// 计算给定整数的位数
/// </summary>
/// <param name="x">给定的整数</param>
/// <returns>位数</returns>
public static int Place(this long x)
{
// Benchmarked and compared as a most optimized solution
return (int)(MathF.Log10(x) + 1);
}
}

View File

@@ -33,17 +33,4 @@ public static class StringBuilderExtensions
{
return condition ? sb.Append(value) : sb;
}
/// <summary>
/// 当条件符合时执行 <see cref="StringBuilder.Append(string?)"/>
/// </summary>
/// <param name="sb">字符串建造器</param>
/// <param name="condition">条件</param>
/// <param name="trueValue">条件符合时附加的字符串</param>
/// <param name="falseValue">条件不符合时附加的字符串</param>
/// <returns>同一个字符串建造器</returns>
public static StringBuilder AppendIfElse(this StringBuilder sb, bool condition, string? trueValue, string? falseValue)
{
return condition ? sb.Append(trueValue) : sb.Append(falseValue);
}
}

View File

@@ -1,76 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Input;
namespace Snap.Hutao.Factory.Abstraction;
/// <summary>
/// Factory for creating <see cref="AsyncRelayCommand"/> with additional processing.
/// </summary>
public interface IAsyncRelayCommandFactory
{
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<Task> execute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="execute">The execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute);
}

View File

@@ -1,102 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory;
/// <inheritdoc cref="IAsyncRelayCommandFactory"/>
[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
private readonly ILogger<AsyncRelayCommandFactory> logger;
/// <summary>
/// 构造一个新的异步命令工厂
/// </summary>
/// <param name="logger">日志器</param>
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger)
{
this.logger = logger;
}
/// <inheritdoc/>
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute)
{
return Register(new AsyncRelayCommand<T>(execute));
}
/// <inheritdoc/>
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute)
{
return Register(new AsyncRelayCommand<T>(cancelableExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute)
{
return Register(new AsyncRelayCommand<T>(execute, canExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute)
{
return Register(new AsyncRelayCommand<T>(cancelableExecute, canExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand Create(Func<Task> execute)
{
return Register(new AsyncRelayCommand(execute));
}
/// <inheritdoc/>
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute)
{
return Register(new AsyncRelayCommand(cancelableExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute)
{
return Register(new AsyncRelayCommand(execute, canExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute)
{
return Register(new AsyncRelayCommand(cancelableExecute, canExecute));
}
private AsyncRelayCommand Register(AsyncRelayCommand command)
{
ReportException(command);
return command;
}
private AsyncRelayCommand<T> Register<T>(AsyncRelayCommand<T> command)
{
ReportException(command);
return command;
}
private void ReportException(IAsyncRelayCommand command)
{
command.PropertyChanged += (sender, args) =>
{
if (sender is IAsyncRelayCommand asyncRelayCommand)
{
if (args.PropertyName == nameof(AsyncRelayCommand.ExecutionTask))
{
if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
{
Exception baseException = exception.GetBaseException();
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
}
}
}
};
}
}

View File

@@ -60,7 +60,7 @@ internal class ContentDialogFactory : IContentDialogFactory
Title = title,
Content = content,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = "确认",
PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText,
};
return dialog;
@@ -75,8 +75,8 @@ internal class ContentDialogFactory : IContentDialogFactory
Title = title,
Content = content,
DefaultButton = defaultButton,
PrimaryButtonText = "确认",
CloseButtonText = "取消",
PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText,
CloseButtonText = SH.ContentDialogCancelCloseButtonText,
};
return dialog;

View File

@@ -13,6 +13,7 @@ global using Snap.Hutao.Core.DependencyInjection;
global using Snap.Hutao.Core.DependencyInjection.Annotation;
global using Snap.Hutao.Core.Threading;
global using Snap.Hutao.Core.Validation;
global using Snap.Hutao.Resource.Localization;
// Runtime
global using System;

View File

@@ -33,11 +33,8 @@ public sealed partial class MainWindow : Window, IExtendedWindowSource, IRecipie
Ioc.Default.GetRequiredService<IMessenger>().Register(this);
// Query the StaticResourceV1Contract & StaticResourceV2Contract.
// If not complete we should present the welcome view.
ContentSwitchPresenter.Value =
!LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false)
|| (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false));
ContentSwitchPresenter.Value = StaticResource.IsAnyUnfulfilledContractPresent();
}
/// <summary>

View File

@@ -1,52 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
[Migration("20220720121521_Logs")]
partial class Logs
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,35 +0,0 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
public partial class Logs : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "logs",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
Category = table.Column<string>(type: "TEXT", nullable: false),
LogLevel = table.Column<int>(type: "INTEGER", nullable: false),
EventId = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", nullable: false),
Exception = table.Column<string>(type: "TEXT", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_logs", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "logs");
}
}
}

View File

@@ -1,55 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
[Migration("20220903071033_LogTime")]
partial class LogTime
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.8");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,27 +0,0 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
public partial class LogTime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Time",
table: "logs",
type: "TEXT",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Time",
table: "logs");
}
}
}

View File

@@ -1,53 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
partial class LogDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.8");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -51,7 +51,7 @@ public class Avatar : ICalculableSource<ICalculableAvatar>
/// <summary>
/// 武器
/// </summary>
public Weapon Weapon { get; set; } = default!;
public Weapon? Weapon { get; set; } = default!;
/// <summary>
/// 圣遗物列表

View File

@@ -4,7 +4,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.Cultivation;
namespace Snap.Hutao.Model.Binding.Cultivation;
@@ -25,7 +24,7 @@ public class CultivateItem : ObservableObject
Inner = inner;
Entity = entity;
isFinished = Entity.IsFinished;
IsToday = CultivateItemHelper.IsTodaysMaterial(inner.Id, DateTimeOffset.Now);
IsToday = inner.IsTodaysItem();
FinishStateCommand = new RelayCommand(FlipIsFinished);
}
@@ -55,7 +54,6 @@ public class CultivateItem : ObservableObject
if (SetProperty(ref isFinished, value))
{
Entity.IsFinished = value;
Ioc.Default.GetRequiredService<ICultivationService>().SaveCultivateItem(Entity);
}
}
}

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