Compare commits

...
Author SHA1 Message Date
ff m 4573a9b494 🐛 优化安装脚本 2024-10-21 17:32:54 +08:00
Aigio Liu 8f29d7b8ea 🔖 3.0.0-rc.12 2024-10-11 16:40:52 +08:00
RMBGAME 7156d90967 🐛 Windows 关机事件遗漏 2024-10-11 16:33:56 +08:00
RMBGAME ec726e8c48 💄 成就 受保护 文本调整 2024-10-10 15:55:08 +08:00
RMBGAME 488403a86b 🐛 修复 新版 Steam 客户端 编辑游戏功能保存不生效问题 2024-10-10 15:50:59 +08:00
Aigio Liu 59474d68cd Revert "🔥 Remove ProxyMode.DNSIntercept with REMOVE_DNS_INTERCEPT"
This reverts commit 7f7fde20bd.
2024-10-09 21:14:50 +08:00
RMBGAME 5ab308bb9b 🐛 修复 ASF 下载通知问题 2024-10-09 19:31:42 +08:00
RMBGAME bf3d6ad778 🐛 修复挂卡和令牌确认交易中可能出现系统代理更改时网络请求出错的问题 2024-10-09 18:33:02 +08:00
RMBGAME 59356f210e Merge pull request #3499 from BeyondDimension/feature/network_test_domain_validation
✨ 网络测试页面域名检测输入检查
2024-10-09 18:31:46 +08:00
k1mlka 42df02f718 🐛 网络测试页面清理 2024-10-09 15:22:33 +08:00
k1mlka 8728126421 Merge branch 'develop' into feature/network_test_domain_validation 2024-10-09 15:03:30 +08:00
RMBGAME e0c36a9545 Merge pull request #3495 from BeyondDimension/refactor_NetworkCheck
💄 网络测试子页面重构
2024-10-09 14:19:00 +08:00
k1mlka 9a2bd1a6cc 🐛 移除多余代码 2024-10-08 11:40:51 +08:00
k1mlka 499edb402c 💄 网络测试子页面ViewModel重构
💄 网络测试子页面NAT检查部分逻辑重构

💄 网络测试子页面ViewModel重构

💄 网络测试子页面NAT检查部分逻辑重构

💄代码Clean,没有引用到的名称空间与枚举类型

💄 网络测试子页面NAT检查部分逻辑重构

💄 网络测试子页面ViewModel重构

💄 网络测试子页面NAT检查部分逻辑重构

💄代码Clean,没有引用到的名称空间与枚举类型
2024-10-08 11:33:37 +08:00
k1mlka 14216ab98f ✨ 网络测试页面域名检测输入检查 2024-09-29 17:26:58 +08:00
john.jiang 95c10dacd4 🎨 ASF 插件 Release 版进程残留问题修复,启动优化,打开文件夹提示优化 2024-09-27 15:30:51 +08:00
Aigio Liu 2883405ace 🎨 ResetSettings Toast 2024-09-25 14:29:59 +08:00
Aigio Liu 75e5b6e897 🔖 3.0.0-rc.11 2024-09-25 10:04:42 +08:00
BuknSS 66f3aa22d6 🐛 Fix missing GetMessage parameters 2024-09-24 18:39:37 +08:00
Aigio Liu 7930120e0b 🐛 ResetProxySettings 2024-09-24 17:44:36 +08:00
RMBGAME aa44b137c4 Merge pull request #3471 from BeyondDimension/feature/ipv6check
Feature/ipv6check
2024-09-24 16:51:10 +08:00
RMBGAME 103918060a ✨ 代理设置增加重置按钮 2024-09-24 16:50:47 +08:00
RMBGAME 0644bbf924 ✨ 完善加速相关QA提示 2024-09-24 16:07:15 +08:00
RMBGAME 3853181d8a 🚧 Misc 2024-09-23 18:12:55 +08:00
RMBGAME de694024bb Merge pull request #3475 from BeyondDimension/fix_accelerator_by_startup
🐛 修复启用 “启动时自动加速” 后,测试页面不显示问题
2024-09-23 17:53:02 +08:00
RMBGAME 6ceaeb0710 🐛 通知消息弹出不再重复弹出同样消息 2024-09-23 17:45:46 +08:00
RMBGAME 71f104b947 🐛 修复默认doh地址导致加速失效的问题 2024-09-23 17:21:26 +08:00
k1mlka a9e913434a 🚧 重新实现EnableProxyDomainGroupVMs数据源订阅 2024-09-23 11:28:23 +08:00
RMBGAME f1ffae11aa 🐛 修复在启用了启动时最小化之后某些情况会导致Toast提示弹出失败的问题 2024-09-20 21:58:36 +08:00
Aigio Liu 7f7fde20bd 🔥 Remove ProxyMode.DNSIntercept with REMOVE_DNS_INTERCEPT 2024-09-20 21:13:49 +08:00
k1mlka 2e97c2edc2 🐛 修复任意输入错误域名,返回延迟和空ip问题 2024-09-12 17:03:44 +08:00
k1mlka 69b8590881 🐛 修复启用 “启动时自动加速” 后,测试页面不显示问题 2024-09-06 11:56:47 +08:00
ff m 7b9ba5e8a0 ➕ BsDiff 2024-09-04 13:30:44 +08:00
k1mlka d6ab3c7f03 💄 网路测试页面重写 2024-09-02 17:27:43 +08:00
k1mlka 12d062d4de ✨ ipv6检测 2024-09-02 17:19:07 +08:00
k1mlka f27d3c9111 Merge pull request #3469 from luojunyuan/develop
🐛 初次启动时"设置页面-启动页设置项"显示修复
2024-09-02 11:59:32 +08:00
k1mlka 6ab74d449e 🐛 初次启动时"设置页面-启动页设置项"显示修复 2024-09-02 11:54:17 +08:00
RMBGAME ceed57c0f2 🐛 Misc 2024-08-31 23:05:35 +08:00
RMBGAME 8ca203f34b 🐛 多个问题修复调整 2024-08-31 19:54:36 +08:00
RMBGAME d4de20aa96 🔖 3.0.0 rc.10 2024-08-31 19:54:36 +08:00
k1mlka c94dec40a1 💄 代理设置开关IsEnable一致性调整 2024-08-31 19:28:03 +08:00
k1mlka b829114c4f 💄 加速服务测试不展开小项目 2024-08-31 19:27:26 +08:00
k1mlka a3a32bf5fa 🐛 修复初次Doh测试回报error 2024-08-31 17:59:49 +08:00
k1mlka faa7cc5b13 💄 加速后延迟测试颜色调整 2024-08-31 17:36:57 +08:00
k1mlka 1e506f9930 🐛 Hack方式修正默认加速器页面动画移动方向 2024-08-31 17:12:38 +08:00
k1mlka 217449ba54 🚧 重构加速器页面部分xaml绑定 2024-08-31 16:55:18 +08:00
k1mlka 55b79c4d4e 💄 网络检测页面DNS与doh输入可编辑 2024-08-31 16:39:09 +08:00
Aigio Liu a6c5fed203 🐛 WinDivertInit 2024-08-31 14:23:47 +08:00
RMBGAME b7e31d1c9c Merge pull request #3461 from BeyondDimension/feature/networktestservice
Feature/networktestservice
2024-08-30 20:13:07 +08:00
RMBGAME 1d0b16895f 💄 UI Adaptive 2024-08-30 20:11:38 +08:00
k1mlka 65d73c2e50 🚧 重构加速器页面到单独的页面 2024-08-30 19:26:53 +08:00
k1mlka 83c390e446 🐛 通过脚本开关来确定是否显示脚本配置页面 2024-08-30 19:20:15 +08:00
k1mlka 31f5fa5ea6 🚧 DNS检测项目输入项调整 2024-08-30 17:19:44 +08:00
k1mlka f42c7cbd30 🐛 尝试修复STUN服务器初始不显示问题 2024-08-30 16:04:07 +08:00
k1mlka 3cb592ce7e 🚧 加速服务显示子项树形结构 2024-08-30 15:54:05 +08:00
k1mlka d5fa02e9a4 Merge remote-tracking branch 'origin/develop' into feature/networktestservice 2024-08-30 13:58:53 +08:00
k1mlka cbb2fac6ea 🐛 停止加速时,不显示脚本配置页面 2024-08-30 13:36:05 +08:00
RMBGAMEandAigio Liu 35747253b8 Feature/ipc console log (#3463)
* 🎨 LogConsole

* 🐛 ref

* 💄 ProxyLog UI

* 🐛 WebApplication Logging

* 🐛 Misc

* 🎨 Log format

* 🎨 LogConsoleService.ConsoleMaxCharCount

* 🐛 Misc

---------

Co-authored-by: Aigio Liu <lysoft.pw@foxmail.com>
2024-08-30 13:33:18 +08:00
k1mlka 17c45fdd65 🚧加速器页面Tab栏目调整,无脚本启动时隐藏 2024-08-30 13:26:23 +08:00
k1mlka 88f02e3faf 🚧 网络测试宽度与项目背景色调整 2024-08-29 18:05:41 +08:00
k1mlka 35a0c4d05f 🐛 google doh 地址修正 2024-08-29 18:04:49 +08:00
k1mlka ca7fa0bc97 Merge remote-tracking branch 'origin/develop' into feature/networktestservice 2024-08-29 14:34:51 +08:00
RMBGAME 6a3c8f6187 🐛 多平台账号切换暂只支持 Windows 2024-08-29 00:59:36 +08:00
Aigio Liu 24bb4bd625 ⬆️ Common 2024-08-28 19:30:19 +08:00
k1mlka a3811a97a0 🚧 清理未启用的代码3 2024-08-28 18:47:03 +08:00
k1mlka dc6723f2e5 🚧 清理未启用的代码2 2024-08-28 18:40:45 +08:00
k1mlka e14f169cb0 🚧 清理未启用的代码 2024-08-28 18:38:40 +08:00
k1mlka 235a1abf34 🚧 调整输入框宽度 2024-08-28 18:36:01 +08:00
RMBGAME c5edf0a8b5 ⬇️ Avalonia 11.1.3 2024-08-28 17:41:52 +08:00
Aigio Liu c0b6d9d8e2 🐛 build error 2024-08-28 16:31:44 +08:00
Aigio Liu eefd2fa705 🐛 WinAuth 2024-08-28 16:16:04 +08:00
Aigio Liu 1a4a5887f2 ⬆️ SDK 2024-08-28 16:07:50 +08:00
Aigio Liu 5a163ae124 🐛 fix build 2024-08-28 15:56:52 +08:00
k1mlka 95c5a9e143 🐛 doh服务器地址修正 2024-08-28 15:20:14 +08:00
k1mlka 94f47b0165 ✨ doh检测 2024-08-28 15:14:31 +08:00
k1mlka 08beb69865 🚧 加速器页面及逻辑调整 2024-08-28 11:38:53 +08:00
Aigio Liu 0e9def7e6a ⬆️ Common 2024-08-28 03:11:55 +08:00
Aigio Liu 6a307d97ab 🐛 try fix build 2024-08-28 02:40:36 +08:00
Aigio Liu 252eaa74c2 🐛 PackageVersion 2024-08-28 02:05:38 +08:00
RMBGAME 4115216a41 ⬆️ Avalonia 11.2.0-beta1 2024-08-28 01:02:12 +08:00
RMBGAME a99e410196 🐛 Misc 2024-08-27 19:01:28 +08:00
RMBGAME 4154b68c79 🐛 修复 RiotGames 账号切换无法添加账号的问题
✨账号切换新增 GOG Discord平台
2024-08-27 18:38:24 +08:00
k1mlka fa20e3a945 💄 窗口无边框化页面调整 2024-08-27 17:55:07 +08:00
ff m a15a166b5c ↑ 更新子模块 2024-08-26 18:41:19 +08:00
ff m 0fcd736259 ↑ 更新子模块修复报错 2024-08-26 18:38:22 +08:00
ff m 925c735557 ↑ 更新子模块 2024-08-26 17:59:00 +08:00
k1mlka 0505158281 ✨ 完善网络测试页面 2024-08-23 12:41:06 +08:00
RMBGAME 19651ddeb1 🐛 update afdian domain 2024-08-22 18:57:56 +08:00
k1mlka 1fe7986e08 🚧 业务功能实现 2024-08-22 15:11:25 +08:00
k1mlka a469e29438 🚧 网路检测页面 2024-08-20 17:58:58 +08:00
yingpanwang ec028a4a5c 🚧 调整代码,添加网络环境检查系列方法 2024-08-20 17:18:53 +08:00
RMBGAME 6ebbeb000f 🐛 账号切换 修正在某些情况下也应该尝试正常打开新账号登录窗口 2024-08-19 21:08:45 +08:00
RMBGAME e3cf3195a8 🐛 FixedWrapPanel adaptive width 2024-08-19 16:52:02 +08:00
Aigio Liu 1734a03d41 IPCSubProcessFileSystem.GetLogDirPath 2024-08-19 16:03:08 +08:00
RMBGAME 7c47f01a77 🚧 GameAccelerator 2024-08-17 05:00:30 +08:00
RMBGAME dd622fb5e9 🚧 GameAccelerator 2024-08-16 20:50:06 +08:00
Aigio Liu 59636940c5 ➕ xunyou_show 2024-08-16 18:24:25 +08:00
Aigio Liu c7922c915b 🚧 XYShow 2024-08-16 17:51:59 +08:00
yingpanwang b2031998d8 🎨 调整命名 2024-08-16 16:38:27 +08:00
Aigio Liu 7d3a74daf2 🎨 Safe P/Invoke 2024-08-16 14:28:46 +08:00
k1mlka dccce87293 💄 无边框窗口页面UI改进 2024-08-09 15:29:31 +08:00
Aigio Liu 442ad2dac7 Revert "🔥 Remove ProxyMode.DNSIntercept"
This reverts commit 3eae8a8c84.
2024-08-08 17:03:03 +08:00
RMBGAME ae9a6350a7 🐛 修复 TaskDialog 在主窗口为null时不在抛出异常
修复加速脚本url匹配问题
2024-08-08 02:38:23 +08:00
Aigio Liu d49e38f314 🐛 ISettings 0kb 2024-08-08 01:14:06 +08:00
lxy829 5244009b02 🎨 cs:go vac 修复功能调整 2024-08-06 17:25:36 +08:00
Aigio Liu eef7478585 🐛 CheckUpdateAsync 2024-08-06 14:19:01 +08:00
Aigio Liu 3eae8a8c84 🔥 Remove ProxyMode.DNSIntercept 2024-08-06 12:24:13 +08:00
Aigio Liu c8509e94cf 🔖 3.0.909 rc.9 ms-store Attempt to fix the audit 2024-08-06 11:48:38 +08:00
RMBGAME 7140fc79c9 🐛 修复最新 SteamBeta 版在某些情况下管理成就无限加载问题 2024-08-06 11:44:58 +08:00
Mossimo 7d25393d6e ✨ 程序版本差异 二进制修补 发布工具 打包 上传 发布差异包 ( br 压缩 ) 修补测试 2024-08-05 18:37:49 +08:00
RMBGAME 149c5bbfd3 fix #3412 #2695 #3115
修复某些情况下加速后会将非加速网站自动跳转至加速页的BUG
修复在开启失效代理设置的情况下 加速进程依然错误的使用失效代理解析doh导致加速失败的问题
2024-08-02 00:49:36 +08:00
RMBGAME 38485f1e24 Revert "🐛 DomainMatch"
This reverts commit 008ab5fd4f.
2024-08-01 15:00:47 +08:00
Aigio Liu 008ab5fd4f 🐛 DomainMatch 2024-07-31 02:58:35 +08:00
john.jiang 178c074eb6 🎨 ASF 增加一键下载功能 2024-07-30 19:17:52 +08:00
yingpanwang c3279e78c2 ✨ 添加网络测试服务 NetworkTestService (#3401)
* ✨ 添加网络测试服务 NetworkTestService

* 🚧 清理日志

* ✨ 添加dns 测试

* ⚡️ 添加 DNS Over Https 测试方式, 调整 DNS解析格式
2024-07-18 11:25:28 +08:00
lxy829 7c8f8448af 🎨 cs:go vac repair 2024-07-18 10:18:48 +08:00
wangyingpan 8affd6a3f5 ⚡️ 添加 DNS Over Https 测试方式, 调整 DNS解析格式 2024-07-17 15:36:59 +08:00
RMBGAME 66fdfb728e ⬆️ Avalonia 11.1.0-rc2 2024-07-17 06:01:11 +08:00
RMBGAME 9261344a3c 🐛 修复高dpi下窗口大小及位置变化问题 2024-07-17 05:47:00 +08:00
wangyingpan 923f2eaf94 ✨ 添加dns 测试 2024-07-15 15:59:43 +08:00
RMBGAME 95190efa5c 🐛 doh默认地址调整
加速项目默认选中问题修复
2024-07-12 19:36:04 +08:00
ff m dd71972adc ✨ macOS 支持从 dock 栏 打开窗口显示 2024-07-11 15:43:34 +08:00
wangyingpan 3a48c00ec6 🚧 清理日志 2024-07-11 14:09:01 +08:00
wangyingpan e3f40a3e40 ✨ 添加网络测试服务 NetworkTestService 2024-07-11 13:56:57 +08:00
wangyingpan 6c1e6d40e9 🎨 优化令牌重置 client 代码 2024-07-02 18:07:34 +08:00
wangyingpan 187c7f3b58 🐛 刷新 access_token 时重置对应 client 状态 2024-07-02 17:16:34 +08:00
john.jiang 4e3c86d009 🎨 挂卡剩最后一个游戏不自动切换 2024-07-02 13:40:10 +08:00
RMBGAME ed5cc929d3 📃 Text 2024-07-02 09:51:11 +08:00
Aigio Liu 213b2c2644 🔖 3.0.0 rc.9 2024-07-01 16:45:04 +08:00
RMBGAME e1adf07789 🐛 加速数据树结构某些情况下选中状态也异常问题修复 2024-07-01 16:38:08 +08:00
john.jiang 83f0634863 🎨 挂卡自动刷新token,bug修复 2024-07-01 16:16:18 +08:00
yingpanwang f08122769f 🐛 本地令牌 "确认交易" 时,当access_token 过期时使用refresh_token 刷新当前 access_token 以保持会话 (#3381)
* 🐛 本地令牌 "确认交易" 时,当access_token 过期时使用refresh_token 刷新当前 access_token 以保持会话

* 🎨 调整代码排版
2024-07-01 16:15:51 +08:00
RMBGAME 49fdeadfd2 🐛 HttpVersion 因按原本逻辑转发 2024-07-01 15:08:46 +08:00
RMBGAME bca80245bb 🐛 Misc 2024-06-30 19:00:17 +08:00
RMBGAME 0f6c127ad3 🐛 Misc 2024-06-30 16:00:46 +08:00
Aigio L c087a02303 Update CI.yml 2024-06-29 17:01:31 +08:00
Aigio Liu 6c84a876ff 🐛 ClrearLastLookNoticeDateTime 2024-06-29 16:52:52 +08:00
ff m df27068329 🐛 防止 添加 cookie 报错 2024-06-29 16:41:52 +08:00
ff m f10df31edd 🐛 更新子模块 适配转发请求头 2024-06-29 16:30:35 +08:00
RMBGAME 902919cf98 💄 Misc 2024-06-29 15:55:11 +08:00
RMBGAME a6aec5ad62 🐛 fixed Misc 2024-06-29 15:20:02 +08:00
RMBGAME 2fd651fd25 🐛 窗口偏移位置调整 2024-06-29 13:52:03 +08:00
RMBGAME 67615028e1 ✨🐛 新增 网络加速 二级代理支持
修复 Request Header 中存在非 ASCII 字符导致代理异常的状况
close #2753 #2523 #1661 #488
fix #1825
2024-06-29 00:43:17 +08:00
RMBGAME 1eca159e23 💄 Misc 2024-06-28 16:32:31 +08:00
RMBGAME d1d9ca82df 🐛 fix #3360
修复重启程序窗口增高问题
修复挂卡登录状态丢失挂卡依然继续问题
2024-06-28 16:02:06 +08:00
RMBGAME 88bd2dbfe9 🚑 库存游戏新 appinfo 解析 2024-06-28 13:57:44 +08:00
Mossimo ae2b2970db 🐛 修复 Linux 非 Host 模式不检测 443 占用 2024-06-27 16:32:47 +08:00
RMBGAME 67209f3861 🚧 Misc 2024-06-25 14:16:31 +08:00
RMBGAME 110c92d909 🐛⬆️ Avalonia 11.0-rc1 2024-06-24 14:26:48 +08:00
Aigio Liu cdc773ba74 📝 System.ArgumentException: Decoded string is not a valid IDN name. (Parameter 'unicode') 2024-06-16 00:48:32 +08:00
Aigio Liu 8ea8b73aab 🐛 System.ArgumentException: Decoded string is not a valid IDN name. (Parameter 'unicode')
at System.Globalization.IdnMapping.IcuGetAsciiCore(String unicodeString, Char* unicode, Int32 count)
   at System.Globalization.IdnMapping.GetAscii(String unicode, Int32 index, Int32 count)
   at System.Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder.AddDnsName(String dnsName)
   at BD.WTTS.Services.Implementation.CertGenerator.Add(SubjectAlternativeNameBuilder builder, String name)
2024-06-16 00:46:51 +08:00
RMBGAME 0614e172b8 ✨ 账号切换新增显示用户名设置项 2024-06-07 23:58:49 +08:00
yingpanwang 4c73a5b622 Feautre/joinsteamauthenticator (#3362)
* ✨ 导入steam令牌 新增添加 联合steam 令牌方式
2024-06-06 15:01:15 +08:00
RMBGAME 34341172b8 🚧 ItemsRepeater 2024-05-29 19:15:59 +08:00
RMBGAME 6813ed9ece ⬆️ FA 2.1.0-preview5 2024-05-27 18:23:02 +08:00
RMBGAME 3eb916e78a 🐛 蒸汽平台切换账号支持 2024-05-18 21:37:25 +08:00
RMBGAME 7424d05f54 💄 Avalonia_Edit 2024-05-17 20:27:04 +08:00
RMBGAME 76829b0ca2 🐛 fixed 挂时长启动暂停按钮显示问题 2024-05-10 17:24:43 +08:00
RMBGAME bbe98f68f6 🐛 游戏已加速情况下启动程序闪退问题修复 2024-05-06 19:04:46 +08:00
john.jiang 96025c9ef5 🎨 fixs 2024-05-06 17:12:18 +08:00
john.jiang 2fb12f1b7f 🎨 修复快速掉卡模式剩最后一个游戏闪退Bug 2024-05-06 17:07:07 +08:00
193 changed files with 7644 additions and 1048 deletions
+5 -5
View File
@@ -68,19 +68,19 @@ jobs:
run: dotnet --info
- name: Build BD.WTTS.Client.AppHost
run: dotnet build -c Release src\BD.WTTS.Client.AppHost\BD.WTTS.Client.AppHost.csproj --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
run: dotnet build -c Release src\BD.WTTS.Client.AppHost\BD.WTTS.Client.AppHost.csproj --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
- name: Build BD.WTTS.Client.Plugins.Accelerator.ReverseProxy
run: dotnet build -c Release src\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
run: dotnet build -c Release src\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
- name: Build BD.WTTS.Client.Avalonia.App
run: dotnet build -c Release src\BD.WTTS.Client.Avalonia.App\BD.WTTS.Client.Avalonia.App.csproj --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
run: dotnet build -c Release src\BD.WTTS.Client.Avalonia.App\BD.WTTS.Client.Avalonia.App.csproj --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
- name: Build BD.WTTS.Client.Avalonia.Designer.HostApp
run: dotnet build -c Release src\BD.WTTS.Client.Avalonia.Designer.HostApp\BD.WTTS.Client.Avalonia.Designer.HostApp.csproj --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
run: dotnet build -c Release src\BD.WTTS.Client.Avalonia.Designer.HostApp\BD.WTTS.Client.Avalonia.Designer.HostApp.csproj --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
- name: Test
run: dotnet test src/BD.WTTS.UnitTest/BD.WTTS.UnitTest.csproj -c Release -p:GeneratePackageOnBuild=false --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
run: dotnet test src/BD.WTTS.UnitTest/BD.WTTS.UnitTest.csproj -c Release -p:GeneratePackageOnBuild=false --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
# release:
# name: Release
+1 -1
View File
@@ -16,7 +16,7 @@
[![NET 7.0](https://img.shields.io/badge/dotnet-7.0-purple.svg?style=flat-square&color=512bd4)](https://learn.microsoft.com/zh-cn/dotnet/core/whats-new/dotnet-7)
[![C# 11](https://img.shields.io/badge/c%23-11-brightgreen.svg?style=flat-square&color=6da86a)](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-11)
[![爱发电](https://img.shields.io/badge/爱发电-软妹币玩家-blue.svg?style=flat-square&color=ea4aaa&logo=github-sponsors)](https://afdian.net/@rmbgame)
[![爱发电](https://img.shields.io/badge/爱发电-软妹币玩家-blue.svg?style=flat-square&color=ea4aaa&logo=github-sponsors)](https://afdian.com/@rmbgame)
[![Kofi](https://img.shields.io/badge/Kofi-RMBGAME-orange.svg?style=flat-square&logo=kofi)](https://ko-fi.com/rmbgame)
[![Patreon](https://img.shields.io/badge/Patreon-RMBGAME-red.svg?style=flat-square&logo=patreon)](https://www.patreon.com/rmbgame)
+63 -9
View File
@@ -3,6 +3,12 @@
default_base_path="$HOME/WattToolkit"
base_path="$default_base_path"
# 检查是否以root身份执行
if [ "$EUID" -eq 0 ]; then
echo "此脚本不能以root身份执行。"
exit 1
fi
# 循环直到用户输入有效路径或直接回车
while true; do
# 使用 zenity 提示用户选择安装路径或使用默认路径
@@ -41,7 +47,45 @@ base_url="https://api.steampp.net"
architecture=1
app_name="Watt Toolkit"
PROCESS_NAMES=("$exec_name" "$app_name")
export LC_ALL=en_US.UTF-8
Check_LC_Code() {
# 检查当前 LANG 环境变量
current_lang="${LC_ALL:-$LANG}"
echo "当前语言环境: $current_lang"
# 检查是否为 C 或 C.UTF-8
if [ "$current_lang" = "C" ] || [ "$current_lang" = "C.UTF-8" ]; then
echo "当前语言环境是 C 或 C.UTF-8,需要修改。"
# 列出系统支持的语言环境
supported_locales=$(locale -a)
if [ -z "$supported_locales" ]; then
echo "未能获取到支持的语言环境列表。请检查系统配置。"
return 1
fi
# 遍历支持的语言环境列表,找到第一个不是 C 或 C.UTF-8 的项
first_non_c_locale=""
for locale in $supported_locales; do
if [ "$locale" != "C" ] && [ "$locale" != "C.UTF-8" ]; then
first_non_c_locale="$locale"
break
fi
done
# 检查是否找到了非 C/C.UTF-8 的语言环境
if [ -n "$first_non_c_locale" ]; then
echo "修改语言环境为支持列表中的第一个非 C/C.UTF-8 项:$first_non_c_locale"
export LC_ALL="$first_non_c_locale"
echo "语言环境已修改为 $first_non_c_locale。"
else
echo "未能找到非 C/C.UTF-8 的语言环境。请检查系统配置。"
return 1
fi
else
echo "当前语言环境不是 C 或 C.UTF-8,无需修改。"
fi
}
Check_LC_Code
Install_certutil() {
# 判断发行版类型
if command -v certutil &>/dev/null; then
@@ -360,34 +404,44 @@ Kill_Process
Decompression
# xdg-icon-resource install "$base_path/Icons/Watt-Toolkit.png" --size 128 Watt-Toolkit
InitDesktop() {
# 检查XDG_DESKTOP_DIR环境变量,如果未设置则使用默认值
XDG_DESKTOP_DIR="${XDG_DESKTOP_DIR:-$HOME/Desktop}"
while true; do
# 使用 zenity 提示用户选择安装路径或使用默认路径
choice=$(zenity --list --radiolist --title="请选择要添加到的位置" --column="选择" --column="路径" TRUE "$HOME/.local/share/applications/" FALSE "$HOME/Desktop")
choice=$(zenity --list --radiolist --title="请选择要添加到的位置" \
--column="选择" --column="路径" \
TRUE "$XDG_DESKTOP_DIR" \
FALSE "$HOME/.local/share/applications/")
# 检查用户输入
if [ "$choice" == "$HOME/.local/share/applications/" ]; then
target_dir="$HOME/.local/share/applications/"
break
elif [ "$choice" == "$HOME/Desktop" ]; then
target_dir="$HOME/Desktop/"
elif [ "$choice" == "$XDG_DESKTOP_DIR" ]; then
target_dir="$XDG_DESKTOP_DIR"
break
else
echo "无效选项,请输入 1 或 2。"
# 无效选项时给出提示,并继续循环
zenity --info --text="无效选项,请重新选择。"
fi
done
#添加桌面文件
# 添加桌面文件
rm -rf "$target_dir/Watt Toolkit.desktop" 2>/dev/null
echo "#!/usr/bin/env xdg-open
cat <<EOT > "$target_dir/Watt Toolkit.desktop"
[Desktop Entry]
Name=Watt Toolkit
Exec=$base_path/$exec_name.sh
Icon=$base_path/Icons/Watt-Toolkit.png
Terminal=false
Type=Application
StartupNotify=false" >"$target_dir/Watt Toolkit.desktop"
StartupNotify=false
EOT
chmod +x "$target_dir/Watt Toolkit.desktop"
}
InitDesktop
# update-desktop-database ~/.local/share/applications
#运行程序
+28 -1
View File
@@ -496,6 +496,15 @@
"Summary": "账号平台设置集合",
"IsRegionOrEndregion": null,
"IsValueType": null
},
{
"TypeName": "bool",
"PropertyName": "IsShowAccountName",
"DefaultValue": "false",
"DefaultValueIsConst": false,
"Summary": "是否显示账号用户名",
"IsRegionOrEndregion": null,
"IsValueType": null
}
],
"Usings": ""
@@ -964,12 +973,30 @@
},
{
"TypeName": "string",
"PropertyName": "CustomDohAddres",
"PropertyName": "CustomDohAddres2",
"DefaultValue": "null",
"DefaultValueIsConst": true,
"Summary": "自定义 DNS over HTTPS 地址",
"IsRegionOrEndregion": null,
"IsValueType": null
},
{
"TypeName": "int",
"PropertyName": "AcceleratorTabsSelectedIndex",
"DefaultValue": "0",
"DefaultValueIsConst": false,
"Summary": "加速页面 Tab 选项卡选中下标",
"IsRegionOrEndregion": null,
"IsValueType": null
},
{
"TypeName": "bool",
"PropertyName": "AutoShowWattAcceleratorWindow",
"DefaultValue": "true",
"DefaultValueIsConst": true,
"Summary": "加速后自动唤起 Watt 加速器界面",
"IsRegionOrEndregion": null,
"IsValueType": null
}
],
"Usings": ""
+5
View File
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<CheckEolWorkloads>false</CheckEolWorkloads>
</PropertyGroup>
</Project>
BIN
View File
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
static void WinDivert()
{
var dllWinDivert = Path.Combine(ProjectUtils.ProjPath, "ref", "WinDivert.dll");
var sysWinDivert64 = Path.Combine(ProjectUtils.ProjPath, "ref", "WinDivert64.sys");
byte[][] data = [File.ReadAllBytes(dllWinDivert), File.ReadAllBytes(sysWinDivert64)];
var bytes = MemoryPackSerializer.Serialize(data);
bytes.AsSpan().Reverse();
File.WriteAllBytes(Path.Combine(ProjectUtils.ProjPath, "ref", "WinDivert.mpo"), bytes);
}
+3 -3
View File
@@ -16,14 +16,14 @@ public static partial class AssemblyInfo
/// <summary>
/// 预览版本号,范围 1~9,只增不减
/// </summary>
const string ver_for_preview = "8";
const string ver_for_preview = "9";
/// <summary>
/// RC 版本号,范围 0~9,只增不减
/// </summary>
const string ver_for_rc = "8";
const string ver_for_rc = "12";
public const string FileVersion = $"{Version2}.{ver_for_rc}0{ver_for_preview}.0";
public const string FileVersion = $"{Version2}.{ver_for_rc}1{ver_for_preview}.0";
//public const string InformationalVersion = Version;
//public const string InformationalVersion = $"{Version}-preview.{ver_for_preview}";
@@ -7,6 +7,7 @@
<DelaySign>false</DelaySign>
<AssemblyOriginatorKeyFile>$(MSBuildProjectDirectory)\..\..\avalonia.snk</AssemblyOriginatorKeyFile>
<!--https://github.com/AvaloniaUI/Avalonia/blob/0.10.10/build/SharedVersion.props#L18-->
<AvaloniaAccessUnstablePrivateApis>true</AvaloniaAccessUnstablePrivateApis>
</PropertyGroup>
<ItemGroup>
@@ -91,9 +91,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BD.Common.XmlnsDefinition.Avalonia">
<Aliases>BD_Common_XmlnsDefinition_Avalonia</Aliases>
</PackageReference>
<ProjectReference Include="..\BD.WTTS.Client.Avalonia\BD.WTTS.Client.Avalonia.csproj" />
<ProjectReference Include="..\BD.WTTS.Client\BD.WTTS.Client.csproj" />
</ItemGroup>
@@ -68,9 +68,6 @@
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="BD.Common.XmlnsDefinition.Avalonia">
<Aliases>BD_Common_XmlnsDefinition_Avalonia</Aliases>
</PackageReference>
<ProjectReference Include="..\BD.WTTS.Client.Avalonia\BD.WTTS.Client.Avalonia.csproj" />
<ProjectReference Include="..\BD.WTTS.Client.Plugins.Accelerator\BD.WTTS.Client.Plugins.Accelerator.csproj" />
<ProjectReference Include="..\BD.WTTS.Client.Plugins.ArchiSteamFarmPlus\BD.WTTS.Client.Plugins.ArchiSteamFarmPlus.csproj" />
@@ -4,6 +4,7 @@
<RootNamespace>BD.WTTS</RootNamespace>
<DefineConstants>AVALONIA;$(DefineConstants)</DefineConstants>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<AvaloniaAccessUnstablePrivateApis>true</AvaloniaAccessUnstablePrivateApis>
</PropertyGroup>
<ItemGroup>
@@ -72,17 +73,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BD.Common.XmlnsDefinition.Avalonia">
<Aliases>BD_Common_XmlnsDefinition_Avalonia</Aliases>
</PackageReference>
<PackageReference Include="BD.Common" />
<PackageReference Include="BD.Common.Primitives" />
<PackageReference Include="BD.Common.Essentials" />
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.ReactiveUI" />
<PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" />
<PackageReference Include="Avalonia.Xaml.Behaviors" />
<!--<PackageReference Include="Avalonia.AvaloniaEdit" />-->
<PackageReference Include="Avalonia.AvaloniaEdit" />
<PackageReference Include="AvaloniaEdit.TextMate" />
<!--<PackageReference Include="Markdown.Avalonia" />-->
<PackageReference Include="FluentAvaloniaUI" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
@@ -0,0 +1,203 @@
using Avalonia.Reactive;
using Avalonia.Xaml.Interactions.Responsive;
using Avalonia.Xaml.Interactivity;
namespace BD.WTTS.Behaviors;
/// <summary>
/// Observes <see cref="Behavior{T}.AssociatedObject"/> control or <see cref="SourceControl"/> control <see cref="Visual.Bounds"/> property changes and if triggered sets or removes style classes when conditions from <see cref="AdaptiveClassSetter"/> are met.
/// </summary>
public class AdaptiveBehavior : Behavior<Control>
{
private IDisposable? _disposable;
private AvaloniaList<AdaptiveClassSetter>? _setters;
/// <summary>
/// Identifies the <seealso cref="SourceControl"/> avalonia property.
/// </summary>
public static readonly StyledProperty<Control?> SourceControlProperty =
AvaloniaProperty.Register<AdaptiveBehavior, Control?>(nameof(SourceControl));
/// <summary>
/// Identifies the <seealso cref="TargetControl"/> avalonia property.
/// </summary>
public static readonly StyledProperty<Control?> TargetControlProperty =
AvaloniaProperty.Register<AdaptiveBehavior, Control?>(nameof(TargetControl));
/// <summary>
/// Identifies the <seealso cref="Setters"/> avalonia property.
/// </summary>
public static readonly DirectProperty<AdaptiveBehavior, AvaloniaList<AdaptiveClassSetter>> SettersProperty =
AvaloniaProperty.RegisterDirect<AdaptiveBehavior, AvaloniaList<AdaptiveClassSetter>>(nameof(Setters), t => t.Setters);
/// <summary>
/// Gets or sets the the source control that <see cref="Visual.BoundsProperty"/> property are observed from, if not set <see cref="Behavior{T}.AssociatedObject"/> is used. This is a avalonia property.
/// </summary>
[ResolveByName]
public Control? SourceControl
{
get => GetValue(SourceControlProperty);
set => SetValue(SourceControlProperty, value);
}
/// <summary>
/// Gets or sets the target control that class name that should be added or removed when triggered, if not set <see cref="Behavior{T}.AssociatedObject"/> is used or <see cref="AdaptiveClassSetter.TargetControl"/> from <see cref="AdaptiveClassSetter"/>. This is a avalonia property.
/// </summary>
[ResolveByName]
public Control? TargetControl
{
get => GetValue(TargetControlProperty);
set => SetValue(TargetControlProperty, value);
}
/// <summary>
/// Gets adaptive class setters collection. This is a avalonia property.
/// </summary>
[Content]
public AvaloniaList<AdaptiveClassSetter> Setters => _setters ??= new AvaloniaList<AdaptiveClassSetter>();
/// <inheritdoc/>
protected override void OnAttachedToVisualTree()
{
base.OnAttachedToVisualTree();
StopObserving();
StartObserving();
}
/// <inheritdoc/>
protected override void OnDetachedFromVisualTree()
{
base.OnDetachedFromVisualTree();
StopObserving();
}
private void StartObserving()
{
var sourceControl = GetValue(SourceControlProperty) is not null
? SourceControl
: AssociatedObject;
if (sourceControl is not null)
{
_disposable = ObserveBounds(sourceControl);
}
}
private void StopObserving()
{
_disposable?.Dispose();
}
private IDisposable ObserveBounds(Control sourceControl)
{
if (sourceControl is null)
{
throw new ArgumentNullException(nameof(sourceControl));
}
return sourceControl.GetObservable(Visual.BoundsProperty)
.Subscribe(new AnonymousObserver<Rect>(bounds => ValueChanged(sourceControl, Setters, bounds)));
}
private void ValueChanged(Control? sourceControl, AvaloniaList<AdaptiveClassSetter>? setters, Rect bounds)
{
if (sourceControl is null || setters is null)
{
return;
}
foreach (var setter in setters)
{
var isMinOrMaxWidthSet = setter.IsSet(AdaptiveClassSetter.MinWidthProperty)
|| setter.IsSet(AdaptiveClassSetter.MaxWidthProperty);
var widthConditionTriggered = GetResult(setter.MinWidthOperator, bounds.Width, setter.MinWidth)
&& GetResult(setter.MaxWidthOperator, bounds.Width, setter.MaxWidth);
var isMinOrMaxHeightSet = setter.IsSet(AdaptiveClassSetter.MinHeightProperty)
|| setter.IsSet(AdaptiveClassSetter.MaxHeightProperty);
var heightConditionTriggered = GetResult(setter.MinHeightOperator, bounds.Height, setter.MinHeight)
&& GetResult(setter.MaxHeightOperator, bounds.Height, setter.MaxHeight);
var isAddClassTriggered = isMinOrMaxWidthSet switch
{
true when !isMinOrMaxHeightSet => widthConditionTriggered,
false when isMinOrMaxHeightSet => heightConditionTriggered,
true when isMinOrMaxHeightSet => widthConditionTriggered && heightConditionTriggered,
_ => false
};
var targetControl = setter.GetValue(AdaptiveClassSetter.TargetControlProperty) is not null
? setter.TargetControl
: GetValue(TargetControlProperty) is not null
? TargetControl
: AssociatedObject;
if (targetControl is not null)
{
var className = setter.ClassName;
var isPseudoClass = setter.IsPseudoClass;
if (isAddClassTriggered)
{
Add(targetControl, className, isPseudoClass);
}
else
{
Remove(targetControl, className, isPseudoClass);
}
}
}
}
private bool GetResult(ComparisonConditionType comparisonConditionType, double property, double value)
{
return comparisonConditionType switch
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
ComparisonConditionType.Equal => property == value,
// ReSharper disable once CompareOfFloatsByEqualityOperator
ComparisonConditionType.NotEqual => property != value,
ComparisonConditionType.LessThan => property < value,
ComparisonConditionType.LessThanOrEqual => property <= value,
ComparisonConditionType.GreaterThan => property > value,
ComparisonConditionType.GreaterThanOrEqual => property >= value,
_ => throw new ArgumentOutOfRangeException()
};
}
private static void Add(Control targetControl, string? className, bool isPseudoClass)
{
if (className is null || string.IsNullOrEmpty(className) || targetControl.Classes.Contains(className))
{
return;
}
if (isPseudoClass)
{
((IPseudoClasses)targetControl.Classes).Add(className);
}
else
{
targetControl.Classes.Add(className);
}
}
private static void Remove(Control targetControl, string? className, bool isPseudoClass)
{
if (className is null || string.IsNullOrEmpty(className) || !targetControl.Classes.Contains(className))
{
return;
}
if (isPseudoClass)
{
((IPseudoClasses)targetControl.Classes).Remove(className);
}
else
{
targetControl.Classes.Remove(className);
}
}
}
@@ -13,6 +13,7 @@ public static class WindowTransparencyLevelExtensions
WindowBackgroundMaterial.Blur => WindowTransparencyLevel.Blur,
WindowBackgroundMaterial.AcrylicBlur => WindowTransparencyLevel.AcrylicBlur,
WindowBackgroundMaterial.Mica => WindowTransparencyLevel.Mica,
WindowBackgroundMaterial.Transparent => WindowTransparencyLevel.Transparent,
_ => WindowTransparencyLevel.None,
};
}
@@ -11,6 +11,7 @@
[assembly: XmlnsDefinition("https://steampp.net/ui", "BD.WTTS.UI.Views.Windows")]
[assembly: XmlnsDefinition("https://steampp.net/ui", "BD.WTTS.Converters")]
[assembly: XmlnsDefinition("https://steampp.net/ui", "BD.WTTS.Markup")]
[assembly: XmlnsDefinition("https://steampp.net/ui", "BD.WTTS.Behaviors")]
[assembly: XmlnsDefinition("https://steampp.net/services", "BD.WTTS.Services")]
[assembly: XmlnsDefinition("https://steampp.net/services", "BD.WTTS.Plugins")]
[assembly: XmlnsDefinition("https://steampp.net/services", "BD.WTTS.Plugins.Abstractions")]
@@ -95,7 +95,7 @@ public sealed class AvaloniaWindowManagerImpl : IWindowManagerImpl
Func<bool>? cancelCloseAction = null,
bool disableScroll = false)
where TPageViewModel : ViewModelBase
{
{
var td = new TaskDialogEx
{
Title = title,
@@ -111,6 +111,12 @@ public sealed class AvaloniaWindowManagerImpl : IWindowManagerImpl
XamlRoot = GetWindowTopLevel(),
};
if (td.XamlRoot == null)
{
Toast.LogAndShowT(new Exception("在 AppWindow 为 Null 时,无法弹出 Taskdialog."));
return false;
}
if (viewModel != null)
{
if (viewModel is IWindowViewModel window)
@@ -44,12 +44,14 @@ sealed class AvaloniaToastServiceImpl : IToastService
if (NotificationManager == null)
{
var host = AvaloniaWindowManagerImpl.GetWindowTopLevel();
NotificationManager = new SnackbarManager(host)
if (host != null)
{
Position = NotificationPosition.BottomRight,
MaxItems = 5,
};
NotificationManager = new SnackbarManager(host)
{
Position = NotificationPosition.BottomRight,
MaxItems = 5,
};
}
}
var notificationType = GetNotificationType(icon);
+41 -1
View File
@@ -63,10 +63,50 @@ partial class App
#else
s.HasTrayIcon ? ShutdownMode.OnExplicitShutdown : ShutdownMode.OnMainWindowClose;
#endif
#if MACOS
if (Current!.TryGetFeature<IActivatableLifetime>() is { } activatableLifetime)
{
activatableLifetime.Activated += ActivatableLifetimeOnActivated;
// macOS 中 右键隐藏 会触发目前不需要使用
//activatableLifetime.Deactivated += ActivatableLifetimeOnDeactivated;
}
#endif
}
}
#if MACOS
/// <summary>
/// 从 macOS Dock 栏恢复窗口显示
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ActivatableLifetimeOnActivated(object? sender, ActivatedEventArgs e)
{
switch (e.Kind)
{
case ActivationKind.Background:
case ActivationKind.Reopen:
RestoreMainWindow();
break;
}
}
///// <summary>
///// macOS 中 右键隐藏 会触发目前不需要使用
///// </summary>
///// <param name="sender"></param>
///// <param name="e"></param>
//private void ActivatableLifetimeOnDeactivated(object? sender, ActivatedEventArgs e)
//{
// switch (e.Kind)
// {
// case ActivationKind.Background:
// Console.WriteLine($"ActivatableLifetimeOnDeactivated {e.Kind}");
// break;
// }
//}
#endif
public void UpdateMenuItems()
{
MainThread2.BeginInvokeOnMainThread(() =>
@@ -111,6 +111,12 @@ public sealed partial class App : Application
{
Task2.InBackground(() => appWindow.SplashScreen?.RunTasks(CancellationToken.None));
}
IPlatformService.Instance.SetSystemSessionEnding(() =>
{
Log.Info("System Shutdown", "Application SafeExit...");
Shutdown();
});
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
{
@@ -65,7 +65,7 @@
</spp:AppItem>
<spp:AppItem
Title="Wo Long: Fallen Dynasty (卧龙:苍天陨落)"
Title="Wo Long: Fallen Dynasty (卧龙:苍天陨落)(卧龙:苍天陨落)(卧龙:苍天陨落)(卧龙:苍天陨落)"
MaxHeight="120"
Description="This is a description of whatver this does"
Tags="111">
@@ -181,7 +181,7 @@
Grid.Row="2"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,5"
Margin="0,5,12,5"
VerticalAlignment="Top"
Classes="CaptionTextBlockStyle"
FontSize="12"
@@ -9,6 +9,11 @@
<StackPanel Spacing="10">
<HyperlinkButton Content="更新日志" />
<Button Content="更新日志" Theme="{StaticResource HyperlinkButton}" />
<Button Classes="TransparentAccent" Content="更新日志" />
<Button
Classes="TransparentAccent"
Content="更新日志"
IsEnabled="False" />
<Button
Content="更新日志"
IsEnabled="False"
@@ -87,7 +92,7 @@
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Template">
<ControlTemplate>
<ui:FABorder
<Border
Name="Root"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
@@ -101,19 +106,19 @@
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True" />
</ui:FABorder>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
@@ -122,7 +127,7 @@
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorDisabledBrush}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
@@ -131,7 +136,7 @@
</Style>
<Style Selector="^:checked">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundChecked}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
@@ -140,7 +145,7 @@
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
@@ -149,7 +154,7 @@
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPressed}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
@@ -158,7 +163,7 @@
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorDisabledBrush}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
@@ -168,19 +173,19 @@
</Style>
<Style Selector="^:indeterminate">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTransparentBrush}" />
</Style>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
@@ -189,7 +194,7 @@
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorDisabledBrush}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
@@ -214,13 +219,22 @@
<Style Selector="Button.Plain:pointerover">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Button.Plain:pointerover /template/ ui|FABorder#Root">
<Style Selector="Button.Plain:pointerover /template/ Border#Root">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Button.Plain:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Button.TransparentAccent /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource HyperlinkButtonForeground}" />
</Style>
<Style Selector="Button.TransparentAccent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource HyperlinkButtonForegroundPointerOver}" />
</Style>
<Style Selector="Button.TransparentAccent:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource HyperlinkButtonForegroundPressed}" />
</Style>
</Styles>
@@ -63,14 +63,13 @@
Height="10"
HorizontalAlignment="Center"
VerticalAlignment="Bottom">
<ItemsRepeater Name="Swiper" IsVisible="False">
<ItemsRepeater.Layout>
<StackLayout
DisableVirtualization="True"
Orientation="Horizontal"
Spacing="8" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<ItemsControl Name="Swiper" IsVisible="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.DataType>
<x:Type x:TypeArguments="x:Int32, x:String" TypeName="generic:KeyValuePair" />
@@ -91,8 +90,8 @@
StrokeThickness="0" />
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Panel>
</Grid>
</Border>
@@ -46,19 +46,19 @@
<Setter Property="Template">
<ControlTemplate>
<StackPanel>
<ui:FABorder
<Border
Name="LayoutRoot"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ui:FABorder.Transitions>
<Border.Transitions>
<Transitions>
<BrushTransition Property="Background" Duration="00:00:00.083" />
<BrushTransition Property="BorderBrush" Duration="00:00:00.083" />
</Transitions>
</ui:FABorder.Transitions>
</Border.Transitions>
<Grid MinHeight="{TemplateBinding MinHeight}" ColumnDefinitions="Auto,*,Auto,Auto">
<Viewbox
Name="IconHost"
@@ -113,7 +113,7 @@
VerticalAlignment="Center"
FontSize="18" />
</Grid>
</ui:FABorder>
</Border>
<ContentPresenter
Name="ExpandedContentPresenter"
@@ -144,10 +144,10 @@
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="spp|OptionsDisplayItem /template/ ui|FABorder#LayoutRoot:pointerover">
<Style Selector="spp|OptionsDisplayItem /template/ Border#LayoutRoot:pointerover">
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}" />
</Style>
<Style Selector="spp|OptionsDisplayItem:pressed /template/ ui|FABorder#LayoutRoot:pointerover">
<Style Selector="spp|OptionsDisplayItem:pressed /template/ Border#LayoutRoot:pointerover">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTertiaryBrush}" />
</Style>
@@ -179,7 +179,7 @@
<Setter Property="CornerRadius" Value="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomCornerRadiusFilterConverter}}" />
</Style>
<Style Selector="spp|OptionsDisplayItem:expanded /template/ ui|FABorder#LayoutRoot">
<Style Selector="spp|OptionsDisplayItem:expanded /template/ Border#LayoutRoot">
<Setter Property="CornerRadius" Value="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopCornerRadiusFilterConverter}}" />
</Style>
@@ -195,19 +195,19 @@
<Style Selector="spp|OptionsDisplayItem.Vertical">
<Setter Property="Template">
<ControlTemplate>
<ui:FABorder
<Border
Name="LayoutRoot"
Padding="12"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ui:FABorder.Transitions>
<Border.Transitions>
<Transitions>
<BrushTransition Property="Background" Duration="00:00:00.083" />
<BrushTransition Property="BorderBrush" Duration="00:00:00.083" />
</Transitions>
</ui:FABorder.Transitions>
</Border.Transitions>
<Grid RowDefinitions="*,*">
<Border
Width="72"
@@ -241,7 +241,7 @@
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</ui:FABorder>
</Border>
</ControlTemplate>
</Setter>
</Style>
@@ -97,7 +97,7 @@
</Design.PreviewWith>
<Styles.Resources>
<Thickness x:Key="AppPageMargin">20</Thickness>
<Thickness x:Key="AppPageMargin">20,10,20,20</Thickness>
</Styles.Resources>
<Style Selector="spp|PageBase">
@@ -313,7 +313,7 @@
<Setter Property="Grid.Row" Value="2" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Margin" Value="0 12" />
<Setter Property="Margin" Value="0 12 0 0" />
</Style>
<Style Selector="^ /template/ Panel#PageDetails">
@@ -38,18 +38,18 @@
<Setter Property="MinHeight" Value="{DynamicResource SettingsExpanderItemMinHeight}" />
<Setter Property="Template">
<ControlTemplate>
<ui:FABorder
<Border
Name="Root"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ui:FABorder.Transitions>
<Border.Transitions>
<Transitions>
<BrushTransition Property="Background" Duration="00:00:00.083" />
<BrushTransition Property="BorderBrush" Duration="00:00:00.083" />
</Transitions>
</ui:FABorder.Transitions>
</Border.Transitions>
<Grid ColumnDefinitions="*,Auto,Auto" RowDefinitions="*,Auto">
<Viewbox
@@ -102,7 +102,7 @@
<ContentPresenter Name="ExpandChevronActionIconContainer" Content="{Binding TemplateSettings.ActionIcon, RelativeSource={RelativeSource TemplatedParent}}" />
</Viewbox>
</Grid>
</ui:FABorder>
</Border>
</ControlTemplate>
</Setter>
@@ -110,7 +110,7 @@
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:nth-last-child(1) /template/ ui|FABorder#Root">
<Style Selector="^:nth-last-child(1) /template/ Border#Root">
<Setter Property="CornerRadius" Value="{Binding Source={StaticResource ControlCornerRadius}, Converter={StaticResource BottomCornerRadiusFilterConverter}}" />
</Style>
@@ -152,11 +152,11 @@
</Style>
<Style Selector="^:allowClick">
<Style Selector="^:pointerover /template/ ui|FABorder#Root">
<Style Selector="^:pointerover /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlFillColorSecondaryBrush}" />
</Style>
<Style Selector="^:pressed /template/ ui|FABorder#Root">
<Style Selector="^:pressed /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTertiaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
@@ -164,7 +164,7 @@
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource ControlFillColorDisabledBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
@@ -95,7 +95,7 @@
<Setter Property="Template">
<ControlTemplate>
<LayoutTransformControl Name="PART_LayoutTransformControl" UseRenderTransform="True">
<ui:FABorder
<Border
Name="ContentRoot"
VerticalAlignment="Top"
BorderBrush="{DynamicResource InfoBarBorderBrush}"
@@ -205,13 +205,13 @@
</Button>
</Grid>
</Panel>
</ui:FABorder>
</Border>
</LayoutTransformControl>
</ControlTemplate>
</Setter>
<Style Selector="^:informational">
<Style Selector="^ /template/ ui|FABorder#ContentRoot">
<Style Selector="^ /template/ Border#ContentRoot">
<!--<Setter Property="Background" Value="{DynamicResource InfoBarInformationalSeverityBackgroundBrush}" />-->
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
</Style>
@@ -226,7 +226,7 @@
</Style>
<Style Selector="^:error">
<Style Selector="^ /template/ ui|FABorder#ContentRoot">
<Style Selector="^ /template/ Border#ContentRoot">
<Setter Property="Background" Value="{DynamicResource InfoBarErrorSeverityBackgroundBrush}" />
</Style>
<Style Selector="^ /template/ Ellipse#IconBackground">
@@ -241,7 +241,7 @@
</Style>
<Style Selector="^:warning">
<Style Selector="^ /template/ ui|FABorder#ContentRoot">
<Style Selector="^ /template/ Border#ContentRoot">
<Setter Property="Background" Value="{DynamicResource InfoBarWarningSeverityBackgroundBrush}" />
</Style>
<Style Selector="^ /template/ Ellipse#IconBackground">
@@ -255,7 +255,7 @@
</Style>
<Style Selector="^:success">
<Style Selector="^ /template/ ui|FABorder#ContentRoot">
<Style Selector="^ /template/ Border#ContentRoot">
<Setter Property="Background" Value="{DynamicResource InfoBarSuccessSeverityBackgroundBrush}" />
</Style>
<Style Selector="^ /template/ Ellipse#IconBackground">
@@ -294,7 +294,7 @@
</Style>
<!-- Visible -->
<Style Selector="^:hidden /template/ ui|FABorder#ContentRoot">
<Style Selector="^:hidden /template/ Border#ContentRoot">
<Setter Property="IsVisible" Value="False" />
</Style>
@@ -75,7 +75,7 @@
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<ui:FABorder
<Border
Name="Root"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
@@ -83,19 +83,19 @@
BorderThickness="1"
CornerRadius="{TemplateBinding CornerRadius}"
TemplatedControl.IsTemplateFocusTarget="True">
<ui:FABorder.Transitions>
<Border.Transitions>
<Transitions>
<BrushTransition Property="Background" Duration="00:00:00.083" />
</Transitions>
</ui:FABorder.Transitions>
</Border.Transitions>
<Panel>
<ui:FABorder
<Border
Name="Hover"
Margin="3"
Background="Transparent"
CornerRadius="{TemplateBinding CornerRadius}">
<ui:FABorder.Transitions>
<Border.Transitions>
<Transitions>
<BrushTransition Property="Background" Duration="00:00:00.083" />
<TransformOperationsTransition
@@ -103,8 +103,8 @@
Property="RenderTransform"
Duration="00:00:00.167" />
</Transitions>
</ui:FABorder.Transitions>
</ui:FABorder>
</Border.Transitions>
</Border>
<ContentPresenter
Name="PART_ContentPresenter"
@@ -139,19 +139,19 @@
</Rectangle>
</Panel>
</ui:FABorder>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ui|FABorder#Hover">
<Style Selector="^ /template/ Border#Hover">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Style>
</Style>
<Style Selector="^:pressed">
<Style Selector="^ /template/ ui|FABorder#Hover">
<Style Selector="^ /template/ Border#Hover">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
<Setter Property="RenderTransform" Value="scale(0.96)" />
</Style>
@@ -161,10 +161,10 @@
</Style>
<Style Selector="^:selected">
<Style Selector="^ /template/ ui|FABorder#Hover">
<Style Selector="^ /template/ Border#Hover">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
</Style>
@@ -181,10 +181,10 @@
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Hover">
<Style Selector="^ /template/ Border#Hover">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ ui|FABorder#Root">
<Style Selector="^ /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource ControlFillColorDisabledBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlAltFillColorSecondaryBrush}" />
</Style>
@@ -80,7 +80,7 @@
<Setter Property="Template">
<ControlTemplate>
<Panel Name="LayoutRoot" Background="{x:Null}">
<ui:FABorder
<Border
Name="ContentRoot"
MinWidth="{DynamicResource TaskDialogMinWidth}"
MinHeight="{DynamicResource TaskDialogMinHeight}"
@@ -216,7 +216,7 @@
</ItemsControl>
</Border>
</Grid>
</ui:FABorder>
</Border>
</Panel>
</ControlTemplate>
</Setter>
@@ -303,7 +303,7 @@
<Style Selector="^ /template/ Panel#LayoutRoot">
<Setter Property="Background" Value="{DynamicResource TaskDialogSmokeFill}" />
</Style>
<Style Selector="^ /template/ ui|FABorder#ContentRoot">
<Style Selector="^ /template/ Border#ContentRoot">
<Setter Property="BorderBrush" Value="{DynamicResource TaskDialogBorderBrush}" />
<Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
<Setter Property="BorderThickness" Value="{DynamicResource TaskDialogBorderWidth}" />
@@ -332,7 +332,7 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="^:hidden /template/ ui|FABorder#ContentRoot">
<Style Selector="^:hidden /template/ Border#ContentRoot">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.167">
<KeyFrame Cue="0%">
@@ -368,7 +368,7 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="^:open /template/ ui|FABorder#ContentRoot">
<Style Selector="^:open /template/ Border#ContentRoot">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.250">
<KeyFrame Cue="0%">
@@ -389,7 +389,7 @@
<Setter Property="Template">
<ControlTemplate>
<Panel Name="LayoutRoot" Background="{x:Null}">
<ui:FABorder
<Border
Name="ContentRoot"
MinWidth="{DynamicResource TaskDialogMinWidth}"
MinHeight="{DynamicResource TaskDialogMinHeight}"
@@ -521,7 +521,7 @@
</ItemsControl>
</Border>
</Grid>
</ui:FABorder>
</Border>
</Panel>
</ControlTemplate>
</Setter>
@@ -14,4 +14,5 @@
</Styles.Resources>
<StyleInclude Source="/UI/Styling/Controls.axaml" />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
</Styles>
@@ -86,4 +86,10 @@
<StreamGeometry x:Key="Phone">
M15.75 2C16.9926 2 18 3.00736 18 4.25V19.75C18 20.9926 16.9926 22 15.75 22H8.25C7.00736 22 6 20.9926 6 19.75V4.25C6 3.00736 7.00736 2 8.25 2H15.75ZM13.25 18H10.75C10.3358 18 10 18.3358 10 18.75C10 19.1642 10.3358 19.5 10.75 19.5H13.25C13.6642 19.5 14 19.1642 14 18.75C14 18.3358 13.6642 18 13.25 18Z
</StreamGeometry>
<StreamGeometry x:Key="GOGGalaxy">
M304.99296 650.144H185.98496a17.12 17.12 0 0 0-16.992 17.184v85.344c0 8.992 7.68 16.992 16.992 16.992h119.52v56.32H156.16096a43.84 43.84 0 0 1-43.488-44v-144.512c0-23.968 19.68-43.488 44-43.488h148.32z m43.168-157.824c0 24.832-20 44.832-44.832 44.832H112.19296v-57.696h161.312a16.896 16.896 0 0 0 17.024-16.992V272.64a17.024 17.024 0 0 0-17.024-17.184H187.36096a17.152 17.152 0 0 0-17.536 17.184v86.144c0 9.856 7.68 17.024 17.024 17.024H256.00096v57.632H157.02496a44.544 44.544 0 0 1-44.832-44.32V242.304c0-24.352 20.192-44.32 44.832-44.32H303.04096c24.672 0 44.64 19.968 44.64 44.32v249.984z m563.168 333.664h-56.32v-175.84h-39.68a17.12 17.12 0 0 0-16.992 17.184v158.624h-56.832v-175.84h-39.68a17.12 17.12 0 0 0-16.992 17.184v158.624h-56.832v-188.512c0-23.968 19.68-43.488 44-43.488h239.328z m0.512-333.664c0 24.832-20.192 44.832-44.832 44.832h-191.136v-57.696h161.312a16.96 16.96 0 0 0 17.024-16.992V272.64a17.12 17.12 0 0 0-17.024-17.184h-86.656a17.024 17.024 0 0 0-17.024 17.184v86.144c0 9.856 7.68 17.024 17.024 17.024h69.152v57.632h-98.688a44.352 44.352 0 0 1-44.64-44.32V242.304c0-24.352 19.968-44.32 44.64-44.32h146.336c24.832 0 44.832 19.968 44.832 44.32v249.984zM585.34496 198.016h-146.688c-24.832 0-44.512 19.968-44.512 44.32v146.816a44.16 44.16 0 0 0 44.512 44.32h146.688a44.48 44.48 0 0 0 44.512-44.32V242.336c0-24.352-20.192-44.32-44.512-44.32z m-12.672 74.656v86.144c0 9.504-7.68 17.024-17.152 17.024h-86.688a16.96 16.96 0 0 1-16.992-17.024V272.96c0-9.312 7.328-16.992 16.992-16.992h85.856c9.824 0 16.992 7.68 16.992 16.992z m-33.824 321.344h-144.672c-24.16 0-43.808 19.488-43.808 43.488v144.512c0 24.32 19.68 44 43.808 44h144.672c24.32 0 44-19.68 44-44v-144.512c0-23.968-19.68-43.488-44-43.488z m-12.864 73.344v85.344a16.96 16.96 0 0 1-16.992 16.992v-0.352h-84.864a17.12 17.12 0 0 1-17.152-17.152v-84.832a17.28 17.28 0 0 1 17.152-17.184h85.344c9.344 0 17.024 7.84 17.024 17.184z m476.16-620.352A74.24 74.24 0 0 0 949.31296 24.64H74.65696A74.304 74.304 0 0 0 0.00096 99.296v825.344a74.304 74.304 0 0 0 74.656 74.656h874.656a74.304 74.304 0 0 0 74.656-74.656V99.296c0-20.32-8.48-39.168-21.856-52.832z m0 877.664a52.416 52.416 0 0 1-52.832 52.832H74.65696a52.576 52.576 0 0 1-53.344-52.832V99.328a52.672 52.672 0 0 1 52.832-52.832H948.80096c29.344 0 52.992 23.68 52.992 52.832v825.344z
</StreamGeometry>
<StreamGeometry x:Key="Discord">
M1049.062 139.672a3 3 0 0 0-1.528-1.4A970.13 970.13 0 0 0 808.162 64.06a3.632 3.632 0 0 0-3.846 1.82 674.922 674.922 0 0 0-29.8 61.2 895.696 895.696 0 0 0-268.852 0 619.082 619.082 0 0 0-30.27-61.2 3.78 3.78 0 0 0-3.848-1.82 967.378 967.378 0 0 0-239.376 74.214 3.424 3.424 0 0 0-1.576 1.352C78.136 367.302 36.372 589.38 56.86 808.708a4.032 4.032 0 0 0 1.53 2.75 975.332 975.332 0 0 0 293.65 148.378 3.8 3.8 0 0 0 4.126-1.352A696.4 696.4 0 0 0 416.24 860.8a3.72 3.72 0 0 0-2.038-5.176 642.346 642.346 0 0 1-91.736-43.706 3.77 3.77 0 0 1-0.37-6.252 502.094 502.094 0 0 0 18.218-14.274 3.638 3.638 0 0 1 3.8-0.512c192.458 87.834 400.82 87.834 591 0a3.624 3.624 0 0 1 3.848 0.466 469.066 469.066 0 0 0 18.264 14.32 3.768 3.768 0 0 1-0.324 6.252 602.814 602.814 0 0 1-91.78 43.66 3.75 3.75 0 0 0-2 5.222 782.11 782.11 0 0 0 60.028 97.63 3.728 3.728 0 0 0 4.126 1.4A972.096 972.096 0 0 0 1221.4 811.458a3.764 3.764 0 0 0 1.53-2.704c24.528-253.566-41.064-473.824-173.868-669.082zM444.982 675.16c-57.944 0-105.688-53.174-105.688-118.478s46.818-118.482 105.688-118.482c59.33 0 106.612 53.64 105.686 118.478 0 65.308-46.82 118.482-105.686 118.482z m390.76 0c-57.942 0-105.686-53.174-105.686-118.478s46.818-118.482 105.686-118.482c59.334 0 106.614 53.64 105.688 118.478 0 65.308-46.354 118.482-105.688 118.482z
</StreamGeometry>
</ResourceDictionary>
@@ -14,6 +14,8 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel>, IV
public static readonly StyledProperty<bool> IsSaveWindowSizeProperty = AvaloniaProperty
.Register<ReactiveAppWindow<TViewModel>, bool>(nameof(IsSaveWindowSize), true);
private bool _isInitialize;
/// <summary>
/// Initializes a new instance of the <see cref="ReactiveAppWindow{TViewModel}"/> class.
/// </summary>
@@ -127,27 +129,32 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel>, IV
{
var primaryScreenBounds = screen.Bounds;
if (CanResize && !IsVisible)
if (CanResize && !_isInitialize && !IsVisible)
{
WindowStartupLocation = WindowStartupLocation.Manual;
var scalingOffset = 31 - ((screen.Scaling - 1) / 0.5); // appwindow偏移量
if (SizePosition.Width > 0 &&
primaryScreenBounds.Width >= SizePosition.Width)
Width = SizePosition.Width;
if (SizePosition.Height > 0 &&
primaryScreenBounds.Height >= SizePosition.Height)
Height = SizePosition.Height;
Height = SizePosition.Height - scalingOffset;
if (SizePosition.X > 0 && SizePosition.Y > 0)
{
var leftTopPoint = new PixelPoint(SizePosition.X, SizePosition.Y);
var offsetY = (int)Math.Round(scalingOffset * screen.Scaling);
//var offsetY = scalingOffset;
var leftTopPoint = new PixelPoint(SizePosition.X, SizePosition.Y + offsetY);
var rightBottomPoint = new PixelPoint(SizePosition.X + (int)Width, SizePosition.Y + (int)Height);
if (primaryScreenBounds.Contains(leftTopPoint) &&
primaryScreenBounds.Contains(rightBottomPoint))
{
Position = leftTopPoint;
WindowStartupLocation = WindowStartupLocation.Manual;
}
}
_isInitialize = true;
}
}
}
@@ -126,7 +126,7 @@ public class OptionsDisplayItem : TemplatedControl
{
base.OnApplyTemplate(e);
_layoutRoot = e.NameScope.Find<FABorder>("LayoutRoot");
_layoutRoot = e.NameScope.Find<Border>("LayoutRoot");
if (_layoutRoot != null)
{
_layoutRoot.PointerPressed += OnLayoutRootPointerPressed;
@@ -172,5 +172,5 @@ public class OptionsDisplayItem : TemplatedControl
private bool _isPressed;
private bool _isExpanded;
private FABorder? _layoutRoot;
private Border? _layoutRoot;
}
@@ -61,6 +61,10 @@ public class SnackbarManager : TemplatedControl, IManagedNotificationManager
{
Install(host);
}
else
{
Log.Error("SnackbarManager", "Install Faild, host is null");
}
UpdatePseudoClasses(Position);
}
@@ -193,4 +197,39 @@ public class SnackbarManager : TemplatedControl, IManagedNotificationManager
PseudoClasses.Set(":bottomleft", position == NotificationPosition.BottomLeft);
PseudoClasses.Set(":bottomright", position == NotificationPosition.BottomRight);
}
/// <inheritdoc/>
public void Close(INotification notification)
{
Dispatcher.UIThread.VerifyAccess();
//if (_items.Remove(notification, out var notificationCard))
//{
// notificationCard.Close();
//}
}
/// <inheritdoc/>
public void Close(object content)
{
Dispatcher.UIThread.VerifyAccess();
//if (_items.Remove(content, out var notificationCard))
//{
// notificationCard.Close();
//}
}
/// <inheritdoc/>
public void CloseAll()
{
Dispatcher.UIThread.VerifyAccess();
//foreach (var kvp in _items)
//{
// kvp.Close();
//}
_items?.Clear();
}
}
@@ -62,14 +62,13 @@
Margin="0,0,0,5"
HorizontalAlignment="Center"
VerticalAlignment="Bottom">
<ItemsRepeater Name="Swiper" IsVisible="False">
<ItemsRepeater.Layout>
<StackLayout
DisableVirtualization="True"
Orientation="Horizontal"
Spacing="8" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<ItemsControl Name="Swiper" IsVisible="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.DataType>
<x:Type x:TypeArguments="x:Int32, x:String" TypeName="generic:KeyValuePair" />
@@ -110,8 +109,8 @@
</i:Interaction.Behaviors>-->
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Panel>
</Panel>
</Border>
@@ -55,7 +55,7 @@ public class CarouselItems : ItemsControl
Carousel? _carouselControl;
Button? _leftButton;
Button? _rightButton;
ItemsRepeater? _swiper;
ItemsControl? _swiper;
Timer? _timer;
public ICommand? CarouselBannerIndexCommand { get; }
@@ -72,7 +72,7 @@ public class CarouselItems : ItemsControl
_carouselControl = e.NameScope.Find<Carousel>("CarouselControl");
_leftButton = e.NameScope.Find<Button>("Left");
_rightButton = e.NameScope.Find<Button>("Right");
_swiper = e.NameScope.Find<ItemsRepeater>("Swiper");
_swiper = e.NameScope.Find<Avalonia.Controls.ItemsControl>("Swiper");
if (_leftButton != null)
{
@@ -85,44 +85,36 @@ public class FixedWrapPanel : Panel, INavigableContainer
/// <inheritdoc/>
protected override Size MeasureOverride(Size constraint)
{
var panelSize = default(MutableSize);
var lineSize = default(MutableSize);
int itemsInCurrentLine = 0;
double itemWidth = (constraint.Width - (Spacing * (ItemsPerLine - 1))) / ItemsPerLine;
MutableSize currentLineSize = default;
MutableSize panelSize = default;
Size lineConstraint = new(constraint.Width, constraint.Height);
Size childConstraint = new(itemWidth - Spacing, constraint.Height);
for (int i = 0, count = Children.Count; i < count; i++)
foreach (var child in Children.OfType<Control>())
{
Control child = Children[i];
if (child is null)
{
continue;
}
child.Measure(new Size(itemWidth, constraint.Height));
var childSize = child.DesiredSize;
child.Measure(childConstraint);
Size childSize = new(itemWidth, child.DesiredSize.Height);
if (MathUtilities.GreaterThan(currentLineSize.Width + childSize.Width + Spacing, lineConstraint.Width))
if (itemsInCurrentLine == ItemsPerLine)
{
// Need to switch to another line
panelSize.Width = Max(currentLineSize.Width, panelSize.Width);
panelSize.Height += currentLineSize.Height;
currentLineSize = new(childSize);
// Start a new line
panelSize.Width = Math.Max(panelSize.Width, lineSize.Width);
panelSize.Height += lineSize.Height + Spacing;
lineSize = new MutableSize(childSize.Width, childSize.Height);
itemsInCurrentLine = 1;
}
else
{
// Continue to accumulate a line
currentLineSize.Width += childSize.Width + Spacing;
currentLineSize.Height = Max(childSize.Height, currentLineSize.Height);
// Add to the current line
lineSize.Width += childSize.Width + (itemsInCurrentLine > 0 ? Spacing : 0);
lineSize.Height = Math.Max(lineSize.Height, childSize.Height);
itemsInCurrentLine++;
}
}
// The last line size, if any should be added
panelSize.Width = Max(currentLineSize.Width, panelSize.Width);
panelSize.Height = currentLineSize.Height;
if (double.IsInfinity(panelSize.Width))
panelSize.Width = 0;
// Add the last line size
panelSize.Width = Math.Max(panelSize.Width, lineSize.Width);
panelSize.Height += lineSize.Height;
return panelSize.ToSize();
}
@@ -130,41 +122,33 @@ public class FixedWrapPanel : Panel, INavigableContainer
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
double itemWidth = (finalSize.Width - (Spacing * (ItemsPerLine - 1))) / ItemsPerLine;
int firstInLine = 0;
double accumulatedHeight = 0;
var currentLineSize = default(MutableSize);
for (int i = 0; i < Children.Count; i++)
var itemWidth = (finalSize.Width - (Spacing * (ItemsPerLine - 1))) / ItemsPerLine;
var position = default(MutablePoint);
var lineHeight = 0.0;
int itemsInCurrentLine = 0;
try
{
Control child = Children[i];
if (child == null)
foreach (var child in Children.OfType<Control>())
{
continue;
}
if (itemsInCurrentLine == ItemsPerLine)
{
// Move to the next line
position.X = 0;
position.Y += lineHeight + Spacing;
lineHeight = 0;
itemsInCurrentLine = 0;
}
MutableSize itemSize = new(itemWidth, child.DesiredSize.Height);
if (MathUtilities.GreaterThan(currentLineSize.Width + itemSize.Width, finalSize.Width))
{
// Need to switch to another line
ArrangeLine(accumulatedHeight, currentLineSize.Height, firstInLine, i, itemWidth);
accumulatedHeight += currentLineSize.Height;
currentLineSize = itemSize;
firstInLine = i;
}
else
{
// Continue to accumulate a line
currentLineSize.Width += itemSize.Width;
currentLineSize.Height = Max(itemSize.Height, currentLineSize.Height);
var childSize = new Size(itemWidth, child.DesiredSize.Height);
child.Arrange(new Rect(position.ToPoint(), childSize));
position.X += itemWidth + Spacing;
lineHeight = Math.Max(lineHeight, childSize.Height);
itemsInCurrentLine++;
}
}
if (firstInLine < Children.Count)
{
// Arrange the last line, if any
ArrangeLine(accumulatedHeight, currentLineSize.Height, firstInLine, Children.Count, itemWidth);
}
catch
{ }
return finalSize;
}
@@ -180,19 +164,32 @@ public class FixedWrapPanel : Panel, INavigableContainer
continue;
}
if (i < end - 1)
try
{
child.Arrange(new Rect(x, y, width - Spacing, height));
x += width + Spacing;
}
else
{
child.Arrange(new Rect(x, y, width, height));
x += width;
if (i < end - 1 || i % ItemsPerLine == 0)
{
child.Arrange(new Rect(x, y, width - Spacing, height));
x += width + Spacing;
}
else
{
child.Arrange(new Rect(x, y, width, height));
x += width;
}
}
catch { }
}
}
record struct MutablePoint
{
internal double X;
internal double Y;
internal readonly Point ToPoint() => new Point(X, Y);
}
record struct MutableSize
{
internal MutableSize(double width, double height)
@@ -139,20 +139,24 @@
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="{Binding Path=Res.Link, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
<ItemsRepeater Margin="0,0,6,6" ItemsSource="{Binding Hyperlinks, Mode=OneTime}">
<ItemsRepeater.Layout>
<UniformGridLayout MinItemWidth="166" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<ItemsControl Margin="0,0,6,6" ItemsSource="{Binding Hyperlinks, Mode=OneTime}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="spp:HyperlinkModel">
<Button
Command="{Binding Command, Mode=OneWay}"
Content="{Binding Text, Mode=OneWay}"
Cursor="Hand"
Theme="{StaticResource HyperlinkButton}" />
<Border MinWidth="166">
<Button
Command="{Binding Command, Mode=OneWay}"
Content="{Binding Text, Mode=OneWay}"
Cursor="Hand"
Theme="{StaticResource HyperlinkButton}" />
</Border>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border
Height="1"
Margin="-50,0"
@@ -162,20 +166,24 @@
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="{Binding Path=Res.OpenSourceLibrary, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
<ItemsRepeater Margin="0,0,6,6" ItemsSource="{Binding OSL, Mode=OneTime}">
<ItemsRepeater.Layout>
<UniformGridLayout MinItemWidth="290" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<ItemsControl Margin="0,0,6,6" ItemsSource="{Binding OSL, Mode=OneTime}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="spp:HyperlinkModel">
<Button
Command="{Binding Command, Mode=OneWay}"
Content="{Binding Text, Mode=OneWay}"
Cursor="Hand"
Theme="{StaticResource HyperlinkButton}" />
<Border MinWidth="290">
<Button
Command="{Binding Command, Mode=OneWay}"
Content="{Binding Text, Mode=OneWay}"
Cursor="Hand"
Theme="{StaticResource HyperlinkButton}" />
</Border>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</spp:OptionsDisplayItem.Content>
</spp:OptionsDisplayItem>
@@ -12,6 +12,7 @@
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="spp:DebugPageViewModel"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
mc:Ignorable="d">
<Design.DataContext>
<spp:DebugPageViewModel />
@@ -134,11 +135,15 @@
<ItemsControl>
<ItemsControl.Items>
<Border Height="30" Background="Red" />
<Border Background="Blue" />
<Border Background="Yellow" />
<Border Height="30" Background="Blue" />
<Border Height="30" Background="Yellow" />
<Border Background="Red" />
<Border Background="Blue" />
<Border Height="30" Background="Yellow" />
<Border Height="30" Background="Red" />
<Border Height="30" Background="Blue" />
<Border Height="30" Background="Blue" />
<Border Height="30" Background="Blue" />
<Border Height="30" Background="Blue" />
<Border Height="30" Background="Red" />
</ItemsControl.Items>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -176,7 +181,7 @@
CornerRadius="2"
IsActive="{Binding #active.IsChecked}"
IsLoading="{Binding #loading.IsChecked}">
<SelectableTextBlock Margin="0,2" Text="是先有鸡还是先有蛋" />
<SelectableTextBlock Margin="0,2" Text="不忘初心,方得始终" />
</spp:Skeleton>
<spp:Skeleton
Margin="0,0,100,0"
@@ -184,14 +189,14 @@
CornerRadius="2"
IsActive="{Binding #active.IsChecked}"
IsLoading="{Binding #loading.IsChecked}">
<SelectableTextBlock Margin="0,2" Text="什么是工匠精神" />
<SelectableTextBlock Margin="0,2" Text="---" />
</spp:Skeleton>
<spp:Skeleton
VerticalAlignment="Bottom"
CornerRadius="2"
IsActive="{Binding #active.IsChecked}"
IsLoading="{Binding #loading.IsChecked}">
<SelectableTextBlock Margin="0,2" Text="看山不是山,看水不是水,看山还是山,看水还是水" />
<SelectableTextBlock Margin="0,2" Text="路漫漫其修远兮,吾将上下而求索。" />
</spp:Skeleton>
</UniformGrid>
</Grid>
@@ -74,13 +74,18 @@
MaxWidth="1080"
Spacing="20">
<spp:Snackbar
Title="{Binding Path=Res.Notice_Tray_Title, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
VerticalAlignment="Top"
IsClosable="False"
IsOpen="True"
IsVisible="{spp:OnPlatform !GA}"
Message="当前使用的程序版本为测试版本,可能会有各种各样不可言明的 Bug,遇到问题请理性反馈,谢谢。"
Severity="Warning" />
<spp:Snackbar
VerticalAlignment="Top"
IsClosable="False"
IsOpen="True"
IsVisible="{spp:OnPlatform !GA}"
Message="如果您不喜欢当前主页为默认启动页面可以在【设置-界面设置-启动时默认打开页面】处更改" />
<spp:Snackbar
Title="{Binding Path=Res.Notice_Tray_Title, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
VerticalAlignment="Top"
@@ -282,6 +287,7 @@
<!--<Border spp:Animations.EnableAnimations="True">-->
<spp:AppItem
Title="{Binding Name}"
MinHeight="250"
MaxWidth="260"
Classes="Vertical Scale"
ClickCommand="{Binding OpenBrowserCommand, Source={x:Static spp:App.Instance}}"
@@ -218,6 +218,7 @@
<TextBlock Text="{Binding Path=User.NickName, Mode=OneWay, Source={x:Static s:UserService.Current}}" />
<Border
Padding="5,1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
CornerRadius="2"
IsVisible="{Binding Path=User.UserType, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:UserType.Sponsor}}">
@@ -341,6 +341,7 @@
<ComboBox SelectedItem="{Binding Source={x:Static ms:UISettings.WindowBackgroundMaterial}, Path=Value, Mode=TwoWay}">
<ComboBox.Items>
<spp:WindowBackgroundMaterial>None</spp:WindowBackgroundMaterial>
<spp:WindowBackgroundMaterial>Transparent</spp:WindowBackgroundMaterial>
<spp:WindowBackgroundMaterial>Blur</spp:WindowBackgroundMaterial>
<spp:WindowBackgroundMaterial>AcrylicBlur</spp:WindowBackgroundMaterial>
<spp:WindowBackgroundMaterial>Mica</spp:WindowBackgroundMaterial>
@@ -62,7 +62,7 @@ public sealed partial class MainWindow : ReactiveAppWindow<MainWindowViewModel>
pageType = page.PageType;
}
}
INavigationService.Instance.Navigate(pageType ?? typeof(PluginStorePage));
INavigationService.Instance.Navigate(pageType ?? typeof(HomePage));
return true;
}
@@ -311,10 +311,29 @@ sealed class IPCSubProcessFileSystem : IOPath.FileSystemBase
config.LoggingRules.Add(new LoggingRule("*", minLevel, target));
}
public static string GetLogDirPath(string? moduleName = null, string? cacheDirectory = null)
{
cacheDirectory ??= IOPath.CacheDirectory;
string logDirPath;
if (moduleName == null)
{
logDirPath = Path.Combine(cacheDirectory, LogDirName);
}
else
{
var dirName = GetDirectoryName(moduleName);
logDirPath = Path.Combine(cacheDirectory, LogDirName, dirName);
}
IOPath.DirCreateByNotExists(logDirPath);
return logDirPath;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InitLog(
ref string? logDirPath,
string? mainProcessModuleName = null,
string? moduleName = null,
string? alias = null,
string? cacheDirectory = null
#if STARTUP_WATCH_TRACE || DEBUG
@@ -335,16 +354,8 @@ sealed class IPCSubProcessFileSystem : IOPath.FileSystemBase
cacheDirectory ??= IOPath.CacheDirectory;
if (mainProcessModuleName == null)
{
logDirPath = Path.Combine(cacheDirectory, LogDirName);
}
else
{
var dirName = GetDirectoryName(mainProcessModuleName);
logDirPath = Path.Combine(cacheDirectory, LogDirName, dirName);
}
IOPath.DirCreateByNotExists(logDirPath);
logDirPath = GetLogDirPath(moduleName, cacheDirectory);
#if (STARTUP_WATCH_TRACE || DEBUG) && !LIB_CLIENT_IPC
if (watchTrace) Startup.WatchTrace.Record("InitLog.IOPath");
#endif
@@ -76,6 +76,9 @@
<Compile Include="..\BD.WTTS.Client\Services\Platform\IPlatformService.OS.Linux.Etc.Os_Release.cs">
<LinkBase>Services\Platform</LinkBase>
</Compile>
<Compile Include="..\BD.WTTS.Client\Services\Mvvm\LogConsoleService.Logger.cs">
<LinkBase>Services\Mvvm</LinkBase>
</Compile>
</ItemGroup>
<ItemGroup>
@@ -104,6 +107,7 @@
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="System.Runtime.Caching" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="Utf8StringInterpolation" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('windows')) AND ($(RuntimeIdentifier.StartsWith('win-')) OR $(RuntimeIdentifier) == '')">
@@ -123,7 +123,7 @@ public static class KestrelServerOptionsExtensions
var httpsPort = IReverseProxyConfig.HttpsPort;
options.Listen(IReverseProxyService.Constants.Instance.ProxyIp, httpsPort, listen =>
{
listen.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
listen.Protocols = HttpProtocols.Http1AndHttp2;
listen.UseFlowAnalyze();
listen.UseTls();
});
@@ -20,7 +20,7 @@ public partial interface IReverseProxyConfig
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
bool IsMatch(string domain) => TryGetDomainConfig(domain, out _);
bool IsMatch(string url) => TryGetDomainConfig(url, out _);
/// <summary>
/// 尝试获取域名配置
@@ -28,7 +28,7 @@ public partial interface IReverseProxyConfig
/// <param name="domain"></param>
/// <param name="value"></param>
/// <returns></returns>
bool TryGetDomainConfig(string domain, [MaybeNullWhen(false)] out IDomainConfig value);
bool TryGetDomainConfig(string url, [MaybeNullWhen(false)] out IDomainConfig value);
/// <summary>
/// 尝试获取脚本配置
@@ -36,7 +36,7 @@ public partial interface IReverseProxyConfig
/// <param name="domain"></param>
/// <param name="value"></param>
/// <returns></returns>
bool TryGetScriptConfig(string domain, [MaybeNullWhen(false)] out IEnumerable<IScriptConfig> value);
bool TryGetScriptConfig(string url, [MaybeNullWhen(false)] out IEnumerable<IScriptConfig> value);
/// <summary>
/// 尝试获取脚本内容
@@ -148,26 +148,26 @@ sealed class ReverseProxyConfig : IReverseProxyConfig
// return result;
//}
public bool TryGetDomainConfig(string domain, [MaybeNullWhen(false)] out IDomainConfig value)
public bool TryGetDomainConfig(string url, [MaybeNullWhen(false)] out IDomainConfig value)
{
//value = domainConfigCache.GetOrAdd(domain.Host, GetDomainConfig);
var uri = new UriBuilder(domain).Uri;
var uri = new UriBuilder(url).Uri;
domainConfigCache.TryGetValue(uri.Host, out value);
if (value != null)
return true;
value = GetDomainConfig(domain);
value = GetDomainConfig(url);
if (value == null)
return false;
domainConfigCache.TryAdd(uri.Host, value);
return true;
IDomainConfig? GetDomainConfig(string domain)
IDomainConfig? GetDomainConfig(string url)
{
var key = domainConfigs.Keys.FirstOrDefault(item => item.IsMatch(domain));
var key = domainConfigs.Keys.FirstOrDefault(item => item.IsMatch(url));
return key == null ? null : domainConfigs[key];
}
}
@@ -81,6 +81,7 @@ static void ConfigureServices(IServiceCollection services)
#if DEBUG
l.AddConsole();
#endif
l.AddProvider(new LogConsoleService.Utf8StringLoggerProvider(moduleName));
});
// 设置仓储层数据库文件存放路径
@@ -155,7 +155,21 @@ public static class CertGenerator
}
else
{
builder.AddDnsName(name);
try
{
builder.AddDnsName(name);
}
catch
{
/* name = Environment.MachineName
* System.ArgumentException: Decoded string is not a valid IDN name. (Parameter 'unicode')
* at System.Globalization.IdnMapping.IcuGetAsciiCore(String unicodeString, Char* unicode, Int32 count)
* at System.Globalization.IdnMapping.GetAscii(String unicode, Int32 index, Int32 count)
* at System.Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder.AddDnsName(String dnsName)
* at BD.WTTS.Services.Implementation.CertGenerator.Add(SubjectAlternativeNameBuilder builder, String name)
* at BD.WTTS.Services.Implementation.CertGenerator.CreateEndCertificate(X509Certificate2 issuerCertificate, X500DistinguishedName subjectName, IEnumerable`1 extraDnsNames, Nullable`1 notBefore, Nullable`1 notAfter, Int32 rsaKeySizeInBits)
*/
}
}
}
@@ -12,10 +12,10 @@ sealed class LifetimeHttpHandler : DelegatingHandler
public LifeTimeKey LifeTimeKey { get; }
public LifetimeHttpHandler(IDomainResolver domainResolver, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action<LifetimeHttpHandler> deactivateAction)
public LifetimeHttpHandler(IDomainResolver domainResolver, IWebProxy webProxy, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action<LifetimeHttpHandler> deactivateAction)
{
LifeTimeKey = lifeTimeKey;
InnerHandler = new ReverseProxyHttpClientHandler(lifeTimeKey.DomainConfig, domainResolver);
InnerHandler = new ReverseProxyHttpClientHandler(lifeTimeKey.DomainConfig, domainResolver, webProxy);
timer = new Timer(OnTimerCallback, deactivateAction, lifeTime, Timeout.InfiniteTimeSpan);
}
@@ -15,7 +15,7 @@ public class ReverseProxyHttpClient : HttpMessageInvoker
// = new(new ProductHeaderValue(Constants.HARDCODED_APP_NAME_NEW + "_" + DateTimeOffset.UtcNow.Ticks, ThisAssembly.Version));
public ReverseProxyHttpClient(IDomainConfig domainConfig, IDomainResolver domainResolver)
: this(new ReverseProxyHttpClientHandler(domainConfig, domainResolver), disposeHandler: true)
: this(new ReverseProxyHttpClientHandler(domainConfig, domainResolver, HttpNoProxy.Instance), disposeHandler: true)
{
}
@@ -7,12 +7,14 @@ sealed class ReverseProxyHttpClientHandler : DelegatingHandler
{
readonly IDomainConfig domainConfig;
readonly IDomainResolver domainResolver;
readonly IWebProxy webProxy;
readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
public ReverseProxyHttpClientHandler(IDomainConfig domainConfig, IDomainResolver domainResolver)
public ReverseProxyHttpClientHandler(IDomainConfig domainConfig, IDomainResolver domainResolver, IWebProxy webProxy)
{
this.domainConfig = domainConfig;
this.domainResolver = domainResolver;
this.webProxy = webProxy;
InnerHandler = CreateSocketsHttpHandler();
}
@@ -53,12 +55,199 @@ sealed class ReverseProxyHttpClientHandler : DelegatingHandler
{
Proxy = HttpNoProxy.Instance,
UseProxy = false,
PreAuthenticate = false,
UseCookies = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
ConnectCallback = ConnectCallback,
EnableMultipleHttp2Connections = true,
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8,
ResponseHeaderEncodingSelector = (_, _) => Encoding.UTF8,
};
private async ValueTask<Stream> ConnectThroughProxyAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
if (context.InitialRequestMessage.RequestUri == null)
{
throw new InvalidOperationException("Request URI is null");
}
var proxyUri = webProxy.GetProxy(context.InitialRequestMessage.RequestUri);
if (proxyUri == null)
{
throw new InvalidOperationException("Proxy URI is null");
}
var proxyEndPoint = new DnsEndPoint(proxyUri.Host, proxyUri.Port);
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(proxyEndPoint, cancellationToken);
var stream = new NetworkStream(socket, ownsSocket: true);
// 确定代理类型
var proxyType = DetermineProxyType(proxyUri);
switch (proxyType)
{
case ExternalProxyType.Http:
await ConnectHttpProxy(stream, context, cancellationToken);
break;
case ExternalProxyType.Socks4:
await ConnectSocks4Proxy(stream, context, cancellationToken);
break;
case ExternalProxyType.Socks5:
await ConnectSocks5Proxy(stream, context, cancellationToken);
break;
}
if (context.InitialRequestMessage.RequestUri.Scheme == "https" || context.InitialRequestMessage.RequestUri.Port == 443)
{
var requestContext = context.InitialRequestMessage.GetRequestContext();
var tlsSniValue = requestContext.TlsSniValue;
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
{
TargetHost = tlsSniValue.Value,
RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true,
}, cancellationToken);
return sslStream;
}
return stream;
}
private ExternalProxyType DetermineProxyType(Uri proxyUri)
{
return proxyUri.Scheme.ToLower() switch
{
"socks4" => ExternalProxyType.Socks4,
"socks5" => ExternalProxyType.Socks5,
_ => ExternalProxyType.Http
};
throw new NotImplementedException("Proxy type determination needs to be implemented");
}
private async Task ConnectHttpProxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var credentials = webProxy.Credentials as NetworkCredential;
var authHeader = credentials != null
? $"Proxy-Authorization: Basic {Convert.ToBase64String(Encoding.ASCII.GetBytes($"{credentials.UserName}:{credentials.Password}"))}\r\n"
: string.Empty;
var connectRequest = $"CONNECT {context.DnsEndPoint.Host}:{context.DnsEndPoint.Port} HTTP/1.1\r\n" +
$"Host: {context.DnsEndPoint.Host}:{context.DnsEndPoint.Port}\r\n" +
$"{authHeader}\r\n";
var connectBytes = Encoding.ASCII.GetBytes(connectRequest);
await stream.WriteAsync(connectBytes, cancellationToken);
var buffer = new byte[1024];
var bytesRead = await stream.ReadAsync(buffer, cancellationToken);
var response = Encoding.ASCII.GetString(buffer, 0, bytesRead);
if (!response.StartsWith("HTTP/1.1 200"))
{
throw new Exception($"Proxy connection failed: {response}");
}
}
private async Task ConnectSocks4Proxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var credentials = webProxy.Credentials as NetworkCredential;
var userId = credentials?.UserName ?? string.Empty;
var destinationAddress = await GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken).FirstAsync();
var portBytes = BitConverter.GetBytes((ushort)context.DnsEndPoint.Port);
if (BitConverter.IsLittleEndian) Array.Reverse(portBytes);
var request = new byte[] { 0x04, 0x01 }
.Concat(portBytes)
.Concat(destinationAddress.Address.GetAddressBytes())
.Concat(Encoding.ASCII.GetBytes(userId))
.Concat(new byte[] { 0x00 })
.ToArray();
await stream.WriteAsync(request, cancellationToken);
var response = new byte[8];
await stream.ReadAsync(response, cancellationToken);
if (response[1] != 0x5A)
{
throw new Exception($"SOCKS4 proxy connection failed: {response[1]}");
}
}
private async Task ConnectSocks5Proxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var credentials = webProxy.Credentials as NetworkCredential;
// SOCKS5 握手
byte[] authMethod = credentials != null ? new byte[] { 0x02 } : new byte[] { 0x00 };
await stream.WriteAsync(new byte[] { 0x05, 0x01 }.Concat(authMethod).ToArray(), cancellationToken);
var response = new byte[2];
await stream.ReadAsync(response, cancellationToken);
if (response[0] != 0x05)
{
throw new Exception("SOCKS5 handshake failed");
}
// 如果需要身份验证
if (response[1] == 0x02 && credentials != null)
{
var userBytes = Encoding.ASCII.GetBytes(credentials.UserName);
var passBytes = Encoding.ASCII.GetBytes(credentials.Password);
var authRequest = new byte[] { 0x01, (byte)userBytes.Length }
.Concat(userBytes)
.Concat(new byte[] { (byte)passBytes.Length })
.Concat(passBytes)
.ToArray();
await stream.WriteAsync(authRequest, cancellationToken);
var authResponse = new byte[2];
await stream.ReadAsync(authResponse, cancellationToken);
if (authResponse[1] != 0x00)
{
throw new Exception("SOCKS5 authentication failed");
}
}
// 发送连接请求
byte[] addressBytes;
byte addressType;
if (IPAddress.TryParse(context.DnsEndPoint.Host, out IPAddress? ipAddress))
{
addressBytes = ipAddress.GetAddressBytes();
addressType = (byte)(ipAddress.AddressFamily == AddressFamily.InterNetwork ? 1 : 4);
}
else
{
addressBytes = Encoding.ASCII.GetBytes(context.DnsEndPoint.Host);
addressType = 3; // 域名
}
var request = new byte[] { 0x05, 0x01, 0x00, addressType }
.Concat(addressType == 3 ? new[] { (byte)addressBytes.Length } : Array.Empty<byte>())
.Concat(addressBytes)
.Concat(BitConverter.GetBytes((ushort)context.DnsEndPoint.Port).Reverse())
.ToArray();
await stream.WriteAsync(request, cancellationToken);
var connectResponse = new byte[10];
await stream.ReadAsync(connectResponse, cancellationToken);
if (connectResponse[1] != 0x00)
{
throw new Exception($"SOCKS5 connection failed: {connectResponse[1]}");
}
}
/// <summary>
/// 连接回调
/// </summary>
@@ -67,6 +256,11 @@ sealed class ReverseProxyHttpClientHandler : DelegatingHandler
/// <returns></returns>
async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
if (webProxy != null && !HttpNoProxy.IsNoProxy(webProxy))
{
return await ConnectThroughProxyAsync(context, cancellationToken);
}
var innerExceptions = new List<Exception>();
var ipEndPoints = GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken);
@@ -1,3 +1,4 @@
using System.Net;
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;
// ReSharper disable once CheckNamespace
@@ -140,11 +141,6 @@ sealed class HttpLocalRequestMiddleware
return null;
}
StringValues cookie = context.Request.Headers["cookie-steamTool"];
if (StringValues.IsNullOrEmpty(cookie))
cookie = context.Request.Headers["Cookie"];
StringValues referer = context.Request.Headers["Referer-steamTool"];
context.Response.Headers.AccessControlAllowOrigin = context.Request.Headers.Origin.Count == 0 ? "*" : context.Request.Headers.Origin;
context.Response.Headers.AccessControlAllowHeaders = "*";
context.Response.Headers.AccessControlAllowMethods = "*";
@@ -166,29 +162,30 @@ sealed class HttpLocalRequestMiddleware
Method = methodObj,
Content = hasReqContent ? new StreamContent(context.Request.Body) : null,
};
req.Headers.UserAgent.ParseAdd(context.Request.Headers.UserAgent);
if (!StringValues.IsNullOrEmpty(cookie))
foreach (var item in context.Request.Headers)
{
CookieHttpClient.CookieContainer.Add(new Cookie
{
CommentUri = requestUri,
Domain = requestUri.Host,
Value = cookie,
});
// req.Headers.Add("Cookie", cookie);
}
if (!StringValues.IsNullOrEmpty(referer))
{
try
{
var refererUri = new Uri(referer.ToString());
req.Headers.Referrer = refererUri;
}
catch
var headers = item.Key.ToLower();
if (headers.EndsWith("-steamtool"))
{
if (headers.Equals("cookie", StringComparison.OrdinalIgnoreCase))
{
CookieHttpClient.CookieContainer.Add(new Cookie
{
CommentUri = requestUri,
Domain = requestUri.Host,
Value = item.Value
});
}
if (headers.Equals("referer", StringComparison.OrdinalIgnoreCase))
{
var refererUri = new Uri(item.Value.ToString());
req.Headers.Referrer = refererUri;
}
req.Headers.TryAddWithoutValidation(item.Key.TrimEnd("-steamtool"), (IEnumerable<string?>)item.Value);
}
}
req.Headers.UserAgent.ParseAdd(context.Request.Headers.UserAgent);
if (hasReqContent)
{
req.Content!.Headers.ContentType = MediaTypeHeaderValue.Parse(context.Request.ContentType.ThrowIsNull());
@@ -1,5 +1,6 @@
// https://github.com/dotnetcore/FastGithub/blob/2.1.4/FastGithub.HttpServer/HttpReverseProxyMiddleware.cs
using Microsoft.AspNetCore.Http;
using Yarp.ReverseProxy.Forwarder;
// ReSharper disable once CheckNamespace
@@ -38,6 +39,7 @@ sealed partial class HttpReverseProxyMiddleware
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var url = context.Request.GetDisplayUrl();
//var url = context.Request.GetDisplayUrl().Remove(0, context.Request.Scheme.Length + 3);
var isScriptInject = reverseProxyConfig.TryGetScriptConfig(url, out var scriptConfigs);
@@ -50,9 +52,31 @@ sealed partial class HttpReverseProxyMiddleware
context.Response.Body = memoryStream;
}
if (TryGetDomainConfig(url, out var domainConfig) == false)
if (TryGetDomainConfig(url.Remove(0, context.Request.Scheme.Length + 3), out var domainConfig) == false)
{
await next(context);
if (reverseProxyConfig.Service.TwoLevelAgentEnable)
{
var httpClient = httpClientFactory.CreateHttpClient("GlobalProxy", defaultDomainConfig);
var destinationPrefix = GetDestinationPrefix(context.Request.Scheme, context.Request.Host, null);
var forwarderRequestConfig = new ForwarderRequestConfig()
{
Version = context.Request.Protocol switch
{
var protocol when protocol.StartsWith("HTTP/2") => System.Net.HttpVersion.Version20,
var protocol when protocol.StartsWith("HTTP/3") => System.Net.HttpVersion.Version30,
_ => System.Net.HttpVersion.Version11,
},
};
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, forwarderRequestConfig, HttpTransformer.Empty);
if (error != ForwarderError.None)
{
await HandleErrorAsync(context, error);
}
}
else
{
await next(context);
}
return;
}
@@ -60,7 +84,7 @@ sealed partial class HttpReverseProxyMiddleware
!reverseProxyConfig.Service.OnlyEnableProxyScript)
{
// 部分运营商将奇怪的域名解析到 127.0.0.1 再此排除这些不支持的代理域名
var ip = await reverseProxyConfig.DnsAnalysis.AnalysisDomainIpAsync(context.Request.Host.Value, IDnsAnalysisService.DNS_Dnspods).FirstOrDefaultAsync();
var ip = await reverseProxyConfig.DnsAnalysis.AnalysisDomainIpAsync(context.Request.Host.Value!, IDnsAnalysisService.DNS_Dnspods).FirstOrDefaultAsync();
if (ip == null || IPAddress.IsLoopback(ip))
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
@@ -158,7 +182,7 @@ sealed partial class HttpReverseProxyMiddleware
return true;
}
var host = new Uri(uri).Host;
var host = new UriBuilder(uri).Host;
// 未配置的域名,但仍然被解析到本机 IP 的域名
if (IsDomain(host))
{
@@ -23,6 +23,8 @@ sealed class LazyReverseProxyServiceImpl : IReverseProxyService
public byte[]? GetFlowStatistics_Bytes() => impl().GetFlowStatistics_Bytes();
public string? GetLogAllMessage() => impl().GetLogAllMessage();
public async Task<StartProxyResult> StartProxyAsync(byte[] reverseProxySettings)
{
var result = await impl().StartProxyAsync(reverseProxySettings);
@@ -24,7 +24,7 @@ sealed class DnsDohAnalysisService : GeneralHttpClientFactory
{
}
public const string DefaultDohAddres = Dnspod_DohAddres;
public const string DefaultDohAddres = Dnspod_DohAddres2;
static Uri GetDohAddres(string? dohAddres)
{
@@ -53,16 +53,22 @@ sealed class DnsDohAnalysisService : GeneralHttpClientFactory
{
if (dnsClients.TryGetValue(dohAddresUri, out var value))
return value;
var client = CreateClient($"{TAG}_{dohAddresUri}", HttpHandlerCategory.Default);
//var handler = new HttpClientHandler
//{
// UseCookies = false,
// UseProxy = false,
// Proxy = HttpNoProxy.Instance,
//};
//var client = new HttpClient(handler);
//var client = CreateClient($"{TAG}_{dohAddresUri}", HttpHandlerCategory.Default);
var handler = new HttpClientHandler
{
UseCookies = false,
UseProxy = false,
Proxy = HttpNoProxy.Instance,
};
var client = new HttpClient(handler);
client.BaseAddress = dohAddresUri;
var dnsClient = new DnsHttpClient(client);
//暂未考虑释放问题 注意!
//foreach (var dc in dnsClients.Values)
//{
// dc.Dispose();
//}
//dnsClients.Clear();
dnsClients.TryAdd(dohAddresUri, dnsClient);
return dnsClient;
}
@@ -1,11 +1,14 @@
// https://github.com/dotnetcore/FastGithub/blob/2.1.4/FastGithub.Http/HttpClientFactory.cs
// ReSharper disable once CheckNamespace
using System.Net;
namespace BD.WTTS.Services.Implementation;
sealed class ReverseProxyHttpClientFactory : IReverseProxyHttpClientFactory
{
readonly IDomainResolver domainResolver;
readonly IWebProxy webProxy;
/// <summary>
/// 首次生命周期
@@ -27,9 +30,19 @@ sealed class ReverseProxyHttpClientFactory : IReverseProxyHttpClientFactory
/// </summary>
readonly ConcurrentDictionary<LifeTimeKey, Lazy<LifetimeHttpHandler>> httpHandlerLazyCache = new();
public ReverseProxyHttpClientFactory(IDomainResolver domainResolver)
public ReverseProxyHttpClientFactory(IDomainResolver domainResolver, IReverseProxyConfig reverseProxyConfig)
{
this.domainResolver = domainResolver;
if (reverseProxyConfig.Service.TwoLevelAgentEnable)
{
this.webProxy = new WebProxy($"{reverseProxyConfig.Service.TwoLevelAgentProxyType}://{reverseProxyConfig.Service.TwoLevelAgentIp}:{reverseProxyConfig.Service.TwoLevelAgentPortId}", true, null, null);
if (!string.IsNullOrEmpty(reverseProxyConfig.Service.TwoLevelAgentUserName) || !string.IsNullOrEmpty(reverseProxyConfig.Service.TwoLevelAgentPassword))
{
webProxy.Credentials = new NetworkCredential(reverseProxyConfig.Service.TwoLevelAgentUserName, reverseProxyConfig.Service.TwoLevelAgentPassword);
}
}
else
this.webProxy = HttpNoProxy.Instance;
}
public ReverseProxyHttpClient CreateHttpClient(string domain, IDomainConfig domainConfig)
@@ -43,7 +56,7 @@ sealed class ReverseProxyHttpClientFactory : IReverseProxyHttpClientFactory
}
LifetimeHttpHandler CreateLifetimeHttpHandler(LifeTimeKey lifeTimeKey, TimeSpan lifeTime)
=> new(domainResolver, lifeTimeKey, lifeTime, OnLifetimeHttpHandlerDeactivate);
=> new(domainResolver, webProxy, lifeTimeKey, lifeTime, OnLifetimeHttpHandlerDeactivate);
void OnLifetimeHttpHandlerDeactivate(LifetimeHttpHandler lifetimeHttpHandler)
{
@@ -164,10 +164,13 @@ abstract class ReverseProxyServiceImpl : IReverseProxySettings
{
if (string.IsNullOrWhiteSpace(reverseProxySettings.ProxyIp))
return StartProxyResultCode.Exception;
var inUsePort = SocketHelper.IsUsePort(IPAddress.Parse(reverseProxySettings.ProxyIp!), 443);
if (inUsePort)
if (reverseProxySettings.ProxyMode == ProxyMode.Hosts)
{
return StartProxyResultCode.BindPortError;
var inUsePort = SocketHelper.IsUsePort(IPAddress.Parse(reverseProxySettings.ProxyIp!), 443);
if (inUsePort)
{
return StartProxyResultCode.BindPortError;
}
}
}
@@ -65,6 +65,8 @@ sealed partial class YarpReverseProxyServiceImpl : ReverseProxyServiceImpl, IRev
WebRootPath = RootPath,
});
builder.Logging.AddProvider(new LogConsoleService.Utf8StringLoggerProvider(AssemblyInfo.Accelerator));
builder.Services.Configure<HostFilteringOptions>(static o =>
{
o.AllowEmptyHosts = true;
@@ -80,17 +82,19 @@ sealed partial class YarpReverseProxyServiceImpl : ReverseProxyServiceImpl, IRev
builder.WebHost.UseKestrel(options =>
{
options.AddServerHeader = false;
options.RequestHeaderEncodingSelector = _ => Encoding.UTF8;
options.ResponseHeaderEncodingSelector = _ => Encoding.UTF8;
options.NoLimit();
#if WINDOWS
#if !NET7_0_OR_GREATER
if (OperatingSystem2.IsWindows7())
{
//https://github.com/dotnet/aspnetcore/issues/22563
options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
});
}
if (OperatingSystem2.IsWindows7())
{
//https://github.com/dotnet/aspnetcore/issues/22563
options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
});
}
#endif
//options.ListenSshReverseProxy();
//options.ListenGitReverseProxy();
@@ -165,6 +169,12 @@ sealed partial class YarpReverseProxyServiceImpl : ReverseProxyServiceImpl, IRev
return bytes;
}
public string? GetLogAllMessage()
{
var result = LogConsoleService.Builder;
return result.ToString();
}
// IDisposable
protected override void DisposeCore()
@@ -15,6 +15,12 @@ partial interface IReverseProxyService
/// </summary>
/// <returns></returns>
byte[]? GetFlowStatistics_Bytes();
/// <summary>
/// 获取所有日志信息
/// </summary>
/// <returns></returns>
string? GetLogAllMessage();
}
public static partial class ReverseProxyServiceExtensions
@@ -8,18 +8,18 @@ public partial interface IDnsAnalysisService // 公共常量定义
{
#region DNS 常量
const string DNS_Ali_DohAddres = "https://dns.alidns.com";
const string DNS_Ali_DohAddres2 = "https://223.6.6.6/dns-query";
const string DNS_Ali_DohAddres3 = "https://223.5.5.5/dns-query";
const string DNS_Ali_DohAddres = "https://dns.alidns.com/resolve";
const string DNS_Ali_DohAddres2 = "https://223.6.6.6/resolve";
const string DNS_Ali_DohAddres3 = "https://223.5.5.5/resolve";
const string Dnspod_DohAddres = "https://1.12.12.12";
const string Dnspod_DohAddres2 = "https://doh.pub";
const string Dnspod_DohAddres3 = "https://120.53.53.53/dns-query";
const string Dnspod_DohAddres = "https://1.12.12.12/resolve";
const string Dnspod_DohAddres2 = "https://doh.pub/resolve";
const string Dnspod_DohAddres3 = "https://120.53.53.53/resolve";
const string Google_DohAddres = "https://dns.google";
const string Cloudflare_DohAddres = "https://cloudflare-dns.com";
const string DohAddres_360 = "https://doh.360.cn/dns-query";
const string TUNA_DohAddres = "https://101.6.6.6:8443/dns-query";
const string Google_DohAddres = "https://dns.google/resolve";
const string Cloudflare_DohAddres = "https://cloudflare-dns.com/resolve";
const string DohAddres_360 = "https://doh.360.cn/resolve";
const string TUNA_DohAddres = "https://101.6.6.6:8443/resolve";
const string PrimaryDNS_IPV6_Ali = "2400:3200::1";
@@ -60,5 +60,5 @@ public partial interface IDnsAnalysisService // 公共常量定义
public static readonly IPEndPoint Cloudflare2 = new(IPAddress.Parse("1.0.0.1"), 53);
}
#endregion
#endregion DNS 常量
}
@@ -0,0 +1,22 @@
namespace BD.WTTS.Enums;
/// <summary>
/// 网络环境检查步骤
/// </summary>
public enum NetworkEnvCheckStep
{
/// <summary>
/// Hosts文件检查
/// </summary>
HostsFileCheck = 1,
/// <summary>
/// 网卡检查
/// </summary>
NetworkInterfaceCheck = 2,
/// <summary>
/// LSP检查
/// </summary>
LSPCheck = 3
}
@@ -0,0 +1,7 @@
namespace BD.WTTS.Enums;
public enum TransportProtocol
{
Tcp,
Udp,
}
@@ -42,6 +42,16 @@ static class WinDivertInitHelper
/// </summary>
public static async Task InitializeAsync()
{
(byte[] WinDivert_dll, byte[] WinDivert64_sys) data = default;
void GetData()
{
if (data == default)
{
data = GetWinDivertBinRes();
}
}
if (libraryPath != null && libraryDirPath != null)
{
bool isWrite;
@@ -55,9 +65,10 @@ static class WinDivertInitHelper
}
else
{
GetData();
var libraryBytes = await File.ReadAllBytesAsync(libraryPath);
var libraryHash = SHA256.HashData(libraryBytes);
var libraryHashRes = SHA256.HashData(WinDivertBinRes.WinDivert_dll);
var libraryHashRes = SHA256.HashData(data.WinDivert_dll!);
if (libraryHash.SequenceEqual(libraryHashRes)) // 哈希值相同,不写入
{
isWrite = false;
@@ -76,14 +87,15 @@ static class WinDivertInitHelper
if (isWrite) // 检查文件不存在时写入
{
GetData();
switch (RuntimeInformation.ProcessArchitecture)
{
//case Architecture.X86:
// break;
case Architecture.X64:
IOPath.DirCreateByNotExists(libraryDirPath);
var task1 = File.WriteAllBytesAsync(Path.Combine(libraryDirPath, "WinDivert64.sys"), WinDivertBinRes.WinDivert64_sys);
var task2 = File.WriteAllBytesAsync(libraryPath, WinDivertBinRes.WinDivert_dll);
var task1 = File.WriteAllBytesAsync(Path.Combine(libraryDirPath, "WinDivert64.sys"), data.WinDivert64_sys!);
var task2 = File.WriteAllBytesAsync(libraryPath, data.WinDivert_dll!);
await Task.WhenAll(task1, task2);
break;
}
@@ -128,5 +140,15 @@ static class WinDivertInitHelper
break;
}
}
static (byte[] WinDivert_dll, byte[] WinDivert64_sys) GetWinDivertBinRes()
{
byte[] bytes = WinDivertBinRes.WinDivert_mpo;
bytes.AsSpan().Reverse();
var data = MemoryPackSerializer.Deserialize<byte[][]>(bytes)!;
return (data[0], data[1]);
}
}
#endif
@@ -0,0 +1,48 @@
using FluentAvalonia.Core;
using System.Reactive;
namespace BD.WTTS.Models;
public class ProxyDomainGroupViewModel : ReactiveObject
{
private readonly INetworkTestService networkTestService = INetworkTestService.Instance;
public string Name { get; set; } = string.Empty;
public string IconUrl { get; set; } = string.Empty;
public ReadOnlyCollection<ProxyDomainViewModel>? EnableProxyDomainVMs { get; set; }
public ReactiveCommand<Unit, Unit> ConnectTestCommand { get; set; }
public ProxyDomainGroupViewModel()
{
ConnectTestCommand = ReactiveCommand.CreateFromTask(async () =>
{
if (EnableProxyDomainVMs == null)
{
Toast.Show(ToastIcon.Warning, "没有可测试的项目");
return;
}
var tasks = EnableProxyDomainVMs.Select(async enableDomain =>
{
enableDomain.DelayMillseconds = "- ms";
var (success, delayMs) = await networkTestService.TestOpenUrlAsync(enableDomain.Url);
enableDomain.DelayMillseconds = success switch
{
true when delayMs > 20000 => "Timeout",
true => delayMs.ToString() + " ms",
false => "error",
};
});
await Task.WhenAll(tasks);
if (EnableProxyDomainVMs.Count(x => x.DelayMillseconds == "Timeout") == EnableProxyDomainVMs.Count)
{
Toast.Show(ToastIcon.Error, "当前测试项全部未通过,请检查网络链接状况,以及代理设置里的设置项是否正常。");
}
});
}
}
@@ -0,0 +1,55 @@
using Avalonia.Media;
namespace BD.WTTS.Models;
public class ProxyDomainViewModel : ReactiveObject
{
private readonly INetworkTestService networkTestService = INetworkTestService.Instance;
public string Name { get; }
public ProxyType ProxyType { get; }
public string Url { get; }
[Reactive]
public string DelayMillseconds { get; set; } = string.Empty;
[ObservableAsProperty]
public bool RetestButtonVisible { get; }
[ObservableAsProperty]
public IBrush DelayColor { get; } = null!;
public ReadOnlyCollection<ProxyDomainViewModel>? Children { get; }
public ProxyDomainViewModel(string name, ProxyType proxyType, string url, List<ProxyDomainViewModel>? children = null)
{
Name = name;
ProxyType = proxyType;
Url = url;
Children = children?.AsReadOnly();
this.WhenAnyValue(x => x.DelayMillseconds)
.Subscribe(delay => Children?.ForEach(child => child.DelayMillseconds = delay));
this.WhenAnyValue(x => x.DelayMillseconds)
.Select(d => d != string.Empty && d != "-")
.ToPropertyEx(this, x => x.RetestButtonVisible);
const int DelayMiddle = 1000;
this.WhenAnyValue(x => x.DelayMillseconds)
.Select(d => d switch
{
"Timeout" or "error" => Brushes.Red,
var s when s.Split(' ') is [var num, "ms"] && int.TryParse(num, out int ms)
=> ms switch
{
<= DelayMiddle => Brushes.Green,
> DelayMiddle => Brushes.Orange,
},
_ => Brushes.Gray,
})
.ToPropertyEx(this, x => x.DelayColor);
}
}
@@ -66,6 +66,9 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
services.AddSingleton(_ => certificateManager.Task.GetAwaiter().GetResult());
#endif
}
services.AddSingleton<INetworkTestService, NetworkTestService>();
//if (startup.HasIPCRoot)
//{
// services.AddSingleton<IAcceleratorService, BackendAcceleratorServiceImpl>();
@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
@@ -63,19 +63,9 @@ namespace BD.WTTS.Properties {
/// <summary>
/// 查找 System.Byte[] 类型的本地化资源。
/// </summary>
internal static byte[] WinDivert_dll {
internal static byte[] WinDivert_mpo {
get {
object obj = ResourceManager.GetObject("WinDivert_dll", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// 查找 System.Byte[] 类型的本地化资源。
/// </summary>
internal static byte[] WinDivert64_sys {
get {
object obj = ResourceManager.GetObject("WinDivert64_sys", resourceCulture);
object obj = ResourceManager.GetObject("WinDivert_mpo", resourceCulture);
return ((byte[])(obj));
}
}
@@ -99,10 +99,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="WinDivert64_sys" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\..\..\ref\WinDivert64.sys;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="WinDivert_dll" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\..\..\ref\WinDivert.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<data name="WinDivert_mpo" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\..\..\ref\WinDivert.mpo;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>
@@ -80,6 +80,11 @@ partial class BackendAcceleratorServiceImpl
/// <inheritdoc/>
public async Task<ApiRsp<bool>> XY_IsInstall(CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok(false);
}
var result = XunYouSDK.IsInstall();
await Task.CompletedTask;
return result;
@@ -88,6 +93,11 @@ partial class BackendAcceleratorServiceImpl
/// <inheritdoc/>
public async Task<ApiRsp<XunYouUninstallCode>> XY_Uninstall(CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok(XunYouUninstallCode.卸载成功);
}
var result = XunYouSDK.Uninstall();
await Task.CompletedTask;
return result;
@@ -104,9 +114,16 @@ partial class BackendAcceleratorServiceImpl
string? svrPath = default,
CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok(0);
}
var show = XunYouShowCommands.SW_HIDE;
var result = XunYouSDK.StartEx2(
openid,
nickname,
show,
gameid,
area,
server,
@@ -128,6 +145,11 @@ partial class BackendAcceleratorServiceImpl
string? svrName = default,
CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok(0);
}
var result = XunYouSDK.StartAccel(
gameid,
area,
@@ -141,6 +163,11 @@ partial class BackendAcceleratorServiceImpl
/// <inheritdoc/>
public async Task<ApiRsp<XunYouAccelStateModel?>> XY_GetAccelStateEx(CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok((XunYouAccelStateModel?)null);
}
var result = GetXunYouAccelStateModel(lastXunYouState);
await Task.CompletedTask;
return result;
@@ -149,6 +176,11 @@ partial class BackendAcceleratorServiceImpl
/// <inheritdoc/>
public async IAsyncEnumerable<ApiRsp<int>> XY_Install(string installPath, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
yield break;
}
var channel = Channel.CreateUnbounded<int>();
int XunYouDownLoadCallback(int par, nint thisptr)
{
@@ -177,6 +209,11 @@ partial class BackendAcceleratorServiceImpl
/// <inheritdoc/>
public async Task<ApiRsp<XunYouSendResultCode>> XY_StopAccel(CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok(XunYouSendResultCode.发送成功);
}
var result = XunYouSDK.Stop();
await Task.CompletedTask;
return result;
@@ -185,6 +222,11 @@ partial class BackendAcceleratorServiceImpl
/// <inheritdoc/>
public async Task<ApiRsp<XunYouIsRunningCode>> XY_IsRunning(CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok(XunYouIsRunningCode.加速器未启动);
}
var result = XunYouSDK.IsRunning();
await Task.CompletedTask;
return result;
@@ -193,8 +235,26 @@ partial class BackendAcceleratorServiceImpl
/// <inheritdoc/>
public async Task<ApiRsp<XunYouStartGameCode>> XY_StartGame(CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok(XunYouStartGameCode.失败);
}
var result = XunYouSDK.StartGame();
await Task.CompletedTask;
return result;
}
/// <inheritdoc/>
public async Task<ApiRsp<int>> XY_ShowWinodw(bool showHide, CancellationToken cancellationToken = default)
{
if (!XunYouSDK.IsSupported)
{
return ApiRspHelper.Ok(0);
}
XunYouSDK.xunyou_show(Convert.ToInt32(showHide));
await Task.CompletedTask;
return ApiRspHelper.Ok(0);
}
}
@@ -0,0 +1,322 @@
using System.Net.NetworkInformation;
namespace BD.WTTS.Services.Implementation;
public class NetworkEnvCheckService
{
const string HOST_FILE_PATH =
#if WINDOWS
@"C:\Windows\System32\drivers\etc\hosts";
#else
"/etc/hosts";
#endif
public async Task CheckNetworkEnvAsync(IProgress<(NetworkEnvCheckStep Step, bool HasError, string Msg)> progress, CancellationToken cancellationToken = default)
{
await Parallel.ForAsync(
(int)NetworkEnvCheckStep.HostsFileCheck,
(int)NetworkEnvCheckStep.LSPCheck + 1,
cancellationToken,
async (step, ct) =>
{
(bool pass, string msg) = step switch
{
(int)NetworkEnvCheckStep.HostsFileCheck => await CheckHostsAsync(),
(int)NetworkEnvCheckStep.NetworkInterfaceCheck => CheckNetworkInterfaces(),
(int)NetworkEnvCheckStep.LSPCheck =>
#if WINDOWS
CheckLSP(HandleWSAProtocolInfo),
#else
(true, string.Empty),
#endif
_ => (true, string.Empty)
};
progress.Report(((NetworkEnvCheckStep)step, pass, msg));
}
).ConfigureAwait(false);
}
private static Task<(bool Pass, string Msg)> CheckHostsAsync()
{
bool hostFileExist = File.Exists(HOST_FILE_PATH);
if (!hostFileExist)
return Task.FromResult((false, "未找到 hosts 文件"));
return Task.FromResult((hostFileExist, string.Empty));
}
private static (bool Pass, string ErrorMsg) CheckNetworkInterfaces(NetworkInterface[]? interfaces = null)
{
interfaces ??= NetworkInterface.GetAllNetworkInterfaces();
if (interfaces == null || interfaces.Length == 0)
return (false, "未获取到网络适配器信息!");
// 如何判断虚拟网卡?
var useableNetworkInterfaces =
interfaces.Where(x =>
!x.Description.Contains("Virtual", StringComparison.OrdinalIgnoreCase) &&
(x.NetworkInterfaceType == NetworkInterfaceType.Ethernet || x.NetworkInterfaceType == NetworkInterfaceType.Wireless80211));
if (!useableNetworkInterfaces.Any())
return (false, "未获取到可用的以太网,无线网络适配器");
var checkResults =
useableNetworkInterfaces.Select(x => new
{
Source = x,
Result = CheckNetworkInterface(x)
});
if (checkResults.Any(x => x.Result.Pass))
return (true, string.Empty);
var firstError = checkResults.First(x => !x.Result.Pass);
return (firstError.Result.Pass, $"{firstError.Source.Name}|{firstError.Source.Description} {firstError.Result.ErrorMsg}");
}
private static (bool Pass, string ErrorMsg) CheckNetworkInterface(NetworkInterface @interface)
{
var name = @interface.Name;
var desc = @interface.Description;
var ipProps = @interface.GetIPProperties();
if (@interface.OperationalStatus != OperationalStatus.Up)
return (false, "未启用网卡");
if (ipProps == null)
return (false, $"获取网络信息失败!");
bool hasIPv4 = ipProps.UnicastAddresses
.Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork);
bool hasIPv6 = ipProps.UnicastAddresses
.Any(x => x.Address.AddressFamily == AddressFamily.InterNetworkV6);
if (!hasIPv4 && !hasIPv6)
return (false, "网络适配器未启用TCP/IP服务");
// 如何判断虚拟网卡?
var dns = ipProps.DnsAddresses;
#if WINDOWS
var isDHCPEnable = ipProps.GetIPv4Properties().IsDhcpEnabled;
#endif
return (true, string.Empty);
}
private static (bool Pass, string ErrorMsg) CheckLSP(CheckLSPPredicate predicate)
{
int bufferSize = 0;
int error = 0;
// 获取 buffer 的大小
_ = WSCEnumProtocols(null, IntPtr.Zero, ref bufferSize, ref error);
IntPtr protocolBuffer = Marshal.AllocHGlobal(bufferSize);
try
{
int result = WSCEnumProtocols(null, protocolBuffer, ref bufferSize, ref error);
if (result == -1)
{
return (false, "获取LSP信息失败!");
}
int structSize = Marshal.SizeOf(typeof(WSAPROTOCOL_INFO));
int count = bufferSize / structSize;
for (int i = 0; i < count; i++)
{
IntPtr item = new(protocolBuffer + (i * structSize));
WSAPROTOCOL_INFO protocolInfo = Marshal.PtrToStructure<WSAPROTOCOL_INFO>(item);
#if DEBUG
// Console.WriteLine($"Protocol: {protocolInfo.szProtocol}");
// Console.WriteLine($"Address Family: {protocolInfo.iAddressFamily}");
// Console.WriteLine($"Socket Type: {protocolInfo.iSocketType}");
// Console.WriteLine($"Protocol: {protocolInfo.iProtocol}");
// Console.WriteLine($"Provider ID: {protocolInfo.ProviderId}");
// Console.WriteLine();
#endif
var handle = predicate(in protocolInfo);
if (!handle)
return (false, "LSP响应异常!");
}
return (true, string.Empty);
}
finally
{
Marshal.FreeHGlobal(protocolBuffer);
}
}
#region P/Invoke
/// <summary>
/// 处理判断 协议信息是否有效
/// </summary>
/// <param name="info"></param>
/// <returns></returns>
static bool HandleWSAProtocolInfo(in WSAPROTOCOL_INFO info)
{
const int MAX = 255;
StringBuilder pathBuilder = new StringBuilder(MAX);
int pathLength = MAX;
Guid queryProviderId = info.ProviderId;
int result = WSCGetProviderPath(ref queryProviderId, pathBuilder, ref pathLength, out int errorCode);
if (result != 0)
{
// 获取失败
}
// path 可能包含系统环境变量例如 %SystemRoot%
// 是否需要解析
string path = pathBuilder.ToString();
Console.WriteLine("path:" + path);
return true;
}
[DllImport("Ws2_32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern int WSCEnumProtocols(
int[]? lpiProtocols,
IntPtr lpProtocolBuffer,
ref int lpdwBufferLength,
ref int lpErrno);
[DllImport("Ws2_32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern int WSCGetProviderPath(
[In] ref Guid providerId,
[Out][MarshalAs(UnmanagedType.LPWStr)] StringBuilder providerDllPath,
[In, Out] ref int providerDllPathLength,
[Out] out int lpErrno
);
private delegate bool CheckLSPPredicate(in WSAPROTOCOL_INFO protocolInfo);
/// <summary>
/// WSAPROTOCOL_INFO 结构用于存储或检索给定协议的完整信息。
/// <see cref="https://learn.microsoft.com/zh-cn/windows/win32/api/winsock2/ns-winsock2-wsaprotocol_infoa"/>
/// </summary>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "<挂起>")]
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WSAPROTOCOL_INFO
{
/// <summary>
/// 描述协议提供的服务的位掩码
/// </summary>
public uint dwServiceFlags1;
/// <summary>
/// 保留给其他协议属性定义。
/// </summary>
public uint dwServiceFlags2;
/// <summary>
/// 保留给其他协议属性定义。
/// </summary>
public uint dwServiceFlags3;
/// <summary>
/// 保留给其他协议属性定义。
/// </summary>
public uint dwServiceFlags4;
/// <summary>
/// 一组标志,提供有关如何在 Winsock 目录中表示此协议的信息
/// </summary>
public uint dwProviderFlags;
/// <summary>
/// 服务提供商供应商) 分配给提供程序的 GUID
/// </summary>
public Guid ProviderId;
/// <summary>
/// 由 WS2_32.DLL 为每个WSAPROTOCOL_INFO结构分配的唯 一 标识符。
/// </summary>
public uint dwCatalogEntryId;
public WSAPROTOCOLCHAIN ProtocolChain;
/// <summary>
/// 协议版本标识符。
/// </summary>
public int iVersion;
/// <summary>
/// 要作为地址系列参数传递给 套接字 或 WSASocket 函数的
/// </summary>
public int iAddressFamily;
/// <summary>
/// 最大地址长度(以字节为单位)。
/// </summary>
public int iMaxSockAddr;
/// <summary>
/// 最小地址长度(以字节为单位)。
/// </summary>
public int iMinSockAddr;
public int iSocketType;
public int iProtocol;
public int iProtocolMaxOffset;
/// <summary>
/// 目前,这些值是 BIGENDIAN 和 LITTLEENDIAN) (清单常量,分别指示值为 0 和 1 的 big-endian 或 little-endian
/// </summary>
public int iNetworkByteOrder;
public int iSecurityScheme;
/// <summary>
/// 协议支持的最大消息大小(以字节为单位)
/// </summary>
public uint dwMessageSize;
public uint dwProviderReserved;
/// <summary>
/// 一个字符数组,其中包含标识协议的可读名称,例如“MSAFD Tcpip [UDP/IP]”。 允许的最大字符数为 WSAPROTOCOL_LEN,定义为 255。
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string szProtocol;
}
/// <summary>
/// WSAPROTOCOLCHAIN 结构包含构成协议链的目录条目标识符的计数列表。
/// <see cref="https://learn.microsoft.com/zh-cn/windows/win32/api/winsock2/ns-winsock2-wsaprotocolchain"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct WSAPROTOCOLCHAIN
{
/// <summary>
/// 链的长度(以字节为单位)
/// 将 ChainLen 设置为零表示分层协议
/// 将 ChainLen 设置为 1 指示基本协议
/// 将 ChainLen 设置为大于 1 表示协议链
/// ChainEntries[MAX_PROTOCOL_CHAIN]
/// </summary>
public int ChainLen;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
public uint[] ChainEntries;
}
#endregion P/Invoke
}
@@ -0,0 +1,953 @@
using STUN.Client;
using STUN.StunResult;
using System.Net.NetworkInformation;
namespace BD.WTTS.Services.Implementation;
internal partial class NetworkTestService : INetworkTestService
{
#region 常量
// 默认测试地址
const string DEFAULT_TESTSERVER_HOSTNAME = "stun.syncthing.net";
const int DEFAULT_PORT = 3478;
const int DEFAULT_TLSPORT = 5349;
// 默认端口号
const int DEFAULT_TESTSTUN3489_PORT = DEFAULT_PORT;
// 默认端口号
const int DEFAULT_TESTSTUN5389_PORT = DEFAULT_PORT;
#endregion 常量
public NetworkTestService()
{
}
#region Ping
/// <summary>
/// 将 Internet 控制消息协议 (ICMP) 回显请求消息发送到远程计算机,并等待来自该计算机的 ICMP 回送答复消息。 有关 ICMP 消息的详细说明,请参阅 RFC 792,可从 https://www.ietf.org获取。
/// </summary>
/// <param name="testHostNameOrAddress"></param>
/// <param name="timeout"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<PingReply> TestPingAsync(string testHostNameOrAddress, TimeSpan timeout, CancellationToken cancellationToken = default)
{
using Ping p = new Ping();
var reply = await p.SendPingAsync(
testHostNameOrAddress,
timeout,
cancellationToken: cancellationToken
);
return reply;
}
#endregion Ping
#region Open/Upload/Download Speed Test
public async Task<(bool Success, long? DelayMs)> TestOpenUrlAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default)
{
using var testClient = httpClientFunc?.Invoke() ?? new HttpClient();
Stopwatch watch = Stopwatch.StartNew();
try
{
var resp = await testClient.GetAsync(url, HttpCompletionOption.ResponseContentRead, cancellationToken);
watch.Stop();
return (true, watch.ElapsedMilliseconds);
}
catch (Exception ex)
{
Log.Error(nameof(NetworkTestService), ex, "测试打开 URL 异常");
return (false, default);
}
}
public async Task<(bool Success, double? Rate)> TestUploadSpeedAsync(
string uploadServerUrl,
byte[] uploadBytes,
CancellationToken cancellationToken = default
)
{
using HttpClient testClient = new HttpClient();
return await TestUploadSpeedCoreAsync(
() => testClient,
BuildUploadContent,
uploadServerUrl,
uploadBytes,
cancellationToken
);
static MultipartFormDataContent BuildUploadContent(byte[] data)
{
MultipartFormDataContent content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(data));
return content;
}
}
public async Task<(bool Success, double? Rate)> TestDownloadSpeedAsync(string downloadUrl, CancellationToken cancellationToken = default)
{
using HttpClient testClient = new HttpClient();
return await TestDownloadSpeedCoreAsync(downloadUrl, () => testClient, cancellationToken);
}
/// <summary>
/// 测试上传速度
/// </summary>
/// <param name="httpClientFunc">自定义用于发送 http 请求的 client </param>
/// <param name="uploadContentFunc">自定义上传内容类型</param>
/// <param name="uploadServerUrl"></param>
/// <param name="uploadBytes"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
static async Task<(bool Success, double? Rate)> TestUploadSpeedCoreAsync(
Func<HttpClient> httpClientFunc,
Func<byte[], HttpContent> uploadContentFunc,
string uploadServerUrl,
byte[] uploadBytes,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrWhiteSpace(uploadServerUrl))
return (false, null);
var uploadContent = uploadContentFunc(uploadBytes);
try
{
var httpClient = httpClientFunc();
Stopwatch watch = Stopwatch.StartNew();
var resp = await httpClient.PostAsync(uploadServerUrl, uploadContent, cancellationToken);
resp.EnsureSuccessStatusCode();
watch.Stop();
return (true, GetRate(uploadBytes.Length, watch.Elapsed.TotalSeconds));
}
catch (Exception ex)
{
Log.Error(nameof(NetworkTestService), ex, "测试上传异常");
}
return (false, null);
}
/// <summary>
/// 测试下载速度
/// </summary>
/// <param name="url"></param>
/// <param name="httpClientFunc"></param>
/// <returns></returns>
static async Task<(bool Success, double? Rate)> TestDownloadSpeedCoreAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default)
{
var httpClient = httpClientFunc?.Invoke() ?? new HttpClient();
try
{
Stopwatch watch = Stopwatch.StartNew();
var resp = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
resp.EnsureSuccessStatusCode();
var bytesArray = await resp.Content.ReadAsByteArrayAsync(cancellationToken);
watch.Stop();
return (true, GetRate(bytesArray.Length, watch.Elapsed.TotalSeconds));
}
catch (Exception ex)
{
Log.Error(nameof(NetworkTestService), ex, "测试下载异常");
return (false, null);
}
finally
{
if (httpClientFunc == null)
httpClient.Dispose();
}
}
static double GetRate(long byteArrayLength, double seconds)
{
if (seconds == 0)
throw new ArgumentException("时间不能小于0", nameof(seconds));
long bits = byteArrayLength * 8;
double megabits = bits / 1_000_000.0; // 将比特转换为兆比特
return megabits / seconds; // 返回速率,单位是 Mbps
}
#endregion Open/Upload/Download Speed Test
#region STUN 测试
#region RFC3489
StunClient3489? _stunClient;
/// <summary>
/// 测试 STUN 客户端 基于 RFC3489
/// </summary>
/// <param name="testServerHostName">测试服务地址</param>
/// <param name="testServerPort">测试服务端口</param>
/// <param name="localIPEndPoint">本机 IP </param>
/// <param name="force">强制刷新</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<ClassicStunResult?> TestStunClient3489Async(
string? testServerHostName = DEFAULT_TESTSERVER_HOSTNAME,
int? testServerPort = DEFAULT_TESTSTUN3489_PORT,
IPEndPoint? localIPEndPoint = null,
bool force = false,
CancellationToken cancellationToken = default
)
{
testServerHostName ??= DEFAULT_TESTSERVER_HOSTNAME;
testServerPort ??= DEFAULT_TESTSTUN3489_PORT;
if (_stunClient == null || force)
_stunClient = await GetStunClient3489Async(testServerHostName, testServerPort.Value, localIPEndPoint);
try
{
await _stunClient.QueryAsync(cancellationToken);
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
Log.Error(nameof(NetworkTestService), ex, "TestStunClient3489Async 异常");
return null;
}
return _stunClient.State;
}
private async ValueTask<StunClient3489> GetStunClient3489Async(string serverHostName, int port, IPEndPoint? localIPEndPoint = null)
{
IPEndPoint serverEndpoint = await GetServerIPEndPointAsync(serverHostName, port);
IPEndPoint localEndpoint = localIPEndPoint ?? new IPEndPoint(IPAddress.Any, default);
var client = new StunClient3489(
serverEndpoint,
localEndpoint
);
return client;
}
private async Task<IPEndPoint> GetServerIPEndPointAsync(string hostNameOrAddress, int port)
{
var serverIps = await Dns.GetHostAddressesAsync(hostNameOrAddress);
IPEndPoint serverEndpoint = new IPEndPoint(serverIps.First(), port);
return serverEndpoint;
}
#endregion RFC3489
#region RFC5389
/// <summary>
/// 测试 STUN 客户端 基于 RFC5389
/// </summary>
/// <param name="protocol"></param>
/// <param name="testServerHostName"></param>
/// <param name="testServerPort"></param>
/// <param name="localIPEndPoint"></param>
/// <param name="force"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<StunResult5389?> TestStunClient5389Async(
TransportProtocol protocol,
string? testServerHostName = DEFAULT_TESTSERVER_HOSTNAME,
int? testServerPort = DEFAULT_TESTSTUN5389_PORT,
IPEndPoint? localIPEndPoint = null,
CancellationToken cancellationToken = default
)
{
testServerHostName ??= DEFAULT_TESTSERVER_HOSTNAME;
testServerPort ??= DEFAULT_TESTSTUN5389_PORT;
IStunClient5389 client = await GetStunClient5389Async(protocol, testServerHostName, testServerPort.Value, localIPEndPoint);
if (client == null)
return null;
try
{
await client.QueryAsync(cancellationToken);
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
Log.Error(nameof(NetworkTestService), ex, "TestStunClient5389Async 异常");
return null;
}
return client.State;
}
private async ValueTask<IStunClient5389> GetStunClient5389Async(TransportProtocol protocol, string serverHostName, int port, IPEndPoint? localIPEndPoint = null)
{
IPEndPoint serverEndpoint = await GetServerIPEndPointAsync(serverHostName, port);
IPEndPoint localEndpoint = localIPEndPoint ?? new IPEndPoint(IPAddress.Any, default);
IStunClient5389 client = protocol == TransportProtocol.Tcp
? new StunClient5389TCP(serverEndpoint, localEndpoint)
: new StunClient5389UDP(serverEndpoint, localEndpoint);
return client;
}
#endregion RFC5389
#endregion STUN 测试
#region DNS 查询
/// <summary>
/// 测试 DNS UDP
/// </summary>
/// <param name="testDomain">要测试的域名</param>
/// <param name="dnsServerIp">DNS 服务地址</param>
/// <param name="dnsServerPort">DNS 服务端口</param>
/// <param name="dnsRecordType"> dns 测试类型 </param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<(long DelayMs, IPAddress[] Address)> TestDNSAsync(
string testDomain,
string dnsServerIp,
int dnsServerPort,
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
CancellationToken cancellationToken = default
)
{
using UdpClient udpClient = new UdpClient(dnsServerIp, dnsServerPort);
var query = new DnsQueryUdpRequest(testDomain, dnsRecordType);
Stopwatch watch = Stopwatch.StartNew();
await udpClient.SendAsync(query, cancellationToken);
var receivedData = await udpClient.ReceiveAsync(cancellationToken);
watch.Stop();
var dnsQueryResponse = new DnsQueryUdpResponse(receivedData.Buffer);
return (watch.ElapsedMilliseconds, dnsQueryResponse.GetAddresses().ToArray());
}
/// <summary>
/// 测试 DNS Over Https
/// </summary>
/// <param name="testDomain"></param>
/// <param name="dohServer"></param>
/// <param name="dnsRecordType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<(long DelayMs, IPAddress[] Address)> TestDNSOverHttpsAsync(
string testDomain,
string dohServer,
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
CancellationToken cancellationToken = default)
{
var handler = new HttpClientHandler
{
UseCookies = false,
UseProxy = false,
Proxy = HttpNoProxy.Instance,
};
using HttpClient client = new HttpClient(handler);
string queryUrl = $"{dohServer}?name={testDomain}&type={dnsRecordType}";
Stopwatch watch = Stopwatch.StartNew();
HttpResponseMessage? resp = null;
try
{
resp = await client.GetAsync(queryUrl, HttpCompletionOption.ResponseContentRead, cancellationToken);
resp.EnsureSuccessStatusCode();
}
finally
{
watch.Stop();
}
var queryResp = await resp.Content.ReadFromJsonAsync<DnsQueryJsonResponse>();
return (watch.ElapsedMilliseconds, queryResp?.GetAddresses().ToArray() ?? Array.Empty<IPAddress>());
}
#region DNS 解析定义
internal interface IDnsQueryResponse
{
List<DnsQueryAnswerRecord> Answers { get; }
IEnumerable<IPAddress> GetAddresses();
}
internal record DnsQueryUdpResponse : DnsQueryUdpRequest, IDnsQueryResponse
{
public ushort AnswerCount { get; init; } // (ushort)((_origin[6] << 8) | _origin[7]);
public ushort AuthorityCount { get; init; } // (ushort)((_origin[8] << 8) | _origin[9]);
public ushort AdditionalCount { get; init; } // (ushort)((_origin[10] << 8) | _origin[11]);
public List<DnsQueryAnswerRecord> Answers { get; init; }
private readonly byte[] _origin;
public DnsQueryUdpResponse(byte[] origin) : base(origin)
{
_origin = origin;
AnswerCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
AuthorityCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
AdditionalCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
// 跳过查询问题部分
for (int i = 0; i < QuestionCount; i++)
{
Position = SkipName(origin, Position);
Position += 4; // 跳过类型
}
Answers = new List<DnsQueryAnswerRecord>();
Position = ParseRecords(origin, Position, AnswerCount, Answers);
Position = ParseRecords(origin, Position, AuthorityCount, Answers);
Position = ParseRecords(origin, Position, AdditionalCount, Answers);
}
private static int ParseRecords(byte[] response, int position, int recordCount, List<DnsQueryAnswerRecord> answers)
{
for (int i = 0; i < recordCount; i++)
{
position = ParseName(response, position, out var parsedName);
int @type = (response[position++] << 8) | response[position++];
int @class = (response[position++] << 8) | response[position++];
long ttl = (response[position++] << 24) | (response[position++] << 16) | (response[position++] << 8) | response[position++];
int dataLength = (response[position++] << 8) | response[position++];
if (@type == (int)DnsQueryAnswerRecord.DnsRecordType.A && dataLength == 4) // Type A (IPv4)
{
var ipAddress = new byte[4];
Array.Copy(response, position, ipAddress, 0, 4);
answers.Add(
new DnsQueryAnswerRecord.A(
parsedName!,
(DnsQueryAnswerRecord.DnsRecordType)@type,
ttl,
new IPAddress(ipAddress)
)
);
}
else if (@type == (int)DnsQueryAnswerRecord.DnsRecordType.AAAA && dataLength == 16) // Type AAAA (IPv6)
{
var ipAddress = new byte[16];
Array.Copy(response, position, ipAddress, 0, 16);
answers.Add(
new DnsQueryAnswerRecord.AAAA(
parsedName!,
(DnsQueryAnswerRecord.DnsRecordType)@type,
ttl,
new IPAddress(ipAddress)
)
);
}
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.CNAME)
{
ParseName(response, position, out var parsedCName);
if (string.IsNullOrEmpty(parsedCName))
{
Log.Error(nameof(NetworkTestService), $"解析 DNS CName字段异常 name :{parsedName}, data length :{dataLength}");
continue;
}
answers.Add(
new DnsQueryAnswerRecord.CName(
parsedName!,
(DnsQueryAnswerRecord.DnsRecordType)@type,
ttl,
parsedCName!
)
);
}
else
{
Log.Error(nameof(NetworkTestService), $"未支持的 DNS响应记录 type :{@type} ,class :{@class} name :{parsedName}, data length :{dataLength}");
continue;
}
position += dataLength;
}
return position;
}
private static int ParseName(byte[] response, int position, out string? name)
{
StringBuilder nameBuilder = new StringBuilder();
int originalPosition = position;
bool isPointer = false;
// 0表示结束
while (response[position] != 0)
{
// dns 协议 名称可能是 指针 或者 直接表示
// 如果大于等于 192 表示是指针开始
if (response[position] >= 192)
{
if (!isPointer)
{
originalPosition = position + 2;
isPointer = true;
}
// 因为 00111111 可以忽略前两位 用 0x3F 掩码获取指针
int pointer = ((response[position] & 0x3F) << 8) | response[position + 1];
position = pointer;
}
else
{
int length = response[position];
position++;
nameBuilder.Append(Encoding.ASCII.GetString(response, position, length));
position += length;
if (response[position] != 0)
{
nameBuilder.Append('.');
}
}
}
if (!isPointer)
{
position++;
}
else
{
position = originalPosition;
}
name = nameBuilder.ToString();
return position;
}
private static int SkipName(byte[] response, int position)
{
if (response[position] >= 192)
{
return position + 2; // Name is a pointer
}
while (response[position] != 0)
{
position += response[position] + 1;
}
return position + 1;
}
public IEnumerable<IPAddress> GetAddresses()
{
foreach (var answer in Answers)
{
if (answer is DnsQueryAnswerRecord.A a)
{
yield return a.Data;
}
else if (answer is DnsQueryAnswerRecord.AAAA aaaa)
{
yield return aaaa.Data;
}
}
yield break;
}
}
internal record DnsQueryUdpRequest
{
public ushort QueryId { get; init; } // (ushort)((_origin[0] << 8) | _origin[1]);
public ushort Flags { get; init; } //(ushort)((_origin[2] << 8) | _origin[3]);
public ushort QuestionCount { get; init; } // (ushort)((_origin[4] << 8) | _origin[5]);
public int Length => _origin.Length;
protected int Position { get; set; }
private readonly byte[] _origin;
public DnsQueryUdpRequest(byte[] origin)
{
_origin = origin;
Position = default;
QueryId = (ushort)((origin[Position++] << 8) | origin[Position++]);
Flags = (ushort)((origin[Position++] << 8) | origin[Position++]);
QuestionCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
}
public DnsQueryUdpRequest(string domainName, DnsQueryAnswerRecord.DnsRecordType type = DnsQueryAnswerRecord.DnsRecordType.A)
: this(CreateDnsQuery(domainName, type))
{
}
public static byte[] CreateDnsQuery(string domainName, DnsQueryAnswerRecord.DnsRecordType type = DnsQueryAnswerRecord.DnsRecordType.A)
{
// 12字节报文头部
const int DNS_QUERY_HEADER_BYTECOUNT = 12;
// 2字节终止符长度
const int DNS_QUERY_END_BYTECOUNT = 2;
// 4字节查询类型和查询类字段
const int DNS_QUERY_QUERYTYPE_BYTECOUNT = 4;
// dns queryId 范围是一个16位字段,16位无符号的范围就是0-65535
var queryId = (ushort)Random.Shared.Next(0, 65536);
byte[] dnsQuery = new byte[
DNS_QUERY_HEADER_BYTECOUNT
+ domainName.Length
+ DNS_QUERY_END_BYTECOUNT
+ DNS_QUERY_QUERYTYPE_BYTECOUNT
];
BuildQueryHeader(queryId, dnsQuery);
BuildBody(domainName, dnsQuery);
return dnsQuery;
void BuildQueryHeader(ushort queryId, byte[] query)
{
// 查询 ID 是一个 16 位(2 字节)的字段。
// 为了将这个 16 位的整数值分配到字节数组 dnsQuery 的两个字节中,
// 我们需要将其高 8 位和低 8 位分别存储在数组的第一个和第二个字节中
query[0] = (byte)(queryId >> 8);
query[1] = (byte)(queryId & 0xFF);
// 标志字段(Flags) 2 - 3 包含各种控制标志
// 如查询/响应(QR)、操作码(Opcode)、授权回答(AA)、截断(TC)、递归期望(RD)、递归可用(RA)等
query[2] = 0x01;
// 问题计数(QDCOUNT)4 - 5 表示查询问题部分的数量
query[5] = 0x01;
// 其他查询报文中通常为 0
// 回答记录计数(ANCOUNT),授权记录计数(NSCOUNT),附加记录计数(ARCOUNT)
}
void BuildBody(string domainName, byte[] query)
{
// 获取域名组成部分 因为后面要根据部分长度和内容填充结果
string[] domainParts = domainName.Split('.');
int currentPosition = 12;
// 根据 dns 查询报文 每一部分开始都要填充这一部分的长度
foreach (var domainPart in domainParts)
{
// 标志当前 part 的长度
query[currentPosition++] = (byte)domainPart.Length;
// 获取填充的字节
var fillBytes = Encoding.ASCII.GetBytes(domainPart);
// 移动到当前查询体中
fillBytes.CopyTo(query, currentPosition);
// 移动当前填充开始位置
currentPosition += fillBytes.Length;
}
// 0 表示域名结束
query[currentPosition++] = 0x00;
// 填充 查询类型和查询类字段
query[currentPosition++] = 0x00;
query[currentPosition++] = (byte)type; // 表示查询类型为 A 记录(IPv4 地址)。
query[currentPosition++] = 0x00;
query[currentPosition++] = 0x01; // 表示查询类为 IN(Internet 类)。
}
}
public static implicit operator byte[](DnsQueryUdpRequest data)
{
return data._origin;
}
public static implicit operator ReadOnlyMemory<byte>(DnsQueryUdpRequest data)
{
return data._origin;
}
}
internal record DnsQueryJsonResponse : IDnsQueryResponse
{
[S_JsonProperty("Status")]
public int Status { get; init; }
[S_JsonProperty("TC")]
public bool TC { get; init; }
[S_JsonProperty("RD")]
public bool RD { get; init; }
[S_JsonProperty("RA")]
public bool RA { get; init; }
[S_JsonProperty("AD")]
public bool AD { get; init; }
[S_JsonProperty("CD")]
public bool CD { get; init; }
[S_JsonProperty("Answer")]
public List<DnsQueryAnswerRecord> Answers { get; init; }
public DnsQueryJsonResponse(
int status,
bool tc,
bool rd,
bool ra,
bool ad,
bool cd,
List<DnsQueryAnswerRecord>? answers
)
{
Status = status;
TC = tc;
RD = rd;
RA = ra;
AD = ad;
CD = cd;
Answers = answers?.ToList() ?? new();
}
public IEnumerable<IPAddress> GetAddresses()
{
return Answers.Aggregate(Enumerable.Empty<IPAddress>(), (acc, x) =>
{
if (x is DnsQueryAnswerRecord.A ipv4)
{
return acc.Append(ipv4.Data);
}
else if (x is DnsQueryAnswerRecord.AAAA ipv6)
{
return acc.Append(ipv6.Data);
}
return acc;
});
}
}
private class DnsQueryAnswerRecordConverter : System.Text.Json.Serialization.JsonConverter<DnsQueryAnswerRecord>
{
public override DnsQueryAnswerRecord? Read(ref Utf8JsonReader reader, Type typeToConvert, SystemTextJsonSerializerOptions options)
{
if (typeToConvert == typeof(DnsQueryAnswerRecord))
{
if (reader.TokenType == JsonTokenType.StartObject)
{
var json = JsonDocument.ParseValue(ref reader);
if (json.RootElement.TryGetProperty("type", out var typeElement))
{
var type = typeElement.GetInt32();
if (type == (int)DnsQueryAnswerRecord.DnsRecordType.A)
{
return json.RootElement.Deserialize<DnsQueryAnswerRecord.A>();
}
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.AAAA)
{
return json.RootElement.Deserialize<DnsQueryAnswerRecord.AAAA>();
}
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.CNAME)
{
return json.RootElement.Deserialize<DnsQueryAnswerRecord.CName>();
}
}
}
}
return null;
}
public override void Write(Utf8JsonWriter writer, DnsQueryAnswerRecord value, SystemTextJsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
[System.Text.Json.Serialization.JsonConverter(typeof(DnsQueryAnswerRecordConverter))]
[JsonDerivedType(typeof(DnsQueryAnswerRecord.A))]
[JsonDerivedType(typeof(DnsQueryAnswerRecord.AAAA))]
[JsonDerivedType(typeof(DnsQueryAnswerRecord.CName))]
internal record DnsQueryAnswerRecord
{
[S_JsonProperty("name")]
public string Name { get; set; }
[S_JsonProperty("type")]
public DnsRecordType Type { get; set; }
[S_JsonProperty("TTL")]
public long Ttl { get; set; }
public DnsQueryAnswerRecord(
string name,
DnsRecordType type,
long ttl
)
{
Name = name;
Type = @type;
Ttl = ttl;
//Data = data;
//DataLength = data.Length;
}
#region DnsQueryAnswerRecord 子类派生
internal record A : DnsQueryAnswerRecord
{
public A(string name, DnsRecordType type, long ttl, IPAddress data) : base(name, type, ttl)
{
Data = data;
}
[S_JsonProperty("data")]
[SystemTextJsonConverter(typeof(IPAddressConverter))]
public IPAddress Data { get; set; }
}
internal record AAAA : DnsQueryAnswerRecord
{
public AAAA(string name, DnsRecordType type, long ttl, IPAddress data) : base(name, type, ttl)
{
Data = data;
}
[S_JsonProperty("data")]
[SystemTextJsonConverter(typeof(IPAddressConverter))]
public IPAddress Data { get; set; }
}
internal record CName : DnsQueryAnswerRecord
{
public CName(string name, DnsRecordType type, long ttl, string data) : base(name, type, ttl)
{
Data = data;
}
[S_JsonProperty("data")]
public string Data { get; set; }
}
#endregion DnsQueryAnswerRecord 子类派生
class IPAddressConverter : System.Text.Json.Serialization.JsonConverter<IPAddress>
{
public override IPAddress? Read(ref Utf8JsonReader reader, Type typeToConvert, SystemTextJsonSerializerOptions options)
{
IPAddress? result = null;
// 目前只解析字符串
if (reader.TokenType == JsonTokenType.String)
{
string? ipString = reader.GetString();
result = !string.IsNullOrWhiteSpace(ipString) ? IPAddress.Parse(ipString) : default;
}
// byte 数组支持
else if (reader.TokenType == JsonTokenType.StartArray)
{
List<byte> bytes = new List<byte>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
if (reader.TokenType == JsonTokenType.Number)
{
bytes.Add((byte)reader.GetInt32());
}
else if (reader.TokenType == JsonTokenType.String)
{
if (byte.TryParse(reader.GetString(), out byte byteValue))
{
bytes.Add(byteValue);
}
}
}
result = bytes.Count == 4 || bytes.Count == 16 ? new IPAddress(bytes.ToArray()) : default;
}
return result;
}
public override void Write(Utf8JsonWriter writer, IPAddress value, SystemTextJsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
internal enum DnsRecordClass : ushort
{
Internet = 1, // Internet(IN)
Chaos = 3, // Chaos(CH)
Hesiod = 4, // Hesiod(HS)
QCLASSNONE = 254, // QCLASS NONE
QCLASSANY = 255, // QCLASS * (ANY)
}
// 域名系统记录类型 (省略了有些未使用的类型)
internal enum DnsRecordType : byte
{
A = 1, // 指定域名对应的IPv4地址
AAAA = 28, // 指定域名对应的IPv6地址
CNAME = 5, // 别名记录,
MX = 15, // 邮件交换记录,
NS = 2, // 指定该域名由哪个DNS服务器来进行解析
TXT = 16, // 主机名或域名的说明
SOA = 6, // 起始授权机构
PTR = 12, // 反向IP查询
ANY = 255, // 所有DNS记录类型
}
}
#endregion DNS 解析定义
#endregion DNS 查询
}
@@ -87,6 +87,12 @@ partial interface IAcceleratorService // XunYou
/// </summary>
/// <returns></returns>
Task<ApiRsp<XunYouStartGameCode>> XY_StartGame(CancellationToken cancellationToken = default);
/// <summary>
/// 迅游加速器显示隐藏
/// </summary>
/// <returns></returns>
Task<ApiRsp<int>> XY_ShowWinodw(bool showHide, CancellationToken cancellationToken = default);
}
partial interface IXunYouAccelStateToFrontendCallback
@@ -0,0 +1,122 @@
using STUN.StunResult;
using System.Net.NetworkInformation;
using static BD.WTTS.Services.Implementation.NetworkTestService;
namespace BD.WTTS.Services;
internal interface INetworkTestService : IHttpRequestTestService, IStunTestService
{
static INetworkTestService Instance => Ioc.Get<INetworkTestService>();
/// <summary>
/// Ping
/// </summary>
/// <param name="testHostNameOrAddress"></param>
/// <param name="timeout"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<PingReply> TestPingAsync(string testHostNameOrAddress, TimeSpan timeout, CancellationToken cancellationToken = default);
/// <summary>
/// 测试 DNS UDP
/// </summary>
/// <param name="testDomain"></param>
/// <param name="dnsServerIp"></param>
/// <param name="dnsServerPort"></param>
/// <param name="dnsRecordType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<(long DelayMs, IPAddress[] Address)> TestDNSAsync(
string testDomain,
string dnsServerIp,
int dnsServerPort,
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
CancellationToken cancellationToken = default
);
/// <summary>
/// 测试 DNS Over Https
/// </summary>
/// <param name="testDomain"></param>
/// <param name="dohServer"></param>
/// <param name="dnsRecordType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<(long DelayMs, IPAddress[] Address)> TestDNSOverHttpsAsync(
string testDomain,
string dohServer,
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
CancellationToken cancellationToken = default
);
}
internal interface IHttpRequestTestService
{
/// <summary>
/// 测试打开网址
/// </summary>
/// <param name="url"></param>
/// <param name="httpClientFunc"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<(bool Success, long? DelayMs)> TestOpenUrlAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default);
/// <summary>
/// 测试上传速度
/// </summary>
/// <param name="uploadServerUrl"></param>
/// <param name="uploadBytes"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<(bool Success, double? Rate)> TestUploadSpeedAsync(
string uploadServerUrl,
byte[] uploadBytes,
CancellationToken cancellationToken = default
);
/// <summary>
/// 测试下载速度
/// </summary>
/// <param name="downloadUrl"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<(bool Success, double? Rate)> TestDownloadSpeedAsync(string downloadUrl, CancellationToken cancellationToken = default);
}
internal interface IStunTestService
{
/// <summary>
/// 测试 STUN Client RFC3489
/// </summary>
/// <param name="testServerHostName">STUN 服务地址</param>
/// <param name="testServerPort">STUN 服务端口</param>
/// <param name="localIPEndPoint">本机IP</param>
/// <param name="force">刷新测试 Client</param>
/// <remarks></remarks>
/// <returns></returns>
Task<ClassicStunResult?> TestStunClient3489Async(
string? testServerHostName = default,
int? testServerPort = default,
IPEndPoint? localIPEndPoint = default,
bool force = false,
CancellationToken cancellationToken = default
);
/// <summary>
/// 测试 STUN Client RFC5389
/// </summary>
/// <param name="protocol">使用的传输协议 Tcp,Udp</param>
/// <param name="testServerHostName">STUN 服务地址</param>
/// <param name="testServerPort">STUN 服务端口</param>
/// <param name="localIPEndPoint">本机IP</param>
/// <param name="force"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<StunResult5389?> TestStunClient5389Async(
TransportProtocol protocol,
string? testServerHostName = default,
int? testServerPort = default,
IPEndPoint? localIPEndPoint = null,
CancellationToken cancellationToken = default
);
}
@@ -66,7 +66,14 @@ public sealed partial class GameAcceleratorService
DeleteMyGameCommand = ReactiveCommand.Create<XunYouGameViewModel>(DeleteMyGame);
GameAcceleratorCommand = ReactiveCommand.CreateFromTask<XunYouGameViewModel>(GameAccelerator);
GameLaunchCommand = ReactiveCommand.CreateFromTask<XunYouGameViewModel>(GameLaunch);
InstallAcceleratorCommand = ReactiveCommand.CreateFromTask(InstallAccelerator);
InstallAcceleratorCommand = ReactiveCommand.CreateFromTask(async () =>
{
if (await IWindowManager.Instance.ShowTaskDialogAsync(new MessageBoxWindowViewModel(),
pageContent: new AcceleratorPathAskBox(), title: "游戏加速", isCancelButton: false, isOkButton: false))
{
await InstallAccelerator();
}
});
UninstallAcceleratorCommand = ReactiveCommand.Create(UninstallAccelerator);
AcceleratorChangeAreaCommand = ReactiveCommand.Create<XunYouGameViewModel>(AcceleratorChangeArea);
@@ -247,8 +254,13 @@ public sealed partial class GameAcceleratorService
/// <summary>
/// 设置加速游戏状态
/// </summary>
void SetGameStatus(XunYouGameViewModel game, int areaId = 0, int serverId = 0)
async void SetGameStatus(XunYouGameViewModel game, int areaId = 0, int serverId = 0)
{
if (!XunYouSDK.IsSupported)
{
return;
}
if (CurrentAcceleratorGame != null && CurrentAcceleratorGame.Id == game.Id)
{
CurrentAcceleratorGame.IsAccelerating = false;
@@ -295,6 +307,10 @@ public sealed partial class GameAcceleratorService
//加速后
Toast.Show(ToastIcon.Success, "加速成功");
if (ProxySettings.AutoShowWattAcceleratorWindow.Value)
{
_ = ShowXunYouWindow(true);
}
int testSpeedCallback(SpeedCallbackWrapper w)
{
if (CurrentAcceleratorGame != null)
@@ -321,6 +337,11 @@ public sealed partial class GameAcceleratorService
public async Task GameAccelerator(XunYouGameViewModel app)
{
if (!XunYouSDK.IsSupported)
{
return;
}
if (app.IsAccelerating)
return;
@@ -348,7 +369,7 @@ public sealed partial class GameAcceleratorService
//}
if (!await IWindowManager.Instance.ShowTaskDialogAsync(new MessageBoxWindowViewModel(),
pageContent: new AcceleratorPathAskBox(), title: "未安装加速插件", isCancelButton: true))
pageContent: new AcceleratorPathAskBox(), title: "游戏加速", isCancelButton: false, isOkButton: false))
{
app.IsAccelerating = false;
return;
@@ -503,7 +524,8 @@ public sealed partial class GameAcceleratorService
if (GameAcceleratorSettings.MyGames.Any_Nullable())
{
Games.Clear();
foreach (var item in GameAcceleratorSettings.MyGames.Value!.Values)
var temp = GameAcceleratorSettings.MyGames.Value!.Values.ToArray();
foreach (var item in temp)
{
if (item.PicInfo == null)
item.PicInfo = XunYouSDK.GetPicInfo(item.Id);
@@ -517,7 +539,8 @@ public sealed partial class GameAcceleratorService
{
Games.Clear();
GameAcceleratorSettings.MyGames.AddRange(games.Select(s => new KeyValuePair<int, XunYouGameViewModel>(s.Id, new XunYouGameViewModel(s))));
foreach (var item in GameAcceleratorSettings.MyGames.Value!.Values)
var temp = GameAcceleratorSettings.MyGames.Value!.Values.ToArray();
foreach (var item in temp)
{
if (item.PicInfo == null)
item.PicInfo = XunYouSDK.GetPicInfo(item.Id);
@@ -744,4 +767,14 @@ public sealed partial class GameAcceleratorService
}
}
}
public async Task ShowXunYouWindow(bool showHide)
{
//if (UserService.Current.User?.WattOpenId != null)
// await Ioc.Get<IAcceleratorService>().XY_StartEx2(
// UserService.Current.User.WattOpenId,
// UserService.Current.User.NickName, 0, 0, 0);
var result = await Ioc.Get<IAcceleratorService>().XY_ShowWinodw(showHide);
}
}
@@ -39,7 +39,7 @@ partial class ProxyService
IReadOnlyCollection<ScriptIPCDTO>? scripts = default;
bool isEnableScript = ProxySettings.IsEnableScript.Value;
bool isOnlyWorkSteamBrowser = ProxySettings.IsOnlyWorkSteamBrowser.Value;
ushort proxyPort = ProxySettings.SystemProxyPortId.Value;
ushort proxyPort = ProxySettings.SystemProxyPortId.Value == 0 ? ProxySettings.SystemProxyPortId.Default : ProxySettings.SystemProxyPortId.Value;
string? proxyIp = ProxySettings.SystemProxyIp.Value;
proxyMode = defaultProxyMode;
bool isProxyGOG = ProxySettings.IsProxyGOG.Value;
@@ -64,7 +64,7 @@ partial class ProxyService
string? proxyDNS = ProxySettings.ProxyMasterDns.Value;
bool isSupportIpv6 = await RefreshIpv6Support();
bool useDoh = ProxySettings.UseDoh.Value;
string? customDohAddres = ProxySettings.CustomDohAddres.Value;
string? customDohAddres = ProxySettings.CustomDohAddres2.Value;
Lazy<IPAddress> proxyIp_ = new(() => ReverseProxySettings.GetProxyIp(proxyIp));
void SetProxyIp(IPAddress proxyIPAddress)
@@ -206,7 +206,7 @@ partial class ProxyService
}
}
string? proxyToken = proxyDomains.Any_Nullable(s => s.ProxyType == ProxyType.ServerAccelerate) ?
string? proxyToken = proxyDomains.Any_Nullable(s => s.ProxyType == ProxyType.ServerAccelerate || s.Items.Any_Nullable(x => x.ProxyType == ProxyType.ServerAccelerate)) ?
await TryRequestServerSideProxyToken() : null;
ReverseProxySettings reverseProxySettings = new(proxyDomains, scripts,
@@ -1,5 +1,8 @@
// ReSharper disable once CheckNamespace
using Avalonia.Data;
using Google.Protobuf.WellKnownTypes;
using Org.BouncyCastle.Bcpg.OpenPgp;
using System.Linq;
namespace BD.WTTS.Services;
@@ -65,12 +68,12 @@ public sealed partial class ProxyService
.ToObservableChangeSet()
.AutoRefresh(x => x.ObservableItems)
.TransformMany(t => t.ObservableItems ?? new ObservableCollection<AccelerateProjectDTO>())
.AutoRefresh(x => x.Checked)
.WhenPropertyChanged(x => x.Checked, false)
.AutoRefresh(x => x.ThreeStateEnable)
.WhenPropertyChanged(x => x.ThreeStateEnable, false)
.Subscribe(_ =>
{
IsChangeSupportProxyServicesStatus = true;
ProxySettings.SupportProxyServicesStatus.Value = EnableProxyDomains?.Select(k => k.Id.ToString()).ToImmutableHashSet();
ProxySettings.SupportProxyServicesStatus.Value = GetAccelerateEnableAllIds(EnableProxyDomains).ToImmutableHashSet();
}));
this.WhenAnyValue(v => v.ProxyScripts)
@@ -149,13 +152,14 @@ public sealed partial class ProxyService
ProxyStarting = false;
ProxyStatus = startOrStop;
IsAnyProxyScripts = ProxyStatus && IsEnableScript;
}
}
public IEnumerable<AccelerateProjectDTO>? GetEnableProxyDomains()
public IEnumerable<AccelerateProjectDTO> GetEnableProxyDomains()
{
if (!ProxyDomains.Items.Any_Nullable())
return null;
return [];
var data = ProxyDomains.Items
.Where(x => x.Items != null)
.SelectMany(s => s.Items!.Where(w => w.ThreeStateEnable != false));
@@ -163,7 +167,7 @@ public sealed partial class ProxyService
return data;
}
public IReadOnlyCollection<AccelerateProjectDTO>? EnableProxyDomains => GetEnableProxyDomains()?.ToImmutableArray();
public IReadOnlyCollection<AccelerateProjectDTO>? EnableProxyDomains => GetEnableProxyDomains().ToImmutableArray();
//static IEnumerable<AccelerateProjectDTO>? GetProxyDomainsItems(AccelerateProjectDTO accelerates)
//{
@@ -258,6 +262,16 @@ public sealed partial class ProxyService
[Reactive]
public bool ProxyStatus { get; set; }
[Reactive]
public bool IsAnyProxyScripts { get; set; }
private bool CheckProxyScriptsEnable()
{
if (!ProxyScripts.Items.Any_Nullable())
return false;
return ProxyScripts.Items!.Any(w => !w.Disable);
}
#endregion 代理状态启动退出
static bool IsProgramStartupRunProxy()
@@ -403,22 +417,8 @@ public sealed partial class ProxyService
{
var items = ProxyDomains.Items!.SelectMany(s => s.Items!);
var enableItems = ProxySettings.SupportProxyServicesStatus.Value;
foreach (var item in items)
{
if (enableItems.Contains(item.Id.ToString()))
{
item.ThreeStateEnable = true;
}
}
RestoreAccelerateEnableAllIds(items, enableItems);
}
//if (IsLoadImage && ProxyDomains.Items.Any_Nullable())
//{
// foreach (var item in ProxyDomains.Items)
// {
// item.ImageStream = ImageChannelType.AccelerateGroup.GetImageAsync(ImageUrlHelper.GetImageApiUrlById(item.ImageId));
// }
//}
}
public static bool IsChangeSupportProxyServicesStatus { get; set; }
@@ -462,6 +462,29 @@ public sealed partial class ProxyService
}
}
private IEnumerable<string> GetAccelerateEnableAllIds(IEnumerable<AccelerateProjectDTO>? nodes)
{
if (nodes == null)
return Enumerable.Empty<string>();
return nodes.Where(s => s.ThreeStateEnable == true).SelectMany(node => new[] { node.Id.ToString() }.Concat(GetAccelerateEnableAllIds(node.Items)));
}
private void RestoreAccelerateEnableAllIds(IEnumerable<AccelerateProjectDTO> nodes, IReadOnlyCollection<string> enableItems)
{
foreach (var node in nodes)
{
if (node.Items.Any_Nullable())
{
RestoreAccelerateEnableAllIds(node.Items, enableItems);
continue;
}
if (enableItems.Contains(node.Id.ToString()))
{
node.ThreeStateEnable = true;
}
}
}
Timer? timer;
public void StartTimer()
@@ -153,7 +153,17 @@ public partial interface IProxySettings
/// <summary>
/// 自定义 DNS over HTTPS 地址
/// </summary>
string? CustomDohAddres { get; set; }
string? CustomDohAddres2 { get; set; }
/// <summary>
/// 加速页面 Tab 选项卡选中下标
/// </summary>
int AcceleratorTabsSelectedIndex { get; set; }
/// <summary>
/// 加速后自动唤起 Watt 加速器界面
/// </summary>
bool AutoShowWattAcceleratorWindow { get; set; }
/// <summary>
/// 启用脚本自动检查更新的默认值
@@ -268,6 +278,16 @@ public partial interface IProxySettings
/// <summary>
/// 自定义 DNS over HTTPS 地址的默认值
/// </summary>
const string? DefaultCustomDohAddres = null;
const string? DefaultCustomDohAddres2 = null;
/// <summary>
/// 加速页面 Tab 选项卡选中下标的默认值
/// </summary>
static readonly int DefaultAcceleratorTabsSelectedIndex = 0;
/// <summary>
/// 加速后自动唤起 Watt 加速器界面的默认值
/// </summary>
const bool DefaultAutoShowWattAcceleratorWindow = true;
}
@@ -195,13 +195,19 @@ public sealed partial class ProxySettings_ : IProxySettings, ISettings, ISetting
/// 自定义 DNS over HTTPS 地址
/// </summary>
[MPKey(22), MP2Key(22), JsonPropertyOrder(22)]
public string? CustomDohAddres { get; set; } = IProxySettings.DefaultCustomDohAddres;
public string? CustomDohAddres2 { get; set; } = IProxySettings.DefaultCustomDohAddres2;
/// <summary>
/// 加速页面 Tab 选项卡选中下标
/// </summary>
[MPKey(23), MP2Key(23), JsonPropertyOrder(23)]
public int AcceleratorTabsSelectedIndex { get; set; }
public int AcceleratorTabsSelectedIndex { get; set; } = IProxySettings.DefaultAcceleratorTabsSelectedIndex;
/// <summary>
/// 加速后自动唤起 Watt 加速器界面
/// </summary>
[MPKey(24), MP2Key(24), JsonPropertyOrder(24)]
public bool AutoShowWattAcceleratorWindow { get; set; } = IProxySettings.DefaultAutoShowWattAcceleratorWindow;
}
@@ -362,12 +368,19 @@ public static partial class ProxySettings
/// <summary>
/// 自定义 DNS over HTTPS 地址
/// </summary>
public static SettingsProperty<string, ProxySettings_> CustomDohAddres { get; }
= new(DefaultCustomDohAddres);
public static SettingsProperty<string, ProxySettings_> CustomDohAddres2 { get; }
= new(DefaultCustomDohAddres2);
/// <summary>
/// 加速页面 Tab 选项卡选中下标
/// </summary>
public static SettingsStructProperty<int, ProxySettings_> AcceleratorTabsSelectedIndex { get; }
= new();
= new(DefaultAcceleratorTabsSelectedIndex);
/// <summary>
/// 加速后自动唤起 Watt 加速器界面
/// </summary>
public static SettingsStructProperty<bool, ProxySettings_> AutoShowWattAcceleratorWindow { get; }
= new(DefaultAutoShowWattAcceleratorWindow);
}
@@ -1,12 +1,12 @@
using AppResources = BD.WTTS.Client.Resources.Strings;
using BD.WTTS.Client.Resources;
using BD.WTTS.UI.Views.Pages;
using AppResources = BD.WTTS.Client.Resources.Strings;
namespace BD.WTTS.UI.ViewModels;
public sealed partial class AcceleratorPageViewModel
{
public NetworkCheckControlViewModel NetworkCheckControlViewModel { get; } = new();
DateTime _initializeTime;
readonly IHostsFileService? hostsFileService;
readonly IPlatformService platformService = IPlatformService.Instance;
@@ -15,6 +15,31 @@ public sealed partial class AcceleratorPageViewModel
public AcceleratorPageViewModel()
{
ProxyService.Current.WhenValueChanged(x => x.ProxyStatus)
.Where(x => x == true)
.Subscribe(_ =>
{
// Create new ProxyEnableDomain for 加速服务 page
var enableGroupDomain = ProxyService.Current.ProxyDomainsList
.Where(list => list.ThreeStateEnable == true || list.ThreeStateEnable == null)
.Select(list => new ProxyDomainGroupViewModel
{
Name = list.Name,
IconUrl = list.IconUrl ?? string.Empty,
EnableProxyDomainVMs = new(
list.Items!
.Where(i => i.ThreeStateEnable == true)
.Select(i => new ProxyDomainViewModel(i.Name, i.ProxyType, "https://" + i.ListenDomainNames.Split(";")[0],
i.Items?
.Select(c => new ProxyDomainViewModel(c.Name, c.ProxyType, "https://" + c.ListenDomainNames.Split(';')[0]))
.ToList()))
.ToList()),
})
.ToList();
EnableProxyDomainGroupVMs = enableGroupDomain.AsReadOnly();
});
StartProxyCommand = ReactiveCommand.CreateFromTask(async _ =>
{
#if LINUX
@@ -29,6 +54,7 @@ public sealed partial class AcceleratorPageViewModel
if (_initializeTime > DateTime.Now.AddSeconds(-2))
{
Toast.Show(ToastIcon.Warning, Strings.Warning_DoNotOperateFrequently);
return;
}
@@ -46,7 +72,7 @@ public sealed partial class AcceleratorPageViewModel
ProxySettingsCommand = ReactiveCommand.Create(() =>
{
var vm = new ProxySettingsWindowViewModel();
IWindowManager.Instance.ShowTaskDialogAsync(vm, vm.Title, pageContent: new ProxySettingsPage(), isOkButton: false);
_ = IWindowManager.Instance.ShowTaskDialogAsync(vm, vm.Title, pageContent: new ProxySettingsPage(), isOkButton: false);
});
if (IApplication.IsDesktop())
@@ -65,10 +91,15 @@ public sealed partial class AcceleratorPageViewModel
certificateManager.GetCerFilePathGeneratedWhenNoFileExists();
platformService.OpenFolder(certificateManager.PfxFilePath);
});
OpenLogFileCommand = ReactiveCommand.Create(() =>
{
platformService.OpenFolder(IPCSubProcessFileSystem.GetLogDirPath(Plugin.Instance.UniqueEnglishName));
});
}
}
#if LINUX
public bool EnvironmentCheck()
{
try
@@ -89,6 +120,7 @@ public sealed partial class AcceleratorPageViewModel
return false;
}
}
#endif
public void TrustCer_OnClick()
@@ -1,4 +1,3 @@
using BD.WTTS.Client.Resources;
using System.Reactive;
namespace BD.WTTS.UI.ViewModels;
@@ -7,6 +6,9 @@ public sealed partial class AcceleratorPageViewModel : TabItemViewModel
{
public override string Name => Strings.Welcome;
[Reactive]
public ReadOnlyCollection<ProxyDomainGroupViewModel>? EnableProxyDomainGroupVMs { get; set; }
public ICommand StartProxyCommand { get; }
public ICommand RefreshCommand { get; }
@@ -30,4 +32,6 @@ public sealed partial class AcceleratorPageViewModel : TabItemViewModel
public ICommand? ProxySettingsCommand { get; }
public ICommand? TrustCerCommand { get; }
}
public ICommand? OpenLogFileCommand { get; }
}
@@ -9,17 +9,20 @@ public sealed class AllGamesListPageViewModel : WindowViewModel
{
XunYouGames = new ObservableCollection<XunYouGameViewModel>();
Task2.InBackground(() =>
if (XunYouSDK.IsSupported)
{
var games = XunYouSDK.GetAllGames();
if (games != null)
Task2.InBackground(() =>
{
XunYouGames.Clear();
foreach (var game in games)
var games = XunYouSDK.GetAllGames();
if (games != null)
{
XunYouGames.Add(new XunYouGameViewModel(game));
XunYouGames.Clear();
foreach (var game in games)
{
XunYouGames.Add(new XunYouGameViewModel(game));
}
}
}
});
});
}
}
}
@@ -0,0 +1,202 @@
using STUN.Enums;
using STUN.StunResult;
using System.Reactive;
namespace BD.WTTS.UI.ViewModels;
public partial class NetworkCheckControlViewModel : ViewModelBase
{
[GeneratedRegex(DomainValidationAttribute.RegPattern)]
private static partial Regex DomainRegExp();
public class DomainValidationAttribute : RegularExpressionAttribute
{
public const string RegPattern = @"^[^/]+\.[^/]{2,}$";
public override string FormatErrorMessage(string name) => "请填入不带分隔符 \"/\" 的域名";
public DomainValidationAttribute() : base(RegPattern)
{
}
}
private readonly INetworkTestService _networkTestService = INetworkTestService.Instance;
public record NATFetchResult(string PublicEndPoint, string LocalEndPoint, string NATLevel, string NATTypeTip, bool PingResult);
public string DefaultTestDomain { get; } = "store.steampowered.com";
[Reactive]
public string SelectedSTUNAddress { get; set; }
public string[] STUNAddress { get; } =
[
"stun.syncthing.net",
"stun.hot-chilli.net",
"stun.fitauto.ru",
"stun.miwifi.com",
];
[ObservableAsProperty]
public string NATLevel { get; } = string.Empty;
[ObservableAsProperty]
public string NATTypeTip { get; } = string.Empty;
[ObservableAsProperty]
public string LocalEndPoint { get; } = string.Empty;
[ObservableAsProperty]
public string PublicEndPoint { get; } = string.Empty;
[ObservableAsProperty]
public bool PingOkVisible { get; }
[ObservableAsProperty]
public bool PingErrorVisible { get; }
[ObservableAsProperty]
public bool IsNATChecking { get; }
[ObservableAsProperty]
public bool IsDNSChecking { get; }
[ObservableAsProperty]
public bool IsIPv6Checking { get; }
[DomainValidation]
[Reactive]
public string DomainPendingTest { get; set; } = string.Empty;
[Reactive]
public string DNSTestDelay { get; set; } = string.Empty;
[Reactive]
public string DNSTestResult { get; set; } = string.Empty;
[Reactive]
public string IPv6Address { get; set; } = string.Empty;
[Reactive]
public bool IsSupportIPv6 { get; set; }
public ReactiveCommand<Unit, NATFetchResult> NATCheckCommand { get; }
public ReactiveCommand<Unit, Unit> DNSCheckCommand { get; }
public ReactiveCommand<Unit, Unit> IPv6CheckCommand { get; }
public NetworkCheckControlViewModel()
{
SelectedSTUNAddress = STUNAddress[0];
NATCheckCommand = ReactiveCommand.CreateFromTask(async () =>
{
var natCheckResult = await _networkTestService.TestStunClient3489Async(testServerHostName: SelectedSTUNAddress) ?? new ClassicStunResult { NatType = NatType.Unknown };
var (netSucc, _) = await _networkTestService.TestOpenUrlAsync("https://www.baidu.com");
var publicEndPoint = natCheckResult.PublicEndPoint?.Address.ToString() ?? "Unknown";
var localEndPoint = natCheckResult.LocalEndPoint?.Address.ToString() ?? "Unknown";
var (natLevel, natTypeTip) = natCheckResult.NatType switch
{
// Open
NatType.OpenInternet or NatType.FullCone => ("开放 NAT", "您可与在其网络上具有任意 NAT 类型的用户玩多人游戏和发起多人游戏。"),
// Moderate
NatType.RestrictedCone or NatType.PortRestrictedCone or NatType.SymmetricUdpFirewall => ("中等 NAT", "您可与一些用户玩多人游戏;但是,并且通常你将不会被选为比赛的主持人。"),
// Strict
NatType.Symmetric or NatType.UdpBlocked => ("严格 NAT", "您只能与具有开放 NAT 类型的用户玩多人游戏。您不能被选为比赛的主持人。"),
// Unknown
NatType.Unknown or NatType.UnsupportedServer or _ => ("不可用 NAT", "如果 NAT 不可用,您将无法使用群聊天或连接到某些 Xbox 游戏的多人游戏。"),
};
return new NATFetchResult(publicEndPoint, localEndPoint, natLevel, natTypeTip, netSucc);
});
NATCheckCommand
.IsExecuting
.ToPropertyEx(this, x => x.IsNATChecking);
NATCheckCommand
.Select(x => x.PublicEndPoint)
.ToPropertyEx(this, x => x.PublicEndPoint);
NATCheckCommand
.Select(x => x.LocalEndPoint)
.ToPropertyEx(this, x => x.LocalEndPoint);
NATCheckCommand
.Select(x => x.NATLevel)
.Merge(Observable.Return("未知"))
.ToPropertyEx(this, x => x.NATLevel);
NATCheckCommand
.Select(x => x.NATTypeTip)
.Merge(Observable.Return("未知类型"))
.ToPropertyEx(this, x => x.NATTypeTip);
var hidePingResultStream = NATCheckCommand
.IsExecuting
.Where(x => x == true)
.Select(x => false);
NATCheckCommand
.Select(x => x.PingResult == true)
.Merge(hidePingResultStream)
.ToPropertyEx(this, x => x.PingOkVisible);
NATCheckCommand
.Select(x => x.PingResult == false)
.Merge(hidePingResultStream)
.ToPropertyEx(this, x => x.PingErrorVisible);
var canDNSCheck = this.WhenAnyValue(x => x.DomainPendingTest)
.Select(domain => domain == string.Empty || DomainRegExp().IsMatch(domain));
DNSCheckCommand = ReactiveCommand.CreateFromTask(async () =>
{
var testDomain = DomainPendingTest == string.Empty ? DefaultTestDomain : DomainPendingTest;
try
{
long delayMs;
IPAddress[] address;
if (ProxySettings.UseDoh)
{
var configDoh = ProxySettings.CustomDohAddres2.Value ?? ProxySettingsWindowViewModel.DohAddress.FirstOrDefault() ?? string.Empty;
(delayMs, address) = await _networkTestService.TestDNSOverHttpsAsync(testDomain, configDoh);
}
else
{
var configDns = ProxySettings.ProxyMasterDns.Value ?? string.Empty;
(delayMs, address) = await _networkTestService.TestDNSAsync(testDomain, configDns, 53);
}
if (address.Length == 0)
throw new Exception("Parsing failed. Return empty ip address.");
DNSTestDelay = delayMs + "ms ";
DNSTestResult = string.Empty + address.FirstOrDefault();
}
catch (Exception ex)
{
Log.Error(nameof(AcceleratorPageViewModel), ex.ToString());
DNSTestDelay = string.Empty;
DNSTestResult = "error";
}
}, canDNSCheck);
DNSCheckCommand
.IsExecuting
.ToPropertyEx(this, x => x.IsDNSChecking);
IPv6CheckCommand = ReactiveCommand.CreateFromTask(async () =>
{
var result = await IMicroServiceClient.Instance.Accelerate.GetMyIP(ipV6: true);
if (result.IsSuccess)
{
IsSupportIPv6 = true;
IPv6Address = result.Content ?? string.Empty;
}
else
{
IsSupportIPv6 = false;
IPv6Address = string.Empty;
}
});
IPv6CheckCommand
.IsExecuting
.ToPropertyEx(this, x => x.IsIPv6Checking);
IPv6CheckCommand.Execute().Subscribe();
}
}
@@ -11,10 +11,10 @@ public sealed class ProxySettingsWindowViewModel : WindowViewModel
Title = DisplayName;
}
public IEnumerable<ExternalProxyType> ProxyTypes { get; }
= Enum2.GetAll<ExternalProxyType>();
//public IEnumerable<ExternalProxyType> ProxyTypes { get; }
// = Enum2.GetAll<ExternalProxyType>();
public IEnumerable<string> ProxyDNSs { get; } = new[]
public static IEnumerable<string> ProxyDNSs { get; } = new[]
{
"System Default",
PrimaryDNS_114,
@@ -31,7 +31,7 @@ public sealed class ProxySettingsWindowViewModel : WindowViewModel
"127.0.0.1",
};
public IEnumerable<string> DohAddress { get; }
public static IEnumerable<string> DohAddress { get; }
= new[] {
Dnspod_DohAddres,
Dnspod_DohAddres2,
@@ -44,4 +44,22 @@ public sealed class ProxySettingsWindowViewModel : WindowViewModel
DohAddres_360,
TUNA_DohAddres,
};
}
public void ResetSettings()
{
// 更改多个设置项不立即保存
ProxySettings.SystemProxyIp.Reset(save: false);
ProxySettings.ProxyMasterDns.Reset(save: false);
ProxySettings.SystemProxyPortId.Reset(save: false);
ProxySettings.ProgramStartupRunProxy.Reset(save: false);
ProxySettings.EnableHttpProxyToHttps.Reset(save: false);
ProxySettings.UseDoh.Reset(save: false);
ProxySettings.CustomDohAddres2.Reset(save: false);
ProxySettings.OnlyEnableProxyScript.Reset(save: false);
// 更改完成后保存一次
ProxySettings.OnlyEnableProxyScript.Save();
Toast.Show(ToastIcon.Success, "重置成功");
}
}
@@ -10,7 +10,14 @@
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<StackPanel Spacing="6">
<StackPanel Spacing="15">
<TextBlock FontSize="21" TextAlignment="Center">
<Run Text="下载 Watt加速器" />
<LineBreak />
<Run Text="拒绝游戏延迟,提高稳定性" />
</TextBlock>
<ui:SettingsExpander
Description="{Binding Source={x:Static ms:GameAcceleratorSettings.WattAcceleratorDirPath}, Path=Value}"
Header="安装路径"
@@ -22,6 +29,16 @@
<Button Click="SelectWattAcceleratorInstallPath" Content="选择路径" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<TextBlock Text="需要下载 Watt 加速器插件才可使用,确定要下载吗?" />
<!--<TextBlock Text="需要下载 Watt 加速器插件才可使用,确定要下载吗?" />-->
<Button
Margin="0,15"
Padding="20,10"
HorizontalAlignment="Center"
Classes="TransparentAccent"
Click="OKButton_Click"
Content="立即下载"
FontSize="14" />
</StackPanel>
</UserControl>
@@ -1,9 +1,10 @@
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.ReactiveUI;
namespace BD.WTTS.UI.Views.Controls;
public partial class AcceleratorPathAskBox : UserControl
public partial class AcceleratorPathAskBox : ReactiveUserControl<MessageBoxWindowViewModel>
{
public AcceleratorPathAskBox()
{
@@ -30,4 +31,9 @@ public partial class AcceleratorPathAskBox : UserControl
GameAcceleratorSettings.WattAcceleratorDirPath.Value = Path.Combine(path, "WattAccelerator");
}
}
private void OKButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
this.ViewModel?.Close?.Invoke(true);
}
}
@@ -0,0 +1,94 @@
<UserControl
x:Class="BD.WTTS.UI.Views.Pages.AcceleratorServiceStatus"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:enum="using:BD.WTTS.Enums"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:spp="https://steampp.net/ui"
xmlns:ui="using:FluentAvalonia.UI.Controls"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="spp:AcceleratorPageViewModel"
mc:Ignorable="d">
<ScrollViewer
Padding="0,0,10,0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding EnableProxyDomainGroupVMs}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="{StaticResource SettingsSpacing}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="spp:ProxyDomainGroupViewModel">
<Border>
<ui:SettingsExpander IsClickEnabled="False" IsExpanded="True">
<ui:SettingsExpander.Header>
<StackPanel Orientation="Horizontal" Spacing="10">
<spp:Image2
Width="30"
Height="30"
VerticalAlignment="Center"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding IconUrl}"
Stretch="UniformToFill" />
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
</StackPanel>
</ui:SettingsExpander.Header>
<ui:SettingsExpander.Footer>
<Button
HorizontalAlignment="Right"
Click="ConnectTestButtonOnClick"
Command="{Binding ConnectTestCommand}"
Content="连通性测试"
DockPanel.Dock="Right" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem Theme="{StaticResource SettingsExpanderItem2}">
<TreeView ItemsSource="{Binding EnableProxyDomainVMs}" SelectionMode="Single">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<DockPanel Margin="8,0">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
<Border
Padding="5,0"
CornerRadius="5"
IsVisible="{Binding ProxyType, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:ProxyType.ServerAccelerate}}"
ToolTip.Tip="Beta 测试加速项,不稳定且随时可能调整">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Offset="0.0" Color="#43cbff" />
<GradientStop Offset="1.0" Color="#9708cc" />
</LinearGradientBrush>
</Border.Background>
<TextBlock
VerticalAlignment="Center"
FontSize="10"
Text="Beta" />
</Border>
</StackPanel>
<TextBlock
Margin="0,0,4,0"
HorizontalAlignment="Right"
DockPanel.Dock="Right"
Foreground="{Binding DelayColor}"
Text="{Binding DelayMillseconds}"
ToolTip.ShowDelay="0"
ToolTip.Tip="此数值代表测试网页打开访问所需时间,请以实际浏览器访问网页状况为准" />
</DockPanel>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,25 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using FluentAvalonia.UI.Controls;
namespace BD.WTTS.UI.Views.Pages;
public partial class AcceleratorServiceStatus : UserControl
{
public AcceleratorServiceStatus()
{
InitializeComponent();
}
private void ConnectTestButtonOnClick(object? sender, RoutedEventArgs e)
{
if (sender is Button button)
{
var expander = button.FindAncestorOfType<SettingsExpander>();
if (expander != null) expander.IsExpanded = true;
}
}
}
@@ -177,6 +177,15 @@
FontSize="18"
IsVisible="{Binding ShowStart}" />
<Button
Width="175"
Height="42"
Classes="accent"
Command="{Binding ShowXunYouWindow, Source={x:Static s:GameAcceleratorService.Current}}"
CommandParameter="True"
Content="{x:Static spp:Strings.GameAccelerator_ShowWattAccelerator}"
FontSize="18" />
<Button
Width="150"
Height="42"
@@ -0,0 +1,373 @@
<UserControl
x:Class="BD.WTTS.UI.Views.Pages.NetworkCheck"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ms="https://steampp.net/settings"
xmlns:s="https://steampp.net/services"
xmlns:spp="https://steampp.net/ui"
xmlns:sys="using:System"
xmlns:ui="using:FluentAvalonia.UI.Controls"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="spp:NetworkCheckControlViewModel"
mc:Ignorable="d">
<ScrollViewer
Padding="0,0,10,0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8">
<!-- NAT -->
<ui:SettingsExpander Header="NAT 类型检测" IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource FontFamily="Segoe Fluent Icons" Glyph="&#xE969;" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<WrapPanel>
<ui:FAComboBox
Margin="0,0,4,4"
IsEditable="False"
IsEnabled="{Binding !IsNATChecking}"
ItemsSource="{Binding STUNAddress}"
SelectedItem="{Binding SelectedSTUNAddress, Mode=TwoWay}"
ToolTip.Tip="STUN 服务器地址">
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="sys:String">
<TextBlock Text="{Binding}" />
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
<Button
x:Name="NATCheckButton"
Width="100"
Margin="0,0,0,4"
Command="{Binding NATCheckCommand}"
Content="检测"
IsEnabled="True" />
</WrapPanel>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem Content="NAT 类型:">
<ui:SettingsExpanderItem.Footer>
<Grid>
<ui:ProgressRing
Width="22"
Margin="0,-4,0,-10"
HorizontalAlignment="Right"
IsVisible="{Binding IsNATChecking}" />
<DockPanel IsVisible="{Binding !IsNATChecking}">
<SelectableTextBlock
x:Name="NATTextBlock"
VerticalAlignment="Center"
Foreground="Gray"
Text="{Binding NATLevel}" />
<ui:FontIcon
Margin="3,0"
FontSize="20"
Glyph="&#xE946;"
ToolTip.ShowDelay="0">
<ToolTip.Tip>
<TextBlock x:Name="NATTypeTip" Text="{Binding NATTypeTip}" />
</ToolTip.Tip>
</ui:FontIcon>
</DockPanel>
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="本机IP地址:">
<ui:SettingsExpanderItem.Footer>
<Grid>
<ui:ProgressRing
Width="22"
Margin="0,-4,0,-10"
HorizontalAlignment="Right"
IsVisible="{Binding IsNATChecking}" />
<SelectableTextBlock
Foreground="Gray"
IsVisible="{Binding !IsNATChecking}"
Text="{Binding LocalEndPoint}" />
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<TextBlock
VerticalAlignment="Center"
Text="对外IP地址:"
ToolTip.Tip="若与您路由器 WAN IP 地址相同,则为公共网络" />
<ui:SettingsExpanderItem.Footer>
<Grid>
<ui:ProgressRing
Width="22"
Margin="0,-4,0,-10"
HorizontalAlignment="Right"
IsVisible="{Binding IsNATChecking}" />
<SelectableTextBlock
Foreground="Gray"
IsVisible="{Binding !IsNATChecking}"
Text="{Binding PublicEndPoint}" />
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="互联网连通情况:">
<ui:SettingsExpanderItem.Footer>
<Grid>
<ui:ProgressRing
Width="22"
Margin="0,-4,0,-10"
HorizontalAlignment="Right"
IsVisible="{Binding IsNATChecking}" />
<ui:FontIcon
x:Name="PingOK"
HorizontalAlignment="Right"
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="26"
Foreground="Green"
Glyph="&#xe73d;"
IsVisible="{Binding PingOkVisible}" />
<Grid x:Name="PingError" IsVisible="{Binding PingErrorVisible}">
<ui:FontIcon
Margin="3"
HorizontalAlignment="Right"
FontFamily="Segoe Fluent Icons"
FontSize="20"
Foreground="Red"
Glyph="&#xf127;" />
<ui:FontIcon
HorizontalAlignment="Right"
FontSize="26"
Foreground="White"
Glyph="&#xea39;" />
</Grid>
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<StackPanel Spacing="8">
<TextBlock
Margin="-28,0,0,0"
VerticalAlignment="Center"
FontWeight="Bold"
Text="相关链接" />
<WrapPanel Margin="-10,0,0,0">
<HyperlinkButton
VerticalAlignment="Center"
Content="XBOX 关于 NAT 文档"
NavigateUri="https://support.xbox.com/zh-CN/help/hardware-network/connect-network/xbox-one-nat-error" />
<HyperlinkButton
VerticalAlignment="Center"
Content="PlayStation 关于 NAT 文档"
NavigateUri="https://manuals.playstation.net/document/ct/ps4/settings/nw_test.html" />
<HyperlinkButton
VerticalAlignment="Center"
Content="Nintendo Switch 关于 NAT 文档"
NavigateUri="https://www.nintendoswitch.com.cn/support/faq/373.html" />
<HyperlinkButton
VerticalAlignment="Center"
Content="Bungie 关于 NAT 文档"
NavigateUri="https://help.bungie.net/hc/zh-cn/articles/360049496751-%E9%AB%98%E7%BA%A7%E7%96%91%E9%9A%BE%E6%8E%92%E8%A7%A3-%E9%80%9A%E7%94%A8%E5%8D%B3%E6%8F%92%E5%8D%B3%E7%94%A8-%E7%AB%AF%E5%8F%A3%E8%BD%AC%E9%80%81%E4%B8%8E%E7%BD%91%E7%BB%9C%E5%9C%B0%E5%9D%80%E8%BD%AC%E6%8D%A2%E7%B1%BB%E5%9E%8B" />
<HyperlinkButton
VerticalAlignment="Center"
Content="Activision 关于 NAT 文档"
NavigateUri="https://support.activision.com/tw/zh/articles/port-forward-and-nat-faq" />
<HyperlinkButton
VerticalAlignment="Center"
Content="RockStar Games 关于 NAT 文档"
NavigateUri="https://support.rockstargames.com/tw/articles/206210548/%E5%A6%82%E4%BD%95%E8%A7%A3%E6%B1%BA-GTA-%E7%B7%9A%E4%B8%8A%E6%A8%A1%E5%BC%8F%E4%B8%AD%E8%88%87%E5%9A%B4%E6%A0%BC-Strict-NAT-%E9%A1%9E%E5%9E%8B%E7%9B%B8%E9%97%9C%E7%9A%84%E9%8C%AF%E8%AA%A4" />
<HyperlinkButton
VerticalAlignment="Center"
Content="迅游关于 NAT 文档"
NavigateUri="https://cs.xunyou.com/html/282/15271.shtml" />
</WrapPanel>
</StackPanel>
</ui:SettingsExpander>
<!-- DNS -->
<ui:SettingsExpander Header="DNS 检测" IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource FontFamily="Segoe Fluent Icons" Glyph="&#xEDA3;" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<Button
Width="100"
Command="{Binding DNSCheckCommand}"
Content="检测" />
</ui:SettingsExpander.Footer>
<TextBlock
Margin="-28,0,0,-4"
VerticalAlignment="Center"
FontWeight="Bold"
Text="检测项目" />
<ui:SettingsExpanderItem Content="待检测的域名">
<ui:SettingsExpanderItem.Footer>
<TextBox
Width="220"
Text="{Binding DomainPendingTest, Mode=TwoWay}"
Watermark="{Binding DefaultTestDomain}">
<TextBox.Styles>
<Style Selector="DataValidationErrors">
<Setter Property="Template">
<ControlTemplate>
<DockPanel LastChildFill="True">
<ContentControl
Content="{Binding (DataValidationErrors.Errors)}"
ContentTemplate="{TemplateBinding ErrorTemplate}"
DataContext="{TemplateBinding Owner}"
DockPanel.Dock="Right"
IsVisible="{Binding (DataValidationErrors.HasErrors)}" />
<ContentPresenter
Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
CornerRadius="{TemplateBinding CornerRadius}" />
</DockPanel>
</ControlTemplate>
</Setter>
<Setter Property="ErrorTemplate">
<DataTemplate x:DataType="{x:Type x:Object}">
<Canvas
Width="14"
Height="14"
Margin="4,0,1,0"
Background="Transparent">
<ToolTip.Tip>
<ItemsControl ItemsSource="{Binding}" />
</ToolTip.Tip>
<Path
Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2"
Stroke="Red"
StrokeThickness="2" />
</Canvas>
</DataTemplate>
</Setter>
</Style>
</TextBox.Styles>
</TextBox>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{Binding Path=Res.Settings_Proxy_CustomDNS, Mode=OneWay, Source={x:Static s:ResourceService.Current}}">
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox
Width="220"
IsEditable="True"
IsEnabled="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=!Value}"
ItemsSource="{Binding Source={x:Static spp:ProxySettingsWindowViewModel.ProxyDNSs}}"
Text="{Binding Source={x:Static ms:ProxySettings.ProxyMasterDns}, Path=Value, Mode=TwoWay}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{Binding Path=Res.CommunityFix_EnableUseDoh, Mode=OneWay, Source={x:Static s:ResourceService.Current}}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch Classes="LeftContent" IsChecked="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=Value, Mode=TwoWay}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<TextBlock Text="{Binding Path=Res.CommunityFix_CustomDohAddres, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox
Width="220"
IsEditable="True"
IsEnabled="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=Value}"
ItemsSource="{Binding Source={x:Static spp:ProxySettingsWindowViewModel.DohAddress}}"
PlaceholderText="{Binding Source={x:Static s:IDnsAnalysisService.Dnspod_DohAddres2}, Mode=OneWay}"
Text="{Binding Source={x:Static ms:ProxySettings.CustomDohAddres2}, Path=Value, Mode=TwoWay}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<TextBlock
Margin="-28,0,0,-4"
VerticalAlignment="Center"
FontWeight="Bold"
Text="测试结果" />
<ui:SettingsExpanderItem Content="延迟">
<ui:SettingsExpanderItem.Footer>
<Grid>
<ui:ProgressRing
Width="22"
Margin="0,-4,0,-10"
IsVisible="{Binding IsDNSChecking}" />
<SelectableTextBlock IsVisible="{Binding !IsDNSChecking}" Text="{Binding DNSTestDelay}" />
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="解析地址">
<ui:SettingsExpanderItem.Footer>
<Grid>
<ui:ProgressRing
Width="22"
Margin="0,-4,0,-10"
IsVisible="{Binding IsDNSChecking}" />
<SelectableTextBlock IsVisible="{Binding !IsDNSChecking}" Text="{Binding DNSTestResult}" />
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- IPV6 -->
<ui:SettingsExpander Header="IPv6 检测" IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource FontFamily="Segoe Fluent Icons" Glyph="&#xE968;" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<Button
Width="100"
Command="{Binding IPv6CheckCommand}"
Content="检测" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem Content="支持情况">
<ui:SettingsExpanderItem.Footer>
<Grid>
<ui:ProgressRing
Width="22"
Margin="0,-4,0,-10"
IsVisible="{Binding IsIPv6Checking}" />
<Grid IsVisible="{Binding !IsIPv6Checking}">
<TextBlock
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
IsVisible="{Binding IsSupportIPv6}"
Text="已支持 IPv6 访问" />
<TextBlock
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
IsVisible="{Binding !IsSupportIPv6}"
Text="不支持 IPv6 访问" />
</Grid>
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="IPv6 地址">
<ui:SettingsExpanderItem.Footer>
<Grid>
<ui:ProgressRing
Width="22"
Margin="0,-4,0,-10"
IsVisible="{Binding IsIPv6Checking}" />
<SelectableTextBlock
Foreground="Gray"
IsVisible="{Binding !IsIPv6Checking}"
Text="{Binding IPv6Address}" />
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace BD.WTTS.UI.Views.Pages;
public partial class NetworkCheck : UserControl
{
public NetworkCheck()
{
InitializeComponent();
}
}
@@ -1,14 +1,10 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Threading;
using Avalonia.VisualTree;
using LiveChartsCore;
using LiveChartsCore.Motion;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Avalonia;
using LiveChartsCore.SkiaSharpView.Drawing;
using SkiaSharp;
namespace BD.WTTS.UI.Views.Controls;
@@ -0,0 +1,20 @@
<UserControl
x:Class="BD.WTTS.UI.Views.Controls.ProxyLog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvaloniaEdit="using:AvaloniaEdit"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:s="https://steampp.net/services"
xmlns:spp="https://steampp.net/ui"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{DynamicResource ControlCornerRadius}">
<AvaloniaEdit:TextEditor
x:Name="LogTextbox"
HorizontalScrollBarVisibility="Disabled"
IsReadOnly="True"
ShowLineNumbers="True" />
</Border>
</UserControl>

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