mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-26 03:59:34 +08:00
Compare commits
260
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4573a9b494 | ||
|
|
8f29d7b8ea | ||
|
|
7156d90967 | ||
|
|
ec726e8c48 | ||
|
|
488403a86b | ||
|
|
59474d68cd | ||
|
|
5ab308bb9b | ||
|
|
bf3d6ad778 | ||
|
|
59356f210e | ||
|
|
42df02f718 | ||
|
|
8728126421 | ||
|
|
e0c36a9545 | ||
|
|
9a2bd1a6cc | ||
|
|
499edb402c | ||
|
|
14216ab98f | ||
|
|
95c10dacd4 | ||
|
|
2883405ace | ||
|
|
75e5b6e897 | ||
|
|
66f3aa22d6 | ||
|
|
7930120e0b | ||
|
|
aa44b137c4 | ||
|
|
103918060a | ||
|
|
0644bbf924 | ||
|
|
3853181d8a | ||
|
|
de694024bb | ||
|
|
6ceaeb0710 | ||
|
|
71f104b947 | ||
|
|
a9e913434a | ||
|
|
f1ffae11aa | ||
|
|
7f7fde20bd | ||
|
|
2e97c2edc2 | ||
|
|
69b8590881 | ||
|
|
7b9ba5e8a0 | ||
|
|
d6ab3c7f03 | ||
|
|
12d062d4de | ||
|
|
f27d3c9111 | ||
|
|
6ab74d449e | ||
|
|
ceed57c0f2 | ||
|
|
8ca203f34b | ||
|
|
d4de20aa96 | ||
|
|
c94dec40a1 | ||
|
|
b829114c4f | ||
|
|
a3a32bf5fa | ||
|
|
faa7cc5b13 | ||
|
|
1e506f9930 | ||
|
|
217449ba54 | ||
|
|
55b79c4d4e | ||
|
|
a6c5fed203 | ||
|
|
b7e31d1c9c | ||
|
|
1d0b16895f | ||
|
|
65d73c2e50 | ||
|
|
83c390e446 | ||
|
|
31f5fa5ea6 | ||
|
|
f42c7cbd30 | ||
|
|
3cb592ce7e | ||
|
|
d5fa02e9a4 | ||
|
|
cbb2fac6ea | ||
|
|
35747253b8 | ||
|
|
17c45fdd65 | ||
|
|
88f02e3faf | ||
|
|
35a0c4d05f | ||
|
|
ca7fa0bc97 | ||
|
|
6a3c8f6187 | ||
|
|
24bb4bd625 | ||
|
|
a3811a97a0 | ||
|
|
dc6723f2e5 | ||
|
|
e14f169cb0 | ||
|
|
235a1abf34 | ||
|
|
c5edf0a8b5 | ||
|
|
c0b6d9d8e2 | ||
|
|
eefd2fa705 | ||
|
|
1a4a5887f2 | ||
|
|
5a163ae124 | ||
|
|
95c5a9e143 | ||
|
|
94f47b0165 | ||
|
|
08beb69865 | ||
|
|
0e9def7e6a | ||
|
|
6a307d97ab | ||
|
|
252eaa74c2 | ||
|
|
4115216a41 | ||
|
|
a99e410196 | ||
|
|
4154b68c79 | ||
|
|
fa20e3a945 | ||
|
|
a15a166b5c | ||
|
|
0fcd736259 | ||
|
|
925c735557 | ||
|
|
0505158281 | ||
|
|
19651ddeb1 | ||
|
|
1fe7986e08 | ||
|
|
a469e29438 | ||
|
|
ec028a4a5c | ||
|
|
6ebbeb000f | ||
|
|
e3cf3195a8 | ||
|
|
1734a03d41 | ||
|
|
7c47f01a77 | ||
|
|
dd622fb5e9 | ||
|
|
59636940c5 | ||
|
|
c7922c915b | ||
|
|
b2031998d8 | ||
|
|
7d3a74daf2 | ||
|
|
dccce87293 | ||
|
|
442ad2dac7 | ||
|
|
ae9a6350a7 | ||
|
|
d49e38f314 | ||
|
|
5244009b02 | ||
|
|
eef7478585 | ||
|
|
3eae8a8c84 | ||
|
|
c8509e94cf | ||
|
|
7140fc79c9 | ||
|
|
7d25393d6e | ||
|
|
149c5bbfd3 | ||
|
|
38485f1e24 | ||
|
|
008ab5fd4f | ||
|
|
178c074eb6 | ||
|
|
c3279e78c2 | ||
|
|
7c8f8448af | ||
|
|
8affd6a3f5 | ||
|
|
66fdfb728e | ||
|
|
9261344a3c | ||
|
|
923f2eaf94 | ||
|
|
95190efa5c | ||
|
|
dd71972adc | ||
|
|
3a48c00ec6 | ||
|
|
e3f40a3e40 | ||
|
|
6c1e6d40e9 | ||
|
|
187c7f3b58 | ||
|
|
4e3c86d009 | ||
|
|
ed5cc929d3 | ||
|
|
213b2c2644 | ||
|
|
e1adf07789 | ||
|
|
83f0634863 | ||
|
|
f08122769f | ||
|
|
49fdeadfd2 | ||
|
|
bca80245bb | ||
|
|
0f6c127ad3 | ||
|
|
c087a02303 | ||
|
|
6c84a876ff | ||
|
|
df27068329 | ||
|
|
f10df31edd | ||
|
|
902919cf98 | ||
|
|
a6aec5ad62 | ||
|
|
2fd651fd25 | ||
|
|
67615028e1 | ||
|
|
1eca159e23 | ||
|
|
d1d9ca82df | ||
|
|
88bd2dbfe9 | ||
|
|
ae2b2970db | ||
|
|
67209f3861 | ||
|
|
110c92d909 | ||
|
|
cdc773ba74 | ||
|
|
8ea8b73aab | ||
|
|
0614e172b8 | ||
|
|
4c73a5b622 | ||
|
|
34341172b8 | ||
|
|
6813ed9ece | ||
|
|
3eb916e78a | ||
|
|
7424d05f54 | ||
|
|
76829b0ca2 | ||
|
|
bbe98f68f6 | ||
|
|
96025c9ef5 | ||
|
|
2fb12f1b7f | ||
|
|
542b528b93 | ||
|
|
0a4326924d | ||
|
|
e8a9990354 | ||
|
|
4e72f9ccf9 | ||
|
|
6df6ed93cb | ||
|
|
605beeb918 | ||
|
|
2bdfbefda9 | ||
|
|
6747549cbc | ||
|
|
0d4c0b27f7 | ||
|
|
c501066a3d | ||
|
|
57a6b24dad | ||
|
|
143011fbd0 | ||
|
|
11bc9cd440 | ||
|
|
72e9068571 | ||
|
|
5842d5f80f | ||
|
|
bc19a02f4f | ||
|
|
d59d63d57e | ||
|
|
9ad6e8fa8d | ||
|
|
45ef15b220 | ||
|
|
25dfd1f888 | ||
|
|
0495cd3079 | ||
|
|
ea9c45d78b | ||
|
|
4427fd1a5f | ||
|
|
edf3c3a72c | ||
|
|
55490d2351 | ||
|
|
17ae6655b4 | ||
|
|
342d25deac | ||
|
|
f5616dac2e | ||
|
|
28914c53f6 | ||
|
|
f0cbe8ef88 | ||
|
|
e0b45bf8af | ||
|
|
854fe420fd | ||
|
|
c95ce1087a | ||
|
|
14ae662675 | ||
|
|
1594bd250e | ||
|
|
c83ab4fb08 | ||
|
|
e690454e0c | ||
|
|
27b4f4bb10 | ||
|
|
70315c3a99 | ||
|
|
b8e49a5332 | ||
|
|
1374dc52b8 | ||
|
|
150f93e7b7 | ||
|
|
d1aea71f67 | ||
|
|
37a522ec7f | ||
|
|
8b816b9787 | ||
|
|
4f487a5ca8 | ||
|
|
6c7a547a83 | ||
|
|
bde5751d69 | ||
|
|
23544b6c39 | ||
|
|
2c60a9fb69 | ||
|
|
4ce67c8f1a | ||
|
|
e8a1a6153e | ||
|
|
452fa2ac84 | ||
|
|
60e6d71d48 | ||
|
|
ae0e2349e7 | ||
|
|
3a0f2a4950 | ||
|
|
5263863c23 | ||
|
|
c52f3c3e00 | ||
|
|
be61df890e | ||
|
|
6a7c9113a0 | ||
|
|
ac953af7a1 | ||
|
|
147a6b911f | ||
|
|
972054ca7c | ||
|
|
0cea809b40 | ||
|
|
7ec34e59a7 | ||
|
|
00d41dfbc8 | ||
|
|
0272a34e5c | ||
|
|
d450753c92 | ||
|
|
1e6263c241 | ||
|
|
a4602d29a7 | ||
|
|
7623788991 | ||
|
|
8075183be1 | ||
|
|
058f1a9bf2 | ||
|
|
196b7f0f5e | ||
|
|
99d24b2513 | ||
|
|
7e3fb6cf06 | ||
|
|
a0a0dd9bc7 | ||
|
|
81f75e61ca | ||
|
|
11890c68a8 | ||
|
|
be8621d4d5 | ||
|
|
511c84b2ba | ||
|
|
9e75da9e46 | ||
|
|
547b360db8 | ||
|
|
9be21a8e0a | ||
|
|
efbb41bf3f | ||
|
|
c2220359dd | ||
|
|
a788afdeeb | ||
|
|
b543a889a0 | ||
|
|
478950b75f | ||
|
|
27b4114d7d | ||
|
|
6ead1f3666 | ||
|
|
22a015d8b9 | ||
|
|
f5b7cd6db8 | ||
|
|
5a397d1311 | ||
|
|
0c1bedaeb1 | ||
|
|
e959dcc017 | ||
|
|
41610453a0 | ||
|
|
adf3e0ca9d | ||
|
|
21e80a5b7b |
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
[](https://learn.microsoft.com/zh-cn/dotnet/core/whats-new/dotnet-7)
|
||||
[](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-11)
|
||||
|
||||
[](https://afdian.net/@rmbgame)
|
||||
[](https://afdian.com/@rmbgame)
|
||||
[](https://ko-fi.com/rmbgame)
|
||||
[](https://www.patreon.com/rmbgame)
|
||||
|
||||
|
||||
@@ -172,17 +172,36 @@ else
|
||||
fi
|
||||
chmod +x "$base_path/$exec_name.sh"
|
||||
# xdg-icon-resource install "$base_path/Icons/Watt-Toolkit.png" --size 128 Watt-Toolkit
|
||||
#添加桌面文件
|
||||
rm -rf "$HOME/Desktop/Watt Toolkit.desktop" 2>/dev/null
|
||||
echo "#!/usr/bin/env xdg-open
|
||||
InitDesktop() {
|
||||
while true; do
|
||||
# 使用 zenity 提示用户选择安装路径或使用默认路径
|
||||
choice=$(zenity --list --radiolist --title="请选择要添加到的位置" --column="选择" --column="路径" TRUE "$HOME/.local/share/applications/" FALSE "$HOME/Desktop")
|
||||
|
||||
# 检查用户输入
|
||||
if [ "$choice" == "$HOME/.local/share/applications/" ]; then
|
||||
target_dir="$HOME/.local/share/applications/"
|
||||
break
|
||||
elif [ "$choice" == "$HOME/Desktop" ]; then
|
||||
target_dir="$HOME/Desktop/"
|
||||
break
|
||||
else
|
||||
echo "无效选项,请输入 1 或 2。"
|
||||
fi
|
||||
done
|
||||
#添加桌面文件
|
||||
rm -rf "$target_dir/Watt Toolkit.desktop" 2>/dev/null
|
||||
echo "#!/usr/bin/env xdg-open
|
||||
[Desktop Entry]
|
||||
Name=Watt Toolkit
|
||||
Exec=$base_path/$exec_name.sh
|
||||
Icon=$base_path/Icons/Watt-Toolkit.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
StartupNotify=false" >"$HOME/Desktop/Watt Toolkit.desktop"
|
||||
chmod +x "$HOME/Desktop/Watt Toolkit.desktop"
|
||||
StartupNotify=false" >"$target_dir/Watt Toolkit.desktop"
|
||||
chmod +x "$target_dir/Watt Toolkit.desktop"
|
||||
|
||||
}
|
||||
InitDesktop
|
||||
# update-desktop-database ~/.local/share/applications
|
||||
#运行程序
|
||||
Show_Run "下载安装完成,是否启动程序?"
|
||||
|
||||
+116
-10
@@ -1,7 +1,44 @@
|
||||
#!/bin/bash
|
||||
base_path="$HOME/WattToolkit"
|
||||
mkdir -p "$base_path"
|
||||
cd "$base_path" || exit 1
|
||||
# 设置默认的 base_path 参数
|
||||
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 提示用户选择安装路径或使用默认路径
|
||||
custom_base_path=$(zenity --entry --title="安装路径" --text="请输入安装路径(默认为 $default_base_path,不输入则使用默认路径)")
|
||||
|
||||
# 如果用户提供了自定义路径,则使用该路径
|
||||
if [ -n "$custom_base_path" ]; then
|
||||
base_path="$custom_base_path"
|
||||
break
|
||||
elif [ -z "$custom_base_path" ]; then
|
||||
# 用户直接回车,使用默认路径
|
||||
break
|
||||
else
|
||||
# 用户输入无效路径,重新提示
|
||||
echo "无效路径,请重新输入。"
|
||||
fi
|
||||
done
|
||||
|
||||
# 创建安装路径
|
||||
mkdir -p "$base_path" || {
|
||||
echo "无法创建安装路径 $base_path,请检查权限或路径是否正确。"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 切换到安装路径
|
||||
cd "$base_path" || {
|
||||
echo "无法切换到安装路径 $base_path,请检查权限或路径是否正确。"
|
||||
exit 1
|
||||
}
|
||||
|
||||
appVer_path="$base_path/WattToolkit.AppVer"
|
||||
exec_name="Steam++"
|
||||
tar_name="WattToolkit.tgz"
|
||||
@@ -10,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
|
||||
@@ -182,7 +257,9 @@ Get_NewVer() {
|
||||
|
||||
sleep 1
|
||||
#本地版本 Hash
|
||||
o_sha384=$(cat "AppVer")
|
||||
if [ -f "AppVer" ]; then
|
||||
o_sha384=$(cat "AppVer")
|
||||
fi
|
||||
if [ -e "AppVer" ]; then
|
||||
if [ "${o_sha384,,}" = "${n_sha384,,}" ]; then
|
||||
Show_Run "已是最新版本,是否启动程序?"
|
||||
@@ -326,17 +403,46 @@ Kill_Process
|
||||
#解压
|
||||
Decompression
|
||||
# xdg-icon-resource install "$base_path/Icons/Watt-Toolkit.png" --size 128 Watt-Toolkit
|
||||
#添加桌面文件
|
||||
rm -rf "$HOME/Desktop/Watt Toolkit.desktop" 2>/dev/null
|
||||
echo "#!/usr/bin/env xdg-open
|
||||
InitDesktop() {
|
||||
# 检查XDG_DESKTOP_DIR环境变量,如果未设置则使用默认值
|
||||
XDG_DESKTOP_DIR="${XDG_DESKTOP_DIR:-$HOME/Desktop}"
|
||||
|
||||
while true; do
|
||||
# 使用 zenity 提示用户选择安装路径或使用默认路径
|
||||
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" == "$XDG_DESKTOP_DIR" ]; then
|
||||
target_dir="$XDG_DESKTOP_DIR"
|
||||
break
|
||||
else
|
||||
# 无效选项时给出提示,并继续循环
|
||||
zenity --info --text="无效选项,请重新选择。"
|
||||
fi
|
||||
done
|
||||
|
||||
# 添加桌面文件
|
||||
rm -rf "$target_dir/Watt Toolkit.desktop" 2>/dev/null
|
||||
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" >"$HOME/Desktop/Watt Toolkit.desktop"
|
||||
chmod +x "$HOME/Desktop/Watt Toolkit.desktop"
|
||||
StartupNotify=false
|
||||
EOT
|
||||
chmod +x "$target_dir/Watt Toolkit.desktop"
|
||||
}
|
||||
|
||||
|
||||
InitDesktop
|
||||
# update-desktop-database ~/.local/share/applications
|
||||
#运行程序
|
||||
Show_Run "下载安装完成,是否启动程序?"
|
||||
|
||||
@@ -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": ""
|
||||
|
||||
+1
-1
Submodule ref/Avalonia.Image2 updated: c4162f332f...e685ce072c
+1
-1
Submodule ref/Common updated: 4fd40563b7...9b75e1c591
@@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<CheckEolWorkloads>false</CheckEolWorkloads>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
+1
-1
Submodule ref/DirectoryPackages updated: 6974b6215e...7a43b4afd3
Submodule ref/SteamAchievementManager updated: 4c578c1cc4...39dbdc9678
+1
-1
Submodule ref/SteamClient updated: 079a2c22ef...b449d0d434
Submodule ref/WTTS.MicroServices.ClientSDK updated: 29a5a63dcb...cb3580aa1f
+1
-1
Submodule ref/WinAuth updated: d3f45cffe2...4e698bc59d
Binary file not shown.
@@ -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);
|
||||
}
|
||||
@@ -16,14 +16,14 @@ public static partial class AssemblyInfo
|
||||
/// <summary>
|
||||
/// 预览版本号,范围 1~9,只增不减
|
||||
/// </summary>
|
||||
const string ver_for_preview = "7";
|
||||
const string ver_for_preview = "9";
|
||||
|
||||
/// <summary>
|
||||
/// RC 版本号,范围 0~9,只增不减
|
||||
/// </summary>
|
||||
const string ver_for_rc = "7";
|
||||
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>
|
||||
|
||||
@@ -38,9 +38,9 @@ public interface IFontManagerImpl2 : IFontManagerImpl
|
||||
return Impl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
|
||||
}
|
||||
|
||||
bool IFontManagerImpl.TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface)
|
||||
bool IFontManagerImpl.TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface)
|
||||
{
|
||||
return Impl.TryCreateGlyphTypeface(stream, out glyphTypeface);
|
||||
return Impl.TryCreateGlyphTypeface(stream, fontSimulations, out glyphTypeface);
|
||||
}
|
||||
|
||||
//IGlyphTypeface IFontManagerImpl.CreateGlyphTypeface(Typeface typeface)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
@@ -8,70 +8,76 @@
|
||||
xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"
|
||||
IgnorableNamespaces="uap rescap desktop desktop2">
|
||||
|
||||
<Identity
|
||||
Name="4651ED44255E.47979655102CE"
|
||||
Publisher="CN=A90E406B-B2D3-4A23-B061-0FA1D65C4F66"
|
||||
Version="3.0.207.0" />
|
||||
<Identity
|
||||
Name="4651ED44255E.47979655102CE"
|
||||
Publisher="CN=A90E406B-B2D3-4A23-B061-0FA1D65C4F66"
|
||||
Version="3.0.207.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Watt Toolkit</DisplayName>
|
||||
<PublisherDisplayName>软妹币玩家</PublisherDisplayName>
|
||||
<Logo>Images\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
<Properties>
|
||||
<DisplayName>Watt Toolkit</DisplayName>
|
||||
<PublisherDisplayName>软妹币玩家</PublisherDisplayName>
|
||||
<Logo>Images\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="Watt Toolkit"
|
||||
Description="「Watt Toolkit」是一个开源跨平台的多功能游戏工具箱,此工具的大部分功能都是需要您下载安装 Steam 才能使用。"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png" ShortName="Steam++">
|
||||
<uap:ShowNameOnTiles>
|
||||
<uap:ShowOn Tile="square150x150Logo"/>
|
||||
<uap:ShowOn Tile="wide310x150Logo"/>
|
||||
<uap:ShowOn Tile="square310x310Logo"/>
|
||||
</uap:ShowNameOnTiles>
|
||||
</uap:DefaultTile >
|
||||
<uap:SplashScreen Image="Images\SplashScreen.png" />
|
||||
<uap:InitialRotationPreference>
|
||||
<uap:Rotation Preference="landscape"/>
|
||||
</uap:InitialRotationPreference>
|
||||
<uap:LockScreen BadgeLogo="Images\BadgeLogo.png" Notification="badgeAndTileText"/>
|
||||
</uap:VisualElements>
|
||||
<Extensions>
|
||||
<desktop:Extension Category="windows.fullTrustProcess" Executable="Steam++\Steam++.exe" />
|
||||
<desktop:Extension Category="windows.startupTask" Executable="Steam++\Steam++.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<desktop:StartupTask TaskId="BootAutoStartTask" Enabled="true" DisplayName="Steam++ System Boot Run" />
|
||||
</desktop:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="Watt Toolkit"
|
||||
Description="「Watt Toolkit」是一个开源跨平台的多功能游戏工具箱,此工具的大部分功能都是需要您下载安装 Steam 才能使用。"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png" ShortName="Steam++">
|
||||
<uap:ShowNameOnTiles>
|
||||
<uap:ShowOn Tile="square150x150Logo"/>
|
||||
<uap:ShowOn Tile="wide310x150Logo"/>
|
||||
<uap:ShowOn Tile="square310x310Logo"/>
|
||||
</uap:ShowNameOnTiles>
|
||||
</uap:DefaultTile >
|
||||
<uap:SplashScreen Image="Images\SplashScreen.png" />
|
||||
<uap:InitialRotationPreference>
|
||||
<uap:Rotation Preference="landscape"/>
|
||||
</uap:InitialRotationPreference>
|
||||
<uap:LockScreen BadgeLogo="Images\BadgeLogo.png" Notification="badgeAndTileText"/>
|
||||
</uap:VisualElements>
|
||||
<Extensions>
|
||||
<desktop:Extension Category="windows.fullTrustProcess" Executable="Steam++\Steam++.exe" />
|
||||
<desktop:Extension Category="windows.startupTask" Executable="Steam++\Steam++.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<desktop:StartupTask TaskId="BootAutoStartTask" Enabled="true" DisplayName="Steam++ System Boot Run" />
|
||||
</desktop:Extension>
|
||||
<uap:Extension Category="windows.protocol">
|
||||
<uap:Protocol Name="spp">
|
||||
<uap:Logo>Images\Square44x44Logo.png</uap:Logo>
|
||||
<uap:DisplayName>Steam++ URI Scheme</uap:DisplayName>
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Extensions>
|
||||
<desktop2:Extension Category="windows.firewallRules">
|
||||
<desktop2:FirewallRules Executable="Steam++\modules\Accelerator\Steam++.Accelerator.exe">
|
||||
<desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all"/>
|
||||
<desktop2:Rule Direction="in" IPProtocol="UDP" Profile="all"/>
|
||||
</desktop2:FirewallRules>
|
||||
</desktop2:Extension>
|
||||
</Extensions>
|
||||
<Extensions>
|
||||
<desktop2:Extension Category="windows.firewallRules">
|
||||
<desktop2:FirewallRules Executable="Steam++\modules\Accelerator\Steam++.Accelerator.exe">
|
||||
<desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all"/>
|
||||
<desktop2:Rule Direction="in" IPProtocol="UDP" Profile="all"/>
|
||||
</desktop2:FirewallRules>
|
||||
</desktop2:Extension>
|
||||
</Extensions>
|
||||
|
||||
<Capabilities>
|
||||
<Capability Name="internetClient" />
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<rescap:Capability Name="allowElevation" />
|
||||
</Capabilities>
|
||||
<Capabilities>
|
||||
<Capability Name="internetClient" />
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<rescap:Capability Name="allowElevation" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
|
||||
@@ -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>
|
||||
|
||||
-3
@@ -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" />
|
||||
@@ -130,6 +130,9 @@
|
||||
<Compile Update="UI\Views\Controls\Widgets\CarouselItems.cs">
|
||||
<DependentUpon>%(Filename)</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UI\Views\Pages\PluginStorePage.axaml.cs">
|
||||
<DependentUpon>PluginStorePage.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\TFM_NETX_WITH_DESKTOP.props" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
namespace BD.WTTS.Converters;
|
||||
|
||||
public sealed class WaveProgressValueConverter : IValueConverter
|
||||
{
|
||||
public static readonly WaveProgressValueConverter Instance = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not double i) return 0;
|
||||
return 155 - (i * 2.1);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return BindingOperations.DoNothing;
|
||||
}
|
||||
}
|
||||
|
||||
public class WaveProgressValueColorConverter : IValueConverter
|
||||
{
|
||||
public static readonly WaveProgressValueColorConverter Instance = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not double d) return Brushes.Black;
|
||||
if (d > 50) return Brushes.GhostWhite;
|
||||
return Application.Current?.ActualThemeVariant == ThemeVariant.Dark ? Brushes.GhostWhite : Brushes.Black;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return BindingOperations.DoNothing;
|
||||
}
|
||||
}
|
||||
|
||||
public class WaveProgressValueTextConverter : IValueConverter
|
||||
{
|
||||
public static readonly WaveProgressValueTextConverter Instance = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return value is not double d ? "0%" : $"{d:#0}%";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return BindingOperations.DoNothing;
|
||||
}
|
||||
}
|
||||
|
||||
public class WaveProgressGradientOffsetConverter : IValueConverter
|
||||
{
|
||||
public static readonly WaveProgressGradientOffsetConverter Instance = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not double v)
|
||||
return Brushes.Blue;
|
||||
|
||||
var app = Application.Current;
|
||||
if (app == null)
|
||||
return null;
|
||||
|
||||
Color primaryColor = Colors.DodgerBlue;
|
||||
Color accentColor = Colors.Transparent;
|
||||
|
||||
try
|
||||
{
|
||||
if (app.FindResource("SystemAccentColor") is Color primaryColor2)
|
||||
{
|
||||
primaryColor = primaryColor2;
|
||||
}
|
||||
if (app.FindResource("SystemAccentColorLight2") is Color accentColor2)
|
||||
{
|
||||
accentColor = accentColor2;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
var isLight = app.RequestedThemeVariant == ThemeVariant.Light;
|
||||
|
||||
v /= 100;
|
||||
v += isLight ? 0.2 : 0.4;
|
||||
if (v > 1)
|
||||
v = 1;
|
||||
|
||||
return new LinearGradientBrush()
|
||||
{
|
||||
EndPoint = new RelativePoint(0.5, 1, RelativeUnit.Relative),
|
||||
StartPoint = new RelativePoint(0.5, 0, RelativeUnit.Relative),
|
||||
GradientStops = new GradientStops()
|
||||
{
|
||||
new GradientStop() { Color = primaryColor, Offset = 0 },
|
||||
new GradientStop() { Color = isLight ? Colors.Transparent : accentColor, Offset = v },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return BindingOperations.DoNothing;
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
+7
-1
@@ -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)
|
||||
|
||||
+7
-5
@@ -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);
|
||||
|
||||
@@ -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(() =>
|
||||
@@ -125,12 +165,20 @@ partial class App
|
||||
{
|
||||
subMenu = new NativeMenu();
|
||||
foreach (var sub in item.Value.Items)
|
||||
subMenu.Add(new NativeMenuItem
|
||||
{
|
||||
var menu = new NativeMenuItem
|
||||
{
|
||||
Header = sub.Name,
|
||||
Command = sub.Command,
|
||||
CommandParameter = sub.CommandParameter,
|
||||
});
|
||||
};
|
||||
|
||||
if (sub.IsVisible is Avalonia.Data.IBinding v)
|
||||
menu[!NativeMenuItem.IsVisibleProperty] = v;
|
||||
if (sub.IsEnabled is Avalonia.Data.IBinding e)
|
||||
menu[!NativeMenuItem.IsEnabledProperty] = e;
|
||||
subMenu.Add(menu);
|
||||
}
|
||||
menus.Add(new NativeMenuItem
|
||||
{
|
||||
Header = item.Value.Name,
|
||||
|
||||
@@ -11,6 +11,12 @@ public sealed partial class App : Application
|
||||
{
|
||||
const string TAG = "AvaApp";
|
||||
|
||||
public App()
|
||||
{
|
||||
OpenBrowserCommand = ReactiveCommand.Create<object?>(OpenBrowserCommandCore);
|
||||
CopyToClipboardCommand = ReactiveCommand.Create<object?>(CopyToClipboardCommandCore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前主窗口
|
||||
/// </summary>
|
||||
@@ -105,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)
|
||||
{
|
||||
@@ -225,16 +237,32 @@ public sealed partial class App : Application
|
||||
|
||||
public static FontFamily DefaultFontFamily => _DefaultFontFamily.Value;
|
||||
|
||||
public void OpenBrowserCommand(object url)
|
||||
public ICommand OpenBrowserCommand { get; }
|
||||
|
||||
public async void OpenBrowserCommandCore(object? url)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (url == null || string.IsNullOrEmpty(url.ToString()))
|
||||
var urlString = url?.ToString();
|
||||
if (string.IsNullOrEmpty(urlString))
|
||||
{
|
||||
Toast.Show(ToastIcon.Warning, "打开链接失败");
|
||||
return;
|
||||
}
|
||||
Browser2.Open(url.ToString());
|
||||
try
|
||||
{
|
||||
Uri uri = new(urlString);
|
||||
if (uri.Host.EndsWith("steampp.net", StringComparison.OrdinalIgnoreCase) ||
|
||||
uri.Host.EndsWith("mossimo.net", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await UserService.Current.OpenAuthUrl(urlString);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
Browser2.Open(urlString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -242,7 +270,9 @@ public sealed partial class App : Application
|
||||
}
|
||||
}
|
||||
|
||||
public async void CopyToClipboardCommand(object text)
|
||||
public ICommand CopyToClipboardCommand { get; }
|
||||
|
||||
public async void CopyToClipboardCommandCore(object? text)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -6,8 +6,8 @@
|
||||
x:CompileBindings="True">
|
||||
<Styles.Resources>
|
||||
<ResourceDictionary>
|
||||
<sys:Double x:Key="TitleBarHeight">60</sys:Double>
|
||||
<Thickness x:Key="TitleBarAfterMargin">0,60,0,0</Thickness>
|
||||
<sys:Double x:Key="TitleBarHeight">50</sys:Double>
|
||||
<Thickness x:Key="TitleBarAfterMargin">0,50,0,0</Thickness>
|
||||
<sys:Double x:Key="SplashScreenImageWidth">100</sys:Double>
|
||||
|
||||
<Thickness x:Key="PageMargin">10</Thickness>
|
||||
@@ -66,6 +66,7 @@
|
||||
<StyleInclude Source="/UI/Styling/Controls/IconElement.axaml" />
|
||||
<StyleInclude Source="/UI/Styling/Controls/CustomFilePicker.axaml" />
|
||||
<StyleInclude Source="/UI/Styling/Controls/NumericUpDown.axaml" />
|
||||
<StyleInclude Source="/UI/Styling/Controls/Skeleton.axaml" />
|
||||
|
||||
<Style Selector="FlyoutPresenter.NoPadding">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -7,8 +7,13 @@
|
||||
<Border Padding="20">
|
||||
<!-- Add Controls for Previewer Here -->
|
||||
<StackPanel Spacing="10">
|
||||
<ui:HyperlinkButton Content="更新日志" />
|
||||
<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,10 +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>
|
||||
|
||||
@@ -18,27 +18,15 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{TemplateBinding IsLoading}"
|
||||
Spacing="8">
|
||||
<spp:Image2
|
||||
Spacing="10">
|
||||
<!--<spp:Image2
|
||||
Width="100"
|
||||
Height="100"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
Source="avares://BD.WTTS.Client.Avalonia/UI/Assets/mona-loading-default.gif"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Binding Path=Res.GameList_IsLoading, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
Theme="{StaticResource SubtitleTextBlockStyle}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsVisible="{TemplateBinding CustomLoadingText,
|
||||
Converter={StaticResource IsNullConverter},
|
||||
ConverterParameter=invert}"
|
||||
Text="{TemplateBinding CustomLoadingText}"
|
||||
Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
Stretch="UniformToFill" />-->
|
||||
<Border
|
||||
Width="35"
|
||||
Height="35"
|
||||
@@ -51,6 +39,18 @@
|
||||
Minimum="{TemplateBinding Minimum}"
|
||||
Value="{TemplateBinding ProgressValue}" />
|
||||
</Border>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Binding Path=Res.GameList_IsLoading, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
Theme="{StaticResource SubtitleTextBlockStyle}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsVisible="{TemplateBinding CustomLoadingText,
|
||||
Converter={StaticResource IsNullConverter},
|
||||
ConverterParameter=invert}"
|
||||
Text="{TemplateBinding CustomLoadingText}"
|
||||
Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<!--<TextBlock
|
||||
@@ -101,5 +101,8 @@
|
||||
</Panel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:loading /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Effect" Value="blur(5)" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -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}" />
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<Styles
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
x:CompileBindings="True">
|
||||
<Design.PreviewWith>
|
||||
<Border Padding="20">
|
||||
<!-- Add Controls for Previewer Here -->
|
||||
<StackPanel>
|
||||
<ToggleSwitch Name="loading" Content="Loading" />
|
||||
<ToggleSwitch Name="active" Content="Active" />
|
||||
<Grid
|
||||
MinWidth="400"
|
||||
HorizontalAlignment="Left"
|
||||
ColumnDefinitions="auto,*">
|
||||
<spp:Skeleton
|
||||
Width="100"
|
||||
Height="100"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="2"
|
||||
IsActive="{Binding #active.IsChecked}"
|
||||
IsLoading="{Binding #loading.IsChecked}">
|
||||
<Image RenderOptions.BitmapInterpolationMode="HighQuality" Source="avares://BD.WTTS.Client.Avalonia/UI/Assets/ApplicationIcon.ico" />
|
||||
</spp:Skeleton>
|
||||
<UniformGrid
|
||||
Grid.Column="1"
|
||||
Margin="5,0"
|
||||
Columns="1"
|
||||
Rows="3">
|
||||
<spp:Skeleton
|
||||
Margin="0,0,200,0"
|
||||
VerticalAlignment="Top"
|
||||
CornerRadius="2"
|
||||
IsActive="{Binding #active.IsChecked}"
|
||||
IsLoading="{Binding #loading.IsChecked}">
|
||||
<SelectableTextBlock Margin="0,2" Text="是先有鸡还是先有蛋" />
|
||||
</spp:Skeleton>
|
||||
<spp:Skeleton
|
||||
Margin="0,0,100,0"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="2"
|
||||
IsActive="{Binding #active.IsChecked}"
|
||||
IsLoading="{Binding #loading.IsChecked}">
|
||||
<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="看山不是山,看水不是水,看山还是山,看水还是水" />
|
||||
</spp:Skeleton>
|
||||
</UniformGrid>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<Styles.Resources>
|
||||
<ControlTheme x:Key="{x:Type spp:Skeleton}" TargetType="spp:Skeleton">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="spp:Skeleton">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
ClipToBounds="{TemplateBinding ClipToBounds}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Panel>
|
||||
<ContentPresenter
|
||||
x:Name="PART_ContentPresenter"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
<spp:PureRectangle
|
||||
x:Name="PART_LoadingBorder"
|
||||
Background="{DynamicResource SkeletonDefaultBackground}"
|
||||
Classes.Active="{Binding Path=IsActive, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
IsHitTestVisible="{TemplateBinding IsLoading}"
|
||||
IsVisible="{TemplateBinding IsLoading}" />
|
||||
<spp:PureRectangle
|
||||
x:Name="PART_ActiveBorder"
|
||||
IsHitTestVisible="{TemplateBinding IsLoading}"
|
||||
IsVisible="{TemplateBinding IsLoading}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
</Styles.Resources>
|
||||
|
||||
<Style Selector="spp|Skeleton[IsActive=True][IsLoading=True] /template/ spp|PureRectangle#PART_ActiveBorder">
|
||||
<Style.Animations>
|
||||
<Animation
|
||||
Easing="CubicEaseInOut"
|
||||
FillMode="None"
|
||||
IterationCount="Infinite"
|
||||
PlaybackDirection="Alternate"
|
||||
Duration="0:0:1.4">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Background" Value="{DynamicResource SkeletonStartAnimationBackground}" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Background" Value="{DynamicResource SkeletonEndAnimationBackground}" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
</Styles>
|
||||
@@ -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>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<x:String x:Key="Back"></x:String>
|
||||
<x:String x:Key="Sort"></x:String>
|
||||
<x:String x:Key="Move"></x:String>
|
||||
<x:String x:Key="Device"></x:String>
|
||||
|
||||
|
||||
<ui:ImageIconSource x:Key="AppIcon" Source="avares://BD.WTTS.Client.Avalonia/UI/Assets/ApplicationIcon.ico" />
|
||||
@@ -85,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>
|
||||
|
||||
@@ -21,4 +21,16 @@
|
||||
</OnPlatform>
|
||||
</RenderOptions.BitmapInterpolationMode>-->
|
||||
</ImageBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="SkeletonStartAnimationBackground" StartPoint="0%,100%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#FF1C1F23" />
|
||||
<GradientStop Offset="0" Color="#FF2E3238" />
|
||||
<GradientStop Offset="1" Color="#FF1C1F23" />
|
||||
</LinearGradientBrush>
|
||||
<LinearGradientBrush x:Key="SkeletonEndAnimationBackground" StartPoint="0%,100%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#FF1C1F23" />
|
||||
<GradientStop Offset="1" Color="#FF2E3238" />
|
||||
<GradientStop Offset="1" Color="#FF1C1F23" />
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="SkeletonDefaultBackground" Color="#FF1C1F23" />
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -21,4 +21,16 @@
|
||||
</OnPlatform>
|
||||
</RenderOptions.BitmapInterpolationMode>-->
|
||||
</ImageBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="SkeletonStartAnimationBackground" StartPoint="0%,100%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#FFF9F9F9" />
|
||||
<GradientStop Offset="0" Color="#FFE6E8EA" />
|
||||
<GradientStop Offset="1" Color="#FFF9F9F9" />
|
||||
</LinearGradientBrush>
|
||||
<LinearGradientBrush x:Key="SkeletonEndAnimationBackground" StartPoint="0%,100%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#FFF9F9F9" />
|
||||
<GradientStop Offset="1" Color="#FFE6E8EA" />
|
||||
<GradientStop Offset="1" Color="#FFF9F9F9" />
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="SkeletonDefaultBackground" Color="#FFF9F9F9" />
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
<wnd:MinMaxCloseControl
|
||||
Name="SystemCaptionButtons"
|
||||
Height="{Binding TemplateSettings.TitleBarHeight, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Margin="18,0"
|
||||
Margin="14,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Avalonia.Markup.Xaml.MarkupExtensions;
|
||||
using Avalonia.Rendering.Composition;
|
||||
using FluentAvalonia.UI.Controls.Experimental;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Pages;
|
||||
|
||||
@@ -7,6 +9,8 @@ public class PageBase : UserControl
|
||||
{
|
||||
public PageBase() : base()
|
||||
{
|
||||
_disposables = new();
|
||||
|
||||
SizeChanged += PageBaseSizeChanged;
|
||||
|
||||
if (HeaderBackground == null)
|
||||
@@ -14,6 +18,8 @@ public class PageBase : UserControl
|
||||
this[!HeaderBackgroundProperty] = new DynamicResourceExtension("PageHeaderBackgroundBrush");
|
||||
}
|
||||
|
||||
_disposables.Add(UISettings.WindowBackgroundCustomImage.Subscribe(x => IsShowBackgroundImage = !x));
|
||||
|
||||
//AddHandler(Frame.NavigatingFromEvent, FrameNavigatingFrom, RoutingStrategies.Direct);
|
||||
//AddHandler(Frame.NavigatedToEvent, FrameNavigatedTo, RoutingStrategies.Direct);
|
||||
}
|
||||
@@ -139,6 +145,7 @@ public class PageBase : UserControl
|
||||
|
||||
//_previewImageHost = e.NameScope.Find<IconSourceElement>("PreviewImageElement");
|
||||
//_detailsHost = e.NameScope.Find<StackPanel>("DetailsTextHost");
|
||||
|
||||
_optionsHost = e.NameScope.Find<Panel>("OptionsRegion");
|
||||
_tabsHost = e.NameScope.Find<Border>("TabRegion");
|
||||
_detailsPanel = e.NameScope.Find<Panel>("PageDetails");
|
||||
@@ -157,6 +164,7 @@ public class PageBase : UserControl
|
||||
{
|
||||
base.OnUnloaded(e);
|
||||
_hasLoaded = false;
|
||||
_disposables.Dispose();
|
||||
}
|
||||
|
||||
private void PageBaseSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
@@ -266,33 +274,28 @@ public class PageBase : UserControl
|
||||
ec.ImplicitAnimations = ani;
|
||||
}
|
||||
|
||||
//private void FrameNavigatingFrom(object sender, NavigatingCancelEventArgs e)
|
||||
//private void FrameNavigatingFrom(object? sender, NavigatingCancelEventArgs e)
|
||||
//{
|
||||
// // If TargetType is not set, we know we're currently on a CoreControls page since those
|
||||
// // are grouped pages - whereas, FA controls only display one control per page and
|
||||
// // set all the extra properties
|
||||
// bool isFAControlPage = TargetType != null;
|
||||
|
||||
// // Only setup the ConnectedAnimation if it makes sense
|
||||
// if ((!isFAControlPage && e.SourcePageType == typeof(CoreControlsPageViewModel)) ||
|
||||
// (isFAControlPage && e.SourcePageType == typeof(FAControlsOverviewPageViewModel)))
|
||||
// if ((e.SourcePageType == typeof(HomePageViewModel)) ||
|
||||
// (e.SourcePageType == typeof(PluginStorePageViewModel)))
|
||||
// {
|
||||
// // Only setup the Back connected animation if we're going back to the
|
||||
// // controls list pages
|
||||
// var svc = ConnectedAnimationService.GetForView(TopLevel.GetTopLevel(this));
|
||||
// svc.PrepareToAnimate("BackAnimation", (Control)_previewImageHost.Parent);
|
||||
// NavigationService.Instance.PreviousPage = this;
|
||||
// //NavigationService.Instance.PreviousPage = this;
|
||||
// }
|
||||
//}
|
||||
|
||||
//private void FrameNavigatedTo(object sender, NavigationEventArgs e)
|
||||
//private void FrameNavigatedTo(object? sender, NavigationEventArgs e)
|
||||
//{
|
||||
// var svc = ConnectedAnimationService.GetForView(TopLevel.GetTopLevel(this));
|
||||
// var animation = svc.GetAnimation("ForwardAnimation");
|
||||
|
||||
// if (animation != null)
|
||||
// {
|
||||
// var coordinated = new List<Visual>
|
||||
// var coordinated = new List<Visual?>
|
||||
// {
|
||||
// _optionsHost,
|
||||
// _detailsHost,
|
||||
@@ -315,6 +318,7 @@ public class PageBase : UserControl
|
||||
//private IconSourceElement? _previewImageHost;
|
||||
//private StackPanel? _detailsHost;
|
||||
private ScrollViewer? _scroller;
|
||||
private CompositeDisposable _disposables;
|
||||
}
|
||||
|
||||
public class PageBase<TViewModel> : PageBase, IViewFor<TViewModel> where TViewModel : class
|
||||
@@ -324,13 +328,14 @@ public class PageBase<TViewModel> : PageBase, IViewFor<TViewModel> where TViewMo
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PageBase{TViewModel}"/> class.
|
||||
/// </summary>
|
||||
public PageBase()
|
||||
public PageBase() : base()
|
||||
{
|
||||
// This WhenActivated block calls ViewModel's WhenActivated
|
||||
// block if the ViewModel implements IActivatableViewModel.
|
||||
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
disposables.Add(UISettings.WindowBackgroundCustomImage.Subscribe(x => IsShowBackgroundImage = !x));
|
||||
//disposables.Add(UISettings.WindowBackgroundCustomImage.Subscribe(x => IsShowBackgroundImage = !x));
|
||||
});
|
||||
this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public class PureCircle : Control
|
||||
{
|
||||
public static readonly StyledProperty<double> DiameterProperty = AvaloniaProperty.Register<PureCircle, double>(
|
||||
nameof(Diameter));
|
||||
|
||||
public double Diameter
|
||||
{
|
||||
get => GetValue(DiameterProperty);
|
||||
set => SetValue(DiameterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?>
|
||||
BackgroundProperty = Border.BackgroundProperty.AddOwner<PureCircle>();
|
||||
|
||||
public IBrush? Background
|
||||
{
|
||||
get => GetValue(BackgroundProperty);
|
||||
set => SetValue(BackgroundProperty, value);
|
||||
}
|
||||
|
||||
static PureCircle()
|
||||
{
|
||||
AffectsMeasure<PureCircle>(DiameterProperty);
|
||||
AffectsArrange<PureCircle>(DiameterProperty);
|
||||
AffectsRender<PureCircle>(BackgroundProperty, DiameterProperty);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
double diameter = Math.Max(0, Diameter);
|
||||
return new Size(diameter, diameter);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
double diameter = Math.Max(0, Diameter);
|
||||
return new Size(diameter, diameter);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
double radius = Math.Max(0, Diameter) / 2;
|
||||
context.DrawEllipse(Background, null, new(radius, radius), radius, radius);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Provide the most simplified shape implementation. This is a rectangle with a background, without border and corner radius.
|
||||
/// </summary>
|
||||
public class PureRectangle : Control
|
||||
{
|
||||
public static readonly StyledProperty<IBrush?> BackgroundProperty =
|
||||
Border.BackgroundProperty.AddOwner<PureRectangle>();
|
||||
|
||||
public IBrush? Background
|
||||
{
|
||||
get => GetValue(BackgroundProperty);
|
||||
set => SetValue(BackgroundProperty, value);
|
||||
}
|
||||
|
||||
static PureRectangle()
|
||||
{
|
||||
AffectsRender<PureRectangle>(BackgroundProperty);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
base.Render(context);
|
||||
context.DrawRectangle(Background, null, new Rect(Bounds.Size));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public class PureRing : PureCircle
|
||||
{
|
||||
public static readonly StyledProperty<double> InnerDiameterProperty = AvaloniaProperty.Register<PureRing, double>(
|
||||
nameof(InnerDiameter));
|
||||
|
||||
public double InnerDiameter
|
||||
{
|
||||
get => GetValue(InnerDiameterProperty);
|
||||
set => SetValue(InnerDiameterProperty, value);
|
||||
}
|
||||
|
||||
static PureRing()
|
||||
{
|
||||
AffectsRender<PureCircle>(InnerDiameterProperty);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
double outer = Diameter / 2;
|
||||
double inner = InnerDiameter / 2;
|
||||
double thickness = outer - inner;
|
||||
context.DrawEllipse(null,
|
||||
new Pen(Background, outer - inner),
|
||||
new Point(outer, outer),
|
||||
outer - thickness / 2,
|
||||
outer - thickness / 2);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public class Skeleton : ContentControl
|
||||
{
|
||||
public static readonly StyledProperty<bool> IsActiveProperty =
|
||||
AvaloniaProperty.Register<Skeleton, bool>(nameof(IsActive));
|
||||
|
||||
public bool IsActive
|
||||
{
|
||||
get { return GetValue(IsActiveProperty); }
|
||||
set { SetValue(IsActiveProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsLoadingProperty =
|
||||
AvaloniaProperty.Register<Skeleton, bool>(nameof(IsLoading));
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => GetValue(IsLoadingProperty);
|
||||
set => SetValue(IsLoadingProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<UserControl
|
||||
x:Class="BD.WTTS.UI.Views.Controls.WaveProgress"
|
||||
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:spp="https://steampp.net/ui"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="spp|WaveProgress">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
Canvas.Left="20"
|
||||
Canvas.Top="20"
|
||||
Canvas.Right="20"
|
||||
Canvas.Bottom="20"
|
||||
Width="200"
|
||||
Height="200"
|
||||
Margin="20"
|
||||
Background="Transparent"
|
||||
BorderBrush="{DynamicResource ControlElevationBorderBrush}"
|
||||
BorderThickness="1.2"
|
||||
BoxShadow="{DynamicResource LowShadow}"
|
||||
ClipToBounds="False"
|
||||
CornerRadius="100">
|
||||
<Border.Resources>
|
||||
<spp:WaveProgressValueConverter x:Key="WValueConvert" />
|
||||
<spp:WaveProgressValueTextConverter x:Key="WValueTextConvert" />
|
||||
<spp:WaveProgressValueColorConverter x:Key="WValueColorConvert" />
|
||||
<spp:WaveProgressGradientOffsetConverter x:Key="WGradientConverter" />
|
||||
</Border.Resources>
|
||||
|
||||
|
||||
<Border
|
||||
Width="200"
|
||||
Height="200"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="100">
|
||||
<Panel>
|
||||
<Canvas Width="200" Height="200">
|
||||
<Path
|
||||
Canvas.Top="{TemplateBinding Value,
|
||||
Converter={StaticResource WValueConvert}}"
|
||||
Classes="WaveAnimatedFast"
|
||||
Data="M 0,50 H 700 V 350 H 0 M 0,50 C 25,55 75,45 100,50 S 175,55 200,50 S 275,45 300,50 S 375,55 400,50 S 475,45 500,50 S 575,55 600,50 S 675,45 700,50"
|
||||
Fill="{TemplateBinding Value,
|
||||
Converter={StaticResource WGradientConverter}}"
|
||||
Opacity="0.45"
|
||||
StrokeThickness="0">
|
||||
|
||||
<Path.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Canvas.Top" Duration="0:0:1.5">
|
||||
<DoubleTransition.Easing>
|
||||
<CircularEaseOut />
|
||||
</DoubleTransition.Easing>
|
||||
</DoubleTransition>
|
||||
</Transitions>
|
||||
</Path.Transitions>
|
||||
</Path>
|
||||
<Path
|
||||
Canvas.Top="{TemplateBinding Value,
|
||||
Converter={StaticResource WValueConvert}}"
|
||||
Classes="WaveAnimated"
|
||||
Data="M 0,50 H 700 V 350 H 0 M 0,50 C 25,55 75,45 100,50 S 175,55 200,50 S 275,45 300,50 S 375,55 400,50 S 475,45 500,50 S 575,55 600,50 S 675,45 700,50"
|
||||
Fill="{TemplateBinding Value,
|
||||
Converter={StaticResource WGradientConverter}}"
|
||||
Opacity="0.8"
|
||||
StrokeThickness="0">
|
||||
<Path.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Canvas.Top" Duration="0:0:2.5">
|
||||
<DoubleTransition.Easing>
|
||||
<CircularEaseOut />
|
||||
</DoubleTransition.Easing>
|
||||
</DoubleTransition>
|
||||
</Transitions>
|
||||
</Path.Transitions>
|
||||
</Path>
|
||||
</Canvas>
|
||||
<TextBlock
|
||||
Name="PART_Text"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{TemplateBinding Value,
|
||||
Converter={StaticResource WValueColorConvert}}"
|
||||
Text="{TemplateBinding Value,
|
||||
Converter={StaticResource WValueTextConvert}}"
|
||||
Theme="{StaticResource TitleTextBlockStyle}">
|
||||
<TextBlock.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.3" />
|
||||
</Transitions>
|
||||
</TextBlock.Transitions>
|
||||
</TextBlock>
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="spp|WaveProgress[IsTextVisible=True] /template/ TextBlock#PART_Text">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="spp|WaveProgress[IsTextVisible=False] /template/ TextBlock#PART_Text">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Path.WaveAnimated">
|
||||
<Style.Animations>
|
||||
<Animation IterationCount="INFINITE" Duration="0:0:6">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Canvas.Left" Value="-100" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Canvas.Left" Value="-500" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
<Style Selector="Path.WaveAnimatedFast">
|
||||
<Style.Animations>
|
||||
<Animation IterationCount="INFINITE" Duration="0:0:3">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Canvas.Left" Value="-100" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Canvas.Left" Value="-500" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public sealed partial class WaveProgress : UserControl
|
||||
{
|
||||
public static readonly DirectProperty<WaveProgress, double> ValueProperty =
|
||||
AvaloniaProperty.RegisterDirect<WaveProgress, double>(
|
||||
nameof(Value),
|
||||
o => o.Value,
|
||||
(o, v) => o.Value = v,
|
||||
defaultBindingMode: BindingMode.TwoWay,
|
||||
enableDataValidation: true);
|
||||
|
||||
public static readonly StyledProperty<bool> IsTextVisibleProperty =
|
||||
AvaloniaProperty.Register<WaveProgress, bool>(nameof(IsTextVisible), defaultValue: true);
|
||||
|
||||
public WaveProgress()
|
||||
{
|
||||
InitializeComponent();
|
||||
//var theme = App.Current.thm;
|
||||
//theme.OnBaseThemeChanged += _ =>
|
||||
//{
|
||||
// Value++;
|
||||
// Value--;
|
||||
//};
|
||||
//theme.OnColorThemeChanged += _ =>
|
||||
//{
|
||||
// Value++;
|
||||
// Value--;
|
||||
//};
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private double _value = 65;
|
||||
|
||||
public double Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (value is < 0 or > 100)
|
||||
return;
|
||||
|
||||
SetAndRaise(ValueProperty, ref _value, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsTextVisible
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetValue(IsTextVisibleProperty);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
SetValue(IsTextVisibleProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -149,7 +154,53 @@
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
<TextBlock Text="Test Control" />
|
||||
<StackPanel>
|
||||
<ToggleSwitch Name="loading" Content="Loading" />
|
||||
<ToggleSwitch Name="active" Content="Active" />
|
||||
<Grid
|
||||
MinWidth="400"
|
||||
HorizontalAlignment="Left"
|
||||
ColumnDefinitions="auto,*">
|
||||
<spp:Skeleton
|
||||
Width="100"
|
||||
Height="100"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="2"
|
||||
IsActive="{Binding #active.IsChecked}"
|
||||
IsLoading="{Binding #loading.IsChecked}">
|
||||
<Image RenderOptions.BitmapInterpolationMode="HighQuality" Source="avares://BD.WTTS.Client.Avalonia/UI/Assets/ApplicationIcon.ico" />
|
||||
</spp:Skeleton>
|
||||
<UniformGrid
|
||||
Grid.Column="1"
|
||||
Margin="5,0"
|
||||
Columns="1"
|
||||
Rows="3">
|
||||
<spp:Skeleton
|
||||
Margin="0,0,200,0"
|
||||
VerticalAlignment="Top"
|
||||
CornerRadius="2"
|
||||
IsActive="{Binding #active.IsChecked}"
|
||||
IsLoading="{Binding #loading.IsChecked}">
|
||||
<SelectableTextBlock Margin="0,2" Text="不忘初心,方得始终" />
|
||||
</spp:Skeleton>
|
||||
<spp:Skeleton
|
||||
Margin="0,0,100,0"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="2"
|
||||
IsActive="{Binding #active.IsChecked}"
|
||||
IsLoading="{Binding #loading.IsChecked}">
|
||||
<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="路漫漫其修远兮,吾将上下而求索。" />
|
||||
</spp:Skeleton>
|
||||
</UniformGrid>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Carousel>
|
||||
</spp:PageBase>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<Panel>
|
||||
<Image Source="/UI/Assets/back.png" Stretch="UniformToFill" />
|
||||
<!--<Image Source="/UI/Assets/back.png" Stretch="UniformToFill" />-->
|
||||
<!--<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -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"
|
||||
@@ -279,60 +284,61 @@
|
||||
ItemsSource="{Binding Shops}">
|
||||
<spp:CarouselItems.ItemTemplate>
|
||||
<DataTemplate x:DataType="spp:ShopRecommendGoodItem">
|
||||
<Border spp:Animations.EnableAnimations="True">
|
||||
<spp:AppItem
|
||||
Title="{Binding Name}"
|
||||
MaxWidth="260"
|
||||
Classes="Vertical Scale"
|
||||
ClickCommand="{Binding OpenBrowserCommand, Source={x:Static spp:App.Instance}}"
|
||||
Cursor="Hand"
|
||||
Tags="{Binding GoodsAllCategorysText}"
|
||||
ToolTip.Tip="{Binding Brief}">
|
||||
<spp:AppItem.ClickCommandParameter>
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<CompiledBinding Path="." Source="{x:Static spp:HomePageViewModel.WattGame_Goods_Detail_}" />
|
||||
<CompiledBinding Path="Id" />
|
||||
</MultiBinding>
|
||||
</spp:AppItem.ClickCommandParameter>
|
||||
<spp:AppItem.Image>
|
||||
<spp:Image2
|
||||
EnableCancelToken="False"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
Source="{Binding Image}" />
|
||||
</spp:AppItem.Image>
|
||||
<spp:AppItem.Status>
|
||||
<Border Classes="Status">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsVisible="{Binding !!DiscountValue}"
|
||||
Text="{Binding Mktprice, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}"
|
||||
TextDecorations="Strikethrough" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{Binding Price, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</spp:AppItem.Status>
|
||||
<spp:AppItem.ActionButton>
|
||||
<Border
|
||||
Height="24"
|
||||
Padding="8,0"
|
||||
Background="#4c6b22"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="4"
|
||||
IsVisible="{Binding !!DiscountValue}">
|
||||
<!--<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}}"
|
||||
Cursor="Hand"
|
||||
Tags="{Binding GoodsAllCategorysText}"
|
||||
ToolTip.Tip="{Binding Brief}">
|
||||
<spp:AppItem.ClickCommandParameter>
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<CompiledBinding Path="." Source="{x:Static spp:HomePageViewModel.WattGame_Goods_Detail_}" />
|
||||
<CompiledBinding Path="Id" />
|
||||
</MultiBinding>
|
||||
</spp:AppItem.ClickCommandParameter>
|
||||
<spp:AppItem.Image>
|
||||
<spp:Image2
|
||||
EnableCancelToken="False"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
Source="{Binding Image}" />
|
||||
</spp:AppItem.Image>
|
||||
<spp:AppItem.Status>
|
||||
<Border Classes="Status">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#beee11"
|
||||
Text="{Binding DiscountValue, StringFormat={}-{0}%}" />
|
||||
</Border>
|
||||
</spp:AppItem.ActionButton>
|
||||
</spp:AppItem>
|
||||
</Border>
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsVisible="{Binding !!DiscountValue}"
|
||||
Text="{Binding Mktprice, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}"
|
||||
TextDecorations="Strikethrough" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{Binding Price, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</spp:AppItem.Status>
|
||||
<spp:AppItem.ActionButton>
|
||||
<Border
|
||||
Height="24"
|
||||
Padding="8,0"
|
||||
Background="#4c6b22"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="4"
|
||||
IsVisible="{Binding !!DiscountValue}">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#beee11"
|
||||
Text="{Binding DiscountValue, StringFormat={}-{0}%}" />
|
||||
</Border>
|
||||
</spp:AppItem.ActionButton>
|
||||
</spp:AppItem>
|
||||
<!--</Border>-->
|
||||
</DataTemplate>
|
||||
</spp:CarouselItems.ItemTemplate>
|
||||
</spp:CarouselItems>
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NavigationView.AppNav[IsBackButtonVisible=False] SplitView /template/ ContentPresenter#PART_PanePresenter">
|
||||
<Setter Property="Margin" Value="0 50 0 0" />
|
||||
<Setter Property="Margin" Value="0 42 0 0" />
|
||||
</Style>
|
||||
<Style Selector="ui|NavigationView.AppNav[IsBackButtonVisible=True] SplitView /template/ ContentPresenter#PART_PanePresenter">
|
||||
<Setter Property="Margin" Value="0 10 0 0" />
|
||||
<Setter Property="Margin" Value="0 2 0 0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NavigationViewItem.AppNav">
|
||||
@@ -216,9 +216,23 @@
|
||||
|
||||
<StackPanel VerticalAlignment="Center" IsVisible="{Binding Path=User, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}">
|
||||
<TextBlock Text="{Binding Path=User.NickName, Mode=OneWay, Source={x:Static s:UserService.Current}}" />
|
||||
|
||||
<Border VerticalAlignment="Center" IsVisible="{Binding Path=User.UserType, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:UserType.Sponsor}}">
|
||||
<TextBlock FontSize="10" Text="{Binding Path=Res.User_Sponsor, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}❤️{0}}" />
|
||||
<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}}">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0.0" Color="#453a94" />
|
||||
<GradientStop Offset="1.0" Color="#f43b47" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="White"
|
||||
Text="{Binding Path=Res.User_Sponsor, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}❤️{0}}" />
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
@@ -263,18 +277,18 @@
|
||||
</MenuItem.Header>
|
||||
</MenuItem>
|
||||
<MenuItem Header="-" IsVisible="{Binding Path=User, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}" />
|
||||
<MenuItem
|
||||
<MenuItem
|
||||
Command="{Binding Path=OpenAuthOfficialCommand, Source={x:Static s:UserService.Current}}"
|
||||
CommandParameter="https://steampp.net/account/center/info"
|
||||
Header="{Binding Path=Res.PersonalCenter, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
IsVisible="{Binding Path=User, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}" />
|
||||
<!--<MenuItem Header="{Binding Path=Res.MyOrder, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" IsVisible="{Binding Path=User, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}" />-->
|
||||
<MenuItem
|
||||
<!--<MenuItem Header="{Binding Path=Res.MyOrder, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" IsVisible="{Binding Path=User, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}" />-->
|
||||
<MenuItem
|
||||
Command="{Binding Path=OpenAuthOfficialCommand, Source={x:Static s:UserService.Current}}"
|
||||
CommandParameter="https://steampp.net/account/center/bind"
|
||||
Header="{Binding Path=Res.BindThirdPartyAccount, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
IsVisible="{Binding Path=User, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}" />
|
||||
<MenuItem
|
||||
<MenuItem
|
||||
Command="{Binding Path=OpenAuthOfficialCommand, Source={x:Static s:UserService.Current}}"
|
||||
CommandParameter="https://steampp.net/account/center/safe"
|
||||
Header="{Binding Path=Res.PersonalSafe, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
@@ -318,7 +332,7 @@
|
||||
PaneDisplayMode="Left">
|
||||
<ui:NavigationView.Resources>
|
||||
<!-- Override this -->
|
||||
<Thickness x:Key="NavigationViewContentMargin">0,60,0,0</Thickness>
|
||||
<Thickness x:Key="NavigationViewContentMargin">0,50,0,0</Thickness>
|
||||
</ui:NavigationView.Resources>
|
||||
<ui:NavigationView.Styles>
|
||||
<Style Selector="Button#NavigationViewBackButton">
|
||||
@@ -329,7 +343,7 @@
|
||||
<Setter Property="Content" Value="" />
|
||||
</Style>
|
||||
<Style Selector="Grid#PaneToggleButtonGrid">
|
||||
<Setter Property="Margin" Value="5 5 0 0" />
|
||||
<Setter Property="Margin" Value="5 0 0 0" />
|
||||
<Setter Property="ToolTip.Tip" Value="{Binding Path=Res.Back, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Bottom" />
|
||||
</Style>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<UserControl
|
||||
x:Class="BD.WTTS.UI.Views.Pages.PluginStorePage"
|
||||
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:s="https://steampp.net/services"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="spp:PluginStorePageViewModel"
|
||||
mc:Ignorable="d">
|
||||
<ScrollViewer Padding="20">
|
||||
<!--<spp:ContentLoader IsLoading="False">-->
|
||||
<StackPanel
|
||||
x:Name="HomeRoot"
|
||||
MaxWidth="1080"
|
||||
Spacing="20">
|
||||
<TextBlock
|
||||
Margin="-3,0,0,0"
|
||||
Text="插件商店是什么?"
|
||||
Theme="{StaticResource TitleTextBlockStyle}" />
|
||||
|
||||
<TextBlock Text="3.0 版本的计划是将所有功能插件化,拆分后按需下载安装,每个人都可以只下载自己需要的插件功能使用,并且支持所有人都可以开发插件,等等还有许多暂时不方便说的内容,因为计划的过于异想天开,且遇到的问题有点多,所以开发难度有点点大,这个功能上线还需要一点时间,上线后 3.0 也将正式发布正式版,对一直等待的大家说声抱歉麻烦在多等等了 😭,如果您不习惯启动后是插件商店页面也可以在界面设置中修改启动默认页。" TextWrapping="WrapWithOverflow" />
|
||||
<TextBlock
|
||||
Margin="-3,0,0,0"
|
||||
Text="已安装功能"
|
||||
Theme="{StaticResource TitleTextBlockStyle}" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding MainWindow2.TabItems, Mode=OneWay, Source={x:Static s:IViewModelManager.Instance}}">
|
||||
<ItemsControl.Resources>
|
||||
<ControlTheme
|
||||
x:Key="NavButtonStyle"
|
||||
BasedOn="{StaticResource {x:Type Button}}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Width" Value="120" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="6" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
|
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="ClipToBounds" Value="True" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ItemsControl.Resources>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="spp:TabItemViewModel">
|
||||
<Border spp:Animations.EnableAnimations="True">
|
||||
<Button
|
||||
Command="{Binding $parent[spp:PluginStorePage].((spp:PluginStorePageViewModel)DataContext).NavgationToMenuPageCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Theme="{StaticResource NavButtonStyle}">
|
||||
<Grid RowDefinitions="3*,*">
|
||||
<Border
|
||||
Name="IconHost"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="4 4 0 0"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality">
|
||||
<spp:Image2
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0,10,0,0"
|
||||
Source="{Binding IconKey}"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="12"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,92 @@
|
||||
using FluentAvalonia.UI.Controls.Experimental;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Pages;
|
||||
|
||||
public partial class PluginStorePage : UserControl
|
||||
{
|
||||
public PluginStorePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.SetViewModel<PluginStorePageViewModel>(useCache: true);
|
||||
|
||||
// Use the frame events here to ensure ConnectedAnimations still work with
|
||||
// Back/Forward navigation and not just explicit page invokes
|
||||
//AddHandler(Frame.NavigatingFromEvent, OnNavigatingFrom, RoutingStrategies.Direct);
|
||||
//AddHandler(Frame.NavigatedToEvent, OnNavigatedTo, RoutingStrategies.Direct);
|
||||
|
||||
}
|
||||
|
||||
//private void PluginStorePage_Click(object? sender, RoutedEventArgs e)
|
||||
//{
|
||||
// if (sender != null && sender is Button b && b.DataContext is TabItemViewModel fci)
|
||||
// {
|
||||
// _animationPage = fci;
|
||||
// //NavigationService.Instance.NavigateFromContext(fci);
|
||||
// }
|
||||
//}
|
||||
|
||||
//private void OnNavigatedTo(object? sender, NavigationEventArgs e)
|
||||
//{
|
||||
// if (_animationPage == null)
|
||||
// return;
|
||||
|
||||
// var svc = ConnectedAnimationService.GetForView(TopLevel.GetTopLevel(this));
|
||||
// var anim = svc.GetAnimation("BackAnimation");
|
||||
|
||||
// if (anim == null)
|
||||
// return;
|
||||
|
||||
// var item = this.GetVisualDescendants()
|
||||
// ?.Where(x => x is Button && x.DataContext == _animationPage)
|
||||
// ?.FirstOrDefault()
|
||||
// ?.GetVisualDescendants()
|
||||
// .Where(x => x is Border && x.Name == "IconHost")
|
||||
// .FirstOrDefault();
|
||||
|
||||
// var presenter = item;// GetAnimationSource();
|
||||
|
||||
// // In WinUI, ConnectedAnimation is somehow exempt from all clipping behaviors
|
||||
// // Here, we are not, so disable ClipToBounds on all elements in the SettingsExpander
|
||||
// // The rest are taken care of in the xaml.
|
||||
// // NOTE: The ScrollViewer is not changed here as that's important for scrolling - thus
|
||||
// // the animation will be cut off, but the back animation is pretty fast and mostly is
|
||||
// // only visible closer to the element so we're ok, I think
|
||||
// var x = presenter?.GetVisualParent();
|
||||
|
||||
// while (!(x is ScrollContentPresenter) && x != null)
|
||||
// {
|
||||
// x.ClipToBounds = false;
|
||||
// x = x.GetVisualParent();
|
||||
// }
|
||||
|
||||
// anim.Configuration = new DirectConnectedAnimationConfiguration();
|
||||
// anim.TryStart(presenter);
|
||||
//}
|
||||
|
||||
//private void OnNavigatingFrom(object? sender, NavigatingCancelEventArgs e)
|
||||
//{
|
||||
// if (_animationPage == null)
|
||||
// return;
|
||||
|
||||
// // We're not navigating to a control page, don't set up the animation & clear
|
||||
// // the previous animation source
|
||||
// if (!e.SourcePageType.Name.Equals(nameof(TabItemViewModel)))
|
||||
// {
|
||||
// _animationPage = null;
|
||||
// _animationPage = null;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var item = this.GetVisualDescendants()
|
||||
// ?.Where(x => x is ListBoxItem && x.DataContext == _animationPage)
|
||||
// ?.FirstOrDefault()
|
||||
// ?.GetVisualDescendants()
|
||||
// .Where(x => x is Viewbox && x.Name == "IconHost")
|
||||
// .FirstOrDefault();
|
||||
// var svc = ConnectedAnimationService.GetForView(TopLevel.GetTopLevel(this));
|
||||
// svc.PrepareToAnimate("ForwardAnimation", item);
|
||||
//}
|
||||
|
||||
//private TabItemViewModel? _animationPage;
|
||||
}
|
||||
@@ -22,13 +22,10 @@
|
||||
Text="{Binding Path=Res.Settings_UI, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
Theme="{StaticResource SubtitleTextBlockStyle}" />
|
||||
|
||||
<Border IsEnabled="{Binding Path=User, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}">
|
||||
<ui:SettingsExpander
|
||||
Description="{Binding Path=Res.Advertisement_NoShow_Tip, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
Header="{Binding Path=Res.Settings_UI_StartDefaultPage, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
IsEnabled="{Binding Path=User.UserType, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:UserType.Sponsor}}">
|
||||
<Border>
|
||||
<ui:SettingsExpander Header="{Binding Path=Res.Settings_UI_StartDefaultPage, Mode=OneWay, Source={x:Static s:ResourceService.Current}}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<ui:FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="{StaticResource HeartBroken}" />
|
||||
<ui:FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="{StaticResource Device}" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox ItemsSource="{Binding Path=MainWindow2.TabItems, Mode=OneWay, Source={x:Static s:IViewModelManager.Instance}}" SelectedItem="{Binding SelectDefaultPage, Mode=TwoWay}">
|
||||
@@ -344,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>
|
||||
|
||||
@@ -172,13 +172,13 @@
|
||||
|
||||
<WrapPanel Margin="0,20" HorizontalAlignment="Center">
|
||||
<TextBlock Text="{Binding Path=Res.User_LoginUnexpectedPrompt, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" Theme="{StaticResource CaptionTextBlockStyle}" />
|
||||
<ui:HyperlinkButton
|
||||
<HyperlinkButton
|
||||
Margin="5,0"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding ManualLogin}">
|
||||
<TextBlock Text="{Binding Path=Res.Login_ManualLogin, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" Theme="{StaticResource CaptionTextBlockStyle}" />
|
||||
</ui:HyperlinkButton>
|
||||
</HyperlinkButton>
|
||||
</WrapPanel>
|
||||
|
||||
<ProgressBar
|
||||
@@ -222,23 +222,23 @@
|
||||
FontSize="12"
|
||||
Text="{Binding Path=Res.User_RegisterAgreed, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
Theme="{StaticResource CaptionTextBlockStyle}" />
|
||||
<ui:HyperlinkButton
|
||||
<HyperlinkButton
|
||||
Margin="5,0"
|
||||
Padding="0"
|
||||
Command="{Binding OpenHyperlink}"
|
||||
CommandParameter="Agreement"
|
||||
Cursor="Hand">
|
||||
<TextBlock Text="{Binding Path=Res.User_Agreement, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" Theme="{StaticResource CaptionTextBlockStyle}" />
|
||||
</ui:HyperlinkButton>
|
||||
</HyperlinkButton>
|
||||
<TextBlock Text="{Binding Path=Res.And, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" Theme="{StaticResource CaptionTextBlockStyle}" />
|
||||
<ui:HyperlinkButton
|
||||
<HyperlinkButton
|
||||
Margin="5,0"
|
||||
Padding="0"
|
||||
Command="{Binding OpenHyperlink}"
|
||||
CommandParameter="Privacy"
|
||||
Cursor="Hand">
|
||||
<TextBlock Text="{Binding Path=Res.User_Privacy, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" Theme="{StaticResource CaptionTextBlockStyle}" />
|
||||
</ui:HyperlinkButton>
|
||||
</HyperlinkButton>
|
||||
</WrapPanel>
|
||||
|
||||
<StackPanel
|
||||
|
||||
@@ -45,6 +45,17 @@ public partial class LoginOrRegisterPage : ReactiveUserControl<LoginOrRegisterWi
|
||||
this.ViewModel.Close(false);
|
||||
}
|
||||
}
|
||||
// 11.1 使用新 API https://docs.avaloniaui.net/docs/concepts/services/activatable-lifetime#handling-uri-activation
|
||||
//if (Application.Current.TryGetFeature<IActivatableLifetime>() is { } activatableLifetime)
|
||||
//{
|
||||
// activatableLifetime.Activated += (s, a) =>
|
||||
// {
|
||||
// if (a is ProtocolActivatedEventArgs protocolArgs && protocolArgs.Kind == ActivationKind.OpenUri)
|
||||
// {
|
||||
// Console.WriteLine($"App activated via Uri: {protocolArgs.Uri}");
|
||||
// }
|
||||
// };
|
||||
//}
|
||||
Application.Current!.UrlsOpened += Current_UrlsOpened;
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ public sealed class AppSplashScreen : IApplicationSplashScreen
|
||||
#endif
|
||||
new MenuTabItemViewModel("Plugin_Store")
|
||||
{
|
||||
PageType = null,
|
||||
PageType = typeof(PluginStorePage),
|
||||
IsResourceGet = true,
|
||||
IconKey = "avares://BD.WTTS.Client.Avalonia/UI/Assets/Icons/store.ico",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
@@ -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) == '')">
|
||||
|
||||
+1
@@ -123,6 +123,7 @@ public static class KestrelServerOptionsExtensions
|
||||
var httpsPort = IReverseProxyConfig.HttpsPort;
|
||||
options.Listen(IReverseProxyService.Constants.Instance.ProxyIp, httpsPort, listen =>
|
||||
{
|
||||
listen.Protocols = HttpProtocols.Http1AndHttp2;
|
||||
listen.UseFlowAnalyze();
|
||||
listen.UseTls();
|
||||
});
|
||||
|
||||
+3
-3
@@ -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));
|
||||
});
|
||||
|
||||
// 设置仓储层数据库文件存放路径
|
||||
|
||||
+15
-1
@@ -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)
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
@@ -450,4 +450,36 @@ sealed partial class CertificateManagerImpl : ICertificateManager
|
||||
var result = ICertificateManager.Constants.IsCertificateInstalled(platformService, packable);
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetCertificateInfo()
|
||||
{
|
||||
var rootCert = RootCertificate;
|
||||
if (rootCert == null)
|
||||
{
|
||||
GenerateCertificate();
|
||||
}
|
||||
rootCert = RootCertificate;
|
||||
|
||||
if (rootCert == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder b = new();
|
||||
b.AppendLine("Subject:");
|
||||
b.AppendLine(rootCert.Subject);
|
||||
b.AppendLine("SerialNumber:");
|
||||
b.AppendLine(rootCert.SerialNumber);
|
||||
b.AppendLine("PeriodValidity:");
|
||||
b.Append(rootCert.GetEffectiveDateString());
|
||||
b.Append(" ~ ");
|
||||
b.Append(rootCert.GetExpirationDateString());
|
||||
b.AppendLine();
|
||||
b.AppendLine("SHA256:");
|
||||
b.AppendLine(rootCert.GetCertHashStringCompat(HashAlgorithmName.SHA256));
|
||||
b.AppendLine("SHA1:");
|
||||
b.AppendLine(rootCert.GetCertHashStringCompat(HashAlgorithmName.SHA1));
|
||||
|
||||
return b.ToString();
|
||||
}
|
||||
}
|
||||
+5
@@ -46,4 +46,9 @@ sealed class LazyCertificateManager : ICertificateManager
|
||||
{
|
||||
return impl().DeleteRootCertificate();
|
||||
}
|
||||
|
||||
public string GetCertificateInfo()
|
||||
{
|
||||
return impl().GetCertificateInfo();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+196
-2
@@ -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();
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ sealed class ReverseProxyHttpClientHandler : DelegatingHandler
|
||||
var tlsSniValue = domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom();
|
||||
request.SetRequestContext(new RequestContext(isHttps, tlsSniValue));
|
||||
|
||||
// 设置请求头 host,修改协议为 http
|
||||
// 设置请求头 host,因为连接中已带端口号故 修改协议为 http
|
||||
request.Headers.Host = uri.Host;
|
||||
request.RequestUri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+20
-23
@@ -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());
|
||||
|
||||
+57
-6
@@ -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;
|
||||
@@ -80,7 +104,15 @@ sealed partial class HttpReverseProxyMiddleware
|
||||
return;
|
||||
}
|
||||
|
||||
var destinationPrefix = GetDestinationPrefix(context.Request.Scheme, context.Request.Host, domainConfig.Destination);
|
||||
var destination = domainConfig.Destination;
|
||||
if (domainConfig.Destination?.AbsoluteUri.Contains("@") == true)
|
||||
{
|
||||
var newUrl = domainConfig.Destination.AbsoluteUri.Replace("@domain", context.Request.Host.Host);
|
||||
newUrl = newUrl.Replace("@uri", context.Request.RawUrl());
|
||||
destination = new Uri(newUrl);
|
||||
}
|
||||
|
||||
var destinationPrefix = GetDestinationPrefix(context.Request.Scheme, context.Request.Host, destination);
|
||||
var httpClient = httpClientFactory.CreateHttpClient(context.Request.Host.Host, domainConfig);
|
||||
if (!string.IsNullOrEmpty(domainConfig.UserAgent))
|
||||
{
|
||||
@@ -89,9 +121,19 @@ sealed partial class HttpReverseProxyMiddleware
|
||||
|
||||
var forwarderRequestConfig = new ForwarderRequestConfig()
|
||||
{
|
||||
Version = context.Request.Protocol.StartsWith("HTTP/2") ? System.Net.HttpVersion.Version20 : System.Net.HttpVersion.Version11,
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
if (domainConfig.IsServerSideProxy)
|
||||
{
|
||||
SetWattHeaders(context, reverseProxyConfig.Service.ServerSideProxyToken);
|
||||
}
|
||||
|
||||
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, forwarderRequestConfig, HttpTransformer.Empty);
|
||||
|
||||
if (error != ForwarderError.None)
|
||||
@@ -140,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))
|
||||
{
|
||||
@@ -393,6 +435,15 @@ sealed partial class HttpReverseProxyMiddleware
|
||||
return false;
|
||||
}
|
||||
|
||||
static void SetWattHeaders(HttpContext context, string? token)
|
||||
{
|
||||
context.Request.Headers.TryAdd("X-Watt-Origin-Dest-Scheme", context.Request.Scheme);
|
||||
context.Request.Headers.TryAdd("X-Watt-Origin-Dest-Host", context.Request.Host.ToString());
|
||||
context.Request.Headers.TryAdd("X-Watt-Origin-Dest-PathAndQuery", context.Request.GetEncodedPathAndQuery());
|
||||
|
||||
context.Request.Headers.TryAdd("X-Watt-Token", token ?? string.Empty);
|
||||
}
|
||||
|
||||
const int UTF8PreambleLength = 3;
|
||||
const byte UTF8PreambleByte2 = 0xBF;
|
||||
const int UTF8PreambleFirst2Bytes = 0xEFBB;
|
||||
|
||||
+2
@@ -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);
|
||||
|
||||
+15
-9
@@ -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;
|
||||
}
|
||||
|
||||
+15
-2
@@ -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)
|
||||
{
|
||||
|
||||
+9
-5
@@ -89,6 +89,8 @@ abstract class ReverseProxyServiceImpl : IReverseProxySettings
|
||||
|
||||
public string? CustomDohAddres { get; set; }
|
||||
|
||||
public string? ServerSideProxyToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个随机的未使用的端口
|
||||
/// </summary>
|
||||
@@ -157,15 +159,18 @@ abstract class ReverseProxyServiceImpl : IReverseProxySettings
|
||||
return StartProxyResultCode.DeserializeReverseProxySettingsFail;
|
||||
}
|
||||
|
||||
// Linux 如果是443 需要验证 是否允许使用
|
||||
// Linux 如果是443 需要验证 是否允许使用
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +183,6 @@ abstract class ReverseProxyServiceImpl : IReverseProxySettings
|
||||
|
||||
protected virtual void CheckRootCertificate()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected abstract Task<StartProxyResult> StartProxyImpl();
|
||||
|
||||
+18
-8
@@ -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()
|
||||
|
||||
+9
@@ -193,6 +193,9 @@ public interface ICertificateManager
|
||||
var isRootCertificateInstalled = IsCertificateInstalled();
|
||||
if (!isRootCertificateInstalled)
|
||||
{
|
||||
// 生成证书
|
||||
certificateManager.GenerateCertificate();
|
||||
|
||||
// 安装证书
|
||||
ICertificateManager.Constants.TrustRootCertificate(
|
||||
GetCerFilePath, platformService, certificate2);
|
||||
@@ -271,4 +274,10 @@ public interface ICertificateManager
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
bool? GenerateCertificate();
|
||||
|
||||
/// <summary>
|
||||
/// 获取证书信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetCertificateInfo();
|
||||
}
|
||||
+16
-3
@@ -1,6 +1,9 @@
|
||||
using dotnetCampus.Ipc.CompilerServices.Attributes;
|
||||
|
||||
#if APP_REVERSE_PROXY
|
||||
|
||||
using InstanceReverseProxyService = BD.WTTS.Services.Implementation.YarpReverseProxyServiceImpl;
|
||||
|
||||
#else
|
||||
using InstanceReverseProxyService = BD.WTTS.Services.IReverseProxyService;
|
||||
#endif
|
||||
@@ -59,6 +62,7 @@ public partial interface IReverseProxyService : IDisposable
|
||||
Task StopProxyAsync();
|
||||
|
||||
#if DEBUG
|
||||
|
||||
string GetDebugString()
|
||||
{
|
||||
return $"Pid: {Environment.ProcessId}, Exe: {Environment.ProcessPath}, Asm: {Assembly.GetAssembly(GetType())?.FullName}";
|
||||
@@ -69,6 +73,7 @@ public partial interface IReverseProxyService : IDisposable
|
||||
StartProxyResult r = new(StartProxyResultCode.Exception, new Exception("aaaa"));
|
||||
return Task.FromResult(r);
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -139,7 +144,7 @@ public partial interface IReverseProxySettings
|
||||
/// </summary>
|
||||
ushort Socks5ProxyPortId { get; set; }
|
||||
|
||||
#endregion
|
||||
#endregion Socks5
|
||||
|
||||
#region TwoLevelAgent(二级代理)
|
||||
|
||||
@@ -155,7 +160,7 @@ public partial interface IReverseProxySettings
|
||||
|
||||
string? TwoLevelAgentPassword { get; set; }
|
||||
|
||||
#endregion
|
||||
#endregion TwoLevelAgent(二级代理)
|
||||
|
||||
IPAddress? ProxyDNS { get; set; }
|
||||
|
||||
@@ -167,6 +172,11 @@ public partial interface IReverseProxySettings
|
||||
bool UseDoh { get; set; }
|
||||
|
||||
string? CustomDohAddres { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务端代理令牌
|
||||
/// </summary>
|
||||
string? ServerSideProxyToken { get; set; }
|
||||
}
|
||||
|
||||
[MP2Obj(SerializeLayout.Explicit)]
|
||||
@@ -214,7 +224,9 @@ public readonly partial record struct ReverseProxySettings(
|
||||
[property:MP2Key(20)]
|
||||
bool UseDoh,
|
||||
[property:MP2Key(21)]
|
||||
string? CustomDohAddres)
|
||||
string? CustomDohAddres,
|
||||
[property:MP2Key(22)]
|
||||
string? ServerSideProxyToken)
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal IPAddress GetProxyIp() => GetProxyIp(ProxyIp);
|
||||
@@ -267,6 +279,7 @@ public readonly partial record struct ReverseProxySettings(
|
||||
settings.IsSupportIpv6 = IsSupportIpv6;
|
||||
settings.UseDoh = UseDoh;
|
||||
settings.CustomDohAddres = CustomDohAddres;
|
||||
settings.ServerSideProxyToken = ServerSideProxyToken;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ partial interface IReverseProxyService
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
byte[]? GetFlowStatistics_Bytes();
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有日志信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string? GetLogAllMessage();
|
||||
}
|
||||
|
||||
public static partial class ReverseProxyServiceExtensions
|
||||
|
||||
+11
-11
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user