mirror of
https://github.com/wangdage12/Snap.Hutao.git
synced 2026-02-18 02:42:15 +08:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
274b2766ce | ||
|
|
5019987c08 | ||
|
|
adaf0972ce | ||
|
|
b9169ad41a | ||
|
|
00af738520 | ||
|
|
3538665bc6 | ||
|
|
c1a1871284 | ||
|
|
01f965d260 | ||
|
|
cadd35a93e | ||
|
|
a83f119e8a | ||
|
|
2278c83dd2 | ||
|
|
867f8b6558 | ||
|
|
7e6a6a509c | ||
|
|
7aaa61dcf3 | ||
|
|
1fe5b4969e | ||
|
|
e52ed5470e | ||
|
|
4434d76e35 | ||
|
|
4a9c3229a0 | ||
|
|
1090dfa7c6 | ||
|
|
a4eb3d6f8a | ||
|
|
d9c43844b7 | ||
|
|
2cc2cc80ac | ||
|
|
cc926e4352 | ||
|
|
fa4b975f46 | ||
|
|
299cdee329 | ||
|
|
f49f40b5fd | ||
|
|
ff21a91cc6 | ||
|
|
cc47e050d1 | ||
|
|
c2d0156a9d | ||
|
|
eac314f7d9 | ||
|
|
f1132b79f0 | ||
|
|
63c4792e00 | ||
|
|
5f196253b3 | ||
|
|
a7bb931ea5 | ||
|
|
d318dcbfd0 | ||
|
|
e045413ac1 | ||
|
|
1039623cbf | ||
|
|
3f50507490 | ||
|
|
e55d6ea1c5 | ||
|
|
e96c9f1f49 | ||
|
|
f3d47aeffd | ||
|
|
7ee92db156 | ||
|
|
7c25951950 | ||
|
|
2ad17f60bf | ||
|
|
4fdea542b0 | ||
|
|
4d5e5342f2 | ||
|
|
fde960b7cc | ||
|
|
cfc7ba6274 | ||
|
|
c463f1809c | ||
|
|
3d352c1c12 | ||
|
|
fbf3c0c695 |
13
.github/FUNDING.yml
vendored
13
.github/FUNDING.yml
vendored
@@ -1,13 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: [DGP-Studio]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: snaphutao
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
custom: https://afdian.com/a/DismissedLight
|
|
||||||
2
.github/workflows/alpha.yml
vendored
2
.github/workflows/alpha.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
|||||||
runs-on: ${{ needs.select-runner.outputs.runner }}
|
runs-on: ${{ needs.select-runner.outputs.runner }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup .NET (self-hosted)
|
- name: Setup .NET (self-hosted)
|
||||||
if: ${{ needs.select-runner.outputs.runner == 'sjc1' }}
|
if: ${{ needs.select-runner.outputs.runner == 'sjc1' }}
|
||||||
|
|||||||
2
.github/workflows/canary.yml
vendored
2
.github/workflows/canary.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: develop
|
ref: develop
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
38
.github/workflows/msi-build.yml
vendored
Normal file
38
.github/workflows/msi-build.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Build MSI Installer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET SDK
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
- name: Install Wix Toolset 4
|
||||||
|
run: dotnet tool install --global wix --version 4.0.1
|
||||||
|
|
||||||
|
- name: Restore NuGet packages
|
||||||
|
run: dotnet restore src/Snap.Hutao/Snap.Hutao.slnx
|
||||||
|
|
||||||
|
- name: Build WinUI 3 project (self-contained)
|
||||||
|
run: dotnet build src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj -c Release
|
||||||
|
|
||||||
|
- name: Build MSI installer
|
||||||
|
run: dotnet build src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj -c Release
|
||||||
|
|
||||||
|
- name: Upload MSI Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Snap.Hutao-MSI
|
||||||
|
path: |
|
||||||
|
src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US/*.msi
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
desktop.ini
|
desktop.ini
|
||||||
|
|
||||||
*.csproj.user
|
*.csproj.user
|
||||||
*.DotSettings.user
|
*.DotSettings.user
|
||||||
@@ -25,3 +25,6 @@ src/Snap.Hutao/Snap.Hutao/Generated Files/
|
|||||||
tools/
|
tools/
|
||||||
|
|
||||||
src/Snap.Hutao/Snap.Hutao/AppPackages
|
src/Snap.Hutao/Snap.Hutao/AppPackages
|
||||||
|
/src/Snap.Hutao/Snap.Hutao.Installer/obj
|
||||||
|
/src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US
|
||||||
|
/src/Snap.Hutao/Snap.Hutao.Installer/bin
|
||||||
|
|||||||
198
README.md
198
README.md
@@ -1,54 +1,11 @@
|
|||||||
<p align="center">
|
|
||||||
<img src="https://github.com/user-attachments/assets/976e057c-f01e-486b-9fa0-04744ae96f99" alt="Snap Hutao Banner" width="600"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">Snap Hutao</h1>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
🎮 开源的原神工具箱,专为 Windows 平台设计,改善桌面端玩家的游戏体验
|
|
||||||
<br/>
|
|
||||||
🎮 An open-source Genshin Impact toolkit for Windows, designed to improve the desktop gaming experience
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="padding:0 10px;">
|
|
||||||
<b>Latest CI/CD Build</sub>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="padding:0 10px;">
|
|
||||||
<b>Latest Release</sub>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="padding:0 10px;">
|
|
||||||
<b>Downloads</sub>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="padding:0 10px;">
|
|
||||||
<a href="https://ci.appveyor.com/project/DGP-Studio/snap-hutao">
|
|
||||||
<img src="https://ci.appveyor.com/api/projects/status/n4s40t9llru4si9y?svg=true" alt="Build Status"/>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="padding:0 10px;">
|
|
||||||
<a href="https://github.com/DGP-Studio/Snap.Hutao/releases/latest">
|
|
||||||
<img src="https://img.shields.io/github/release/DGP-Studio/Snap.Hutao?style=flat" alt="Release"/>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center" style="padding:0 10px;">
|
|
||||||
<img src="https://img.shields.io/github/downloads/DGP-Studio/Snap.Hutao/total.svg?style=flat" alt="Downloads"/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📖 简介 / Introduction
|
## 📖 简介 / Introduction
|
||||||
|
|
||||||
**中文**
|
**中文**
|
||||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。
|
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。
|
||||||
|
|
||||||
|
该版本注入功能暂不可用,并且由于缺失资源和开发能力,不建议长期使用
|
||||||
|
|
||||||
**English**
|
**English**
|
||||||
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players.
|
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players.
|
||||||
|
|
||||||
@@ -56,89 +13,98 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
|||||||
|
|
||||||
## 🚀 安装 / Installation
|
## 🚀 安装 / Installation
|
||||||
|
|
||||||
**中文**
|
> 如果你的设备不支持ipv6,请下载末尾带有`ipv4`的压缩包,正常情况下请尽量下载普通包(服务器速度快)
|
||||||
你可以按照 [快速开始](https://hut.ao/zh/quick-start.html) 文档中提供的流程安装并设置 Snap Hutao。
|
|
||||||
|
|
||||||
**English**
|
目前 Sanp.Hutao.Rev 更新了打包方式,并采用了标准现代的 msi 安装,方便程序获取管理员权限和更多的功能设置,不再需要原 Depolyment
|
||||||
You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-start.html) document to install and set up Snap Hutao.
|
|
||||||
|
可以和之前的版本共存,将之前版本的数据文件夹里面的文件复制到该版本的数据文件夹中即可恢复数据
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌍 本地化翻译 / Localization
|
## 开发
|
||||||
|
项目启动位置已升级为 VS2026 的 slnx 格式 Snap.Hutao\src\Snap.Hutao\Snap.Hutao.slnx
|
||||||
|
> [!WARNING]
|
||||||
|
> 要使该项目可以长期运行,我们需要以下资源
|
||||||
|
> 1. `src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DataSigning/SaltConstants.cs`中的新签名值
|
||||||
|
> 2. 元数据的编写
|
||||||
|
> 3. 图片资源
|
||||||
|
|
||||||
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
|
V6.2的元数据已在编写中
|
||||||
|
仓库位置:http://server.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata.Test
|
||||||
|
**目前元数据的编写进度:**
|
||||||
|
|
||||||
Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translation platform where you can submit translated text for languages you are familiar with. We are grateful to every community member who has contributed to Snap Hutao and welcome more friends to participate in this project.
|
| 项目(V6.2) | 是否完成 |
|
||||||
|
| ----------- | ----------- |
|
||||||
|
| 新角色的基本数据 | ✔️ |
|
||||||
|
| 新版本角色/怪物基础数值 | ❔ |
|
||||||
|
| 新角色的详细资料、名片等 | ❌ |
|
||||||
|
| 新武器 | ✔️ |
|
||||||
|
| 新材料 | ❇️ |
|
||||||
|
| 新怪物 | ❌ |
|
||||||
|
| 新圣遗物 | / |
|
||||||
|
| 新卡池 | ❇️ |
|
||||||
|
| 新成就 | ❌ |
|
||||||
|
| 深境螺旋 | 💠 |
|
||||||
|
| 幻想真境剧诗 | 💠 |
|
||||||
|
| 幽境危战 | ❌ |
|
||||||
|
|
||||||
| Language | Status |
|
✔️:已完成
|
||||||
|----------|--------|
|
❌:未编写
|
||||||
| zh-TW | [](https://crowdin.com/project/snap-hutao) |
|
❇️:编写中
|
||||||
| en | [](https://crowdin.com/project/snap-hutao) |
|
❔:数据暂时无法得到
|
||||||
| fr | [](https://crowdin.com/project/snap-hutao) |
|
/ :似乎不需要变动
|
||||||
| id | [](https://crowdin.com/project/snap-hutao) |
|
💠:低优先级,以后编写
|
||||||
| ja | [](https://crowdin.com/project/snap-hutao) |
|
|
||||||
| ko | [](https://crowdin.com/project/snap-hutao) |
|
**若需编译项目,请使用[Visual Studio 2026](https://visualstudio.microsoft.com/zh-hans/)**
|
||||||
| pt-PT | [](https://crowdin.com/project/snap-hutao) |
|
调试选项请选择unpackaged(不打包)
|
||||||
| ru | [](https://crowdin.com/project/snap-hutao) |
|
**原开发文档现在还可使用(其中的AI功能很好用),以下是开发文档链接:**
|
||||||
| vi | [](https://crowdin.com/project/snap-hutao) |
|
|
||||||
|
https://deepwiki.com/DGP-Studio/Snap.Hutao
|
||||||
|
|
||||||
|
https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
|
||||||
|
## 打包测试
|
||||||
|
|
||||||
|
由于采用了 wix 进行打包程序,VS 需要安装 **HeatWave for VS2022**(2026兼容)。需要 msi 安装包时,右键选中 Snap.Hutao.Installer 生成后即可在目标目录找到。默认目录:Snap.Hutao.Installer\bin\x64\Release\en-US\Snap.Hutao.Installer.msi
|
||||||
|
|
||||||
|
### 资源
|
||||||
|
|
||||||
|
> 注意:普通包的资源服务器只能使用ipv6连接,也就是说,你的电脑必须有ipv6,并且建议你手动配置DNS为`223.5.5.5`
|
||||||
|
> 如果你的设备不支持ipv6,请下载末尾带有`ipv4`的压缩包
|
||||||
|
> 由于数据文件夹中有元数据的仓库和图片缓存,才得以恢复资源文件
|
||||||
|
> 如果你发现之前版本可以显示的图片不能显示了,请查找旧数据文件夹
|
||||||
|
> `C:\Users\<用户名>\AppData\Local\Packages\xxxDGPStudio.SnapHutao_xxx\LocalCache\ImageCache`
|
||||||
|
> 并将`ImageCache`文件夹提供给我,我会尽力恢复资源
|
||||||
|
|
||||||
|
[服务器状态页面](http://serverjp.wdg.cloudns.ch:3001/status/hts)
|
||||||
|
|
||||||
|
**元数据仓库:**
|
||||||
|
https://github.com/wangdage12/Snap.Metadata
|
||||||
|
|
||||||
|
镜像:
|
||||||
|

|
||||||
|
|
||||||
|
http://server.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
http://serverjp.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ 贡献 / Contribute
|
**临时API:**
|
||||||
|
|
||||||
- [向我们提交 PR / Make Pull Requests](https://hut.ao/development/contribute.html)
|

|
||||||
- [为我们更新文档 / Enhance our Document](https://github.com/DGP-Studio/Snap.Hutao.Docs)
|
|
||||||
- [通过 DeepWiKi 了解项目结构 / Understand Project Structure with DeepWiKi](https://deepwiki.com/DGP-Studio/Snap.Hutao)
|
http://server.wdg.cloudns.ch:5222/
|
||||||
- [](https://deepwiki.com/DGP-Studio/Snap.Hutao)
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
http://serverjp.wdg.cloudns.ch:5222/
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🙏 特别感谢 / Special Thanks
|
**临时资源站:**
|
||||||
|
http://server.wdg.cloudns.ch:8007/
|
||||||
|
|
||||||
- [HolographicHat](https://github.com/HolographicHat)
|
http://serverjp.wdg.cloudns.ch:8001/
|
||||||
- [UIGF organization](https://uigf.org)
|
|
||||||
|
|
||||||
**特定的原神项目 / Specific Genshin-related Projects**
|
|
||||||
- [Scighost/Starward](https://github.com/Scighost/Starward)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ 使用的技术栈 / Tech Stack
|
|
||||||
|
|
||||||
- [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
|
|
||||||
- [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
|
|
||||||
- [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
|
|
||||||
- [dotnet/efcore](https://github.com/dotnet/efcore)
|
|
||||||
- [dotnet/runtime](https://github.com/dotnet/runtime)
|
|
||||||
- [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)
|
|
||||||
- [quartznet/quartznet](https://github.com/quartznet/quartznet)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ❤️ 赞助商 / Sponsorship
|
|
||||||
|
|
||||||
Snap Hutao is currently using sponsored software from the following service providers.
|
|
||||||
|
|
||||||
<img src="./res/assets/readmeSponsors.svg" alt="Readme Sponsors" />
|
|
||||||
|
|
||||||
- 🏠 [Netlify](https://www.netlify.com/) provides document and home page hosting service for Snap Hutao
|
|
||||||
- 🌍 [Crowdin](https://crowdin.com/) provides its SaaS platform to help Snap Hutao's localization
|
|
||||||
- 🗄️ [Navicat](https://navicat.com/) provides Snap Hutao with advanced database management tools
|
|
||||||
- 🔒 Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
|
|
||||||
- 🔑 [1Password](https://1password.com/) provides Snap Hutao development team with their amazing password management software
|
|
||||||
- 🐳 [DigitalOcean](https://www.digitalocean.com) provides reliable cloud database and container service for Snap Hutao database backup
|
|
||||||
- 📊 [Ducalis.io](https://hi.ducalis.io/) provides Snap Hutao project with a complete decision-making toolkit for project management
|
|
||||||
- ☁️ [Cloudflare](https://www.cloudflare.com/) sponsors Snap Hutao with their Business Plan, ensuring secure, fast, and reliable worldwide connection to our infrastructure
|
|
||||||
- 🔐 [Termius](https://termius.com) provides a secure, reliable, and collaborative SSH client
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 开发 / Development
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
|
|
||||||
|
|
||||||
[](https://github.com/DGP-Studio/Snap.Hutao)
|
|
||||||
|
|||||||
3
src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs
Normal file
3
src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||||
|
|
||||||
|
</Wix>
|
||||||
8
src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl
Normal file
8
src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!--
|
||||||
|
This file contains the declaration of all the localizable strings.
|
||||||
|
-->
|
||||||
|
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
|
||||||
|
|
||||||
|
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
|
||||||
|
|
||||||
|
</WixLocalization>
|
||||||
79
src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs
Normal file
79
src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||||
|
<Package
|
||||||
|
Name="Snap.Hutao"
|
||||||
|
Manufacturer="Millennium Science Technology R-D Inst"
|
||||||
|
Version="1.0.0.0"
|
||||||
|
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
|
||||||
|
Scope="perMachine">
|
||||||
|
|
||||||
|
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
||||||
|
<MediaTemplate EmbedCab="yes" />
|
||||||
|
|
||||||
|
<Feature Id="ProductFeature" Title="Snap.Hutao" Level="1">
|
||||||
|
<ComponentGroupRef Id="MainAppComponents" />
|
||||||
|
|
||||||
|
<!-- 快捷方式组件 -->
|
||||||
|
<ComponentRef Id="ApplicationShortcut" />
|
||||||
|
<ComponentRef Id="DesktopShortcut" />
|
||||||
|
</Feature>
|
||||||
|
</Package>
|
||||||
|
|
||||||
|
<!-- 安装目录 -->
|
||||||
|
<Fragment>
|
||||||
|
<StandardDirectory Id="ProgramFiles64Folder">
|
||||||
|
<Directory Id="INSTALLFOLDER" Name="Snap.Hutao" />
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<StandardDirectory Id="ProgramMenuFolder">
|
||||||
|
<Directory Id="ApplicationProgramsFolder" Name="Snap Hutao" />
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<StandardDirectory Id="DesktopFolder" />
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<!-- 桌面快捷方式 -->
|
||||||
|
<Fragment>
|
||||||
|
<Component Id="DesktopShortcut" Directory="DesktopFolder" Guid="*">
|
||||||
|
|
||||||
|
<Shortcut
|
||||||
|
Id="DesktopShortcut_Normal"
|
||||||
|
Name="Snap Hutao"
|
||||||
|
Description="Snap Hutao Client"
|
||||||
|
Target="[INSTALLFOLDER]Snap.Hutao.exe"
|
||||||
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
|
|
||||||
|
<!-- KeyPath 必须是 HKCU,因为快捷方式安装到用户目录 -->
|
||||||
|
<RegistryValue
|
||||||
|
Root="HKCU"
|
||||||
|
Key="Software\Snap.Hutao"
|
||||||
|
Name="DesktopShortcutInstalled"
|
||||||
|
Type="integer"
|
||||||
|
Value="1"
|
||||||
|
KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<!-- 开始菜单快捷方式 -->
|
||||||
|
<Fragment>
|
||||||
|
<Component Id="ApplicationShortcut" Directory="ApplicationProgramsFolder" Guid="*">
|
||||||
|
|
||||||
|
<Shortcut
|
||||||
|
Id="ApplicationStartMenuShortcut"
|
||||||
|
Name="Snap Hutao"
|
||||||
|
Description="Snap Hutao Client"
|
||||||
|
Target="[INSTALLFOLDER]Snap.Hutao.exe"
|
||||||
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
|
|
||||||
|
<RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
||||||
|
|
||||||
|
<!-- KeyPath 依然必须改为 HKCU -->
|
||||||
|
<RegistryValue
|
||||||
|
Root="HKCU"
|
||||||
|
Key="Software\Snap.Hutao"
|
||||||
|
Name="StartMenuShortcutInstalled"
|
||||||
|
Type="integer"
|
||||||
|
Value="1"
|
||||||
|
KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</Fragment>
|
||||||
|
</Wix>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="WixToolset.Sdk/6.0.2">
|
||||||
|
<PropertyGroup>
|
||||||
|
<SuppressIces>ICE03;ICE60</SuppressIces>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Snap.Hutao\Snap.Hutao.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<HarvestDirectory Include="..\Snap.Hutao\bin\Release\net10.0-windows10.0.26100.0\win-x64">
|
||||||
|
<ComponentGroupName>MainAppComponents</ComponentGroupName>
|
||||||
|
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
|
||||||
|
<SuppressCom>true</SuppressCom>
|
||||||
|
<SuppressRegistry>true</SuppressRegistry>
|
||||||
|
<SuppressRootDirectory>true</SuppressRootDirectory>
|
||||||
|
</HarvestDirectory>
|
||||||
|
|
||||||
|
<PackageReference Include="WixToolset.Heat" Version="4.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
# Snap.Hutao.SourceGeneration
|
# Snap.Hutao.SourceGeneration
|
||||||
|
|
||||||
|
> 生成器包的备份,目前还可以从nuget上获取,所以暂时不需要使用该目录
|
||||||
|
> https://www.nuget.org/packages/Snap.Hutao.SourceGeneration/1.3.14
|
||||||
|
|
||||||
Source Code Generator for Snap.Hutao
|
Source Code Generator for Snap.Hutao
|
||||||
|
|
||||||
# Development Guideline
|
# Development Guideline
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Configurations>
|
<Configurations>
|
||||||
<Platform Name="Any CPU" />
|
<Platform Name="Any CPU" />
|
||||||
|
<Platform Name="ARM64" />
|
||||||
<Platform Name="x64" />
|
<Platform Name="x64" />
|
||||||
|
<Platform Name="x86" />
|
||||||
</Configurations>
|
</Configurations>
|
||||||
<Folder Name="/Solution Items/">
|
<Folder Name="/Solution Items/">
|
||||||
<File Path=".editorconfig" />
|
<File Path=".editorconfig" />
|
||||||
<File Path=".vsconfig" />
|
<File Path=".vsconfig" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj" />
|
<Project Path="Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj" Type="b7dd6f7e-def8-4e67-b5b7-07ef123db6f0" Id="91a04cd0-28cc-4562-92e1-202bc163edd7">
|
||||||
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
|
|
||||||
<!-- For Rider -->
|
|
||||||
<Configuration Solution="Debug|Any CPU" Project="Debug|x64|Deploy" />
|
|
||||||
<Configuration Solution="Debug|x64" Project="Debug|x64|Deploy" />
|
|
||||||
<Configuration Solution="Release|Any CPU" Project="Release|x64|Deploy" />
|
|
||||||
<Configuration Solution="Release|x64" Project="Release|x64|Deploy" />
|
|
||||||
<!-- For Visual Studio -->
|
|
||||||
<Platform Solution="*|Any CPU" Project="x64" />
|
<Platform Solution="*|Any CPU" Project="x64" />
|
||||||
<Platform Solution="*|arm64" Project="arm64" />
|
</Project>
|
||||||
<Platform Solution="*|x64" Project="x64" />
|
<Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj">
|
||||||
<Platform Solution="*|x86" Project="x86" />
|
<Build Solution="*|ARM64" Project="false" />
|
||||||
<Deploy />
|
<Build Solution="*|x86" Project="false" />
|
||||||
|
</Project>
|
||||||
|
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
|
||||||
|
<Platform Project="x64" />
|
||||||
|
<Deploy Solution="*|Any CPU" />
|
||||||
|
<Deploy Solution="*|x64" />
|
||||||
</Project>
|
</Project>
|
||||||
</Solution>
|
</Solution>
|
||||||
@@ -13,6 +13,7 @@ using Snap.Hutao.Service;
|
|||||||
using Snap.Hutao.UI.Xaml;
|
using Snap.Hutao.UI.Xaml;
|
||||||
using Snap.Hutao.UI.Xaml.Control.Theme;
|
using Snap.Hutao.UI.Xaml.Control.Theme;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Snap.Hutao;
|
namespace Snap.Hutao;
|
||||||
|
|
||||||
@@ -64,6 +65,11 @@ public sealed partial class App : Application
|
|||||||
|
|
||||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
|
// ⚠️ 添加启动诊断
|
||||||
|
#if DEBUG
|
||||||
|
Core.ApplicationModel.PackageIdentityDiagnostics.LogDiagnostics();
|
||||||
|
#endif
|
||||||
|
|
||||||
DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT();
|
DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT();
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -71,17 +77,40 @@ public sealed partial class App : Application
|
|||||||
// Important: You must call AppNotificationManager::Default().Register
|
// Important: You must call AppNotificationManager::Default().Register
|
||||||
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
|
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
|
||||||
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
|
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
|
||||||
AppNotificationManager.Default.Register();
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AppNotificationManager.Default.Register();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// In unpackaged mode, this might fail - continue anyway
|
||||||
|
}
|
||||||
|
|
||||||
// E_INVALIDARG E_OUTOFMEMORY
|
// E_INVALIDARG E_OUTOFMEMORY
|
||||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
AppActivationArguments? activatedEventArgs = null;
|
||||||
|
PrivateNamedPipeClient? namedPipeClient = null;
|
||||||
|
|
||||||
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
|
try
|
||||||
{
|
{
|
||||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
|
activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
namedPipeClient = serviceProvider.GetRequiredService<PrivateNamedPipeClient>();
|
||||||
Exit();
|
}
|
||||||
return;
|
catch
|
||||||
|
{
|
||||||
|
// In unpackaged mode, AppInstance might not work
|
||||||
|
// Create a default activation argument for launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activatedEventArgs is not null && namedPipeClient is not null)
|
||||||
|
{
|
||||||
|
if (namedPipeClient.TryRedirectActivationTo(activatedEventArgs))
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
|
||||||
|
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
||||||
|
Exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation($"{ConsoleBanner}");
|
logger.LogInformation($"{ConsoleBanner}");
|
||||||
@@ -90,10 +119,30 @@ public sealed partial class App : Application
|
|||||||
|
|
||||||
// Manually invoke
|
// Manually invoke
|
||||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application"));
|
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application"));
|
||||||
activation.ActivateAndInitialize(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
|
||||||
|
HutaoActivationArguments hutaoArgs = activatedEventArgs is not null
|
||||||
|
? HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs)
|
||||||
|
: HutaoActivationArguments.CreateDefaultLaunchArguments();
|
||||||
|
|
||||||
|
activation.ActivateAndInitialize(hutaoArgs);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
// ⚠️ 添加更详细的异常日志
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string errorPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Hutao",
|
||||||
|
"startup_error.txt");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(errorPath)!);
|
||||||
|
File.WriteAllText(errorPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Startup Error:\n{ex}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
SentrySdk.CaptureException(ex);
|
SentrySdk.CaptureException(ex);
|
||||||
SentrySdk.Flush();
|
SentrySdk.Flush();
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,15 @@ public static partial class Bootstrap
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
private static void Main(string[] args)
|
private static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Starting...");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (Mutex.TryOpenExisting(LockName, out _))
|
if (Mutex.TryOpenExisting(LockName, out _))
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Another instance is running");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,9 +49,16 @@ public static partial class Bootstrap
|
|||||||
mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow));
|
mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow));
|
||||||
mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity);
|
mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity);
|
||||||
Debug.Assert(created);
|
Debug.Assert(created);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Mutex created");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
catch (WaitHandleCannotBeOpenedException)
|
catch (WaitHandleCannotBeOpenedException)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] WaitHandleCannotBeOpenedException");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,34 +68,70 @@ public static partial class Bootstrap
|
|||||||
{
|
{
|
||||||
if (!OSPlatformSupported())
|
if (!OSPlatformSupported())
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] OS not supported");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Setting environment variables");
|
||||||
|
#endif
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000");
|
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000");
|
||||||
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128");
|
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128");
|
||||||
AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false);
|
AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing COM wrappers");
|
||||||
|
#endif
|
||||||
|
|
||||||
ComWrappersSupport.InitializeComWrappers();
|
ComWrappersSupport.InitializeComWrappers();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing DI container");
|
||||||
|
#endif
|
||||||
|
|
||||||
// By adding the using statement, we can dispose the injected services when closing
|
// By adding the using statement, we can dispose the injected services when closing
|
||||||
using (ServiceProvider serviceProvider = DependencyInjection.Initialize())
|
using (ServiceProvider serviceProvider = DependencyInjection.Initialize())
|
||||||
{
|
{
|
||||||
Thread.CurrentThread.Name = "Snap Hutao Application Main Thread";
|
Thread.CurrentThread.Name = "Snap Hutao Application Main Thread";
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Calling Application.Start()");
|
||||||
|
#endif
|
||||||
|
|
||||||
// If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging
|
// If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging
|
||||||
// You can delete bin and obj folder and then rebuild.
|
// You can delete bin and obj folder and then rebuild.
|
||||||
// In a Desktop app this runs a message pump internally,
|
// In a Desktop app this runs a message pump internally,
|
||||||
// and does not return until the application shuts down.
|
// and does not return until the application shuts down.
|
||||||
Application.Start(AppInitializationCallback);
|
Application.Start(AppInitializationCallback);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Application.Start() returned");
|
||||||
|
#endif
|
||||||
|
|
||||||
XamlApplicationLifetime.Exited = true;
|
XamlApplicationLifetime.Exited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Flushing Sentry");
|
||||||
|
#endif
|
||||||
|
|
||||||
SentrySdk.Flush();
|
SentrySdk.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Exiting");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void InitializeApp(ApplicationInitializationCallbackParams param)
|
private static void InitializeApp(ApplicationInitializationCallbackParams param)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Callback invoked");
|
||||||
|
#endif
|
||||||
|
|
||||||
Gen2GcCallback.Register(() =>
|
Gen2GcCallback.Register(() =>
|
||||||
{
|
{
|
||||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime"));
|
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime"));
|
||||||
@@ -90,8 +140,17 @@ public static partial class Bootstrap
|
|||||||
|
|
||||||
IServiceProvider serviceProvider = Ioc.Default;
|
IServiceProvider serviceProvider = Ioc.Default;
|
||||||
|
|
||||||
_ = serviceProvider.GetRequiredService<ITaskContext>();
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Creating App instance");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ⚠️ 只创建 App
|
||||||
|
// TaskContext 将在第一次被需要时自动创建(延迟初始化)
|
||||||
_ = serviceProvider.GetRequiredService<App>();
|
_ = serviceProvider.GetRequiredService<App>();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Initialization complete (TaskContext will be lazily created)");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool OSPlatformSupported()
|
private static bool OSPlatformSupported()
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.ApplicationModel;
|
namespace Snap.Hutao.Core.ApplicationModel;
|
||||||
|
|
||||||
internal static class LimitedAccessFeatures
|
internal static class LimitedAccessFeatures
|
||||||
{
|
{
|
||||||
private static readonly string PackagePublisherId = Package.Current.Id.PublisherId;
|
private static readonly string PackagePublisherId = PackageIdentityAdapter.PublisherId;
|
||||||
private static readonly string PackageFamilyName = Package.Current.Id.FamilyName;
|
private static readonly string PackageFamilyName = PackageIdentityAdapter.FamilyName;
|
||||||
|
|
||||||
private static readonly FrozenDictionary<string, string> Features = WinRTAdaptive.ToFrozenDictionary(
|
private static readonly FrozenDictionary<string, string> Features = WinRTAdaptive.ToFrozenDictionary(
|
||||||
[
|
[
|
||||||
@@ -67,8 +66,15 @@ internal static class LimitedAccessFeatures
|
|||||||
KeyValuePair.Create("com.microsoft.windows.windowdecorations", "425261a8-7f73-4319-8a53-fc13f87e1717")
|
KeyValuePair.Create("com.microsoft.windows.windowdecorations", "425261a8-7f73-4319-8a53-fc13f87e1717")
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public static LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId)
|
public static Windows.ApplicationModel.LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId)
|
||||||
{
|
{
|
||||||
|
if (!PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
// In unpackaged mode, we can't unlock limited access features
|
||||||
|
// Create a dummy result - actual implementation will handle the failure
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId));
|
return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.ApplicationModel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adapter to handle both packaged and unpackaged app scenarios
|
||||||
|
/// </summary>
|
||||||
|
internal static class PackageIdentityAdapter
|
||||||
|
{
|
||||||
|
private static readonly Lazy<bool> LazyHasPackageIdentity = new(CheckPackageIdentity);
|
||||||
|
private static readonly Lazy<string> LazyAppDirectory = new(GetAppDirectoryPath);
|
||||||
|
private static readonly Lazy<Version> LazyAppVersion = new(GetAppVersionInternal);
|
||||||
|
private static readonly Lazy<string> LazyFamilyName = new(GetFamilyNameInternal);
|
||||||
|
private static readonly Lazy<string> LazyPublisherId = new(GetPublisherIdInternal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the app has package identity
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasPackageIdentity => LazyHasPackageIdentity.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get application installation directory
|
||||||
|
/// </summary>
|
||||||
|
public static string AppDirectory => LazyAppDirectory.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get application version
|
||||||
|
/// </summary>
|
||||||
|
public static Version AppVersion => LazyAppVersion.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get package family name (or fallback for unpackaged)
|
||||||
|
/// </summary>
|
||||||
|
public static string FamilyName => LazyFamilyName.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get publisher ID (or fallback for unpackaged)
|
||||||
|
/// </summary>
|
||||||
|
public static string PublisherId => LazyPublisherId.Value;
|
||||||
|
|
||||||
|
private static bool CheckPackageIdentity()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Try to access Package.Current - if it throws, we don't have package identity
|
||||||
|
_ = Windows.ApplicationModel.Package.Current.Id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAppDirectoryPath()
|
||||||
|
{
|
||||||
|
if (HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use the exe directory
|
||||||
|
string? exePath = Process.GetCurrentProcess().MainModule?.FileName;
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(exePath);
|
||||||
|
string? directory = Path.GetDirectoryName(exePath);
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Version GetAppVersionInternal()
|
||||||
|
{
|
||||||
|
if (HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return Windows.ApplicationModel.Package.Current.Id.Version.ToVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use assembly version
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
Version? version = assembly.GetName().Version;
|
||||||
|
return version ?? new Version(1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFamilyNameInternal()
|
||||||
|
{
|
||||||
|
if (HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return Windows.ApplicationModel.Package.Current.Id.FamilyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use a deterministic fallback
|
||||||
|
return "Snap.Hutao.Unpackaged";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPublisherIdInternal()
|
||||||
|
{
|
||||||
|
if (HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return Windows.ApplicationModel.Package.Current.Id.PublisherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use a fallback
|
||||||
|
return "CN=Millennium-Science-Technology-R-D-Inst";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.ApplicationModel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diagnostic helper for PackageIdentityAdapter
|
||||||
|
/// </summary>
|
||||||
|
internal static class PackageIdentityDiagnostics
|
||||||
|
{
|
||||||
|
public static void LogDiagnostics()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string logPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Hutao",
|
||||||
|
"startup_diagnostics.txt");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(logPath)!);
|
||||||
|
|
||||||
|
using (StreamWriter writer = File.CreateText(logPath))
|
||||||
|
{
|
||||||
|
writer.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Startup Diagnostics");
|
||||||
|
writer.WriteLine($"HasPackageIdentity: {PackageIdentityAdapter.HasPackageIdentity}");
|
||||||
|
writer.WriteLine($"AppVersion: {PackageIdentityAdapter.AppVersion}");
|
||||||
|
writer.WriteLine($"AppDirectory: {PackageIdentityAdapter.AppDirectory}");
|
||||||
|
writer.WriteLine($"FamilyName: {PackageIdentityAdapter.FamilyName}");
|
||||||
|
writer.WriteLine($"PublisherId: {PackageIdentityAdapter.PublisherId}");
|
||||||
|
writer.WriteLine("---");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"Diagnostics written to: {logPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Failed to write diagnostics: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Model.Entity.Database;
|
using Snap.Hutao.Model.Entity.Database;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -17,7 +18,20 @@ internal sealed partial class HutaoDiagnostics : IHutaoDiagnostics
|
|||||||
[GeneratedConstructor]
|
[GeneratedConstructor]
|
||||||
public partial HutaoDiagnostics(IServiceProvider serviceProvider);
|
public partial HutaoDiagnostics(IServiceProvider serviceProvider);
|
||||||
|
|
||||||
public ApplicationDataContainer LocalSettings { get => ApplicationData.Current.LocalSettings; }
|
public ApplicationDataContainer? LocalSettings
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return ApplicationData.Current.LocalSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In unpackaged mode, ApplicationDataContainer is not available
|
||||||
|
// Return null - scripting/diagnostics code should handle this gracefully
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask<int> ExecuteSqlAsync(string sql)
|
public async ValueTask<int> ExecuteSqlAsync(string sql)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Diagnostics;
|
|||||||
[SuppressMessage("", "SH001", Justification = "IHutaoDiagnostics must be public in order to be exposed to the scripting environment")]
|
[SuppressMessage("", "SH001", Justification = "IHutaoDiagnostics must be public in order to be exposed to the scripting environment")]
|
||||||
public interface IHutaoDiagnostics
|
public interface IHutaoDiagnostics
|
||||||
{
|
{
|
||||||
ApplicationDataContainer LocalSettings { get; }
|
ApplicationDataContainer? LocalSettings { get; }
|
||||||
|
|
||||||
ValueTask<int> ExecuteSqlAsync(string sql);
|
ValueTask<int> ExecuteSqlAsync(string sql);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using Microsoft.Windows.AppNotifications;
|
using Microsoft.Windows.AppNotifications;
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Core.IO;
|
using Snap.Hutao.Core.IO;
|
||||||
using Snap.Hutao.Core.IO.Hashing;
|
using Snap.Hutao.Core.IO.Hashing;
|
||||||
@@ -13,31 +14,32 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
using Windows.Storage;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core;
|
namespace Snap.Hutao.Core;
|
||||||
|
|
||||||
internal static class HutaoRuntime
|
internal static class HutaoRuntime
|
||||||
{
|
{
|
||||||
public static Version Version { get; } = Package.Current.Id.Version.ToVersion();
|
public static Version Version { get; } = PackageIdentityAdapter.AppVersion;
|
||||||
|
|
||||||
public static string UserAgent { get; } = $"Snap Hutao/{Version}";
|
public static string UserAgent { get; } = $"Snap Hutao/{Version}";
|
||||||
|
|
||||||
public static string DataDirectory { get; } = InitializeDataDirectory();
|
public static string DataDirectory { get; } = InitializeDataDirectory();
|
||||||
|
|
||||||
public static string LocalCacheDirectory { get; } = ApplicationData.Current.LocalCacheFolder.Path;
|
public static string LocalCacheDirectory { get; } = InitializeLocalCacheDirectory();
|
||||||
|
|
||||||
public static string FamilyName { get; } = Package.Current.Id.FamilyName;
|
public static string FamilyName { get; } = PackageIdentityAdapter.FamilyName;
|
||||||
|
|
||||||
public static string DeviceId { get; } = InitializeDeviceId();
|
public static string DeviceId { get; } = InitializeDeviceId();
|
||||||
|
|
||||||
public static WebView2Version WebView2Version { get; } = InitializeWebView2();
|
public static WebView2Version WebView2Version { get; } = InitializeWebView2();
|
||||||
|
|
||||||
public static bool IsProcessElevated { get; } = LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess;
|
// ⚠️ 延迟初始化以避免循环依赖
|
||||||
|
private static readonly Lazy<bool> LazyIsProcessElevated = new(GetIsProcessElevated);
|
||||||
|
|
||||||
|
public static bool IsProcessElevated => LazyIsProcessElevated.Value;
|
||||||
|
|
||||||
// Requires main thread
|
// Requires main thread
|
||||||
public static bool IsAppNotificationEnabled { get; } = AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled;
|
public static bool IsAppNotificationEnabled { get; } = CheckAppNotificationEnabled();
|
||||||
|
|
||||||
public static string? GetDisplayName()
|
public static string? GetDisplayName()
|
||||||
{
|
{
|
||||||
@@ -106,32 +108,57 @@ internal static class HutaoRuntime
|
|||||||
return string.Intern(directory);
|
return string.Intern(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string InitializeDataDirectory()
|
private static bool GetIsProcessElevated()
|
||||||
{
|
{
|
||||||
// Delete the previous data folder if it exists
|
// ⚠️ 这里调用 LocalSetting 时,确保 DataDirectory 已经初始化完成
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string previousDirectory = LocalSetting.Get(SettingKeys.PreviousDataDirectoryToDelete, string.Empty);
|
return LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess;
|
||||||
if (!string.IsNullOrEmpty(previousDirectory) && Directory.Exists(previousDirectory))
|
|
||||||
{
|
|
||||||
Directory.SetReadOnly(previousDirectory, false);
|
|
||||||
Directory.Delete(previousDirectory, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
catch
|
||||||
{
|
{
|
||||||
LocalSetting.Set(SettingKeys.PreviousDataDirectoryToDelete, string.Empty);
|
// 如果读取失败,使用默认值
|
||||||
|
return Environment.IsPrivilegedProcess;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the preferred path is set
|
private static string InitializeLocalCacheDirectory()
|
||||||
string currentDirectory = LocalSetting.Get(SettingKeys.DataDirectory, string.Empty);
|
{
|
||||||
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
if (!string.IsNullOrEmpty(currentDirectory))
|
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(currentDirectory);
|
return Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path;
|
||||||
return currentDirectory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use %LOCALAPPDATA%\Snap.Hutao\Cache
|
||||||
|
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
const string FolderName
|
||||||
|
#if IS_ALPHA_BUILD
|
||||||
|
= "HutaoAlpha";
|
||||||
|
#elif IS_CANARY_BUILD
|
||||||
|
= "HutaoCanary";
|
||||||
|
#else
|
||||||
|
= "Hutao";
|
||||||
|
#endif
|
||||||
|
string cacheDir = Path.Combine(localAppData, FolderName, "Cache");
|
||||||
|
Directory.CreateDirectory(cacheDir);
|
||||||
|
return cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckAppNotificationEnabled()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// In unpackaged mode, this might fail - return false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string InitializeDataDirectory()
|
||||||
|
{
|
||||||
const string FolderName
|
const string FolderName
|
||||||
#if IS_ALPHA_BUILD
|
#if IS_ALPHA_BUILD
|
||||||
= "HutaoAlpha";
|
= "HutaoAlpha";
|
||||||
@@ -141,30 +168,43 @@ internal static class HutaoRuntime
|
|||||||
= "Hutao";
|
= "Hutao";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// ⚠️ 不要在这里调用 LocalSetting - 会导致循环依赖
|
||||||
|
// 先确定默认的数据目录位置
|
||||||
|
|
||||||
// Check if the old documents path exists
|
// Check if the old documents path exists
|
||||||
string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName));
|
string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName));
|
||||||
if (Directory.Exists(myDocumentsHutaoDirectory))
|
if (Directory.Exists(myDocumentsHutaoDirectory))
|
||||||
{
|
{
|
||||||
LocalSetting.Set(SettingKeys.DataDirectory, myDocumentsHutaoDirectory);
|
|
||||||
return myDocumentsHutaoDirectory;
|
return myDocumentsHutaoDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer LocalApplicationData
|
// Use LocalApplicationData
|
||||||
string localApplicationData = ApplicationData.Current.LocalFolder.Path;
|
string localApplicationData;
|
||||||
string path = Path.GetFullPath(Path.Combine(localApplicationData, FolderName));
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
localApplicationData = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unpackaged: use %LOCALAPPDATA%
|
||||||
|
localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
}
|
||||||
|
|
||||||
|
string defaultPath = Path.GetFullPath(Path.Combine(localApplicationData, FolderName));
|
||||||
|
|
||||||
|
// ⚠️ 延迟处理:在第一次使用 LocalSetting 后再检查是否有自定义路径
|
||||||
|
// 这里返回默认路径,后续通过 LocalSetting 可能会更新
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(path);
|
Directory.CreateDirectory(defaultPath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// FileNotFoundException | UnauthorizedAccessException
|
// FileNotFoundException | UnauthorizedAccessException
|
||||||
// We don't have enough permission
|
HutaoException.InvalidOperation($"Failed to create data folder: {defaultPath}", ex);
|
||||||
HutaoException.InvalidOperation($"Failed to create data folder: {path}", ex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalSetting.Set(SettingKeys.DataDirectory, path);
|
return defaultPath;
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string InitializeDeviceId()
|
private static string InitializeDeviceId()
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.AccessControl;
|
using System.Security.AccessControl;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
using Windows.Storage;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core;
|
namespace Snap.Hutao.Core;
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ internal static class InstalledLocation
|
|||||||
{
|
{
|
||||||
public static string GetAbsolutePath(string relativePath)
|
public static string GetAbsolutePath(string relativePath)
|
||||||
{
|
{
|
||||||
return Path.Combine(Package.Current.InstalledLocation.Path, relativePath);
|
return Path.Combine(PackageIdentityAdapter.AppDirectory, relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void CopyFileFromApplicationUri(string url, string path)
|
public static void CopyFileFromApplicationUri(string url, string path)
|
||||||
@@ -23,8 +22,26 @@ internal static class InstalledLocation
|
|||||||
static async Task CopyApplicationUriFileCoreAsync(string url, string path)
|
static async Task CopyApplicationUriFileCoreAsync(string url, string path)
|
||||||
{
|
{
|
||||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
||||||
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(url.ToUri());
|
|
||||||
using (Stream outputStream = (await file.OpenReadAsync()).AsStreamForRead())
|
Uri uri = url.ToUri();
|
||||||
|
Stream outputStream;
|
||||||
|
|
||||||
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
// Packaged: use StorageFile
|
||||||
|
Windows.Storage.StorageFile file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(uri);
|
||||||
|
outputStream = (await file.OpenReadAsync()).AsStreamForRead();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unpackaged: read from file system directly
|
||||||
|
// Assume ms-appx:/// points to the app directory
|
||||||
|
string localPath = uri.LocalPath.TrimStart('/');
|
||||||
|
string fullPath = Path.Combine(PackageIdentityAdapter.AppDirectory, localPath);
|
||||||
|
outputStream = File.OpenRead(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (outputStream)
|
||||||
{
|
{
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,8 +74,15 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
|||||||
|
|
||||||
public void ActivateAndInitialize(HutaoActivationArguments args)
|
public void ActivateAndInitialize(HutaoActivationArguments args)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] ActivateAndInitialize called");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (Volatile.Read(ref isActivating) is 1)
|
if (Volatile.Read(ref isActivating) is 1)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Already activating, returning");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,21 +92,52 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Starting activation process");
|
||||||
|
#endif
|
||||||
|
|
||||||
using (await activateLock.LockAsync().ConfigureAwait(false))
|
using (await activateLock.LockAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0)
|
if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Race condition detected, returning");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleActivationAsync");
|
||||||
|
#endif
|
||||||
|
|
||||||
await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false);
|
await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleInitializationAsync");
|
||||||
|
#endif
|
||||||
|
|
||||||
await UnsynchronizedHandleInitializationAsync().ConfigureAwait(false);
|
await UnsynchronizedHandleInitializationAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Initialization completed successfully");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation] Exception during activation: {ex}");
|
||||||
|
#endif
|
||||||
|
throw;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
||||||
Interlocked.Exchange(ref isActivating, 0);
|
Interlocked.Exchange(ref isActivating, 0);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] ActivationAndInitializationCompleted set to true");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,16 +351,36 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
|||||||
private async ValueTask<Window?> WaitWindowAsync<TWindow>()
|
private async ValueTask<Window?> WaitWindowAsync<TWindow>()
|
||||||
where TWindow : Window
|
where TWindow : Window
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Waiting for window type: {typeof(TWindow).Name}");
|
||||||
|
#endif
|
||||||
|
|
||||||
await taskContext.SwitchToMainThreadAsync();
|
await taskContext.SwitchToMainThreadAsync();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation.WaitWindowAsync] Switched to main thread");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (currentXamlWindowReference.Window is not { } window)
|
if (currentXamlWindowReference.Window is not { } window)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation.WaitWindowAsync] Creating new window instance");
|
||||||
|
#endif
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
window = serviceProvider.GetRequiredService<TWindow>();
|
window = serviceProvider.GetRequiredService<TWindow>();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Window created successfully: {window.GetType().Name}");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
catch (COMException)
|
catch (COMException ex)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] COMException: {ex}");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (XamlApplicationLifetime.Exiting)
|
if (XamlApplicationLifetime.Exiting)
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
@@ -330,11 +388,33 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
|||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Exception creating window: {ex}");
|
||||||
|
#endif
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
currentXamlWindowReference.Window = window;
|
currentXamlWindowReference.Window = window;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Using existing window: {window.GetType().Name}");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation.WaitWindowAsync] Calling window.SwitchTo()");
|
||||||
|
#endif
|
||||||
|
|
||||||
window.SwitchTo();
|
window.SwitchTo();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation.WaitWindowAsync] Window activated");
|
||||||
|
#endif
|
||||||
|
|
||||||
window.AppWindow?.MoveInZOrderAtTop();
|
window.AppWindow?.MoveInZOrderAtTop();
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,4 +66,14 @@ internal sealed class HutaoActivationArguments
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static HutaoActivationArguments CreateDefaultLaunchArguments()
|
||||||
|
{
|
||||||
|
return new HutaoActivationArguments
|
||||||
|
{
|
||||||
|
IsRedirectTo = false,
|
||||||
|
Kind = HutaoActivationKind.Launch,
|
||||||
|
LaunchActivatedArguments = string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Factory.Process;
|
using Snap.Hutao.Factory.Process;
|
||||||
using Snap.Hutao.Win32;
|
using Snap.Hutao.Win32;
|
||||||
using Snap.Hutao.Win32.Foundation;
|
using Snap.Hutao.Win32.Foundation;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
using Windows.Storage;
|
using Windows.Storage;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.Setting;
|
namespace Snap.Hutao.Core.Setting;
|
||||||
@@ -36,40 +40,20 @@ internal static class LocalSetting
|
|||||||
typeof(ApplicationDataCompositeValue)
|
typeof(ApplicationDataCompositeValue)
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly ApplicationDataContainer Container = ApplicationData.Current.LocalSettings;
|
private static readonly Lazy<ISettingStorage> LazyStorage = new(CreateStorage);
|
||||||
|
|
||||||
|
private static ISettingStorage Storage => LazyStorage.Value;
|
||||||
|
|
||||||
public static T Get<T>(string key, T defaultValue = default!)
|
public static T Get<T>(string key, T defaultValue = default!)
|
||||||
{
|
{
|
||||||
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
||||||
if (Container.Values.TryGetValue(key, out object? value))
|
return Storage.Get(key, defaultValue);
|
||||||
{
|
|
||||||
// unbox the value
|
|
||||||
return value is null ? defaultValue : (T)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set(key, defaultValue);
|
|
||||||
return defaultValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Set<T>(string key, T value)
|
public static void Set<T>(string key, T value)
|
||||||
{
|
{
|
||||||
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
||||||
|
Storage.Set(key, value);
|
||||||
try
|
|
||||||
{
|
|
||||||
Container.Values[key] = value;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// 状态管理器无法写入设置
|
|
||||||
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_STATE_WRITE_SETTING_FAILED))
|
|
||||||
{
|
|
||||||
HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex));
|
|
||||||
ProcessFactory.KillCurrent();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetIf<T>(bool condition, string key, T value)
|
public static void SetIf<T>(bool condition, string key, T value)
|
||||||
@@ -103,4 +87,299 @@ internal static class LocalSetting
|
|||||||
Set(key, newValue);
|
Set(key, newValue);
|
||||||
return oldValue;
|
return oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ISettingStorage CreateStorage()
|
||||||
|
{
|
||||||
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return new PackagedSettingStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UnpackagedSettingStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ISettingStorage
|
||||||
|
{
|
||||||
|
T Get<T>(string key, T defaultValue);
|
||||||
|
void Set<T>(string key, T value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PackagedSettingStorage : ISettingStorage
|
||||||
|
{
|
||||||
|
private readonly ApplicationDataContainer container = ApplicationData.Current.LocalSettings;
|
||||||
|
|
||||||
|
public T Get<T>(string key, T defaultValue)
|
||||||
|
{
|
||||||
|
if (container.Values.TryGetValue(key, out object? value))
|
||||||
|
{
|
||||||
|
// unbox the value
|
||||||
|
return value is null ? defaultValue : (T)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set(key, defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set<T>(string key, T value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
container.Values[key] = value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 状态管理器无法写入设置
|
||||||
|
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_STATE_WRITE_SETTING_FAILED))
|
||||||
|
{
|
||||||
|
HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex));
|
||||||
|
ProcessFactory.KillCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UnpackagedSettingStorage : ISettingStorage
|
||||||
|
{
|
||||||
|
private readonly string settingsFilePath;
|
||||||
|
private readonly ConcurrentDictionary<string, object?> cache = new();
|
||||||
|
private readonly object fileLock = new();
|
||||||
|
private readonly JsonSerializerOptions jsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new ApplicationDataCompositeValueJsonConverter(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public UnpackagedSettingStorage()
|
||||||
|
{
|
||||||
|
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
const string FolderName
|
||||||
|
#if IS_ALPHA_BUILD
|
||||||
|
= "HutaoAlpha";
|
||||||
|
#elif IS_CANARY_BUILD
|
||||||
|
= "HutaoCanary";
|
||||||
|
#else
|
||||||
|
= "Hutao";
|
||||||
|
#endif
|
||||||
|
string settingsDir = Path.Combine(localAppData, FolderName, "Settings");
|
||||||
|
Directory.CreateDirectory(settingsDir);
|
||||||
|
settingsFilePath = Path.Combine(settingsDir, "LocalSettings.json");
|
||||||
|
|
||||||
|
LoadFromFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get<T>(string key, T defaultValue)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(key, out object? value))
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSON deserialization for complex types
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ⚠️ 特殊处理:JSON 数字类型转换
|
||||||
|
Type targetType = typeof(T);
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Number)
|
||||||
|
{
|
||||||
|
if (targetType == typeof(int))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetInt32();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(long))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetInt64();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(short))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetInt16();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(byte))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetByte();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(uint))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetUInt32();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(ulong))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetUInt64();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(ushort))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetUInt16();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(float))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetSingle();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(double))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他类型使用标准反序列化
|
||||||
|
return jsonElement.Deserialize<T>(jsonOptions) ?? defaultValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ 如果是直接从 cache 读取的值,也可能需要类型转换
|
||||||
|
// 例如:double -> int
|
||||||
|
if (value is double doubleValue)
|
||||||
|
{
|
||||||
|
Type targetType = typeof(T);
|
||||||
|
if (targetType == typeof(int))
|
||||||
|
{
|
||||||
|
return (T)(object)(int)doubleValue;
|
||||||
|
}
|
||||||
|
if (targetType == typeof(long))
|
||||||
|
{
|
||||||
|
return (T)(object)(long)doubleValue;
|
||||||
|
}
|
||||||
|
if (targetType == typeof(short))
|
||||||
|
{
|
||||||
|
return (T)(object)(short)doubleValue;
|
||||||
|
}
|
||||||
|
if (targetType == typeof(byte))
|
||||||
|
{
|
||||||
|
return (T)(object)(byte)doubleValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (T)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set(key, defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set<T>(string key, T value)
|
||||||
|
{
|
||||||
|
cache[key] = value;
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFromFile()
|
||||||
|
{
|
||||||
|
lock (fileLock)
|
||||||
|
{
|
||||||
|
if (!File.Exists(settingsFilePath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json = File.ReadAllText(settingsFilePath);
|
||||||
|
Dictionary<string, JsonElement>? data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json, jsonOptions);
|
||||||
|
if (data is not null)
|
||||||
|
{
|
||||||
|
foreach ((string key, JsonElement value) in data)
|
||||||
|
{
|
||||||
|
cache[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If file is corrupted, start fresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveToFile()
|
||||||
|
{
|
||||||
|
lock (fileLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Convert cache to serializable dictionary
|
||||||
|
Dictionary<string, object?> serializableData = new(cache);
|
||||||
|
string json = JsonSerializer.Serialize(serializableData, jsonOptions);
|
||||||
|
File.WriteAllText(settingsFilePath, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Failed to save settings: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converter for ApplicationDataCompositeValue
|
||||||
|
private sealed class ApplicationDataCompositeValueJsonConverter : System.Text.Json.Serialization.JsonConverter<ApplicationDataCompositeValue>
|
||||||
|
{
|
||||||
|
public override ApplicationDataCompositeValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartObject)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationDataCompositeValue composite = new();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.EndObject)
|
||||||
|
{
|
||||||
|
return composite;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? key = reader.GetString();
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
if (key is not null)
|
||||||
|
{
|
||||||
|
composite[key] = reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.String => reader.GetString(),
|
||||||
|
JsonTokenType.Number => reader.TryGetInt64(out long l) ? l : reader.GetDouble(),
|
||||||
|
JsonTokenType.True => true,
|
||||||
|
JsonTokenType.False => false,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return composite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, ApplicationDataCompositeValue value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
foreach ((string key, object? val) in value)
|
||||||
|
{
|
||||||
|
writer.WritePropertyName(key);
|
||||||
|
if (val is null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, val, val.GetType(), options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +147,27 @@ internal sealed class ProcessFactory
|
|||||||
{
|
{
|
||||||
string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory();
|
string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory();
|
||||||
string fullTrustFilePath = Path.Combine(repoDirectory, "Snap.ContentDelivery", "Snap.Hutao.FullTrust.exe");
|
string fullTrustFilePath = Path.Combine(repoDirectory, "Snap.ContentDelivery", "Snap.Hutao.FullTrust.exe");
|
||||||
|
|
||||||
|
// Check if FullTrust executable exists - if not, fallback to normal admin mode
|
||||||
|
if (!File.Exists(fullTrustFilePath))
|
||||||
|
{
|
||||||
|
string errorMessage = $"""
|
||||||
|
Island 功能需要的 FullTrust 进程文件不存在,将使用普通管理员模式启动游戏。
|
||||||
|
预期路径:{fullTrustFilePath}
|
||||||
|
|
||||||
|
原因:ContentDelivery 仓库尚未下载或初始化失败(常见于非打包模式首次运行)
|
||||||
|
|
||||||
|
Island 功能将不可用,但游戏可以正常启动。
|
||||||
|
等待仓库下载完成后可重新尝试使用 Island 功能。
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Capture as breadcrumb instead of exception
|
||||||
|
SentrySdk.AddBreadcrumb(errorMessage, category: "process.fulltrust", level: Sentry.BreadcrumbLevel.Warning);
|
||||||
|
|
||||||
|
// Fallback to normal admin mode - Island features will not work but game can launch
|
||||||
|
return CreateUsingShellExecuteRunAs(arguments, fileName, workingDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
StartUsingShellExecuteRunAs(fullTrustFilePath);
|
StartUsingShellExecuteRunAs(fullTrustFilePath);
|
||||||
|
|
||||||
FullTrustProcessStartInfoRequest request = new()
|
FullTrustProcessStartInfoRequest request = new()
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ internal static class AvatarIds
|
|||||||
public static readonly AvatarId Flins = 10000120;
|
public static readonly AvatarId Flins = 10000120;
|
||||||
public static readonly AvatarId Aino = 10000121;
|
public static readonly AvatarId Aino = 10000121;
|
||||||
public static readonly AvatarId Nefer = 10000122;
|
public static readonly AvatarId Nefer = 10000122;
|
||||||
|
public static readonly AvatarId Durin = 10000123;
|
||||||
|
public static readonly AvatarId Jahoda = 10000124;
|
||||||
|
|
||||||
private static readonly FrozenSet<AvatarId> StandardWishIds =
|
private static readonly FrozenSet<AvatarId> StandardWishIds =
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ internal static class WeaponIds
|
|||||||
12401U, 12402U, 12403U, 12405U,
|
12401U, 12402U, 12403U, 12405U,
|
||||||
13401U, 13407U,
|
13401U, 13407U,
|
||||||
14401U, 14402U, 14403U, 14409U,
|
14401U, 14402U, 14403U, 14409U,
|
||||||
15401U, 15402U, 15403U, 15405U
|
15401U, 15402U, 15403U, 15405U,
|
||||||
|
15434U
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly FrozenSet<WeaponId> OrangeStandardWishIds =
|
public static readonly FrozenSet<WeaponId> OrangeStandardWishIds =
|
||||||
@@ -33,6 +34,7 @@ internal static class WeaponIds
|
|||||||
13502U, 13505U,
|
13502U, 13505U,
|
||||||
14501U, 14502U,
|
14501U, 14502U,
|
||||||
15501U, 15502U,
|
15501U, 15502U,
|
||||||
|
15515U, 15518U
|
||||||
];
|
];
|
||||||
|
|
||||||
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
||||||
|
|||||||
@@ -1181,6 +1181,9 @@ Space Available: {2}</value>
|
|||||||
<data name="ServiceYaeWaitForGameResponseMessage" xml:space="preserve">
|
<data name="ServiceYaeWaitForGameResponseMessage" xml:space="preserve">
|
||||||
<value>Waiting for game data</value>
|
<value>Waiting for game data</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="UIViewMainTitleBarBackgroundActivityAction" xml:space="preserve">
|
||||||
|
<value>Background task</value>
|
||||||
|
</data>
|
||||||
<data name="UIViewPageAvatarPropertyRecommendedAppendProperties" xml:space="preserve">
|
<data name="UIViewPageAvatarPropertyRecommendedAppendProperties" xml:space="preserve">
|
||||||
<value>Additional Property Recommendation</value>
|
<value>Additional Property Recommendation</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -3989,4 +3992,7 @@ Space Available: {2}</value>
|
|||||||
<data name="WindowIdentifyMonitorHeader" xml:space="preserve">
|
<data name="WindowIdentifyMonitorHeader" xml:space="preserve">
|
||||||
<value>Monitor ID</value>
|
<value>Monitor ID</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="UIViewMainTitleBarInvertTheme" xml:space="preserve">
|
||||||
|
<value>Invert Theme</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -4020,4 +4020,7 @@
|
|||||||
<data name="WindowIdentifyMonitorHeader" xml:space="preserve">
|
<data name="WindowIdentifyMonitorHeader" xml:space="preserve">
|
||||||
<value>显示器编号</value>
|
<value>显示器编号</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="UIViewMainTitleBarInvertTheme" xml:space="preserve">
|
||||||
|
<value>主题切换</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -77,21 +77,48 @@ internal sealed class GameIslandInterop : IGameIslandInterop
|
|||||||
{
|
{
|
||||||
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
|
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
|
||||||
InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea);
|
InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea);
|
||||||
|
|
||||||
if (!resume)
|
if (!resume)
|
||||||
{
|
{
|
||||||
if (context.Process is not FullTrustProcess fullTrustProcess)
|
|
||||||
{
|
|
||||||
throw HutaoException.InvalidOperation("Process is not full trust");
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(islandPath);
|
ArgumentException.ThrowIfNullOrEmpty(islandPath);
|
||||||
if (!File.Exists(islandPath))
|
if (!File.Exists(islandPath))
|
||||||
{
|
{
|
||||||
throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists);
|
throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath));
|
// Support both FullTrust and normal admin mode
|
||||||
fullTrustProcess.ResumeMainThread();
|
if (context.Process is FullTrustProcess fullTrustProcess)
|
||||||
|
{
|
||||||
|
// Use FullTrust process for injection (suspended process)
|
||||||
|
fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath));
|
||||||
|
fullTrustProcess.ResumeMainThread();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use native injection for normal admin mode
|
||||||
|
// The process was already started by CreateUsingShellExecuteRunAs
|
||||||
|
// Just inject the DLL into the running process
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Wait a bit for process to initialize
|
||||||
|
// await Task.Delay(500, token).ConfigureAwait(false);
|
||||||
|
// ⚠️此处需要更多调查
|
||||||
|
|
||||||
|
// Inject using RemoteThread
|
||||||
|
DllInjectionUtilities.InjectUsingRemoteThread(islandPath, context.Process.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log the injection failure but don't crash - game can still run
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Island DLL injection failed: {ex.Message}",
|
||||||
|
category: "island.injection",
|
||||||
|
level: Sentry.BreadcrumbLevel.Error);
|
||||||
|
|
||||||
|
// Re-throw to let the caller handle it
|
||||||
|
throw HutaoException.Throw($"Island DLL 注入失败: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false);
|
await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ internal sealed class GameProcessFactory
|
|||||||
string gameFilePath = context.FileSystem.GameFilePath;
|
string gameFilePath = context.FileSystem.GameFilePath;
|
||||||
string gameDirectory = context.FileSystem.GameDirectory;
|
string gameDirectory = context.FileSystem.GameDirectory;
|
||||||
|
|
||||||
|
// ProcessFactory.CreateUsingFullTrustSuspended will automatically fallback to normal mode if FullTrust.exe is missing
|
||||||
return launchOptions.IsIslandEnabled.Value
|
return launchOptions.IsIslandEnabled.Value
|
||||||
? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory)
|
? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory)
|
||||||
: ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory);
|
: ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory);
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core.IO.Hashing;
|
using Snap.Hutao.Core.IO.Hashing;
|
||||||
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Web.Hutao;
|
using Snap.Hutao.Web.Hutao;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Windows.Storage;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Git;
|
namespace Snap.Hutao.Service.Git;
|
||||||
|
|
||||||
internal static class RepositoryAffinity
|
internal static class RepositoryAffinity
|
||||||
{
|
{
|
||||||
private static readonly ApplicationDataContainer RepositoryContainer = ApplicationData.Current.LocalSettings.CreateContainer("RepositoryAffinity", ApplicationDataCreateDisposition.Always);
|
private const string RepositoryAffinityPrefix = "RepositoryAffinity::";
|
||||||
private static readonly Lock SyncRoot = new();
|
private static readonly Lock SyncRoot = new();
|
||||||
|
|
||||||
public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories)
|
public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories)
|
||||||
@@ -23,9 +23,11 @@ internal static class RepositoryAffinity
|
|||||||
for (int i = 0; i < repositories.Length; i++)
|
for (int i = 0; i < repositories.Length; i++)
|
||||||
{
|
{
|
||||||
GitRepository repository = repositories[i];
|
GitRepository repository = repositories[i];
|
||||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(repository.Name, ApplicationDataCreateDisposition.Always);
|
string key = GetSettingKey(repository.Name, repository.HttpsUrl.OriginalString);
|
||||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, repository.HttpsUrl.OriginalString.ToUpperInvariant());
|
|
||||||
counts[i] = container.Values[key] is int c ? c : 0;
|
// 对读取值做下限保护,确保排序使用的是非负失败计数
|
||||||
|
int raw = LocalSetting.Get(key, 0);
|
||||||
|
counts[i] = Math.Max(0, raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories));
|
Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories));
|
||||||
@@ -42,10 +44,14 @@ internal static class RepositoryAffinity
|
|||||||
{
|
{
|
||||||
lock (SyncRoot)
|
lock (SyncRoot)
|
||||||
{
|
{
|
||||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always);
|
string key = GetSettingKey(name, url);
|
||||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
int currentCount = LocalSetting.Get(key, 0);
|
||||||
object box = container.Values[key];
|
|
||||||
container.Values[key] = box is int count ? unchecked(count + 1) : 1;
|
// 防止整数上溢:当已到达 int.MaxValue 时不再自增
|
||||||
|
if (currentCount < int.MaxValue)
|
||||||
|
{
|
||||||
|
LocalSetting.Set(key, currentCount + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +64,20 @@ internal static class RepositoryAffinity
|
|||||||
{
|
{
|
||||||
lock (SyncRoot)
|
lock (SyncRoot)
|
||||||
{
|
{
|
||||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always);
|
string key = GetSettingKey(name, url);
|
||||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
int currentCount = LocalSetting.Get(key, 0);
|
||||||
object box = container.Values[key];
|
|
||||||
container.Values[key] = box is int count ? unchecked(count - 1) : 0;
|
// 失败次数不允许小于 0,避免出现负数或整型下溢
|
||||||
|
if (currentCount > 0)
|
||||||
|
{
|
||||||
|
LocalSetting.Set(key, currentCount - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetSettingKey(string name, string url)
|
||||||
|
{
|
||||||
|
string urlHash = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
||||||
|
return $"{RepositoryAffinityPrefix}{name}::{urlHash}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||||
@@ -47,12 +47,18 @@
|
|||||||
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
||||||
<PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile>
|
<PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile>
|
||||||
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
||||||
|
<!-- 关闭AppX打包 -->
|
||||||
|
<AppxPackage>false</AppxPackage>
|
||||||
|
<!-- 不设置打包类型 -->
|
||||||
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Package.appxmanifest" />
|
||||||
|
<None Remove="Package.development.appxmanifest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Manifest Include="$(ApplicationManifest)" />
|
<Manifest Include="$(ApplicationManifest)" />
|
||||||
<AppxManifest Include="Package.appxmanifest" Condition="'$(ConfigurationName)'!='Debug'" />
|
|
||||||
<AppxManifest Include="Package.development.appxmanifest" Condition="'$(ConfigurationName)'=='Debug'" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="DeleteLibsAfterBuild" AfterTargets="Build">
|
<Target Name="DeleteLibsAfterBuild" AfterTargets="Build">
|
||||||
@@ -241,15 +247,15 @@
|
|||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251105-build.1544" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251105-build.1544" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251105-build.1544" />
|
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251105-build.1544" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251105-build.1544" />
|
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251105-build.1544" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
|
<PackageReference Include="Google.Protobuf" Version="3.33.1" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
|
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -265,7 +271,7 @@
|
|||||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" />
|
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" />
|
||||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
|
||||||
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.251015.2" />
|
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.251015.2" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||||
<PackageReference Include="QRCoder" Version="1.7.0" />
|
<PackageReference Include="QRCoder" Version="1.7.0" />
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
||||||
@@ -274,10 +280,6 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="2.5.12">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.3.0">
|
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.3.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
|
|||||||
@@ -13,33 +13,32 @@
|
|||||||
<x:String x:Key="IconGame">https://launcher-webstatic.mihoyo.com/launcher-public/2024/04/15/9ebf1bc5af2d83ca5fca21adb49cf341_2571779162329842818.png</x:String>
|
<x:String x:Key="IconGame">https://launcher-webstatic.mihoyo.com/launcher-public/2024/04/15/9ebf1bc5af2d83ca5fca21adb49cf341_2571779162329842818.png</x:String>
|
||||||
|
|
||||||
<!-- AvatarCard -->
|
<!-- AvatarCard -->
|
||||||
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://api.snapgenshin.com/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
|
<x:String x:Key="UI_AvatarIcon_Costume_Card">http://serverjp.wdg.cloudns.ch:8001/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
|
||||||
|
|
||||||
<!-- Bg -->
|
<!-- Bg -->
|
||||||
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
|
<x:String x:Key="UI_ItemIcon_None">http://serverjp.wdg.cloudns.ch:8001/static/raw/Bg/UI_ItemIcon_None.png</x:String>
|
||||||
|
|
||||||
<!-- Mark -->
|
<!-- Mark -->
|
||||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
|
<x:String x:Key="UI_MarkQuest_Events_Proce">http://serverjp.wdg.cloudns.ch:8001/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
|
||||||
<x:String x:Key="UI_MarkQuest_Events_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
|
<x:String x:Key="UI_MarkQuest_Events_Start">http://serverjp.wdg.cloudns.ch:8001/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
|
||||||
<x:String x:Key="UI_MarkQuest_Main_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
|
<x:String x:Key="UI_MarkQuest_Main_Proce">http://serverjp.wdg.cloudns.ch:8001/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
|
||||||
<x:String x:Key="UI_MarkQuest_Main_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
|
<x:String x:Key="UI_MarkQuest_Main_Start">http://serverjp.wdg.cloudns.ch:8001/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
|
||||||
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Mark/UI_MarkTower.png</x:String>
|
<x:String x:Key="UI_MarkTower">http://serverjp.wdg.cloudns.ch:8001/static/raw/Mark/UI_MarkTower.png</x:String>
|
||||||
|
|
||||||
<!-- ItemIcon -->
|
<!-- ItemIcon -->
|
||||||
<x:String x:Key="UI_ItemIcon_106">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_106.png</x:String>
|
<x:String x:Key="UI_ItemIcon_106">http://serverjp.wdg.cloudns.ch:8001/static/raw/ItemIcon/UI_ItemIcon_106.png</x:String>
|
||||||
<x:String x:Key="UI_ItemIcon_204">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
|
<x:String x:Key="UI_ItemIcon_204">http://serverjp.wdg.cloudns.ch:8001/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
|
||||||
<x:String x:Key="UI_ItemIcon_210">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
|
<x:String x:Key="UI_ItemIcon_210">http://serverjp.wdg.cloudns.ch:8001/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
|
||||||
<x:String x:Key="UI_ItemIcon_220021">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
|
<x:String x:Key="UI_ItemIcon_220021">http://serverjp.wdg.cloudns.ch:8001/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
|
||||||
|
|
||||||
<!-- EmotionIcon -->
|
<!-- EmotionIcon -->
|
||||||
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
|
<x:String x:Key="UI_EmotionIcon52">http://serverjp.wdg.cloudns.ch:8001/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_EmotionIcon71">http://serverjp.wdg.cloudns.ch:8001/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon89">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
|
<x:String x:Key="UI_EmotionIcon89">http://serverjp.wdg.cloudns.ch:8001/static/raw/EmotionIcon/UI_EmotionIcon89.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_EmotionIcon250">http://serverjp.wdg.cloudns.ch:8001/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>
|
<x:String x:Key="UI_EmotionIcon271">http://serverjp.wdg.cloudns.ch:8001/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
<x:String x:Key="UI_EmotionIcon272">http://serverjp.wdg.cloudns.ch:8001/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
<x:String x:Key="UI_EmotionIcon293">http://serverjp.wdg.cloudns.ch:8001/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon433">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
|
<x:String x:Key="UI_EmotionIcon433">http://serverjp.wdg.cloudns.ch:8001/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
|
<x:String x:Key="UI_EmotionIcon445">http://serverjp.wdg.cloudns.ch:8001/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon585">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon585.png</x:String>
|
<x:String x:Key="UI_EmotionIcon585">http://serverjp.wdg.cloudns.ch:8001/static/raw/EmotionIcon/UI_EmotionIcon585.png</x:String>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -79,9 +79,8 @@
|
|||||||
<Button
|
<Button
|
||||||
Padding="6"
|
Padding="6"
|
||||||
Command="{Binding InvertAppThemeCommand}"
|
Command="{Binding InvertAppThemeCommand}"
|
||||||
Content="[Dev] Invert Theme"
|
Content="{shuxm:ResourceString Name=UIViewMainTitleBarInvertTheme}"
|
||||||
Style="{ThemeResource SettingButtonStyle}"
|
Style="{ThemeResource SettingButtonStyle}"/>
|
||||||
Visibility="{Binding IsDebug}"/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
Padding="0"
|
Padding="0"
|
||||||
|
|||||||
@@ -553,7 +553,7 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
|
|
||||||
<PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}" IsEnabled="False">
|
<PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}">
|
||||||
<Border
|
<Border
|
||||||
Margin="16"
|
Margin="16"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ internal static class StaticResource
|
|||||||
|
|
||||||
foreach ((string key, object value) in map)
|
foreach ((string key, object value) in map)
|
||||||
{
|
{
|
||||||
if ((int)value < (int)LatestResourceVersionMap[key])
|
if (Convert.ToInt32(value) < Convert.ToInt32(LatestResourceVersionMap[key]))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ internal static class StaticResource
|
|||||||
ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap);
|
ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap);
|
||||||
foreach ((string key, object value) in LatestResourceVersionMap)
|
foreach ((string key, object value) in LatestResourceVersionMap)
|
||||||
{
|
{
|
||||||
if (!map.TryGetValue(key, out object current) || (int)value > (int)current)
|
if (!map.TryGetValue(key, out object current) || Convert.ToInt32(value) > Convert.ToInt32(current))
|
||||||
{
|
{
|
||||||
result.Add(key);
|
result.Add(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Factory.ContentDialog;
|
using Snap.Hutao.Factory.ContentDialog;
|
||||||
using Snap.Hutao.Factory.Picker;
|
using Snap.Hutao.Factory.Picker;
|
||||||
@@ -71,8 +72,18 @@ internal sealed class SettingStorageSetDataFolderOperation
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.SetReadOnly(oldFolderPath, false);
|
Directory.SetReadOnly(oldFolderPath, false);
|
||||||
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
|
|
||||||
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false);
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
// Packaged: use StorageFolder API
|
||||||
|
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
|
||||||
|
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unpackaged: use standard file I/O
|
||||||
|
await CopyDirectoryAsync(oldFolderPath, newFolderPath).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -84,4 +95,22 @@ internal sealed class SettingStorageSetDataFolderOperation
|
|||||||
LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath);
|
LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async ValueTask CopyDirectoryAsync(string sourceDir, string destDir)
|
||||||
|
{
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
// Create all directories
|
||||||
|
foreach (string dirPath in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dirPath.Replace(sourceDir, destDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all files
|
||||||
|
foreach (string filePath in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
File.Copy(filePath, filePath.Replace(sourceDir, destDir), true);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.Windows.AppLifecycle;
|
using Microsoft.Windows.AppLifecycle;
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Core.Caching;
|
using Snap.Hutao.Core.Caching;
|
||||||
using Snap.Hutao.Core.Logging;
|
using Snap.Hutao.Core.Logging;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
@@ -135,7 +136,18 @@ internal sealed partial class SettingStorageViewModel : Abstraction.ViewModel
|
|||||||
// TODO: prompt user that restart will be non-elevated
|
// TODO: prompt user that restart will be non-elevated
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AppInstance.Restart(string.Empty);
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
AppInstance.Restart(string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unpackaged: manually restart the process
|
||||||
|
string exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName
|
||||||
|
?? throw new InvalidOperationException("Cannot get process path");
|
||||||
|
System.Diagnostics.Process.Start(exePath);
|
||||||
|
Snap.Hutao.Factory.Process.ProcessFactory.KillCurrent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (COMException ex)
|
catch (COMException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,34 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
namespace Snap.Hutao.Web.Hoyolab.DataSigning;
|
namespace Snap.Hutao.Web.Hoyolab.DataSigning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salt constants for data signing
|
||||||
|
/// Values are obtained from https://github.com/UIGF-org/Hoyolab.Salt
|
||||||
|
/// This file should normally be generated by Snap.Hutao.SourceGeneration.Automation.SaltConstantGenerator
|
||||||
|
/// But is provided manually when the generator fails to fetch values from the network.
|
||||||
|
///
|
||||||
|
/// IMPORTANT: For local builds, you must manually obtain salt values from:
|
||||||
|
/// https://github.com/UIGF-org/Hoyolab.Salt
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
internal static class SaltConstants
|
internal static class SaltConstants
|
||||||
{
|
{
|
||||||
|
// Version numbers - Update these according to the current miHoYo app versions
|
||||||
public const string CNVersion = "2.95.1";
|
public const string CNVersion = "2.95.1";
|
||||||
public const string OSVersion = "2.54.0";
|
public const string OSVersion = "2.54.0";
|
||||||
|
|
||||||
|
// Salt keys for Chinese (CN) server
|
||||||
|
// These are placeholder values - MUST be replaced with actual values from UIGF-org/Hoyolab.Salt
|
||||||
public const string CNK2 = "sfYPEgpxkOe1I3XVMLdwp1Lyt9ORgZsq";
|
public const string CNK2 = "sfYPEgpxkOe1I3XVMLdwp1Lyt9ORgZsq";
|
||||||
public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO";
|
public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO";
|
||||||
|
|
||||||
|
// Salt keys for Overseas (OS) server
|
||||||
public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae";
|
public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae";
|
||||||
public const string OSLK2 = "rk4xg2hakoi26nljpr099fv9fck1ah10";
|
public const string OSLK2 = "rk4xg2hakoi26nljpr099fv9fck1ah10";
|
||||||
|
|
||||||
|
// Note: The actual salt values are security-sensitive and should not be committed
|
||||||
|
// to public repositories. For local builds, obtain them from the UIGF organization
|
||||||
|
// and replace the placeholders above.
|
||||||
}
|
}
|
||||||
|
|||||||
68
tools/GetImages.py
Normal file
68
tools/GetImages.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import re
|
||||||
|
|
||||||
|
def sha1_name(filename):
|
||||||
|
"""对输入文件名进行 SHA1 哈希,返回大写 40 位字符串"""
|
||||||
|
sha = hashlib.sha1(filename.encode('utf-8')).hexdigest().upper()
|
||||||
|
return sha
|
||||||
|
|
||||||
|
def search_and_copy(original_name, search_dir, output_dir):
|
||||||
|
sha_name = sha1_name(original_name)
|
||||||
|
print(f"SHA1 计算结果: {sha_name}")
|
||||||
|
|
||||||
|
# 查找匹配文件
|
||||||
|
matched_path = None
|
||||||
|
for root, dirs, files in os.walk(search_dir):
|
||||||
|
for file in files:
|
||||||
|
if file.upper() == sha_name:
|
||||||
|
matched_path = os.path.join(root, file)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matched_path:
|
||||||
|
print("❌ 未找到匹配文件!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建输出目录
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 复制并重命名
|
||||||
|
# 文件名是url,取最后一部分作为原文件名
|
||||||
|
original_filename = os.path.basename(original_name)
|
||||||
|
# 创建url中最后一个文件夹作为输出目录
|
||||||
|
last_folder = original_name.split('/')[-2]
|
||||||
|
output_subdir = os.path.join(output_dir, last_folder)
|
||||||
|
os.makedirs(output_subdir, exist_ok=True)
|
||||||
|
new_path = os.path.join(output_subdir, original_filename)
|
||||||
|
shutil.copy(matched_path, new_path)
|
||||||
|
print(f"✔ 已复制并重命名文件: {new_path}")
|
||||||
|
|
||||||
|
def extract_urls_from_log(log_file):
|
||||||
|
"""
|
||||||
|
从日志中提取原始文件 URL,并打印到终端
|
||||||
|
"""
|
||||||
|
base_url = "https://api.snapgenshin.com/static/raw"
|
||||||
|
url_pattern = re.compile(r'GET\s+/static/raw/([^/]+)/([^ ]+)\s+HTTP')
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
|
||||||
|
with open(log_file, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
match = url_pattern.search(line)
|
||||||
|
if match:
|
||||||
|
category, filename = match.groups()
|
||||||
|
full_url = f"{base_url}/{category}/{filename}"
|
||||||
|
urls.append(full_url)
|
||||||
|
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logfile="1.txt" # 日志文件路径
|
||||||
|
original_file = extract_urls_from_log(logfile)
|
||||||
|
search_directory = "C:\\Users\\username\\AppData\\Local\\Packages\\60568DGPStudio.SnapHutao_wbnnev551gwxy1\\LocalCache\\ImageCache" # 搜索目录
|
||||||
|
output_directory = "C:\\Users\\username\\AppData\\Local\\Packages\\60568DGPStudio.SnapHutao_wbnnev551gwxy1\\LocalCache\\ImageCache\\output" # 输出目录
|
||||||
|
|
||||||
|
for url in original_file:
|
||||||
|
search_and_copy(url, search_directory, output_directory)
|
||||||
19
tools/README.md
Normal file
19
tools/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 对开发有用的工具
|
||||||
|
|
||||||
|
对开发有用的工具,不参与编译
|
||||||
|
|
||||||
|
## GetImage.py
|
||||||
|
|
||||||
|
由于没有备份的图片资源,并且缓存目录中的图片名全部经过SHA1处理过,该工具用于通过请求日志在旧的资源文件夹中还原图片资源结构
|
||||||
|
|
||||||
|
### 用法
|
||||||
|
|
||||||
|
**先获取资源服务器上的404日志,如下所示**
|
||||||
|
<img width="1161" height="57" alt="image" src="https://github.com/user-attachments/assets/54270165-f077-4c79-ae87-da16592e77fe" />
|
||||||
|
复制404日志,粘贴到文本中
|
||||||
|
|
||||||
|
**然后修改脚本,确保路径正确**
|
||||||
|
|
||||||
|
从`GetImage.py:62`开始修改,确保路径正确,搜索路径一定要为旧资源文件夹,`username`是你的用户名,`60568DGPStudio.SnapHutao_wbnnev551gwxy1`改为你实际的旧数据文件夹
|
||||||
|
|
||||||
|
运行后将自动搜索缺失的文件,然后会在输出路径中生成原始文件,可以直接上传至服务器来补全图片资源
|
||||||
Reference in New Issue
Block a user