Compare commits

..

125 Commits

Author SHA1 Message Date
qhy040404
974afa86ee impl #1334 2024-02-02 11:06:08 +08:00
Lightczx
7baf125f88 Impl IReorderable 2024-02-02 10:03:46 +08:00
Lightczx
a4e782da78 refine #1316 2024-02-01 16:17:49 +08:00
Lightczx
d5551e5cdf fix #1316 2024-02-01 14:30:50 +08:00
Lightczx
f016a4a27f refine #1347 2024-02-01 09:42:55 +08:00
DismissedLight
8b931b6d89 Merge pull request #1349 from DGP-Studio/fix/1347
fix #1347
2024-02-01 09:23:40 +08:00
Lightczx
b942ceb914 codestyle 2024-02-01 09:22:52 +08:00
qhy040404
f7a49e52e0 fix #1347 2024-02-01 00:10:23 +08:00
qhy040404
d4bd610fe2 temporary fix qr login
let's play zzz :(
2024-01-31 23:00:07 +08:00
Lightczx
a3dcfd3804 metadata special name handling 2024-01-31 17:25:43 +08:00
Lightczx
592525d149 typo fix for #1344 2024-01-31 14:20:57 +08:00
Lightczx
83c4598df5 fix #1333 again 2024-01-31 11:37:48 +08:00
DismissedLight
aa680388ad refine ui 2024-01-30 21:59:42 +08:00
Lightczx
ba4f59de30 update dynamic proxy injection 2024-01-30 17:31:23 +08:00
Lightczx
8780cf385e move class 2024-01-30 15:58:18 +08:00
Lightczx
431cdd1253 refine achievement page 2024-01-30 14:11:54 +08:00
Lightczx
9a8827fb40 fix typo 2024-01-30 11:54:09 +08:00
DismissedLight
d88a6ca301 Merge pull request #1327 from DGP-Studio/l10n_develop 2024-01-30 11:29:56 +08:00
DismissedLight
5d401794e5 Merge branch 'develop' into l10n_develop 2024-01-30 11:29:48 +08:00
Lightczx
26396443dc add 4.4 tower wave description 2024-01-30 11:18:37 +08:00
Lightczx
6b755d934d setting width trigger performance 2024-01-30 09:26:43 +08:00
DismissedLight
7d5b057269 Merge pull request #1337 from Scighost/develop 2024-01-30 09:21:17 +08:00
Lightczx
917c173eb2 code style 2024-01-30 09:18:11 +08:00
Scighost
28d702422e Responsive setting page 2024-01-29 20:21:30 +08:00
Lightczx
7612ab5da3 fix #1331 2024-01-29 16:37:49 +08:00
Lightczx
ab436ecb2f add package convert directory permission override 2024-01-29 16:26:50 +08:00
Lightczx
457e3ff4d5 fix #1333 2024-01-29 16:05:53 +08:00
Lightczx
224c4e52ea Activation using NamedPipe 2024-01-29 15:51:09 +08:00
DismissedLight
2a5c7b21fd Merge pull request #1335 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-5aeabc7cca 2024-01-29 15:50:29 +08:00
dependabot[bot]
53f8291a66 Bump the packages group in /src/Snap.Hutao with 2 updates
Bumps the packages group in /src/Snap.Hutao with 2 updates: [MSTest.TestAdapter](https://github.com/microsoft/testfx) and [MSTest.TestFramework](https://github.com/microsoft/testfx).


Updates `MSTest.TestAdapter` from 3.1.1 to 3.2.0
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.1.1...v.3.2.0)

Updates `MSTest.TestFramework` from 3.1.1 to 3.2.0
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.1.1...v.3.2.0)

---
updated-dependencies:
- dependency-name: MSTest.TestAdapter
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: packages
- dependency-name: MSTest.TestFramework
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: packages
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 07:25:34 +00:00
DismissedLight
b621d5406a ui update 2024-01-28 21:13:50 +08:00
Masterain
12f4847aea New translations sh.resx (Portuguese) 2024-01-28 02:29:48 -08:00
Masterain
39a3d31f38 New translations sh.resx (Indonesian) 2024-01-28 02:29:47 -08:00
Masterain
70ac0b13a5 New translations sh.resx (English) 2024-01-28 02:29:46 -08:00
Masterain
a3520a4991 New translations sh.resx (Chinese Traditional) 2024-01-28 02:29:45 -08:00
Masterain
aed0284e4b New translations sh.resx (Russian) 2024-01-28 02:29:44 -08:00
Masterain
96ef07cbe5 New translations sh.resx (Korean) 2024-01-28 02:29:43 -08:00
Masterain
d1f37f37ac New translations sh.resx (Japanese) 2024-01-28 02:29:42 -08:00
DismissedLight
1fe09f3069 impl #1279 2024-01-28 17:44:15 +08:00
DismissedLight
32d9355c3a refine http related code 2024-01-27 23:11:41 +08:00
Masterain
71f170d51e New translations sh.resx (Portuguese) 2024-01-27 01:57:10 -08:00
Masterain
60015b6354 New translations sh.resx (Indonesian) 2024-01-27 01:57:08 -08:00
Masterain
eecae3ea4f New translations sh.resx (English) 2024-01-27 01:57:07 -08:00
Masterain
1831166f1e New translations sh.resx (Chinese Traditional) 2024-01-27 01:57:06 -08:00
Masterain
a98915ea24 New translations sh.resx (Russian) 2024-01-27 01:57:05 -08:00
Masterain
0d46656f57 New translations sh.resx (Korean) 2024-01-27 01:57:04 -08:00
Masterain
c814a5c28f New translations sh.resx (Japanese) 2024-01-27 01:57:03 -08:00
DismissedLight
9c3d59cc6f fix loopback 2024-01-27 13:52:48 +08:00
DismissedLight
890cf3f3ea Merge pull request #1286 from DGP-Studio/feat/feedback_network 2024-01-27 13:02:28 +08:00
DismissedLight
196bbb54c3 code style 2024-01-27 13:01:56 +08:00
DismissedLight
0481b9e474 pt resx as analyzer files 2024-01-27 09:56:35 +08:00
qhy040404
c4f3eb68e8 code style 2024-01-27 09:19:18 +08:00
qhy040404
c2e9f3a926 verify set 2024-01-27 00:47:09 +08:00
qhy040404
fb1fe3e40f add loopback status 2024-01-27 00:38:41 +08:00
qhy040404
75ed512e4a add current proxy to feedback page 2024-01-26 22:08:38 +08:00
Masterain
dd4dd33d93 New translations sh.resx (Japanese) 2024-01-26 01:47:29 -08:00
Lightczx
1e216e9823 refine com import 2024-01-26 17:18:57 +08:00
Lightczx
f823cb5f1a ui rework 2024-01-26 15:38:00 +08:00
Lightczx
2c6d25f0a3 limit update thread count 2024-01-25 17:22:32 +08:00
Masterain
817f768263 New translations sh.resx (Portuguese) 2024-01-25 00:36:14 -08:00
Masterain
2998fbb167 New translations sh.resx (English) 2024-01-25 00:36:12 -08:00
Masterain
8f0f94054d New translations sh.resx (Russian) 2024-01-25 00:36:10 -08:00
Lightczx
17a5d4d3a2 impl #1320 2024-01-25 15:59:52 +08:00
Lightczx
a1c604e68a add required win32 apis 2024-01-25 15:50:46 +08:00
Lightczx
948ec9a822 code style 2024-01-25 12:55:26 +08:00
Lightczx
f83174d690 com interface impl IUnknown 2024-01-25 11:24:17 +08:00
Lightczx
d686debbfb manually pinvoke from win32metadata 2024-01-25 11:01:45 +08:00
Lightczx
45248d75e1 pt-BR -> pt 2024-01-19 16:52:04 +08:00
Lightczx
22646cfab2 windows app sdk self contained 2024-01-19 16:25:57 +08:00
Lightczx
d0237a3c89 Add pt-BR locale 2024-01-19 15:41:11 +08:00
Tony
73c80fad10 Finish PT-BR translation (#1314) 2024-01-19 08:17:53 +08:00
Masterain
320bed9fcb New Crowdin updates (#1312)
* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Portuguese)
2024-01-17 15:10:26 -08:00
qhy040404
bb12aca3b4 Add PT translation (unfinished) (#1311)
Co-authored-by: t0piy <t.tony.br01@gmail.com>
Co-authored-by: Tony <66571593+t0piy@users.noreply.github.com>
2024-01-17 15:06:53 -08:00
DismissedLight
c7b5d98fb1 Merge pull request #1295 from DGP-Studio/fix/daily_note_task 2024-01-15 14:24:31 +08:00
qhy040404
7cc96f94f2 code style 2024-01-12 09:30:11 +08:00
qhy040404
b35355f9a3 improve daily note information 2024-01-11 20:39:02 +08:00
Masterain
745815657d New Crowdin updates (#1293) 2024-01-11 19:19:43 +08:00
DismissedLight
d93a9f41f3 bump version 2024-01-11 19:07:12 +08:00
qhy040404
910f099c6d reset debug console state (#1290)
Co-authored-by: DismissedLight <1686188646@qq.com>
2024-01-11 19:04:23 +08:00
Lightczx
e68449ec0c Update NativeMethods.txt 2024-01-10 17:06:23 +08:00
Lightczx
e484fbed21 update dependency 2024-01-10 10:42:35 +08:00
Masterain
88af6d28a9 New Crowdin updates (#1282)
* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)
2024-01-09 00:23:28 -08:00
Lightczx
3ab34f0992 Update HtmlDescriptionTextBlock.cs 2024-01-09 10:53:07 +08:00
Lightczx
5e875a7f18 add strings to resources 2 2024-01-09 10:38:33 +08:00
Lightczx
89d98748e8 add strings to resources 2024-01-09 10:10:31 +08:00
Masterain
d33cd894b9 New Crowdin updates (#1256)
* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)
2024-01-08 14:26:30 -08:00
DismissedLight
f0c19b419e Merge pull request #1276 from DGP-Studio/feat/dynamic_proxy 2024-01-08 22:20:30 +08:00
DismissedLight
f1d9787e45 fix method call 2024-01-08 22:18:07 +08:00
DismissedLight
5f9b4a7cb2 refactor RegistryWatcher 2024-01-08 21:55:59 +08:00
qhy040404
8710150897 use reflect to reduce code size 2024-01-08 20:19:05 +08:00
qhy040404
92c1b12764 dynamic proxy 2024-01-08 18:24:02 +08:00
Lightczx
d73bd557f3 remove settings appearance backdrop transparent 2024-01-08 16:52:37 +08:00
Lightczx
777d7d1056 remove winrt marshaller 2024-01-08 16:30:53 +08:00
Lightczx
1a944dae9c add transparent backdrop 2024-01-08 15:16:41 +08:00
Lightczx
a26c52ba97 typo 2024-01-08 11:43:45 +08:00
Lightczx
5fab03d57e Update FeedbackPage.xaml 2024-01-08 11:41:23 +08:00
Lightczx
e8a459cb41 refine #1039 2024-01-08 11:36:16 +08:00
DismissedLight
04df5a7bf1 impl #1039 2024-01-07 23:23:59 +08:00
DismissedLight
1ebcc2fc89 add documentation client 2024-01-07 14:49:02 +08:00
DismissedLight
e9917e788d Merge pull request #1273 from DGP-Studio/feat/identify_monitor 2024-01-06 23:39:41 +08:00
DismissedLight
9665876d52 code style 2 2024-01-06 23:38:26 +08:00
DismissedLight
8921816873 code style 2024-01-06 22:57:25 +08:00
DismissedLight
2698761594 fix convert game path 2024-01-06 20:03:14 +08:00
qhy040404
3ae4210ca0 add i18n 2024-01-06 18:32:39 +08:00
qhy040404
2f5e0cbe39 impl #1261 2024-01-06 18:25:10 +08:00
DismissedLight
d3444a9435 typo 2024-01-06 15:22:40 +08:00
DismissedLight
8b6f95c3d9 add package convert check 2024-01-06 15:21:51 +08:00
DismissedLight
88b8335e5b Merge pull request #1271 from DGP-Studio/feat/refresh_data_size 2024-01-05 23:52:55 +08:00
qhy040404
061aba715b refresh data folder size after deleting server cache 2024-01-05 23:50:28 +08:00
DismissedLight
da80631b72 code style 2024-01-05 23:28:35 +08:00
DismissedLight
97acf872bc remove status when game exited 2024-01-05 23:28:05 +08:00
DismissedLight
addaf1a9e3 Merge pull request #1270 from DGP-Studio/feat/launch-pipeline 2024-01-05 22:46:00 +08:00
DismissedLight
76183901da clean up 2024-01-05 22:33:10 +08:00
Lightczx
87ee81e7fa add handlers 2024-01-05 17:29:30 +08:00
DismissedLight
f2f858de15 create infrastructure 2024-01-04 22:51:58 +08:00
DismissedLight
c434521004 Merge pull request #1265 from DGP-Studio/fix/schedule 2024-01-04 16:03:54 +08:00
Lightczx
27ed2cefc1 fix #1242 2024-01-04 16:01:52 +08:00
qhy040404
6dc1e664b0 add task register check and delete script if register is failed 2024-01-04 13:32:43 +08:00
DismissedLight
51c3dde24b Merge pull request #1263 from DSakura207/main 2024-01-04 09:18:23 +08:00
DSakura207
2d497faaa5 Update Contributing.md 2024-01-03 18:35:47 -06:00
DSakura207
4783934b92 Add .vsconfig for installing workloads and extensions 2024-01-03 18:17:09 -06:00
DismissedLight
03d235876a Merge pull request #1260 from DGP-Studio/develop 2024-01-03 22:18:36 +08:00
DismissedLight
f49e9669af update version 2024-01-03 22:18:08 +08:00
DismissedLight
533c70caaa allow null package convert state 2024-01-03 21:53:13 +08:00
DismissedLight
dd59302bb3 fix bilibili server crash 2024-01-03 20:40:37 +08:00
352 changed files with 17245 additions and 3515 deletions

View File

@@ -51,7 +51,7 @@ body:
description: |
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
如果你的程序已经无法启动,请下载并运行[工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将显示你的设备 ID
如果你的程序已经无法启动,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将显示你的设备 ID
validations:
required: false
@@ -87,7 +87,7 @@ body:
label: 发生了什么?
description: |
详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
如果你无法找到该日志,请下载并运行[工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
如果你无法找到该日志,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
validations:
required: true

View File

@@ -51,7 +51,7 @@ body:
description: |
In Snap Hutao's settings page, you can find and copy your device ID
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
If your program cannot startup, please download and run [this tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will shows your device ID.
If your program cannot startup, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will shows your device ID.
validations:
required: false
@@ -87,7 +87,7 @@ body:
label: What Happened?
description: |
Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
If you cannot find it, please download and run [this tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
If you cannot find it, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
validations:
required: true

View File

@@ -61,6 +61,9 @@ release:
- name: "$THIS_SHA256SUMS_NAME"
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_SHA256SUMS_NAME?inline=false"
link_type: other
- name: "artifact_archive"
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/download?file_type=archive"
link_type: other
Refresh:
stage: refresh

View File

@@ -4,13 +4,15 @@
### Setup Snap.Hutao Project
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)
2. Open Visual Studio Installer to complete Visual Studio installation
- You need to install `.NET desktop development`, `Desktop development with C++` and `Universal Windows Platform development` components
3. Install `Single-project MSIX Packaging Tools for VS 2022` provided by Microsoft in Visual Studio marketplace
4. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device
5. Switch git branch to `develop`
6. Open project solution with your Visual Studio and then you are ready to go
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/).
- No need to select workloads; Visual Studio will handle it automatically.
- Close Visual Studio Installer to ensure a smooth installation experience for workloads.
- If using Visual Studio 2022 17.9 preview, skip step 5, as automatic extension installation is supported in this version.
2. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device.
3. Switch to the`develop` branch using git.
4. Open the project solution with your Visual Studio. Visual Studio will prompt you to install the necessary workloads, closing and reopening automatically.
5. (For Visual Studio 2022 17.8) Install the [Single-project MSIX Packaging Tools for VS 2022](https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17) provided by Microsoft in Visual Studio marketplace.
6. Open the project solution with your Visual Studio, and you are ready to go.
### Start Pull Request

View File

@@ -44,9 +44,7 @@ Install with Snap Hutao MSIX package, can be installed with Windows built-in App
### 特定的原神项目 / Specific Genshin-related Projects
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
* [Scighost/Starward](https://github.com/Scighost/Starward)
### 使用的技术栈 / Tech Stack
@@ -57,7 +55,6 @@ Install with Snap Hutao MSIX package, can be installed with Windows built-in App
* [dotnet/efcore](https://github.com/dotnet/efcore)
* [dotnet/runtime](https://github.com/dotnet/runtime)
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
* [microsoft/CsWin32](https://github.com/microsoft/CsWin32)
* [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)

View File

@@ -124,9 +124,6 @@ dotnet_diagnostic.SA1623.severity = none
# SA1636: File header copyright text should match
dotnet_diagnostic.SA1636.severity = none
# SA1414: Tuple types in signatures should have element names
dotnet_diagnostic.SA1414.severity = none
# SA0001: XML comment analysis disabled
dotnet_diagnostic.SA0001.severity = none
csharp_style_prefer_parameter_null_checking = true:suggestion
@@ -325,7 +322,6 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = true:suggestion
dotnet_diagnostic.SA1010.severity = none
[*.vb]
#### 命名样式 ####

11
src/Snap.Hutao/.vsconfig Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.ManagedDesktop",
"Microsoft.VisualStudio.Workload.NativeDesktop",
"Microsoft.VisualStudio.Workload.Universal"
],
"extensions": [
"https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17"
]
}

View File

@@ -160,9 +160,39 @@ public sealed class GeniusInvokationDecoding
ushort[] testKnownResult =
[
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
060,
019,
001,
079,
120,
120,
129,
151,
151,
153,
153,
181,
184,
184,
185,
185,
194,
194,
200,
200,
201,
201,
217,
217,
219,
241,
241,
244,
244,
245,
245,
270,
270,
];
CollectionAssert.AreEqual(resultArray, testKnownResult);

View File

@@ -14,7 +14,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,11 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
"allowMarshaling": true,
"useSafeHandles": false,
"comInterop": {
"preserveSigMethods": [
"IFileOpenDialog.Show",
"IFileSaveDialog.Show"
]
}
}

View File

@@ -1,94 +0,0 @@
// COMCTL32
DefSubclassProc
RemoveWindowSubclass
SetWindowSubclass
// DWMAPI
DwmSetWindowAttribute
// GDI32
GetDeviceCaps
// KERNEL32
AllocConsole
CloseHandle
CreateEventW
CreateRemoteThread
FreeConsole
GetConsoleMode
GetModuleHandleW
GetProcAddress
GetStdHandle
K32EnumProcessModules
K32GetModuleBaseNameW
K32GetModuleInformation
ReadProcessMemory
SetConsoleMode
SetConsoleTitle
SetEvent
VirtualAlloc
VirtualAllocEx
VirtualFree
VirtualFreeEx
WaitForSingleObject
WriteProcessMemory
// OLE32
CoCreateInstance
CoWaitForMultipleObjects
// SHELL32
SHCreateItemFromParsingName
// USER32
AttachThreadInput
FindWindowExW
GetCursorPos
GetDC
GetDpiForWindow
GetForegroundWindow
GetWindowPlacement
GetWindowThreadProcessId
ReleaseDC
RegisterHotKey
SendInput
SetForegroundWindow
UnregisterHotKey
// COM
FileOpenDialog
FileSaveDialog
IFileOpenDialog
IFileSaveDialog
IPersistFile
IShellLinkDataList
IShellLinkW
ShellLink
SHELL_LINK_DATA_FLAGS
// WinRT
IMemoryBufferByteAccess
// Const value
E_FAIL
INFINITE
RPC_E_WRONG_THREAD
MAX_PATH
WM_GETMINMAXINFO
WM_HOTKEY
WM_NCRBUTTONDOWN
WM_NCRBUTTONUP
WM_NULL
// Type & Enum definition
HRESULT_FROM_WIN32
SLGP_FLAGS
// System.Threading
LPTHREAD_START_ROUTINE
// UI.WindowsAndMessaging
MINMAXINFO
// System.Com
CWMO_FLAGS

View File

@@ -1,17 +0,0 @@
using System;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
namespace Windows.Win32;
internal static partial class PInvoke
{
/// <inheritdoc cref="CoCreateInstance(Guid*, object, CLSCTX, Guid*, out object)"/>
internal static unsafe HRESULT CoCreateInstance<TClass, TInterface>(object? pUnkOuter, CLSCTX dwClsContext, out TInterface ppv)
where TInterface : class
{
HRESULT hr = CoCreateInstance(typeof(TClass).GUID, pUnkOuter, dwClsContext, typeof(TInterface).GUID, out object o);
ppv = (TInterface)o;
return hr;
}
}

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="NativeMethods.json" />
<None Remove="NativeMethods.txt" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="NativeMethods.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using Windows.Win32.UI.WindowsAndMessaging;
[assembly: InternalsVisibleTo("Snap.Hutao")]
namespace Snap.Hutao.Win32;
/// <summary>
/// 结构体封送
/// </summary>
internal static class StructMarshal
{
/// <summary>
/// 构造一个新的 <see cref="Windows.Win32.UI.WindowsAndMessaging.WINDOWPLACEMENT"/>
/// </summary>
/// <returns>新的实例</returns>
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
{
return new() { length = unchecked((uint)sizeof(WINDOWPLACEMENT)) };
}
}

View File

@@ -1,72 +0,0 @@
using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Windows.Win32.CsWin32.InteropServices;
internal class WinRTCustomMarshaler : ICustomMarshaler
{
private static readonly string? AssemblyFullName = typeof(Windows.Foundation.IMemoryBuffer).Assembly.FullName;
private readonly string className;
private bool lookedForFromAbi;
private MethodInfo? fromAbiMethod;
private WinRTCustomMarshaler(string className)
{
this.className = className;
}
public static ICustomMarshaler GetInstance(string cookie)
{
return new WinRTCustomMarshaler(cookie);
}
public void CleanUpManagedData(object ManagedObj)
{
}
public void CleanUpNativeData(nint pNativeData)
{
Marshal.Release(pNativeData);
}
public int GetNativeDataSize()
{
throw new NotSupportedException();
}
public nint MarshalManagedToNative(object ManagedObj)
{
throw new NotSupportedException();
}
public object MarshalNativeToManaged(nint thisPtr)
{
return className switch
{
"Windows.System.DispatcherQueueController" => Windows.System.DispatcherQueueController.FromAbi(thisPtr),
_ => MarshalNativeToManagedSlow(thisPtr),
};
}
private object MarshalNativeToManagedSlow(nint pNativeData)
{
if (!lookedForFromAbi)
{
Type? type = Type.GetType($"{className}, {AssemblyFullName}");
fromAbiMethod = type?.GetMethod("FromAbi");
lookedForFromAbi = true;
}
if (fromAbiMethod is not null)
{
return fromAbiMethod.Invoke(default, new object[] { pNativeData })!;
}
else
{
return Marshal.GetObjectForIUnknown(pNativeData);
}
}
}

View File

@@ -8,12 +8,11 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.vsconfig = .vsconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -66,22 +65,6 @@ Global
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.ActiveCfg = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.Build.0 = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.ActiveCfg = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.Build.0 = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.ActiveCfg = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.Build.0 = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.Build.0 = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.ActiveCfg = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.Build.0 = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.ActiveCfg = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.Build.0 = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.ActiveCfg = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -32,6 +32,7 @@
x:Key="LargeGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}"
TargetType="GridViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
<Style

View File

@@ -6,6 +6,7 @@ using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Shell;
using System.Diagnostics;
@@ -36,8 +37,6 @@ public sealed partial class App : Application
----------------------------------------------------------------
""";
private const string AppInstanceKey = "main";
private readonly IServiceProvider serviceProvider;
private readonly IActivation activation;
private readonly ILogger<App> logger;
@@ -50,7 +49,6 @@ public sealed partial class App : Application
{
// Load app resource
InitializeComponent();
activation = serviceProvider.GetRequiredService<IActivation>();
logger = serviceProvider.GetRequiredService<ILogger<App>>();
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
@@ -64,25 +62,21 @@ public sealed partial class App : Application
try
{
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
if (firstInstance.IsCurrent)
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
{
logger.LogInformation(ConsoleBanner);
LogDiagnosticInformation();
// manually invoke
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
activation.InitializeWith(firstInstance);
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
}
else
{
// Redirect the activation (and args) to the "main" instance, and exit.
firstInstance.RedirectActivationTo(activatedEventArgs);
Process.GetCurrentProcess().Kill();
Exit();
return;
}
logger.LogInformation(ConsoleBanner);
LogDiagnosticInformation();
// manually invoke
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.Initialize();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
}
catch
{

View File

@@ -0,0 +1,39 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("PaneCornerRadius", typeof(CornerRadius), default, nameof(OnPaneCornerRadiusChanged), IsAttached = true, AttachedType = typeof(NavigationView))]
public sealed partial class NavigationViewHelper
{
private static void OnPaneCornerRadiusChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
{
NavigationView navigationView = (NavigationView)dp;
CornerRadius newValue = (CornerRadius)args.NewValue;
if (navigationView.IsLoaded)
{
SetNavigationViewPaneCornerRadius(navigationView, newValue);
return;
}
navigationView.Loaded += (s, e) =>
{
NavigationView loadedNavigationView = (NavigationView)s;
SetNavigationViewPaneCornerRadius(loadedNavigationView, newValue);
};
}
private static void SetNavigationViewPaneCornerRadius(NavigationView navigationView, CornerRadius value)
{
if (navigationView.FindDescendant("RootSplitView") is SplitView splitView)
{
splitView.CornerRadius = value;
}
}
}

View File

@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))]
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
public sealed partial class ScrollViewerHelper
{

View File

@@ -20,4 +20,4 @@ public sealed partial class SettingsExpanderHelper
}
}
}
}
}

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Control;
[TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")]
[TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")]
[TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))]
internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
{
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));

View File

@@ -3,7 +3,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shc="using:Snap.Hutao.Control">
<Style TargetType="shc:Loading">
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shc:Loading"/>
<Style x:Key="DefaultLoadingStyle" TargetType="shc:Loading">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>

View File

@@ -18,12 +18,15 @@ internal sealed class FontIconExtension : MarkupExtension
/// </summary>
public string Glyph { get; set; } = default!;
public double FontSize { get; set; } = 12;
/// <inheritdoc/>
protected override object ProvideValue()
{
return new FontIcon()
{
Glyph = Glyph,
FontSize = FontSize,
};
}
}

View File

@@ -49,7 +49,8 @@ internal struct Rgba32
/// <param name="code">RGBA 代码</param>
public unsafe Rgba32(uint code)
{
// RRGGBBAA -> AABBGGRR
// uint layout: 0xRRGGBBAA -> AABBGGRR
// AABBGGRR -> RRGGBBAA
fixed (Rgba32* pSelf = &this)
{
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(code);

View File

@@ -1,10 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.System.WinRT;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Win32;
using Windows.Win32.System.WinRT;
using WinRT;
namespace Snap.Hutao.Control.Media;

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Controls;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))]
internal sealed partial class UniformPanel : UniformGrid
{
public UniformPanel()
{
Columns = 1;
SizeChanged += OnSizeChanged;
}
private void OnSizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
{
Columns = (int)((e.NewSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
}
}

View File

@@ -8,6 +8,7 @@ using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Control.Theme;
using Snap.Hutao.Metadata;
using Windows.Foundation;
using Windows.UI;
@@ -23,8 +24,11 @@ namespace Snap.Hutao.Control.Text;
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
internal sealed partial class DescriptionTextBlock : ContentControl
{
private static readonly int ColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
private static readonly int ColorTagLeftLength = "<color=#FFFFFFFF>".Length;
private static readonly int RgbaColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
private static readonly int RgbaColorTagLeftLength = "<color=#FFFFFFFF>".Length;
private static readonly int RgbColorTagFullLength = "<color=#FFFFFF></color>".Length;
private static readonly int RgbColorTagLeftLength = "<color=#FFFFFF>".Length;
private static readonly int ItalicTagFullLength = "<i></i>".Length;
private static readonly int ItalicTagLeftLength = "<i>".Length;
@@ -51,9 +55,16 @@ internal sealed partial class DescriptionTextBlock : ContentControl
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
ReadOnlySpan<char> description = (string)e.NewValue;
ReadOnlySpan<char> description = SpecialNameHandler.Handle((string)e.NewValue);
UpdateDescription(textBlock, description);
try
{
UpdateDescription(textBlock, description);
}
catch (Exception ex)
{
_ = ex;
}
}
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -81,13 +92,32 @@ internal sealed partial class DescriptionTextBlock : ContentControl
// color tag
else if (description[i..].StartsWith("<c"))
{
AppendText(textBlock, description[last..i]);
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
AppendColorText(textBlock, description.Slice(i + ColorTagLeftLength, length), color);
switch (description[i..].IndexOf('>'))
{
case 16: // RgbaColorTag
{
AppendText(textBlock, description[last..i]);
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
int length = description[(i + RgbaColorTagLeftLength)..].IndexOf('<');
AppendColorText(textBlock, description.Slice(i + RgbaColorTagLeftLength, length), color);
i += length + ColorTagFullLength;
last = i;
i += length + RgbaColorTagFullLength;
last = i;
break;
}
case 14: // RgbColorTag
{
AppendText(textBlock, description[last..i]);
Rgba32 color = new(description.Slice(i + 8, 6).ToString());
int length = description[(i + RgbColorTagLeftLength)..].IndexOf('<');
AppendColorText(textBlock, description.Slice(i + RgbColorTagLeftLength, length), color);
i += length + RgbColorTagFullLength;
last = i;
break;
}
}
}
// italic

View File

@@ -156,7 +156,7 @@ internal sealed partial class HtmlDescriptionTextBlock : ContentControl
text.Inlines.Add(new Run
{
Text = slice.ToString(),
FontWeight = FontWeights.Bold,
FontWeight = FontWeights.SemiBold,
});
}

View File

@@ -24,10 +24,43 @@
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
</Style>
<Style
x:Key="AcrylicBaseHighBorderCardStyle"
BasedOn="{StaticResource BorderCardStyle}"
TargetType="Border">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{ThemeResource SystemControlBaseHighAcrylicElementBrush}"/>
</Style>
<Style
x:Key="AcrylicBorderCardStyle"
BasedOn="{StaticResource BorderCardStyle}"
TargetType="Border">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"/>
</Style>
<Style x:Key="GridCardStyle" TargetType="Grid">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
</Style>
<Style
x:Key="AcrylicGridCardStyle"
BasedOn="{StaticResource GridCardStyle}"
TargetType="Grid">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"/>
</Style>
<Style
x:Key="AcrylicBaseHighGridCardStyle"
BasedOn="{StaticResource GridCardStyle}"
TargetType="Grid">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{ThemeResource SystemControlBaseHighAcrylicElementBrush}"/>
</Style>
</ResourceDictionary>

View File

@@ -2,6 +2,53 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shch="using:Snap.Hutao.Control.Helper">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<AcrylicBrush
x:Key="InfoBarErrorSeverityBackgroundBrush"
FallbackColor="#FDE7E9"
TintColor="#FDE7E9"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarWarningSeverityBackgroundBrush"
FallbackColor="#FFF4CE"
TintColor="#FFF4CE"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarSuccessSeverityBackgroundBrush"
FallbackColor="#DFF6DD"
TintColor="#DFF6DD"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarInformationalSeverityBackgroundBrush"
FallbackColor="#80F6F6F6"
TintColor="#80F6F6F6"
TintOpacity="0.6"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<AcrylicBrush
x:Key="InfoBarErrorSeverityBackgroundBrush"
FallbackColor="#442726"
TintColor="#442726"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarWarningSeverityBackgroundBrush"
FallbackColor="#433519"
TintColor="#433519"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarSuccessSeverityBackgroundBrush"
FallbackColor="#393D1B"
TintColor="#393D1B"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarInformationalSeverityBackgroundBrush"
FallbackColor="#34424d"
TintColor="#34424d"
TintOpacity="0.6"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Thickness x:Key="InfoBarIconMargin">19,16,19,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">0,0,0,0</Thickness>
<x:Double x:Key="InfoBarIconFontSize">20</x:Double>

View File

@@ -20,6 +20,9 @@
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing8Template">
<StackPanel Spacing="8"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
<cwcont:UniformGrid
ColumnSpacing="2"

View File

@@ -9,6 +9,8 @@
<x:Double x:Key="ContentDialogMinHeight">64</x:Double>
<x:Double x:Key="LargeAppBarButtonWidth">100</x:Double>
<x:Double x:Key="AppBarThemeCompactActualHeight">50</x:Double>
<!-- ProgressBar -->
<x:Double x:Key="LargeBackgroundProgressBarOpacity">0.2</x:Double>
</ResourceDictionary>

View File

@@ -1,5 +1,385 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cw="using:CommunityToolkit.WinUI">
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
<Style x:Key="CardPivotStyle" TargetType="Pivot">
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Background" Value="{ThemeResource PivotBackground}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Grid/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Pivot">
<Grid
x:Name="RootElement"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}">
<Grid.Resources>
<Style x:Key="BaseContentControlStyle" TargetType="ContentControl">
<Setter Property="FontFamily" Value="XamlAutoFontFamily"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
OpticalMarginAlignment="TrimSideBearings"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="TitleContentControlStyle"
BasedOn="{StaticResource BaseContentControlStyle}"
TargetType="ContentControl">
<Setter Property="FontFamily" Value="{ThemeResource PivotTitleFontFamily}"/>
<Setter Property="FontWeight" Value="{ThemeResource PivotTitleThemeFontWeight}"/>
<Setter Property="FontSize" Value="{ThemeResource PivotTitleFontSize}"/>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ContentControl
x:Name="TitleContentControl"
Grid.Row="0"
Margin="{StaticResource PivotPortraitThemePadding}"
Content="{TemplateBinding Title}"
ContentTemplate="{TemplateBinding TitleTemplate}"
IsTabStop="False"
Style="{StaticResource TitleContentControlStyle}"
Visibility="Collapsed"/>
<Grid Grid.Row="1">
<Grid.Resources>
<ControlTemplate x:Key="NextTemplate" TargetType="Button">
<Border
x:Name="Root"
Background="{ThemeResource PivotNextButtonBackground}"
BorderBrush="{ThemeResource PivotNextButtonBorderBrush}"
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
<FontIcon
x:Name="Arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource PivotNextButtonForeground}"
Glyph="&#xE76C;"
MirroredWhenRightToLeft="True"
UseLayoutRounding="False"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
<ControlTemplate x:Key="PreviousTemplate" TargetType="Button">
<Border
x:Name="Root"
Background="{ThemeResource PivotPreviousButtonBackground}"
BorderBrush="{ThemeResource PivotPreviousButtonBorderBrush}"
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
<FontIcon
x:Name="Arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource PivotPreviousButtonForeground}"
Glyph="&#xE76B;"
MirroredWhenRightToLeft="True"
UseLayoutRounding="False"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Grid.Resources>
<ScrollViewer
x:Name="ScrollViewer"
Margin="{TemplateBinding Padding}"
VerticalContentAlignment="Stretch"
BringIntoViewOnFocusChange="False"
HorizontalScrollBarVisibility="Hidden"
HorizontalSnapPointsAlignment="Center"
HorizontalSnapPointsType="MandatorySingle"
Template="{StaticResource ScrollViewerScrollBarlessTemplate}"
VerticalScrollBarVisibility="Disabled"
VerticalScrollMode="Disabled"
VerticalSnapPointsType="None"
ZoomMode="Disabled">
<PivotPanel x:Name="Panel" VerticalAlignment="Stretch">
<Grid x:Name="PivotLayoutElement">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.RenderTransform>
<CompositeTransform x:Name="PivotLayoutElementTranslateTransform"/>
</Grid.RenderTransform>
<ItemsPresenter x:Name="PivotItemPresenter" Grid.Row="1">
<ItemsPresenter.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name="ItemsPresenterTranslateTransform"/>
<CompositeTransform x:Name="ItemsPresenterCompositeTransform"/>
</TransformGroup>
</ItemsPresenter.RenderTransform>
</ItemsPresenter>
<Border
Grid.Row="0"
Margin="16,16,16,0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Style="{ThemeResource AcrylicBaseHighGridCardStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="LeftHeaderPresenter"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding LeftHeader}"
ContentTemplate="{TemplateBinding LeftHeaderTemplate}"/>
<ContentControl
x:Name="HeaderClipper"
Grid.Column="1"
HorizontalContentAlignment="Stretch"
UseSystemFocusVisuals="{StaticResource UseSystemFocusVisuals}">
<ContentControl.Clip>
<RectangleGeometry x:Name="HeaderClipperGeometry"/>
</ContentControl.Clip>
<Grid>
<Grid.RenderTransform>
<CompositeTransform x:Name="HeaderOffsetTranslateTransform"/>
</Grid.RenderTransform>
<PivotHeaderPanel x:Name="StaticHeader" Visibility="Collapsed">
<PivotHeaderPanel.RenderTransform>
<CompositeTransform x:Name="StaticHeaderTranslateTransform"/>
</PivotHeaderPanel.RenderTransform>
</PivotHeaderPanel>
<PivotHeaderPanel x:Name="Header">
<PivotHeaderPanel.RenderTransform>
<CompositeTransform x:Name="HeaderTranslateTransform"/>
</PivotHeaderPanel.RenderTransform>
</PivotHeaderPanel>
<Rectangle
x:Name="FocusFollower"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Control.IsTemplateFocusTarget="True"
Fill="Transparent"
IsHitTestVisible="False"/>
</Grid>
</ContentControl>
<Button
x:Name="PreviousButton"
Grid.Column="1"
Width="20"
Height="36"
Margin="{ThemeResource PivotNavButtonMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="Transparent"
IsEnabled="False"
IsTabStop="False"
Opacity="0"
Template="{StaticResource PreviousTemplate}"
UseSystemFocusVisuals="False"/>
<Button
x:Name="NextButton"
Grid.Column="1"
Width="20"
Height="36"
Margin="{ThemeResource PivotNavButtonMargin}"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="Transparent"
IsEnabled="False"
IsTabStop="False"
Opacity="0"
Template="{StaticResource NextTemplate}"
UseSystemFocusVisuals="False"/>
<ContentPresenter
x:Name="RightHeaderPresenter"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding RightHeader}"
ContentTemplate="{TemplateBinding RightHeaderTemplate}"/>
</Grid>
</Border>
</Grid>
</PivotPanel>
</ScrollViewer>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Orientation">
<VisualState x:Name="Portrait">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPortraitThemePadding}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Landscape">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotLandscapeThemePadding}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="NavigationButtonsVisibility">
<VisualState x:Name="NavigationButtonsHidden"/>
<VisualState x:Name="NavigationButtonsVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PreviousButtonVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="NextButtonVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="HeaderStates">
<VisualState x:Name="HeaderDynamic"/>
<VisualState x:Name="HeaderStatic">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Header" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="StaticHeader" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -42,7 +42,7 @@
Grid.ColumnSpan="2"
Margin="{TemplateBinding Padding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MaxWidth="{Binding Path=(shch:ScrollViewerHelper.LeftPanelMaxWidth), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>

View File

@@ -1,4 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls">
<!-- Settings -->
<x:Double x:Key="SettingsCardSpacing">3</x:Double>
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
@@ -6,6 +9,12 @@
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
<x:Double x:Key="SettingsCardContentControlMinWidth">120</x:Double>
<x:Double x:Key="SettingsCardContentControlMinWidth2">160</x:Double>
<x:Double x:Key="SettingsCardContentControlSpacing">10</x:Double>
<Thickness x:Key="SettingsCardAlignSettingsExpanderPadding">16,16,44,16</Thickness>
<Thickness x:Key="SettingsExpanderItemHasIconPadding">16,8,16,8</Thickness>
<Style
x:Key="SettingsSectionHeaderTextBlockStyle"

View File

@@ -4,6 +4,7 @@
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
<x:String x:Key="DocumentLink_Loopback">https://hut.ao/zh/advanced/FAQ.html#%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86%E4%BD%BF%E7%94%A8%E8%83%A1%E6%A1%83%E5%B7%A5%E5%85%B7%E7%AE%B1</x:String>
<!-- Other -->
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
@@ -27,6 +28,7 @@
<!-- EmotionIcon -->
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
@@ -11,13 +12,12 @@ namespace Snap.Hutao.Core.Database;
internal sealed class ObservableReorderableDbCollection<T> : ObservableCollection<T>
where T : class, IReorderable
{
private readonly DbContext dbContext;
private bool previousChangeIsRemoved;
private readonly IServiceProvider serviceProvider;
public ObservableReorderableDbCollection(List<T> items, DbContext dbContext)
public ObservableReorderableDbCollection(List<T> items, IServiceProvider serviceProvider)
: base(AdjustIndex(items))
{
this.dbContext = dbContext;
this.serviceProvider = serviceProvider;
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
@@ -27,16 +27,8 @@ internal sealed class ObservableReorderableDbCollection<T> : ObservableCollectio
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
previousChangeIsRemoved = true;
break;
case NotifyCollectionChangedAction.Add:
if (!previousChangeIsRemoved)
{
return;
}
OnReorder();
previousChangeIsRemoved = false;
break;
}
}
@@ -57,10 +49,15 @@ internal sealed class ObservableReorderableDbCollection<T> : ObservableCollectio
{
AdjustIndex((List<T>)Items);
DbSet<T> dbSet = dbContext.Set<T>();
foreach (ref readonly T item in CollectionsMarshal.AsSpan((List<T>)Items))
using (IServiceScope scope = serviceProvider.CreateScope())
{
dbSet.UpdateAndSave(item);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<T> dbSet = appDbContext.Set<T>();
foreach (ref readonly T item in CollectionsMarshal.AsSpan((List<T>)Items))
{
dbSet.UpdateAndSave(item);
}
}
}
}

View File

@@ -2,9 +2,12 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Http;
using Snap.Hutao.Core.IO.Http.Proxy;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service;
using System.Globalization;
using System.Net.Http;
using System.Runtime.CompilerServices;
using Windows.Globalization;
@@ -31,7 +34,7 @@ internal static class DependencyInjection
.AddJsonOptions()
.AddDatabase()
.AddInjections()
.AddHttpClients()
.AddAllHttpClients()
// Discrete services
.AddSingleton<IMessenger, WeakReferenceMessenger>()
@@ -48,10 +51,10 @@ internal static class DependencyInjection
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InitializeCulture(this IServiceProvider serviceProvider)
{
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
appOptions.PreviousCulture = CultureInfo.CurrentCulture;
CultureOptions cultureOptions = serviceProvider.GetRequiredService<CultureOptions>();
cultureOptions.SystemCulture = CultureInfo.CurrentCulture;
CultureInfo cultureInfo = appOptions.CurrentCulture;
CultureInfo cultureInfo = cultureOptions.CurrentCulture;
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;

View File

@@ -38,8 +38,8 @@ internal static class IocConfiguration
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
{
RuntimeOptions hutaoOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(hutaoOptions.DataFolder, "Userdata.db");
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// Temporarily create a context

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.IO.Http.Proxy;
using Snap.Hutao.Web.Hoyolab;
using System.Net.Http;
@@ -14,12 +15,26 @@ internal static partial class IocHttpClientConfiguration
{
private const string ApplicationJson = "application/json";
/// <summary>
/// 添加 <see cref="HttpClient"/>
/// 此方法将会自动生成
/// </summary>
/// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddAllHttpClients(this IServiceCollection services)
{
services
.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler())
.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
{
HttpClientHandler clientHandler = (HttpClientHandler)handler;
clientHandler.UseProxy = true;
clientHandler.Proxy = provider.GetRequiredService<DynamicHttpProxy>();
});
})
.AddHttpClients();
return services;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial IServiceCollection AddHttpClients(this IServiceCollection services);
/// <summary>
@@ -29,10 +44,10 @@ internal static partial class IocHttpClientConfiguration
/// <param name="client">配置后的客户端</param>
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
client.DefaultRequestHeaders.UserAgent.ParseAdd(runtimeOptions.UserAgent);
}
/// <summary>
@@ -89,6 +104,7 @@ internal static partial class IocHttpClientConfiguration
/// HoYoLAB web
/// </summary>
/// <param name="client">配置后的客户端</param>
[SuppressMessage("", "IDE0051")]
private static void XRpc4Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;

View File

@@ -11,10 +11,18 @@ internal sealed class HutaoException : Exception
Kind = kind;
}
public HutaoException(string message, Exception? innerException)
private HutaoException(string message, Exception? innerException)
: base($"{message}\n{innerException?.Message}", innerException)
{
}
public HutaoExceptionKind Kind { get; private set; }
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
{
if (condition)
{
throw new HutaoException(kind, message, innerException);
}
}
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Package;
using System.IO;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.ExceptionService;
@@ -35,6 +36,22 @@ internal static class ThrowHelper
throw new GameFileOperationException(message, inner);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static InvalidDataException InvalidData(string message, Exception? inner = default)
{
throw new InvalidDataException(message, inner);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void InvalidDataIf([DoesNotReturnIf(true)] bool condition, string message, Exception? inner = default)
{
if (condition)
{
throw new InvalidDataException(message, inner);
}
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static InvalidOperationException InvalidOperation(string message, Exception? inner = default)

View File

@@ -0,0 +1,83 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.NetworkManagement.WindowsFirewall;
using Snap.Hutao.Win32.Security;
using System.Runtime.InteropServices;
using static Snap.Hutao.Win32.AdvApi32;
using static Snap.Hutao.Win32.ApiMsWinNetIsolation;
using static Snap.Hutao.Win32.Macros;
namespace Snap.Hutao.Core.IO.Http.Loopback;
[Injection(InjectAs.Singleton)]
internal sealed unsafe class LoopbackManager : ObservableObject
{
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
private readonly string hutaoContainerStringSID = default!;
private bool isLoopbackEnabled;
public LoopbackManager(IServiceProvider serviceProvider)
{
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
INET_FIREWALL_APP_CONTAINER* pContainers = default;
try
{
WIN32_ERROR error = NetworkIsolationEnumAppContainers(NETISO_FLAG.NETISO_FLAG_MAX, out uint acCount, out pContainers);
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
for (uint i = 0; i < acCount; i++)
{
INET_FIREWALL_APP_CONTAINER* pContainer = pContainers + i;
ReadOnlySpan<char> appContainerName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pContainer->appContainerName);
if (appContainerName.Equals(runtimeOptions.FamilyName, StringComparison.Ordinal))
{
ConvertSidToStringSidW(pContainer->appContainerSid, out PWSTR stringSid);
hutaoContainerStringSID = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid).ToString();
break;
}
}
}
finally
{
// This function returns 1 rather than 0 specfied in the document.
_ = NetworkIsolationFreeAppContainers(pContainers);
}
WIN32_ERROR error2 = NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error2));
for (uint i = 0; i < accCount; i++)
{
ConvertSidToStringSidW((pSids + i)->Sid, out PWSTR stringSid);
ReadOnlySpan<char> stringSidSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid);
if (stringSidSpan.Equals(hutaoContainerStringSID, StringComparison.Ordinal))
{
IsLoopbackEnabled = true;
break;
}
}
}
public bool IsLoopbackEnabled { get => isLoopbackEnabled; private set => SetProperty(ref isLoopbackEnabled, value); }
public void EnableLoopback()
{
NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
List<SID_AND_ATTRIBUTES> sids = new((int)(accCount + 1));
for (uint i = 0; i < accCount; i++)
{
sids.Add(*(pSids + i));
}
ConvertStringSidToSidW(hutaoContainerStringSID, out PSID pSid);
SID_AND_ATTRIBUTES sidAndAttributes = default;
sidAndAttributes.Sid = pSid;
sids.Add(sidAndAttributes);
IsLoopbackEnabled = NetworkIsolationSetAppContainerConfig(CollectionsMarshal.AsSpan(sids)) is WIN32_ERROR.ERROR_SUCCESS;
}
}

View File

@@ -0,0 +1,110 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Win32.Registry;
using System.Net;
using System.Reflection;
namespace Snap.Hutao.Core.IO.Http.Proxy;
[Injection(InjectAs.Singleton)]
internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, IDisposable
{
private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
private static readonly Lazy<MethodInfo> LazyConstructSystemProxyMethod = new(GetConstructSystemProxyMethod);
private readonly IServiceProvider serviceProvider;
private readonly RegistryWatcher watcher;
private IWebProxy innerProxy = default!;
public DynamicHttpProxy(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
UpdateInnerProxy();
watcher = new(ProxySettingPath, OnSystemProxySettingsChanged);
watcher.Start();
}
public string CurrentProxyUri
{
get
{
Uri? proxyUri = GetProxy("https://hut.ao".ToUri());
return proxyUri is null
? SH.ViewPageFeedbackCurrentProxyNoProxyDescription
: proxyUri.AbsoluteUri;
}
}
public IWebProxy InnerProxy
{
get => innerProxy;
[MemberNotNull(nameof(innerProxy))]
set
{
if (ReferenceEquals(innerProxy, value))
{
return;
}
(innerProxy as IDisposable)?.Dispose();
innerProxy = value;
}
}
public ICredentials? Credentials
{
get => InnerProxy.Credentials;
set => InnerProxy.Credentials = value;
}
public Uri? GetProxy(Uri destination)
{
return InnerProxy.GetProxy(destination);
}
public bool IsBypassed(Uri host)
{
return InnerProxy.IsBypassed(host);
}
public void Dispose()
{
(innerProxy as IDisposable)?.Dispose();
watcher.Dispose();
}
public void OnSystemProxySettingsChanged()
{
UpdateInnerProxy();
// TaskContext can't be injected directly since there are some recursive dependencies.
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
taskContext.BeginInvokeOnMainThread(() => OnPropertyChanged(nameof(CurrentProxyUri)));
}
private static MethodInfo GetConstructSystemProxyMethod()
{
Type? systemProxyInfoType = typeof(System.Net.Http.SocketsHttpHandler).Assembly.GetType("System.Net.Http.SystemProxyInfo");
ArgumentNullException.ThrowIfNull(systemProxyInfoType);
MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public);
ArgumentNullException.ThrowIfNull(constructSystemProxyMethod);
return constructSystemProxyMethod;
}
[MemberNotNull(nameof(innerProxy))]
private void UpdateInnerProxy()
{
IWebProxy? proxy = LazyConstructSystemProxyMethod.Value.Invoke(default, default) as IWebProxy;
ArgumentNullException.ThrowIfNull(proxy);
InnerProxy = proxy;
}
}

View File

@@ -18,6 +18,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
private readonly long contentLength;
private readonly int bufferSize;
private readonly SafeFileHandle destFileHandle;
private readonly int maxDegreeOfParallelism;
private readonly List<Shard> shards;
private HttpShardCopyWorker(HttpShardCopyWorkerOptions<TStatus> options)
@@ -28,6 +29,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
contentLength = options.ContentLength;
bufferSize = options.BufferSize;
destFileHandle = options.GetFileHandle();
maxDegreeOfParallelism = options.MaxDegreeOfParallelism;
shards = CalculateShards(contentLength);
static List<Shard> CalculateShards(long contentLength)
@@ -56,7 +58,11 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
public Task CopyAsync(IProgress<TStatus> progress, CancellationToken token = default)
{
ShardProgress shardProgress = new(progress, statusFactory, contentLength);
return Parallel.ForEachAsync(shards, token, (shard, token) => CopyShardAsync(shard, shardProgress, token));
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = maxDegreeOfParallelism,
};
return Parallel.ForEachAsync(shards, parallelOptions, (shard, token) => CopyShardAsync(shard, shardProgress, token));
async ValueTask CopyShardAsync(Shard shard, IProgress<ShardStatus> progress, CancellationToken token)
{

View File

@@ -22,6 +22,8 @@ internal sealed class HttpShardCopyWorkerOptions<TStatus>
public int BufferSize { get; set; } = 80 * 1024;
public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount;
public SafeFileHandle GetFileHandle()
{
return File.OpenHandle(DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.RandomAccess | FileOptions.Asynchronous, ContentLength);

View File

@@ -11,11 +11,14 @@ namespace Snap.Hutao.Core.IO.Ini;
[HighQuality]
internal static class IniSerializer
{
/// <summary>
/// 反序列化
/// </summary>
/// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns>
public static List<IniElement> DeserializeFromFile(string filePath)
{
using (FileStream readStream = File.OpenRead(filePath))
{
return Deserialize(readStream);
}
}
public static List<IniElement> Deserialize(FileStream fileStream)
{
List<IniElement> results = [];
@@ -50,11 +53,14 @@ internal static class IniSerializer
return results;
}
/// <summary>
/// 序列化
/// </summary>
/// <param name="fileStream">写入的流</param>
/// <param name="elements">元素</param>
public static void SerializeToFile(string filePath, IEnumerable<IniElement> elements)
{
using (FileStream writeStream = File.Create(filePath))
{
Serialize(writeStream, elements);
}
}
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
{
using (StreamWriter writer = new(fileStream))

View File

@@ -3,7 +3,7 @@
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Discord;
@@ -24,24 +24,9 @@ namespace Snap.Hutao.Core.LifeCycle;
[SuppressMessage("", "CA1001")]
internal sealed partial class Activation : IActivation
{
/// <summary>
/// 操作
/// </summary>
public const string Action = nameof(Action);
/// <summary>
/// Uid
/// </summary>
public const string Uid = nameof(Uid);
/// <summary>
/// 启动游戏启动参数
/// </summary>
public const string LaunchGame = nameof(LaunchGame);
/// <summary>
/// 从剪贴板导入成就
/// </summary>
public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard);
private const string CategoryAchievement = "ACHIEVEMENT";
@@ -55,29 +40,20 @@ internal sealed partial class Activation : IActivation
private readonly SemaphoreSlim activateSemaphore = new(1);
/// <inheritdoc/>
public void Activate(object? sender, AppActivationArguments args)
public void Activate(HutaoActivationArguments args)
{
_ = sender;
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
HandleActivationAsync(args, true).SafeForget();
return;
}
HandleActivationAsync(args).SafeForget();
}
/// <inheritdoc/>
public void NonRedirectToActivate(object? sender, AppActivationArguments args)
public void Initialize()
{
_ = sender;
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
HandleActivationAsync(args, false).SafeForget();
}
}
/// <inheritdoc/>
public void InitializeWith(AppInstance appInstance)
{
appInstance.Activated += Activate;
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
}
@@ -95,44 +71,40 @@ internal sealed partial class Activation : IActivation
}
}
private async ValueTask HandleActivationAsync(AppActivationArguments args, bool isRedirected)
private async ValueTask HandleActivationAsync(HutaoActivationArguments args)
{
if (activateSemaphore.CurrentCount > 0)
{
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
await HandleActivationCoreAsync(args, isRedirected).ConfigureAwait(false);
await HandleActivationCoreAsync(args).ConfigureAwait(false);
}
}
}
private async ValueTask HandleActivationCoreAsync(AppActivationArguments args, bool isRedirected)
private async ValueTask HandleActivationCoreAsync(HutaoActivationArguments args)
{
if (args.Kind == ExtendedActivationKind.Protocol)
if (args.Kind is HutaoActivationKind.Protocol)
{
if (args.TryGetProtocolActivatedUri(out Uri? uri))
{
await HandleUrlActivationAsync(uri, isRedirected).ConfigureAwait(false);
}
ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri);
await HandleUrlActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false);
}
else if (args.Kind == ExtendedActivationKind.Launch)
else if (args.Kind is HutaoActivationKind.Launch)
{
if (args.TryGetLaunchActivatedArgument(out string? arguments))
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
switch (args.LaunchActivatedArguments)
{
switch (arguments)
{
case LaunchGame:
{
await HandleLaunchGameActionAsync().ConfigureAwait(false);
break;
}
case LaunchGame:
{
await HandleLaunchGameActionAsync().ConfigureAwait(false);
break;
}
default:
{
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
break;
}
}
default:
{
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
break;
}
}
}
}
@@ -190,11 +162,11 @@ internal sealed partial class Activation : IActivation
serviceProvider
.GetRequiredService<IDiscordService>()
.SetNormalActivity()
.SetNormalActivityAsync()
.SafeForget();
}
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirected)
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo)
{
UriBuilder builder = new(uri);
@@ -207,13 +179,13 @@ internal sealed partial class Activation : IActivation
case CategoryAchievement:
{
await WaitMainWindowAsync().ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter, isRedirected).ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
case CategoryDailyNote:
{
await HandleDailyNoteActionAsync(action, parameter, isRedirected).ConfigureAwait(false);
await HandleDailyNoteActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
@@ -225,10 +197,10 @@ internal sealed partial class Activation : IActivation
}
}
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirected)
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirectTo)
{
_ = parameter;
_ = isRedirected;
_ = isRedirectTo;
switch (action)
{
case UrlActionImport:
@@ -245,7 +217,7 @@ internal sealed partial class Activation : IActivation
}
}
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirected)
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirectTo)
{
_ = parameter;
switch (action)
@@ -264,7 +236,7 @@ internal sealed partial class Activation : IActivation
}
// Check if it's redirected.
if (!isRedirected)
if (!isRedirectTo)
{
// It's a direct open process, should exit immediately.
Process.GetCurrentProcess().Kill();

View File

@@ -36,7 +36,7 @@ internal static class AppActivationArgumentsExtensions
/// <param name="activatedEventArgs">应用程序激活参数</param>
/// <param name="arguments">参数</param>
/// <returns>是否存在参数</returns>
public static bool TryGetLaunchActivatedArgument(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
public static bool TryGetLaunchActivatedArguments(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
{
arguments = null;
if (activatedEventArgs.Data is not ILaunchActivatedEventArgs launchArgs)

View File

@@ -2,10 +2,11 @@
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
using Windows.Win32.Foundation;
using Windows.Win32.Security;
using Windows.Win32.System.Com;
using static Windows.Win32.PInvoke;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Com;
using static Snap.Hutao.Win32.ConstValues;
using static Snap.Hutao.Win32.Kernel32;
using static Snap.Hutao.Win32.Ole32;
namespace Snap.Hutao.Core.LifeCycle;
@@ -18,7 +19,7 @@ internal static class AppInstanceExtension
private static readonly WaitCallback RunActionWaitCallback = RunAction;
// Hold the reference here to prevent memory corruption.
private static HANDLE redirectEventHandle = HANDLE.Null;
private static HANDLE redirectEventHandle;
/// <summary>
/// 同步非阻塞重定向
@@ -27,19 +28,24 @@ internal static class AppInstanceExtension
/// <param name="args">参数</param>
public static unsafe void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
{
redirectEventHandle = CreateEvent(default(SECURITY_ATTRIBUTES*), true, false, null);
// use ThreadPool.UnsafeQueueUserWorkItem to cancel stacktrace
// like ExecutionContext.SuppressFlow
ThreadPool.UnsafeQueueUserWorkItem(RunActionWaitCallback, () =>
try
{
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
SetEvent(redirectEventHandle);
});
redirectEventHandle = CreateEventW(default, true, false, default);
ReadOnlySpan<HANDLE> handles = new(ref redirectEventHandle);
CoWaitForMultipleObjects((uint)CWMO_FLAGS.CWMO_DEFAULT, INFINITE, handles, out uint _);
CloseHandle(redirectEventHandle);
// use ThreadPool.UnsafeQueueUserWorkItem to cancel stacktrace
// like ExecutionContext.SuppressFlow
ThreadPool.UnsafeQueueUserWorkItem(RunActionWaitCallback, () =>
{
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
SetEvent(redirectEventHandle);
});
CoWaitForMultipleObjects(CWMO_FLAGS.CWMO_DEFAULT, INFINITE, [redirectEventHandle], out uint _);
}
finally
{
CloseHandle(redirectEventHandle);
}
}
[SuppressMessage("", "SH007")]

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Win32.Foundation;
using Snap.Hutao.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Core.LifeCycle;
@@ -19,6 +19,6 @@ internal static class CurrentWindowReferenceExtension
{
return reference.Window is IWindowOptionsSource optionsSource
? optionsSource.WindowOptions.Hwnd
: (HWND)WindowNative.GetWindowHandle(reference.Window);
: WindowNative.GetWindowHandle(reference.Window);
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
namespace Snap.Hutao.Core.LifeCycle;
internal sealed class HutaoActivationArguments
{
public bool IsRedirectTo { get; set; }
public HutaoActivationKind Kind { get; set; }
public Uri? ProtocolActivatedUri { get; set; }
public string? LaunchActivatedArguments { get; set; }
public static HutaoActivationArguments FromAppActivationArguments(AppActivationArguments args, bool isRedirected = false)
{
HutaoActivationArguments result = new()
{
IsRedirectTo = isRedirected,
};
switch (args.Kind)
{
case ExtendedActivationKind.Launch:
{
result.Kind = HutaoActivationKind.Launch;
if (args.TryGetLaunchActivatedArguments(out string? arguments))
{
result.LaunchActivatedArguments = arguments;
}
break;
}
case ExtendedActivationKind.Protocol:
{
result.Kind = HutaoActivationKind.Protocol;
if (args.TryGetProtocolActivatedUri(out Uri? uri))
{
result.ProtocolActivatedUri = uri;
}
break;
}
}
return result;
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle;
internal enum HutaoActivationKind
{
None,
Launch,
Protocol,
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
namespace Snap.Hutao.Core.LifeCycle;
/// <summary>
@@ -10,24 +8,7 @@ namespace Snap.Hutao.Core.LifeCycle;
/// </summary>
internal interface IActivation
{
/// <summary>
/// 响应激活事件
/// 激活事件一般不会在UI线程上触发
/// </summary>
/// <param name="sender">发送方</param>
/// <param name="args">激活参数</param>
void Activate(object? sender, AppActivationArguments args);
void Activate(HutaoActivationArguments args);
/// <summary>
/// 使用当前 App 实例初始化激活
/// </summary>
/// <param name="appInstance">App 实例</param>
void InitializeWith(AppInstance appInstance);
/// <summary>
/// 无转发触发激活事件
/// </summary>
/// <param name="sender">发送方</param>
/// <param name="args">激活参数</param>
void NonRedirectToActivate(object? sender, AppActivationArguments args);
void Initialize();
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO.Pipes;
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
internal static class NamedPipeClientStreamExtension
{
public static bool TryConnectOnce(this NamedPipeClientStream clientStream)
{
try
{
clientStream.Connect(TimeSpan.Zero);
return true;
}
catch (TimeoutException)
{
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
internal enum PipePacketCommand : byte
{
None = 0,
RedirectActivation = 10,
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
internal enum PipePacketContentType : byte
{
None = 0,
Json = 1,
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct PipePacketHeader
{
public byte Version;
public PipePacketType Type;
public PipePacketCommand Command;
public PipePacketContentType ContentType;
public int ContentLength;
public ulong Checksum;
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
internal enum PipePacketType : byte
{
None = 0,
Request = 1,
Response = 2,
Termination = 3,
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
using System.IO.Hashing;
using System.IO.Pipes;
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
[Injection(InjectAs.Singleton)]
internal sealed class PrivateNamedPipeClient : IDisposable
{
private readonly NamedPipeClientStream clientStream = new(".", "Snap.Hutao.PrivateNamedPipe", PipeDirection.InOut, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
public unsafe bool TryRedirectActivationTo(AppActivationArguments args)
{
if (clientStream.TryConnectOnce())
{
{
PipePacketHeader redirectActivationPacket = default;
redirectActivationPacket.Version = 1;
redirectActivationPacket.Type = PipePacketType.Request;
redirectActivationPacket.Command = PipePacketCommand.RedirectActivation;
redirectActivationPacket.ContentType = PipePacketContentType.Json;
HutaoActivationArguments hutaoArgs = HutaoActivationArguments.FromAppActivationArguments(args, isRedirected: true);
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(hutaoArgs);
redirectActivationPacket.ContentLength = jsonBytes.Length;
redirectActivationPacket.Checksum = XxHash64.HashToUInt64(jsonBytes);
clientStream.Write(new(&redirectActivationPacket, sizeof(PipePacketHeader)));
clientStream.Write(jsonBytes);
}
{
PipePacketHeader terminationPacket = default;
terminationPacket.Version = 1;
terminationPacket.Type = PipePacketType.Termination;
clientStream.Write(new(&terminationPacket, sizeof(PipePacketHeader)));
}
clientStream.Flush();
return true;
}
return false;
}
public void Dispose()
{
clientStream.Dispose();
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
[Injection(InjectAs.Singleton)]
[ConstructorGenerated]
internal sealed partial class PrivateNamedPipeMessageDispatcher
{
private readonly IServiceProvider serviceProvider;
public void RedirectActivation(HutaoActivationArguments? args)
{
if (args is null)
{
return;
}
serviceProvider.GetRequiredService<IActivation>().Activate(args);
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using System.IO.Hashing;
using System.IO.Pipes;
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
[Injection(InjectAs.Singleton)]
[ConstructorGenerated]
internal sealed partial class PrivateNamedPipeServer : IDisposable
{
private readonly PrivateNamedPipeMessageDispatcher messageDispatcher;
private readonly NamedPipeServerStream serverStream = new("Snap.Hutao.PrivateNamedPipe", PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
private readonly CancellationTokenSource serverTokenSource = new();
private readonly SemaphoreSlim serverSemaphore = new(1);
public void Dispose()
{
serverTokenSource.Cancel();
serverSemaphore.Wait();
serverSemaphore.Dispose();
serverTokenSource.Dispose();
serverStream.Dispose();
}
public async ValueTask RunAsync()
{
using (await serverSemaphore.EnterAsync(serverTokenSource.Token).ConfigureAwait(false))
{
while (!serverTokenSource.IsCancellationRequested)
{
try
{
await serverStream.WaitForConnectionAsync(serverTokenSource.Token).ConfigureAwait(false);
RunPacketSession(serverStream, serverTokenSource.Token);
}
catch (OperationCanceledException)
{
}
}
}
}
private static unsafe byte[] GetValidatedContent(NamedPipeServerStream serverStream, PipePacketHeader* header)
{
byte[] content = new byte[header->ContentLength];
serverStream.ReadAtLeast(content, header->ContentLength, false);
ThrowHelper.InvalidDataIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect");
return content;
}
private unsafe void RunPacketSession(NamedPipeServerStream serverStream, CancellationToken token)
{
Span<byte> headerSpan = stackalloc byte[sizeof(PipePacketHeader)];
bool sessionTerminated = false;
while (serverStream.IsConnected && !sessionTerminated && !token.IsCancellationRequested)
{
serverStream.ReadExactly(headerSpan);
fixed (byte* pHeader = headerSpan)
{
PipePacketHeader* header = (PipePacketHeader*)pHeader;
switch ((header->Type, header->Command, header->ContentType))
{
case (PipePacketType.Request, PipePacketCommand.RedirectActivation, PipePacketContentType.Json):
ReadOnlySpan<byte> content = GetValidatedContent(serverStream, header);
messageDispatcher.RedirectActivation(JsonSerializer.Deserialize<HutaoActivationArguments>(content));
break;
case (PipePacketType.Termination, _, _):
serverStream.Disconnect();
sessionTerminated = true;
return;
}
}
}
}
}

View File

@@ -2,9 +2,9 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Setting;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using static Windows.Win32.PInvoke;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Console;
using static Snap.Hutao.Win32.Kernel32;
namespace Snap.Hutao.Core.Logging;
@@ -26,7 +26,7 @@ internal sealed class ConsoleWindowLifeTime : IDisposable
SetConsoleMode(inputHandle, mode);
}
SetConsoleTitle("Snap Hutao Debug Console");
SetConsoleTitleW("Snap Hutao Debug Console");
}
}
}

View File

@@ -14,113 +14,99 @@ namespace Snap.Hutao.Core;
[Injection(InjectAs.Singleton)]
internal sealed class RuntimeOptions
{
private readonly bool isWebView2Supported;
private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected;
private readonly Lazy<(Version Version, string UserAgent)> lazyVersionAndUserAgent = new(() =>
{
Version version = Package.Current.Id.Version.ToVersion();
return (version, $"Snap Hutao/{version}");
});
private bool? isElevated;
private readonly Lazy<string> lazyDataFolder = new(() =>
{
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
if (!string.IsNullOrEmpty(preferredPath))
{
Directory.CreateDirectory(preferredPath);
return preferredPath;
}
// Fallback to MyDocuments
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocuments, folderName));
Directory.CreateDirectory(path);
return path;
});
private readonly Lazy<string> lazyDeviceId = new(() =>
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
return Convert.ToMd5HexString($"{userName}{machineGuid}");
});
private readonly Lazy<(string Version, bool Supported)> lazyWebViewEnvironment = new(() =>
{
try
{
string version = CoreWebView2Environment.GetAvailableBrowserVersionString();
return (version, true);
}
catch (FileNotFoundException)
{
return (SH.CoreWebView2HelperVersionUndetected, false);
}
});
private readonly Lazy<bool> lazyElevated = new(() =>
{
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
{
return true;
}
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
});
private readonly Lazy<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
private readonly Lazy<string> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
private readonly Lazy<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
public RuntimeOptions(ILogger<RuntimeOptions> logger)
{
AppLaunchTime = DateTimeOffset.UtcNow;
DataFolder = GetDataFolderPath();
LocalCache = ApplicationData.Current.LocalCacheFolder.Path;
InstalledLocation = Package.Current.InstalledLocation.Path;
FamilyName = Package.Current.Id.FamilyName;
Version = Package.Current.Id.Version.ToVersion();
UserAgent = $"Snap Hutao/{Version}";
DeviceId = GetUniqueUserId();
DetectWebView2Environment(logger, out webView2Version, out isWebView2Supported);
static string GetDataFolderPath()
{
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
if (!string.IsNullOrEmpty(preferredPath))
{
Directory.CreateDirectory(preferredPath);
return preferredPath;
}
// Fallback to MyDocuments
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocuments, folderName));
Directory.CreateDirectory(path);
return path;
}
static string GetUniqueUserId()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
return Convert.ToMd5HexString($"{userName}{machineGuid}");
}
static void DetectWebView2Environment(ILogger<RuntimeOptions> logger, out string webView2Version, out bool isWebView2Supported)
{
try
{
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
isWebView2Supported = true;
}
catch (FileNotFoundException ex)
{
webView2Version = SH.CoreWebView2HelperVersionUndetected;
isWebView2Supported = false;
logger.LogError(ex, "WebView2 Runtime not installed.");
}
}
}
public Version Version { get; }
public Version Version { get => lazyVersionAndUserAgent.Value.Version; }
public string UserAgent { get; }
public string UserAgent { get => lazyVersionAndUserAgent.Value.UserAgent; }
public string InstalledLocation { get; }
public string InstalledLocation { get => lazyInstalledLocation.Value; }
public string DataFolder { get; }
public string DataFolder { get => lazyDataFolder.Value; }
public string LocalCache { get; }
public string LocalCache { get => lazyLocalCache.Value; }
public string FamilyName { get; }
public string FamilyName { get => lazyFamilyName.Value; }
public string DeviceId { get; }
public string DeviceId { get => lazyDeviceId.Value; }
public string WebView2Version { get => webView2Version; }
public string WebView2Version { get => lazyWebViewEnvironment.Value.Version; }
public bool IsWebView2Supported { get => isWebView2Supported; }
public bool IsWebView2Supported { get => lazyWebViewEnvironment.Value.Supported; }
public bool IsElevated
{
get
{
return isElevated ??= GetElevated();
static bool GetElevated()
{
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
{
return true;
}
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
}
}
public bool IsElevated { get => lazyElevated.Value; }
public DateTimeOffset AppLaunchTime { get; }
}

View File

@@ -22,7 +22,7 @@ internal static class SettingKeys
public const string DataFolderPath = "DataFolderPath";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled2";
#endregion
#region Passport
@@ -56,5 +56,11 @@ internal static class SettingKeys
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
public const string OverridePackageConvertDirectoryPermissionsRequirement = "OverridePackageConvertDirectoryPermissionsRequirement";
#endregion
#region Obsolete
[Obsolete("重置调试控制台开关")]
public const string IsAllocConsoleDebugModeEnabledLegacy1 = "IsAllocConsoleDebugModeEnabled";
#endregion
}

View File

@@ -39,6 +39,11 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
}
catch (Exception)
{
if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath))
{
File.Delete(fullPath);
}
return false;
}
}
@@ -63,7 +68,7 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
public bool IsDailyNoteRefreshEnabled()
{
return WScriptExists(DailyNoteRefreshScriptName, out _);
return TaskService.Instance.RootFolder.Tasks.Any(task => task.Name is DailyNoteRefreshTaskName);
}
/// <summary>

View File

@@ -1,15 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.UI.Shell;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Storage;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
using static Snap.Hutao.Win32.Macros;
using static Snap.Hutao.Win32.Ole32;
namespace Snap.Hutao.Core.Shell;
@@ -34,31 +33,44 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
return false;
}
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
Marshal.ThrowExceptionForHR(result);
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath);
}
shellLink.SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
shellLink.SetIconLocation(targetLogoPath, 0);
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath)
{
bool result = false;
IShellLinkDataList shellLinkDataList = (IShellLinkDataList)shellLink;
shellLinkDataList.GetFlags(out uint flags);
flags |= (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER;
shellLinkDataList.SetFlags(flags);
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
IPersistFile persistFile = (IPersistFile)shellLink;
try
// DO NOT revert if condition, COM interfaces need to be released properly
HRESULT hr = CoCreateInstance(in ShellLink.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IShellLinkW.IID, out IShellLinkW* pShellLink);
if (SUCCEEDED(hr))
{
persistFile.Save(target, false);
}
catch (UnauthorizedAccessException)
{
return false;
pShellLink->SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
pShellLink->SetIconLocation(targetLogoPath, 0);
if (SUCCEEDED(pShellLink->QueryInterface(in IShellLinkDataList.IID, out IShellLinkDataList* pShellLinkDataList)))
{
pShellLinkDataList->GetFlags(out uint flags);
pShellLinkDataList->SetFlags(flags | (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER);
pShellLinkDataList->Release();
}
if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile)))
{
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
if (SUCCEEDED(pPersistFile->Save(target, false)))
{
result = true;
}
pPersistFile->Release();
}
pShellLink->Release();
}
return true;
return result;
}
}

View File

@@ -8,12 +8,12 @@ internal readonly struct Delay
/// <summary>
/// 随机延迟
/// </summary>
/// <param name="minMilliSeconds">最小,闭</param>
/// <param name="maxMilliSeconds">最小,开</param>
/// <param name="min">最小,闭</param>
/// <param name="max">最小,开</param>
/// <returns>任务</returns>
public static ValueTask Random(int minMilliSeconds, int maxMilliSeconds)
public static ValueTask RandomMilliSeconds(int min, int max)
{
return Task.Delay((int)(System.Random.Shared.NextDouble() * (maxMilliSeconds - minMilliSeconds)) + minMilliSeconds).AsValueTask();
return Task.Delay((int)(System.Random.Shared.NextDouble() * (max - min)) + min).AsValueTask();
}
public static ValueTask FromSeconds(int seconds)

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Windowing;
using Snap.Hutao.Win32;
using Windows.Graphics;
namespace Snap.Hutao.Core.Windowing;

View File

@@ -0,0 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Windowing.Backdrop;
internal interface IBackdropNeedEraseBackground;

View File

@@ -0,0 +1,65 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Snap.Hutao.Core.Windowing.Backdrop;
internal sealed class TransparentBackdrop : SystemBackdrop, IDisposable, IBackdropNeedEraseBackground
{
private readonly object compositorLock = new();
private Color tintColor;
private Windows.UI.Composition.CompositionColorBrush? brush;
private Windows.UI.Composition.Compositor? compositor;
public TransparentBackdrop()
: this(Colors.Transparent)
{
}
public TransparentBackdrop(Color tintColor)
{
this.tintColor = tintColor;
}
internal Windows.UI.Composition.Compositor Compositor
{
get
{
if (compositor is null)
{
lock (compositorLock)
{
if (compositor is null)
{
DispatcherQueue.EnsureSystemDispatcherQueue();
compositor = new Windows.UI.Composition.Compositor();
}
}
}
return compositor;
}
}
public void Dispose()
{
compositor?.Dispose();
}
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
{
brush ??= Compositor.CreateColorBrush(tintColor);
connectedTarget.SystemBackdrop = brush;
}
protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget)
{
disconnectedTarget.SystemBackdrop = null;
}
}

View File

@@ -9,6 +9,8 @@ namespace Snap.Hutao.Core.Windowing;
[HighQuality]
internal enum BackdropType
{
Transparent = -1,
/// <summary>
/// 无
/// </summary>

View File

@@ -5,11 +5,11 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.UI.Input.KeyboardAndMouse;
using System.Text;
using Windows.System;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using static Windows.Win32.PInvoke;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing.HotKey;

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.UI.Input.KeyboardAndMouse;
using System.Runtime.InteropServices;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using static Windows.Win32.PInvoke;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing.HotKey;
@@ -14,7 +14,7 @@ internal sealed partial class HotKeyController : IHotKeyController
{
private static readonly WaitCallback RunMouseClickRepeatForever = MouseClickRepeatForever;
private readonly object locker = new();
private readonly object syncRoot = new();
private readonly HotKeyOptions hotKeyOptions;
@@ -40,7 +40,8 @@ internal sealed partial class HotKeyController : IHotKeyController
private static unsafe INPUT CreateInputForMouseEvent(MOUSE_EVENT_FLAGS flags)
{
INPUT input = new() { type = INPUT_TYPE.INPUT_MOUSE, };
INPUT input = default;
input.type = INPUT_TYPE.INPUT_MOUSE;
input.Anonymous.mi.dwFlags = flags;
return input;
}
@@ -75,7 +76,7 @@ internal sealed partial class HotKeyController : IHotKeyController
private void ToggleMouseClickRepeatForever()
{
lock (locker)
lock (syncRoot)
{
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
{

View File

@@ -1,8 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.UI.Input.KeyboardAndMouse;
using Windows.System;
using Windows.Win32.UI.Input.KeyboardAndMouse;
namespace Snap.Hutao.Core.Windowing.HotKey;

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Win32.UI.WindowsAndMessaging;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao.Core.Windowing;

View File

@@ -9,13 +9,15 @@ using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service;
using Snap.Hutao.Win32;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.Graphics.Dwm;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
using System.IO;
using Windows.Graphics;
using Windows.UI;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
using static Snap.Hutao.Win32.DwmApi;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing;
@@ -53,10 +55,10 @@ internal sealed class WindowController
private void InitializeCore()
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
window.AppWindow.Title = SH.FormatAppNameAndVersion(hutaoOptions.Version);
window.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico"));
window.AppWindow.Title = SH.FormatAppNameAndVersion(runtimeOptions.Version);
window.AppWindow.SetIcon(Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico"));
ExtendsContentIntoTitleBar();
RecoverOrInitWindowSize();
@@ -104,11 +106,11 @@ internal sealed class WindowController
return;
}
WINDOWPLACEMENT windowPlacement = Win32.StructMarshal.WINDOWPLACEMENT();
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create();
GetWindowPlacement(options.Hwnd, ref windowPlacement);
// prevent save value when we are maximized.
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
if (!windowPlacement.ShowCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
{
double scale = 1.0 / options.GetRasterizationScale();
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect().Scale(scale));
@@ -157,6 +159,7 @@ internal sealed class WindowController
{
window.SystemBackdrop = backdropType switch
{
BackdropType.Transparent => new Backdrop.TransparentBackdrop(),
BackdropType.MicaAlt => new MicaBackdrop() { Kind = MicaKind.BaseAlt },
BackdropType.Mica => new MicaBackdrop() { Kind = MicaKind.Base },
BackdropType.Acrylic => new DesktopAcrylicBackdrop(),
@@ -193,7 +196,7 @@ internal sealed class WindowController
private unsafe void UpdateImmersiveDarkMode(FrameworkElement titleBar, object discard)
{
BOOL isDarkMode = Control.Theme.ThemeHelper.IsDarkMode(titleBar.ActualTheme);
DwmSetWindowAttribute(options.Hwnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, &isDarkMode, unchecked((uint)sizeof(BOOL)));
DwmSetWindowAttribute(options.Hwnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, ref isDarkMode);
}
private void UpdateDragRectangles()

View File

@@ -2,7 +2,11 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
using System.Runtime.CompilerServices;
using WinRT.Interop;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing;
@@ -16,4 +20,12 @@ internal static class WindowExtension
WindowController windowController = new(window, window.WindowOptions, serviceProvider);
WindowControllers.Add(window, windowController);
}
public static void SetLayeredWindow(this Window window)
{
HWND hwnd = (HWND)WindowNative.GetWindowHandle(window);
nint style = GetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
style |= (nint)WINDOW_EX_STYLE.WS_EX_LAYERED;
SetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, style);
}
}

View File

@@ -4,10 +4,10 @@
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Snap.Hutao.Win32.Foundation;
using Windows.Graphics;
using Windows.Win32.Foundation;
using WinRT.Interop;
using static Windows.Win32.PInvoke;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing;
@@ -55,7 +55,7 @@ internal readonly struct WindowOptions
/// <param name="persistSize">持久化尺寸</param>
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false)
{
Hwnd = (HWND)WindowNative.GetWindowHandle(window);
Hwnd = WindowNative.GetWindowHandle(window);
InputNonClientPointerSource = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
TitleBar = titleBar;
InitSize = initSize;
@@ -80,8 +80,8 @@ internal readonly struct WindowOptions
{
HWND fgHwnd = GetForegroundWindow();
uint threadIdHwnd = GetWindowThreadProcessId(Hwnd);
uint threadIdFgHwnd = GetWindowThreadProcessId(fgHwnd);
uint threadIdHwnd = GetWindowThreadProcessId(Hwnd, default);
uint threadIdFgHwnd = GetWindowThreadProcessId(fgHwnd, default);
if (threadIdHwnd != threadIdFgHwnd)
{

View File

@@ -2,11 +2,14 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing.Backdrop;
using Snap.Hutao.Core.Windowing.HotKey;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.UI.Shell;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
using static Snap.Hutao.Win32.ComCtl32;
using static Snap.Hutao.Win32.ConstValues;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Core.Windowing;
@@ -25,8 +28,8 @@ internal sealed class WindowSubclass : IDisposable
private readonly IHotKeyController hotKeyController;
// We have to explicitly hold a reference to SUBCLASSPROC
private SUBCLASSPROC? windowProc;
private SUBCLASSPROC? legacyDragBarProc;
private SUBCLASSPROC windowProc = default!;
private SUBCLASSPROC legacyDragBarProc = default!;
public WindowSubclass(Window window, in WindowOptions options, IServiceProvider serviceProvider)
{
@@ -56,7 +59,7 @@ internal sealed class WindowSubclass : IDisposable
}
titleBarHooked = false;
HWND hwndDragBar = FindWindowEx(options.Hwnd, default, "DRAG_BAR_WINDOW_CLASS", default);
HWND hwndDragBar = FindWindowExW(options.Hwnd, default, "DRAG_BAR_WINDOW_CLASS", default);
if (hwndDragBar.IsNull)
{
@@ -75,12 +78,12 @@ internal sealed class WindowSubclass : IDisposable
hotKeyController.UnregisterAll();
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
windowProc = null;
windowProc = default!;
if (options.UseLegacyDragBarImplementation)
{
RemoveWindowSubclass(options.Hwnd, legacyDragBarProc, DragBarSubclassId);
legacyDragBarProc = null;
legacyDragBarProc = default!;
}
}
@@ -93,7 +96,7 @@ internal sealed class WindowSubclass : IDisposable
{
if (window is IMinMaxInfoHandler handler)
{
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam.Value, options.GetRasterizationScale());
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam, options.GetRasterizationScale());
}
break;
@@ -110,6 +113,16 @@ internal sealed class WindowSubclass : IDisposable
hotKeyController.OnHotKeyPressed(*(HotKeyParameter*)&lParam);
break;
}
case WM_ERASEBKGND:
{
if (window.SystemBackdrop is IBackdropNeedEraseBackground)
{
return (LRESULT)(int)BOOL.TRUE;
}
break;
}
}
return DefSubclassProc(hwnd, uMsg, wParam, lParam);

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using System.Text;
@@ -113,6 +114,13 @@ internal static partial class EnumerableExtension
return new ObservableCollection<T>(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ObservableReorderableDbCollection<T> ToObservableReorderableDbCollection<T>(this IEnumerable<T> source, IServiceProvider serviceProvider)
where T : class, IReorderable
{
return new ObservableReorderableDbCollection<T>([.. source], serviceProvider);
}
/// <summary>
/// Concatenates each element from the collection into single string.
/// </summary>

View File

@@ -27,18 +27,4 @@ internal static class StringExtension
{
return source.AsSpan().TrimEnd(value).ToString();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EqualsAny(this string source, in ReadOnlySpan<string> values, StringComparison comparisonType)
{
foreach (ref readonly string value in values)
{
if (source.Equals(value, comparisonType))
{
return true;
}
}
return false;
}
}

View File

@@ -3,13 +3,14 @@
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.UI.Shell;
using Snap.Hutao.Win32.UI.Shell.Common;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.Shell.Common;
using static Windows.Win32.PInvoke;
using static Snap.Hutao.Win32.Macros;
using static Snap.Hutao.Win32.Ole32;
using static Snap.Hutao.Win32.Shell32;
namespace Snap.Hutao.Factory.Picker;
@@ -21,32 +22,33 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
public unsafe ValueResult<bool, ValueFile> PickFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters)
{
CoCreateInstance<FileOpenDialog, IFileOpenDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure();
HRESULT hr = CoCreateInstance(in FileOpenDialog.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileDialog.IID, out IFileDialog* pFileDialog);
Marshal.ThrowExceptionForHR(hr);
FILEOPENDIALOGOPTIONS options =
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM |
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
dialog.SetOptions(options);
SetDesktopAsStartupFolder(dialog);
if (!string.IsNullOrEmpty(title))
{
dialog.SetTitle(title);
}
pFileDialog->SetOptions(options);
SetDesktopAsStartupFolder(pFileDialog);
if (!string.IsNullOrEmpty(defaultFileName))
{
dialog.SetFileName(defaultFileName);
pFileDialog->SetFileName(defaultFileName);
}
if (!string.IsNullOrEmpty(title))
{
pFileDialog->SetTitle(title);
}
if (filters is { Length: > 0 })
{
SetFileTypes(dialog, filters);
SetFileTypes(pFileDialog, filters);
}
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
HRESULT res = pFileDialog->Show(currentWindowReference.GetWindowHandle());
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
{
return new(false, default);
@@ -56,18 +58,18 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
Marshal.ThrowExceptionForHR(res);
}
dialog.GetResult(out IShellItem item);
HRESULT t = pFileDialog->GetResult(out IShellItem* pShellItem);
PWSTR displayName = default;
string file;
try
{
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
file = new((char*)displayName);
pShellItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
file = new(displayName);
}
finally
{
Marshal.FreeCoTaskMem((nint)displayName.Value);
CoTaskMemFree(displayName);
}
return new(true, file);
@@ -75,7 +77,8 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
public unsafe ValueResult<bool, ValueFile> SaveFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters)
{
CoCreateInstance<FileSaveDialog, IFileSaveDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileSaveDialog dialog).ThrowOnFailure();
HRESULT hr = CoCreateInstance(in FileSaveDialog.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileDialog.IID, out IFileDialog* pFileDialog);
Marshal.ThrowExceptionForHR(hr);
FILEOPENDIALOGOPTIONS options =
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
@@ -83,25 +86,25 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
FILEOPENDIALOGOPTIONS.FOS_STRICTFILETYPES |
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
dialog.SetOptions(options);
SetDesktopAsStartupFolder(dialog);
if (!string.IsNullOrEmpty(title))
{
dialog.SetTitle(title);
}
pFileDialog->SetOptions(options);
SetDesktopAsStartupFolder(pFileDialog);
if (!string.IsNullOrEmpty(defaultFileName))
{
dialog.SetFileName(defaultFileName);
pFileDialog->SetFileName(defaultFileName);
}
if (!string.IsNullOrEmpty(title))
{
pFileDialog->SetTitle(title);
}
if (filters is { Length: > 0 })
{
SetFileTypes(dialog, filters);
SetFileTypes(pFileDialog, filters);
}
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
HRESULT res = pFileDialog->Show(currentWindowReference.GetWindowHandle());
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
{
return new(false, default);
@@ -111,18 +114,18 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
Marshal.ThrowExceptionForHR(res);
}
dialog.GetResult(out IShellItem item);
pFileDialog->GetResult(out IShellItem* pShellItem);
PWSTR displayName = default;
string file;
try
{
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
file = new((char*)displayName);
pShellItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
file = new(displayName);
}
finally
{
Marshal.FreeCoTaskMem((nint)displayName.Value);
CoTaskMemFree(displayName);
}
return new(true, file);
@@ -130,7 +133,8 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
public unsafe ValueResult<bool, string> PickFolder(string? title)
{
CoCreateInstance<FileOpenDialog, IFileOpenDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure();
HRESULT hr = CoCreateInstance(in FileOpenDialog.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileDialog.IID, out IFileDialog* pFileDialog);
Marshal.ThrowExceptionForHR(hr);
FILEOPENDIALOGOPTIONS options =
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
@@ -138,15 +142,15 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS |
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
dialog.SetOptions(options);
SetDesktopAsStartupFolder(dialog);
pFileDialog->SetOptions(options);
SetDesktopAsStartupFolder(pFileDialog);
if (!string.IsNullOrEmpty(title))
{
dialog.SetTitle(title);
pFileDialog->SetTitle(title);
}
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
HRESULT res = pFileDialog->Show(currentWindowReference.GetWindowHandle());
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
{
return new(false, default!);
@@ -156,25 +160,24 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
Marshal.ThrowExceptionForHR(res);
}
dialog.GetResult(out IShellItem item);
pFileDialog->GetResult(out IShellItem* pShellItem);
PWSTR displayName = default;
string file;
try
{
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
pShellItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
file = new((char*)displayName);
}
finally
{
Marshal.FreeCoTaskMem((nint)displayName.Value);
CoTaskMemFree(displayName);
}
return new(true, file);
}
private static unsafe void SetFileTypes<TDialog>(TDialog dialog, (string Name, string Type)[] filters)
where TDialog : IFileDialog
private static unsafe void SetFileTypes(IFileDialog* pFileDialog, (string Name, string Type)[] filters)
{
List<nint> unmanagedStringPtrs = new(filters.Length * 2);
List<COMDLG_FILTERSPEC> filterSpecs = new(filters.Length);
@@ -190,10 +193,7 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
filterSpecs.Add(spec);
}
fixed (COMDLG_FILTERSPEC* ptr = CollectionsMarshal.AsSpan(filterSpecs))
{
dialog.SetFileTypes((uint)filterSpecs.Count, ptr);
}
pFileDialog->SetFileTypes(CollectionsMarshal.AsSpan(filterSpecs));
foreach (ref readonly nint ptr in CollectionsMarshal.AsSpan(unmanagedStringPtrs))
{
@@ -201,11 +201,11 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
}
}
private static unsafe void SetDesktopAsStartupFolder<TDialog>(TDialog dialog)
where TDialog : IFileDialog
private static unsafe void SetDesktopAsStartupFolder(IFileDialog* pFileDialog)
{
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
SHCreateItemFromParsingName(desktopPath, default, typeof(IShellItem).GUID, out object shellItem).ThrowOnFailure();
dialog.SetFolder((IShellItem)shellItem);
HRESULT hr = SHCreateItemFromParsingName(desktopPath, default, in IShellItem.IID, out IShellItem* pShellItem);
Marshal.ThrowExceptionForHR(hr);
pFileDialog->SetFolder(pShellItem);
}
}

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Win32.UI.WindowsAndMessaging;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao;
@@ -32,9 +32,9 @@ internal sealed partial class GuideWindow : Window, IWindowOptionsSource, IMinMa
public unsafe void HandleMinMaxInfo(ref MINMAXINFO info, double scalingFactor)
{
info.ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.X);
info.ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.Y);
info.ptMaxTrackSize.X = (int)Math.Min(MaxWidth * scalingFactor, info.ptMaxTrackSize.X);
info.ptMaxTrackSize.Y = (int)Math.Min(MaxHeight * scalingFactor, info.ptMaxTrackSize.Y);
info.ptMinTrackSize.x = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.x);
info.ptMinTrackSize.y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.y);
info.ptMaxTrackSize.x = (int)Math.Min(MaxWidth * scalingFactor, info.ptMaxTrackSize.x);
info.ptMaxTrackSize.y = (int)Math.Min(MaxHeight * scalingFactor, info.ptMaxTrackSize.y);
}
}
}

View File

@@ -0,0 +1,19 @@
<Window
x:Class="Snap.Hutao.IdentifyMonitorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
mc:Ignorable="d">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="3">
<TextBlock Text="{shcm:ResourceString Name=WindowIdentifyMonitorHeader}"/>
<TextBlock
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Monitor}"
TextAlignment="Center"/>
</StackPanel>
</Window>

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Snap.Hutao.Win32;
using Windows.Graphics;
namespace Snap.Hutao;
internal sealed partial class IdentifyMonitorWindow : Window
{
public IdentifyMonitorWindow(DisplayArea displayArea, int index)
{
InitializeComponent();
Monitor = $"{displayArea.DisplayId.Value:X8}:{index}";
OverlappedPresenter presenter = OverlappedPresenter.Create();
presenter.SetBorderAndTitleBar(false, false);
presenter.IsAlwaysOnTop = true;
presenter.IsResizable = false;
AppWindow.SetPresenter(presenter);
PointInt32 point = new(40, 32);
SizeInt32 size = StructMarshal.SizeInt32(displayArea.WorkArea).Scale(0.1);
AppWindow.MoveAndResize(StructMarshal.RectInt32(point, size), displayArea);
}
public string Monitor { get; private set; }
}

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.ViewModel.Game;
using Windows.Win32.UI.WindowsAndMessaging;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao;
@@ -50,9 +50,9 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
/// <inheritdoc/>
public unsafe void HandleMinMaxInfo(ref MINMAXINFO info, double scalingFactor)
{
info.ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.X);
info.ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.Y);
info.ptMaxTrackSize.X = (int)Math.Min(MaxWidth * scalingFactor, info.ptMaxTrackSize.X);
info.ptMaxTrackSize.Y = (int)Math.Min(MaxHeight * scalingFactor, info.ptMaxTrackSize.Y);
info.ptMinTrackSize.x = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.x);
info.ptMinTrackSize.y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.y);
info.ptMaxTrackSize.x = (int)Math.Min(MaxWidth * scalingFactor, info.ptMaxTrackSize.x);
info.ptMaxTrackSize.y = (int)Math.Min(MaxHeight * scalingFactor, info.ptMaxTrackSize.y);
}
}

View File

@@ -2,18 +2,18 @@
x:Class="Snap.Hutao.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shv="using:Snap.Hutao.View"
mc:Ignorable="d">
<Grid>
<shv:MainView/>
<shv:TitleView
x:Name="TitleBarView"
Height="44"
Margin="48,0,0,0"/>
Margin="44,0,0,0"/>
<shv:MainView/>
</Grid>
</Window>

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Win32.UI.WindowsAndMessaging;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao;
@@ -37,7 +37,7 @@ internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMax
/// <inheritdoc/>
public unsafe void HandleMinMaxInfo(ref MINMAXINFO pInfo, double scalingFactor)
{
pInfo.ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo.ptMinTrackSize.X);
pInfo.ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo.ptMinTrackSize.Y);
pInfo.ptMinTrackSize.x = (int)Math.Max(MinWidth * scalingFactor, pInfo.ptMinTrackSize.x);
pInfo.ptMinTrackSize.y = (int)Math.Max(MinHeight * scalingFactor, pInfo.ptMinTrackSize.y);
}
}

View File

@@ -0,0 +1,624 @@
// <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
{
[DbContext(typeof(AppDbContext))]
[Migration("20240202015857_ImplReorderableOnUserAndGameAccount")]
partial class ImplReorderableOnUserAndGameAccount
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<uint>("Current")
.HasColumnType("INTEGER");
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CalculatorRefreshTime")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("GameRecordRefreshTime")
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ShowcaseRefreshTime")
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("cultivate_entries");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("AvatarLevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("AvatarLevelTo")
.HasColumnType("INTEGER");
b.Property<Guid>("EntryId")
.HasColumnType("TEXT");
b.Property<uint>("SkillALevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("SkillALevelTo")
.HasColumnType("INTEGER");
b.Property<uint>("SkillELevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("SkillELevelTo")
.HasColumnType("INTEGER");
b.Property<uint>("SkillQLevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("SkillQLevelTo")
.HasColumnType("INTEGER");
b.Property<uint>("WeaponLevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("WeaponLevelTo")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("EntryId")
.IsUnique();
b.ToTable("cultivate_entry_level_informations");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("Count")
.HasColumnType("INTEGER");
b.Property<Guid>("EntryId")
.HasColumnType("TEXT");
b.Property<bool>("IsFinished")
.HasColumnType("INTEGER");
b.Property<uint>("ItemId")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("EntryId");
b.ToTable("cultivate_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachedUid")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("cultivate_projects");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RefreshTime")
.HasColumnType("TEXT");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<uint>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("Count")
.HasColumnType("INTEGER");
b.Property<uint>("ItemId")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AppendPropIdList")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<int>("MainPropId")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_reliquaries");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<int>("PromoteLevel")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_weapons");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpireTime")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("object_cache");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("ScheduleId")
.HasColumnType("INTEGER");
b.Property<string>("SpiralAbyss")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("spiral_abysses");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Aid")
.HasColumnType("TEXT");
b.Property<string>("CookieToken")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CookieTokenLastUpdateTime")
.HasColumnType("TEXT");
b.Property<string>("Fingerprint")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("FingerprintLastUpdateTime")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<bool>("IsOversea")
.HasColumnType("INTEGER");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("LToken")
.HasColumnType("TEXT")
.HasColumnName("Ltoken");
b.Property<string>("Mid")
.HasColumnType("TEXT");
b.Property<string>("SToken")
.HasColumnType("TEXT")
.HasColumnName("Stoken");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
.WithOne("LevelInformation")
.HasForeignKey("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", "EntryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entry");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
.WithMany()
.HasForeignKey("EntryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entry");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.Navigation("LevelInformation");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,60 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
/// <inheritdoc />
public partial class ImplReorderableOnUserAndGameAccount : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_cultivate_entry_level_informations_EntryId",
table: "cultivate_entry_level_informations");
migrationBuilder.AddColumn<int>(
name: "Index",
table: "users",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "Index",
table: "game_accounts",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_cultivate_entry_level_informations_EntryId",
table: "cultivate_entry_level_informations",
column: "EntryId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_cultivate_entry_level_informations_EntryId",
table: "cultivate_entry_level_informations");
migrationBuilder.DropColumn(
name: "Index",
table: "users");
migrationBuilder.DropColumn(
name: "Index",
table: "game_accounts");
migrationBuilder.CreateIndex(
name: "IX_cultivate_entry_level_informations_EntryId",
table: "cultivate_entry_level_informations",
column: "EntryId");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
@@ -42,7 +42,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ArchiveId");
b.ToTable("achievements", (string)null);
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
@@ -60,7 +60,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId");
b.ToTable("achievement_archives", (string)null);
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
@@ -88,7 +88,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId");
b.ToTable("avatar_infos", (string)null);
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
@@ -110,7 +110,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId");
b.ToTable("cultivate_entries", (string)null);
b.ToTable("cultivate_entries");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
@@ -154,9 +154,10 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId");
b.HasIndex("EntryId");
b.HasIndex("EntryId")
.IsUnique();
b.ToTable("cultivate_entry_level_informations", (string)null);
b.ToTable("cultivate_entry_level_informations");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
@@ -181,7 +182,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("EntryId");
b.ToTable("cultivate_items", (string)null);
b.ToTable("cultivate_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
@@ -202,7 +203,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId");
b.ToTable("cultivate_projects", (string)null);
b.ToTable("cultivate_projects");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
@@ -258,7 +259,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("UserId");
b.ToTable("daily_notes", (string)null);
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
@@ -276,7 +277,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId");
b.ToTable("gacha_archives", (string)null);
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
@@ -307,7 +308,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ArchiveId");
b.ToTable("gacha_items", (string)null);
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
@@ -319,6 +320,9 @@ namespace Snap.Hutao.Migrations
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
@@ -332,7 +336,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId");
b.ToTable("game_accounts", (string)null);
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
@@ -354,7 +358,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId");
b.ToTable("inventory_items", (string)null);
b.ToTable("inventory_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
@@ -383,7 +387,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId");
b.ToTable("inventory_reliquaries", (string)null);
b.ToTable("inventory_reliquaries");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
@@ -408,7 +412,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId");
b.ToTable("inventory_weapons", (string)null);
b.ToTable("inventory_weapons");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
@@ -424,7 +428,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("Key");
b.ToTable("object_cache", (string)null);
b.ToTable("object_cache");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
@@ -437,7 +441,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("Key");
b.ToTable("settings", (string)null);
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
@@ -459,7 +463,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId");
b.ToTable("spiral_abysses", (string)null);
b.ToTable("spiral_abysses");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
@@ -483,6 +487,9 @@ namespace Snap.Hutao.Migrations
b.Property<DateTimeOffset>("FingerprintLastUpdateTime")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<bool>("IsOversea")
.HasColumnType("INTEGER");
@@ -502,7 +509,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId");
b.ToTable("users", (string)null);
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
@@ -530,8 +537,8 @@ namespace Snap.Hutao.Migrations
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
.WithMany()
.HasForeignKey("EntryId")
.WithOne("LevelInformation")
.HasForeignKey("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", "EntryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -603,6 +610,11 @@ namespace Snap.Hutao.Migrations
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.Navigation("LevelInformation");
});
#pragma warning restore 612, 618
}
}

View File

@@ -11,6 +11,12 @@ internal static class CollectionsNameValue
return [.. Enum.GetValues<TEnum>().Select(x => new NameValue<TEnum>(x.ToString(), x))];
}
public static List<NameValue<TEnum>> FromEnum<TEnum>(Func<TEnum, bool> codiction)
where TEnum : struct, Enum
{
return [.. Enum.GetValues<TEnum>().Where(codiction).Select(x => new NameValue<TEnum>(x.ToString(), x))];
}
public static List<NameValue<TSource>> From<TSource>(IEnumerable<TSource> sources, Func<TSource, string> nameSelector)
{
return [.. sources.Select(x => new NameValue<TSource>(nameSelector(x), x))];

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Primitive;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -14,7 +15,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("game_accounts")]
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string, SchemeType>
internal sealed class GameAccount : ObservableObject, IReorderable, IMappingFrom<GameAccount, string, string, SchemeType>
{
/// <summary>
/// 内部Id
@@ -44,6 +45,8 @@ internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount,
/// </summary>
public string MihoyoSDK { get; set; } = default!;
public int Index { get; set; }
public static GameAccount From(string name, string sdk, SchemeType type)
{
return new()

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("users")]
internal sealed class User : ISelectable, IMappingFrom<User, Cookie, bool>
internal sealed class User : ISelectable, IReorderable, IMappingFrom<User, Cookie, bool>
{
/// <summary>
/// 内部Id
@@ -69,6 +69,8 @@ internal sealed class User : ISelectable, IMappingFrom<User, Cookie, bool>
public DateTimeOffset CookieTokenLastUpdateTime { get; set; }
public int Index { get; set; }
/// <summary>
/// 创建一个新的用户
/// </summary>

View File

@@ -3,7 +3,7 @@
using Snap.Hutao.Core;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Model.InterChange.GachaLog;
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// UIGF格式的信息
/// </summary>
[HighQuality]
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, MetadataOptions, string>
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, CultureOptions, string>
{
/// <summary>
/// 用户Uid
@@ -65,12 +65,12 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
[JsonPropertyName("region_time_zone")]
public int? RegionTimeZone { get; set; } = default!;
public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid)
public static UIGFInfo From(RuntimeOptions runtimeOptions, CultureOptions cultureOptions, string uid)
{
return new()
{
Uid = uid,
Language = metadataOptions.LanguageCode,
Language = cultureOptions.LanguageCode,
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = runtimeOptions.Version.ToString(),

View File

@@ -64,14 +64,14 @@ internal sealed class UIIFInfo
/// <returns>专用 UIGF 信息</returns>
public static UIIFInfo From(IServiceProvider serviceProvider, string uid)
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
return new()
{
Uid = uid,
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = hutaoOptions.Version.ToString(),
ExportAppVersion = runtimeOptions.Version.ToString(),
UIIFVersion = UIIF.CurrentVersion,
};
}

View File

@@ -2,7 +2,9 @@
// Licensed under the MIT license.
using Snap.Hutao.Control;
using Snap.Hutao.Control.Text;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Primitive;
using System.Globalization;
@@ -50,8 +52,20 @@ internal sealed partial class DescriptionsParametersDescriptor : ValueConverter<
{
if (desc.AsSpan().TrySplitIntoTwo('|', out ReadOnlySpan<char> description, out ReadOnlySpan<char> format))
{
string resultFormatted = ParamRegex().Replace(format.ToString(), match => ReplaceParamInMatch(match, paramList));
results.Add(new ParameterDescription { Description = description.ToString(), Parameter = resultFormatted });
if (description[0] is not '#')
{
// Fast path
string resultFormatted = ParamRegex().Replace(format.ToString(), match => ReplaceParamInMatch(match, paramList));
results.Add(new ParameterDescription { Description = description.ToString(), Parameter = resultFormatted });
}
else
{
string descriptionString = SpecialNameHandler.Handle(description.ToString());
string formatString = SpecialNameHandler.Handle(format.ToString());
string resultFormatted = ParamRegex().Replace(formatString, match => ReplaceParamInMatch(match, paramList));
results.Add(new ParameterDescription { Description = descriptionString, Parameter = resultFormatted });
}
}
else
{

View File

@@ -3,8 +3,8 @@
using Microsoft.UI;
using Snap.Hutao.Control;
using Snap.Hutao.Core;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Win32;
using Windows.UI;
namespace Snap.Hutao.Model.Metadata.Converter;

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