mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-26 12:09:32 +08:00
Compare commits
127
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 |
@@ -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)
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
default_base_path="$HOME/WattToolkit"
|
||||
base_path="$default_base_path"
|
||||
|
||||
# 检查是否以root身份执行
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "此脚本不能以root身份执行。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 循环直到用户输入有效路径或直接回车
|
||||
while true; do
|
||||
# 使用 zenity 提示用户选择安装路径或使用默认路径
|
||||
@@ -41,7 +47,45 @@ base_url="https://api.steampp.net"
|
||||
architecture=1
|
||||
app_name="Watt Toolkit"
|
||||
PROCESS_NAMES=("$exec_name" "$app_name")
|
||||
export LC_ALL=en_US.UTF-8
|
||||
Check_LC_Code() {
|
||||
# 检查当前 LANG 环境变量
|
||||
current_lang="${LC_ALL:-$LANG}"
|
||||
echo "当前语言环境: $current_lang"
|
||||
|
||||
# 检查是否为 C 或 C.UTF-8
|
||||
if [ "$current_lang" = "C" ] || [ "$current_lang" = "C.UTF-8" ]; then
|
||||
echo "当前语言环境是 C 或 C.UTF-8,需要修改。"
|
||||
|
||||
# 列出系统支持的语言环境
|
||||
supported_locales=$(locale -a)
|
||||
if [ -z "$supported_locales" ]; then
|
||||
echo "未能获取到支持的语言环境列表。请检查系统配置。"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 遍历支持的语言环境列表,找到第一个不是 C 或 C.UTF-8 的项
|
||||
first_non_c_locale=""
|
||||
for locale in $supported_locales; do
|
||||
if [ "$locale" != "C" ] && [ "$locale" != "C.UTF-8" ]; then
|
||||
first_non_c_locale="$locale"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 检查是否找到了非 C/C.UTF-8 的语言环境
|
||||
if [ -n "$first_non_c_locale" ]; then
|
||||
echo "修改语言环境为支持列表中的第一个非 C/C.UTF-8 项:$first_non_c_locale"
|
||||
export LC_ALL="$first_non_c_locale"
|
||||
echo "语言环境已修改为 $first_non_c_locale。"
|
||||
else
|
||||
echo "未能找到非 C/C.UTF-8 的语言环境。请检查系统配置。"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "当前语言环境不是 C 或 C.UTF-8,无需修改。"
|
||||
fi
|
||||
}
|
||||
Check_LC_Code
|
||||
Install_certutil() {
|
||||
# 判断发行版类型
|
||||
if command -v certutil &>/dev/null; then
|
||||
@@ -360,34 +404,44 @@ Kill_Process
|
||||
Decompression
|
||||
# xdg-icon-resource install "$base_path/Icons/Watt-Toolkit.png" --size 128 Watt-Toolkit
|
||||
InitDesktop() {
|
||||
# 检查XDG_DESKTOP_DIR环境变量,如果未设置则使用默认值
|
||||
XDG_DESKTOP_DIR="${XDG_DESKTOP_DIR:-$HOME/Desktop}"
|
||||
|
||||
while true; do
|
||||
# 使用 zenity 提示用户选择安装路径或使用默认路径
|
||||
choice=$(zenity --list --radiolist --title="请选择要添加到的位置" --column="选择" --column="路径" TRUE "$HOME/.local/share/applications/" FALSE "$HOME/Desktop")
|
||||
choice=$(zenity --list --radiolist --title="请选择要添加到的位置" \
|
||||
--column="选择" --column="路径" \
|
||||
TRUE "$XDG_DESKTOP_DIR" \
|
||||
FALSE "$HOME/.local/share/applications/")
|
||||
|
||||
# 检查用户输入
|
||||
if [ "$choice" == "$HOME/.local/share/applications/" ]; then
|
||||
target_dir="$HOME/.local/share/applications/"
|
||||
break
|
||||
elif [ "$choice" == "$HOME/Desktop" ]; then
|
||||
target_dir="$HOME/Desktop/"
|
||||
elif [ "$choice" == "$XDG_DESKTOP_DIR" ]; then
|
||||
target_dir="$XDG_DESKTOP_DIR"
|
||||
break
|
||||
else
|
||||
echo "无效选项,请输入 1 或 2。"
|
||||
# 无效选项时给出提示,并继续循环
|
||||
zenity --info --text="无效选项,请重新选择。"
|
||||
fi
|
||||
done
|
||||
#添加桌面文件
|
||||
|
||||
# 添加桌面文件
|
||||
rm -rf "$target_dir/Watt Toolkit.desktop" 2>/dev/null
|
||||
echo "#!/usr/bin/env xdg-open
|
||||
cat <<EOT > "$target_dir/Watt Toolkit.desktop"
|
||||
[Desktop Entry]
|
||||
Name=Watt Toolkit
|
||||
Exec=$base_path/$exec_name.sh
|
||||
Icon=$base_path/Icons/Watt-Toolkit.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
StartupNotify=false" >"$target_dir/Watt Toolkit.desktop"
|
||||
StartupNotify=false
|
||||
EOT
|
||||
chmod +x "$target_dir/Watt Toolkit.desktop"
|
||||
|
||||
}
|
||||
|
||||
|
||||
InitDesktop
|
||||
# update-desktop-database ~/.local/share/applications
|
||||
#运行程序
|
||||
|
||||
@@ -964,8 +964,8 @@
|
||||
},
|
||||
{
|
||||
"TypeName": "bool",
|
||||
"PropertyName": "UseDoh2",
|
||||
"DefaultValue": "false",
|
||||
"PropertyName": "UseDoh",
|
||||
"DefaultValue": "true",
|
||||
"DefaultValueIsConst": true,
|
||||
"Summary": "启用 DNS over HTTPS",
|
||||
"IsRegionOrEndregion": null,
|
||||
@@ -973,7 +973,7 @@
|
||||
},
|
||||
{
|
||||
"TypeName": "string",
|
||||
"PropertyName": "CustomDohAddres",
|
||||
"PropertyName": "CustomDohAddres2",
|
||||
"DefaultValue": "null",
|
||||
"DefaultValueIsConst": true,
|
||||
"Summary": "自定义 DNS over HTTPS 地址",
|
||||
@@ -988,6 +988,15 @@
|
||||
"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: de291c0d95...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: b6b85044cc...7a43b4afd3
+1
-1
Submodule ref/SteamClient updated: 3cf39ef2c8...b449d0d434
Submodule ref/WTTS.MicroServices.ClientSDK updated: 09d6778004...cb3580aa1f
+1
-1
Submodule ref/WinAuth updated: a73491b482...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 = "8";
|
||||
const string ver_for_preview = "9";
|
||||
|
||||
/// <summary>
|
||||
/// RC 版本号,范围 0~9,只增不减
|
||||
/// </summary>
|
||||
const string ver_for_rc = "9";
|
||||
const string ver_for_rc = "12";
|
||||
|
||||
public const string FileVersion = $"{Version2}.{ver_for_rc}0{ver_for_preview}.0";
|
||||
public const string FileVersion = $"{Version2}.{ver_for_rc}1{ver_for_preview}.0";
|
||||
|
||||
//public const string InformationalVersion = Version;
|
||||
//public const string InformationalVersion = $"{Version}-preview.{ver_for_preview}";
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<DelaySign>false</DelaySign>
|
||||
<AssemblyOriginatorKeyFile>$(MSBuildProjectDirectory)\..\..\avalonia.snk</AssemblyOriginatorKeyFile>
|
||||
<!--https://github.com/AvaloniaUI/Avalonia/blob/0.10.10/build/SharedVersion.props#L18-->
|
||||
<AvaloniaAccessUnstablePrivateApis>true</AvaloniaAccessUnstablePrivateApis>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -91,9 +91,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BD.Common.XmlnsDefinition.Avalonia">
|
||||
<Aliases>BD_Common_XmlnsDefinition_Avalonia</Aliases>
|
||||
</PackageReference>
|
||||
<ProjectReference Include="..\BD.WTTS.Client.Avalonia\BD.WTTS.Client.Avalonia.csproj" />
|
||||
<ProjectReference Include="..\BD.WTTS.Client\BD.WTTS.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
-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,9 +73,6 @@
|
||||
</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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -111,6 +111,12 @@ public sealed partial class App : Application
|
||||
{
|
||||
Task2.InBackground(() => appWindow.SplashScreen?.RunTasks(CancellationToken.None));
|
||||
}
|
||||
|
||||
IPlatformService.Instance.SetSystemSessionEnding(() =>
|
||||
{
|
||||
Log.Info("System Shutdown", "Application SafeExit...");
|
||||
Shutdown();
|
||||
});
|
||||
}
|
||||
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
|
||||
{
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</spp:AppItem>
|
||||
|
||||
<spp:AppItem
|
||||
Title="Wo Long: Fallen Dynasty (卧龙:苍天陨落)"
|
||||
Title="Wo Long: Fallen Dynasty (卧龙:苍天陨落)(卧龙:苍天陨落)(卧龙:苍天陨落)(卧龙:苍天陨落)"
|
||||
MaxHeight="120"
|
||||
Description="This is a description of whatver this does"
|
||||
Tags="111">
|
||||
@@ -181,7 +181,7 @@
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,5"
|
||||
Margin="0,5,12,5"
|
||||
VerticalAlignment="Top"
|
||||
Classes="CaptionTextBlockStyle"
|
||||
FontSize="12"
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<StackPanel Spacing="10">
|
||||
<HyperlinkButton Content="更新日志" />
|
||||
<Button Content="更新日志" Theme="{StaticResource HyperlinkButton}" />
|
||||
<Button Classes="TransparentAccent" Content="更新日志" />
|
||||
<Button
|
||||
Classes="TransparentAccent"
|
||||
Content="更新日志"
|
||||
IsEnabled="False" />
|
||||
<Button
|
||||
Content="更新日志"
|
||||
IsEnabled="False"
|
||||
@@ -221,6 +226,15 @@
|
||||
<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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -86,4 +86,10 @@
|
||||
<StreamGeometry x:Key="Phone">
|
||||
M15.75 2C16.9926 2 18 3.00736 18 4.25V19.75C18 20.9926 16.9926 22 15.75 22H8.25C7.00736 22 6 20.9926 6 19.75V4.25C6 3.00736 7.00736 2 8.25 2H15.75ZM13.25 18H10.75C10.3358 18 10 18.3358 10 18.75C10 19.1642 10.3358 19.5 10.75 19.5H13.25C13.6642 19.5 14 19.1642 14 18.75C14 18.3358 13.6642 18 13.25 18Z
|
||||
</StreamGeometry>
|
||||
<StreamGeometry x:Key="GOGGalaxy">
|
||||
M304.99296 650.144H185.98496a17.12 17.12 0 0 0-16.992 17.184v85.344c0 8.992 7.68 16.992 16.992 16.992h119.52v56.32H156.16096a43.84 43.84 0 0 1-43.488-44v-144.512c0-23.968 19.68-43.488 44-43.488h148.32z m43.168-157.824c0 24.832-20 44.832-44.832 44.832H112.19296v-57.696h161.312a16.896 16.896 0 0 0 17.024-16.992V272.64a17.024 17.024 0 0 0-17.024-17.184H187.36096a17.152 17.152 0 0 0-17.536 17.184v86.144c0 9.856 7.68 17.024 17.024 17.024H256.00096v57.632H157.02496a44.544 44.544 0 0 1-44.832-44.32V242.304c0-24.352 20.192-44.32 44.832-44.32H303.04096c24.672 0 44.64 19.968 44.64 44.32v249.984z m563.168 333.664h-56.32v-175.84h-39.68a17.12 17.12 0 0 0-16.992 17.184v158.624h-56.832v-175.84h-39.68a17.12 17.12 0 0 0-16.992 17.184v158.624h-56.832v-188.512c0-23.968 19.68-43.488 44-43.488h239.328z m0.512-333.664c0 24.832-20.192 44.832-44.832 44.832h-191.136v-57.696h161.312a16.96 16.96 0 0 0 17.024-16.992V272.64a17.12 17.12 0 0 0-17.024-17.184h-86.656a17.024 17.024 0 0 0-17.024 17.184v86.144c0 9.856 7.68 17.024 17.024 17.024h69.152v57.632h-98.688a44.352 44.352 0 0 1-44.64-44.32V242.304c0-24.352 19.968-44.32 44.64-44.32h146.336c24.832 0 44.832 19.968 44.832 44.32v249.984zM585.34496 198.016h-146.688c-24.832 0-44.512 19.968-44.512 44.32v146.816a44.16 44.16 0 0 0 44.512 44.32h146.688a44.48 44.48 0 0 0 44.512-44.32V242.336c0-24.352-20.192-44.32-44.512-44.32z m-12.672 74.656v86.144c0 9.504-7.68 17.024-17.152 17.024h-86.688a16.96 16.96 0 0 1-16.992-17.024V272.96c0-9.312 7.328-16.992 16.992-16.992h85.856c9.824 0 16.992 7.68 16.992 16.992z m-33.824 321.344h-144.672c-24.16 0-43.808 19.488-43.808 43.488v144.512c0 24.32 19.68 44 43.808 44h144.672c24.32 0 44-19.68 44-44v-144.512c0-23.968-19.68-43.488-44-43.488z m-12.864 73.344v85.344a16.96 16.96 0 0 1-16.992 16.992v-0.352h-84.864a17.12 17.12 0 0 1-17.152-17.152v-84.832a17.28 17.28 0 0 1 17.152-17.184h85.344c9.344 0 17.024 7.84 17.024 17.184z m476.16-620.352A74.24 74.24 0 0 0 949.31296 24.64H74.65696A74.304 74.304 0 0 0 0.00096 99.296v825.344a74.304 74.304 0 0 0 74.656 74.656h874.656a74.304 74.304 0 0 0 74.656-74.656V99.296c0-20.32-8.48-39.168-21.856-52.832z m0 877.664a52.416 52.416 0 0 1-52.832 52.832H74.65696a52.576 52.576 0 0 1-53.344-52.832V99.328a52.672 52.672 0 0 1 52.832-52.832H948.80096c29.344 0 52.992 23.68 52.992 52.832v825.344z
|
||||
</StreamGeometry>
|
||||
<StreamGeometry x:Key="Discord">
|
||||
M1049.062 139.672a3 3 0 0 0-1.528-1.4A970.13 970.13 0 0 0 808.162 64.06a3.632 3.632 0 0 0-3.846 1.82 674.922 674.922 0 0 0-29.8 61.2 895.696 895.696 0 0 0-268.852 0 619.082 619.082 0 0 0-30.27-61.2 3.78 3.78 0 0 0-3.848-1.82 967.378 967.378 0 0 0-239.376 74.214 3.424 3.424 0 0 0-1.576 1.352C78.136 367.302 36.372 589.38 56.86 808.708a4.032 4.032 0 0 0 1.53 2.75 975.332 975.332 0 0 0 293.65 148.378 3.8 3.8 0 0 0 4.126-1.352A696.4 696.4 0 0 0 416.24 860.8a3.72 3.72 0 0 0-2.038-5.176 642.346 642.346 0 0 1-91.736-43.706 3.77 3.77 0 0 1-0.37-6.252 502.094 502.094 0 0 0 18.218-14.274 3.638 3.638 0 0 1 3.8-0.512c192.458 87.834 400.82 87.834 591 0a3.624 3.624 0 0 1 3.848 0.466 469.066 469.066 0 0 0 18.264 14.32 3.768 3.768 0 0 1-0.324 6.252 602.814 602.814 0 0 1-91.78 43.66 3.75 3.75 0 0 0-2 5.222 782.11 782.11 0 0 0 60.028 97.63 3.728 3.728 0 0 0 4.126 1.4A972.096 972.096 0 0 0 1221.4 811.458a3.764 3.764 0 0 0 1.53-2.704c24.528-253.566-41.064-473.824-173.868-669.082zM444.982 675.16c-57.944 0-105.688-53.174-105.688-118.478s46.818-118.482 105.688-118.482c59.33 0 106.612 53.64 105.686 118.478 0 65.308-46.82 118.482-105.686 118.482z m390.76 0c-57.942 0-105.686-53.174-105.686-118.478s46.818-118.482 105.686-118.482c59.334 0 106.614 53.64 105.688 118.478 0 65.308-46.354 118.482-105.688 118.482z
|
||||
</StreamGeometry>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -14,6 +14,8 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel>, IV
|
||||
public static readonly StyledProperty<bool> IsSaveWindowSizeProperty = AvaloniaProperty
|
||||
.Register<ReactiveAppWindow<TViewModel>, bool>(nameof(IsSaveWindowSize), true);
|
||||
|
||||
private bool _isInitialize;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReactiveAppWindow{TViewModel}"/> class.
|
||||
/// </summary>
|
||||
@@ -127,27 +129,32 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel>, IV
|
||||
{
|
||||
var primaryScreenBounds = screen.Bounds;
|
||||
|
||||
if (CanResize && !IsVisible)
|
||||
if (CanResize && !_isInitialize && !IsVisible)
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
var scalingOffset = 31 - ((screen.Scaling - 1) / 0.5); // appwindow偏移量
|
||||
|
||||
if (SizePosition.Width > 0 &&
|
||||
primaryScreenBounds.Width >= SizePosition.Width)
|
||||
Width = SizePosition.Width;
|
||||
|
||||
if (SizePosition.Height > 0 &&
|
||||
primaryScreenBounds.Height >= SizePosition.Height)
|
||||
Height = SizePosition.Height - 31; // appwindow偏移量
|
||||
Height = SizePosition.Height - scalingOffset;
|
||||
|
||||
if (SizePosition.X > 0 && SizePosition.Y > 0)
|
||||
{
|
||||
var leftTopPoint = new PixelPoint(SizePosition.X, SizePosition.Y + 31);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -135,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>
|
||||
|
||||
@@ -287,6 +287,7 @@
|
||||
<!--<Border spp:Animations.EnableAnimations="True">-->
|
||||
<spp:AppItem
|
||||
Title="{Binding Name}"
|
||||
MinHeight="250"
|
||||
MaxWidth="260"
|
||||
Classes="Vertical Scale"
|
||||
ClickCommand="{Binding OpenBrowserCommand, Source={x:Static spp:App.Instance}}"
|
||||
|
||||
@@ -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) == '')">
|
||||
|
||||
+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));
|
||||
});
|
||||
|
||||
// 设置仓储层数据库文件存放路径
|
||||
|
||||
+4
-3
@@ -39,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);
|
||||
|
||||
@@ -51,7 +52,7 @@ 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)
|
||||
{
|
||||
if (reverseProxyConfig.Service.TwoLevelAgentEnable)
|
||||
{
|
||||
@@ -83,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;
|
||||
@@ -181,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))
|
||||
{
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+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;
|
||||
@@ -167,6 +169,12 @@ sealed partial class YarpReverseProxyServiceImpl : ReverseProxyServiceImpl, IRev
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public string? GetLogAllMessage()
|
||||
{
|
||||
var result = LogConsoleService.Builder;
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
// IDisposable
|
||||
|
||||
protected override void DisposeCore()
|
||||
|
||||
@@ -15,6 +15,12 @@ partial interface IReverseProxyService
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
byte[]? GetFlowStatistics_Bytes();
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有日志信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string? GetLogAllMessage();
|
||||
}
|
||||
|
||||
public static partial class ReverseProxyServiceExtensions
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,9 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
|
||||
services.AddSingleton(_ => certificateManager.Task.GetAwaiter().GetResult());
|
||||
#endif
|
||||
}
|
||||
|
||||
services.AddSingleton<INetworkTestService, NetworkTestService>();
|
||||
|
||||
//if (startup.HasIPCRoot)
|
||||
//{
|
||||
// services.AddSingleton<IAcceleratorService, BackendAcceleratorServiceImpl>();
|
||||
|
||||
+3
-13
@@ -1,4 +1,4 @@
|
||||
//------------------------------------------------------------------------------
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码由工具生成。
|
||||
// 运行时版本:4.0.30319.42000
|
||||
@@ -63,19 +63,9 @@ namespace BD.WTTS.Properties {
|
||||
/// <summary>
|
||||
/// 查找 System.Byte[] 类型的本地化资源。
|
||||
/// </summary>
|
||||
internal static byte[] WinDivert_dll {
|
||||
internal static byte[] WinDivert_mpo {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("WinDivert_dll", resourceCulture);
|
||||
return ((byte[])(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找 System.Byte[] 类型的本地化资源。
|
||||
/// </summary>
|
||||
internal static byte[] WinDivert64_sys {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("WinDivert64_sys", resourceCulture);
|
||||
object obj = ResourceManager.GetObject("WinDivert_mpo", resourceCulture);
|
||||
return ((byte[])(obj));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +99,7 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="WinDivert64_sys" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\..\..\ref\WinDivert64.sys;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
<data name="WinDivert_dll" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\..\..\ref\WinDivert.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<data name="WinDivert_mpo" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\..\..\ref\WinDivert.mpo;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
</root>
|
||||
+60
@@ -80,6 +80,11 @@ partial class BackendAcceleratorServiceImpl
|
||||
/// <inheritdoc/>
|
||||
public async Task<ApiRsp<bool>> XY_IsInstall(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok(false);
|
||||
}
|
||||
|
||||
var result = XunYouSDK.IsInstall();
|
||||
await Task.CompletedTask;
|
||||
return result;
|
||||
@@ -88,6 +93,11 @@ partial class BackendAcceleratorServiceImpl
|
||||
/// <inheritdoc/>
|
||||
public async Task<ApiRsp<XunYouUninstallCode>> XY_Uninstall(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok(XunYouUninstallCode.卸载成功);
|
||||
}
|
||||
|
||||
var result = XunYouSDK.Uninstall();
|
||||
await Task.CompletedTask;
|
||||
return result;
|
||||
@@ -104,9 +114,16 @@ partial class BackendAcceleratorServiceImpl
|
||||
string? svrPath = default,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok(0);
|
||||
}
|
||||
|
||||
var show = XunYouShowCommands.SW_HIDE;
|
||||
var result = XunYouSDK.StartEx2(
|
||||
openid,
|
||||
nickname,
|
||||
show,
|
||||
gameid,
|
||||
area,
|
||||
server,
|
||||
@@ -128,6 +145,11 @@ partial class BackendAcceleratorServiceImpl
|
||||
string? svrName = default,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok(0);
|
||||
}
|
||||
|
||||
var result = XunYouSDK.StartAccel(
|
||||
gameid,
|
||||
area,
|
||||
@@ -141,6 +163,11 @@ partial class BackendAcceleratorServiceImpl
|
||||
/// <inheritdoc/>
|
||||
public async Task<ApiRsp<XunYouAccelStateModel?>> XY_GetAccelStateEx(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok((XunYouAccelStateModel?)null);
|
||||
}
|
||||
|
||||
var result = GetXunYouAccelStateModel(lastXunYouState);
|
||||
await Task.CompletedTask;
|
||||
return result;
|
||||
@@ -149,6 +176,11 @@ partial class BackendAcceleratorServiceImpl
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<ApiRsp<int>> XY_Install(string installPath, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var channel = Channel.CreateUnbounded<int>();
|
||||
int XunYouDownLoadCallback(int par, nint thisptr)
|
||||
{
|
||||
@@ -177,6 +209,11 @@ partial class BackendAcceleratorServiceImpl
|
||||
/// <inheritdoc/>
|
||||
public async Task<ApiRsp<XunYouSendResultCode>> XY_StopAccel(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok(XunYouSendResultCode.发送成功);
|
||||
}
|
||||
|
||||
var result = XunYouSDK.Stop();
|
||||
await Task.CompletedTask;
|
||||
return result;
|
||||
@@ -185,6 +222,11 @@ partial class BackendAcceleratorServiceImpl
|
||||
/// <inheritdoc/>
|
||||
public async Task<ApiRsp<XunYouIsRunningCode>> XY_IsRunning(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok(XunYouIsRunningCode.加速器未启动);
|
||||
}
|
||||
|
||||
var result = XunYouSDK.IsRunning();
|
||||
await Task.CompletedTask;
|
||||
return result;
|
||||
@@ -193,8 +235,26 @@ partial class BackendAcceleratorServiceImpl
|
||||
/// <inheritdoc/>
|
||||
public async Task<ApiRsp<XunYouStartGameCode>> XY_StartGame(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok(XunYouStartGameCode.失败);
|
||||
}
|
||||
|
||||
var result = XunYouSDK.StartGame();
|
||||
await Task.CompletedTask;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ApiRsp<int>> XY_ShowWinodw(bool showHide, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return ApiRspHelper.Ok(0);
|
||||
}
|
||||
|
||||
XunYouSDK.xunyou_show(Convert.ToInt32(showHide));
|
||||
await Task.CompletedTask;
|
||||
return ApiRspHelper.Ok(0);
|
||||
}
|
||||
}
|
||||
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
namespace BD.WTTS.Services.Implementation;
|
||||
|
||||
public class NetworkEnvCheckService
|
||||
{
|
||||
const string HOST_FILE_PATH =
|
||||
#if WINDOWS
|
||||
@"C:\Windows\System32\drivers\etc\hosts";
|
||||
#else
|
||||
"/etc/hosts";
|
||||
#endif
|
||||
|
||||
public async Task CheckNetworkEnvAsync(IProgress<(NetworkEnvCheckStep Step, bool HasError, string Msg)> progress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Parallel.ForAsync(
|
||||
(int)NetworkEnvCheckStep.HostsFileCheck,
|
||||
(int)NetworkEnvCheckStep.LSPCheck + 1,
|
||||
cancellationToken,
|
||||
async (step, ct) =>
|
||||
{
|
||||
(bool pass, string msg) = step switch
|
||||
{
|
||||
(int)NetworkEnvCheckStep.HostsFileCheck => await CheckHostsAsync(),
|
||||
(int)NetworkEnvCheckStep.NetworkInterfaceCheck => CheckNetworkInterfaces(),
|
||||
(int)NetworkEnvCheckStep.LSPCheck =>
|
||||
#if WINDOWS
|
||||
CheckLSP(HandleWSAProtocolInfo),
|
||||
#else
|
||||
(true, string.Empty),
|
||||
#endif
|
||||
_ => (true, string.Empty)
|
||||
};
|
||||
|
||||
progress.Report(((NetworkEnvCheckStep)step, pass, msg));
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static Task<(bool Pass, string Msg)> CheckHostsAsync()
|
||||
{
|
||||
bool hostFileExist = File.Exists(HOST_FILE_PATH);
|
||||
|
||||
if (!hostFileExist)
|
||||
return Task.FromResult((false, "未找到 hosts 文件"));
|
||||
|
||||
return Task.FromResult((hostFileExist, string.Empty));
|
||||
}
|
||||
|
||||
private static (bool Pass, string ErrorMsg) CheckNetworkInterfaces(NetworkInterface[]? interfaces = null)
|
||||
{
|
||||
interfaces ??= NetworkInterface.GetAllNetworkInterfaces();
|
||||
|
||||
if (interfaces == null || interfaces.Length == 0)
|
||||
return (false, "未获取到网络适配器信息!");
|
||||
|
||||
// 如何判断虚拟网卡?
|
||||
|
||||
var useableNetworkInterfaces =
|
||||
interfaces.Where(x =>
|
||||
!x.Description.Contains("Virtual", StringComparison.OrdinalIgnoreCase) &&
|
||||
(x.NetworkInterfaceType == NetworkInterfaceType.Ethernet || x.NetworkInterfaceType == NetworkInterfaceType.Wireless80211));
|
||||
|
||||
if (!useableNetworkInterfaces.Any())
|
||||
return (false, "未获取到可用的以太网,无线网络适配器");
|
||||
|
||||
var checkResults =
|
||||
useableNetworkInterfaces.Select(x => new
|
||||
{
|
||||
Source = x,
|
||||
Result = CheckNetworkInterface(x)
|
||||
});
|
||||
|
||||
if (checkResults.Any(x => x.Result.Pass))
|
||||
return (true, string.Empty);
|
||||
|
||||
var firstError = checkResults.First(x => !x.Result.Pass);
|
||||
|
||||
return (firstError.Result.Pass, $"{firstError.Source.Name}|{firstError.Source.Description} {firstError.Result.ErrorMsg}");
|
||||
}
|
||||
|
||||
private static (bool Pass, string ErrorMsg) CheckNetworkInterface(NetworkInterface @interface)
|
||||
{
|
||||
var name = @interface.Name;
|
||||
var desc = @interface.Description;
|
||||
|
||||
var ipProps = @interface.GetIPProperties();
|
||||
|
||||
if (@interface.OperationalStatus != OperationalStatus.Up)
|
||||
return (false, "未启用网卡");
|
||||
|
||||
if (ipProps == null)
|
||||
return (false, $"获取网络信息失败!");
|
||||
|
||||
bool hasIPv4 = ipProps.UnicastAddresses
|
||||
.Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork);
|
||||
|
||||
bool hasIPv6 = ipProps.UnicastAddresses
|
||||
.Any(x => x.Address.AddressFamily == AddressFamily.InterNetworkV6);
|
||||
|
||||
if (!hasIPv4 && !hasIPv6)
|
||||
return (false, "网络适配器未启用TCP/IP服务");
|
||||
|
||||
// 如何判断虚拟网卡?
|
||||
|
||||
var dns = ipProps.DnsAddresses;
|
||||
|
||||
#if WINDOWS
|
||||
var isDHCPEnable = ipProps.GetIPv4Properties().IsDhcpEnabled;
|
||||
#endif
|
||||
return (true, string.Empty);
|
||||
}
|
||||
|
||||
private static (bool Pass, string ErrorMsg) CheckLSP(CheckLSPPredicate predicate)
|
||||
{
|
||||
int bufferSize = 0;
|
||||
int error = 0;
|
||||
|
||||
// 获取 buffer 的大小
|
||||
_ = WSCEnumProtocols(null, IntPtr.Zero, ref bufferSize, ref error);
|
||||
|
||||
IntPtr protocolBuffer = Marshal.AllocHGlobal(bufferSize);
|
||||
|
||||
try
|
||||
{
|
||||
int result = WSCEnumProtocols(null, protocolBuffer, ref bufferSize, ref error);
|
||||
if (result == -1)
|
||||
{
|
||||
return (false, "获取LSP信息失败!");
|
||||
}
|
||||
|
||||
int structSize = Marshal.SizeOf(typeof(WSAPROTOCOL_INFO));
|
||||
int count = bufferSize / structSize;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
IntPtr item = new(protocolBuffer + (i * structSize));
|
||||
|
||||
WSAPROTOCOL_INFO protocolInfo = Marshal.PtrToStructure<WSAPROTOCOL_INFO>(item);
|
||||
|
||||
#if DEBUG
|
||||
// Console.WriteLine($"Protocol: {protocolInfo.szProtocol}");
|
||||
// Console.WriteLine($"Address Family: {protocolInfo.iAddressFamily}");
|
||||
// Console.WriteLine($"Socket Type: {protocolInfo.iSocketType}");
|
||||
// Console.WriteLine($"Protocol: {protocolInfo.iProtocol}");
|
||||
// Console.WriteLine($"Provider ID: {protocolInfo.ProviderId}");
|
||||
// Console.WriteLine();
|
||||
#endif
|
||||
var handle = predicate(in protocolInfo);
|
||||
|
||||
if (!handle)
|
||||
return (false, "LSP响应异常!");
|
||||
}
|
||||
|
||||
return (true, string.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(protocolBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
#region P/Invoke
|
||||
|
||||
/// <summary>
|
||||
/// 处理判断 协议信息是否有效
|
||||
/// </summary>
|
||||
/// <param name="info"></param>
|
||||
/// <returns></returns>
|
||||
static bool HandleWSAProtocolInfo(in WSAPROTOCOL_INFO info)
|
||||
{
|
||||
const int MAX = 255;
|
||||
|
||||
StringBuilder pathBuilder = new StringBuilder(MAX);
|
||||
int pathLength = MAX;
|
||||
|
||||
Guid queryProviderId = info.ProviderId;
|
||||
|
||||
int result = WSCGetProviderPath(ref queryProviderId, pathBuilder, ref pathLength, out int errorCode);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
// 获取失败
|
||||
}
|
||||
|
||||
// path 可能包含系统环境变量例如 %SystemRoot%
|
||||
// 是否需要解析
|
||||
string path = pathBuilder.ToString();
|
||||
|
||||
Console.WriteLine("path:" + path);
|
||||
return true;
|
||||
}
|
||||
|
||||
[DllImport("Ws2_32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
static extern int WSCEnumProtocols(
|
||||
int[]? lpiProtocols,
|
||||
IntPtr lpProtocolBuffer,
|
||||
ref int lpdwBufferLength,
|
||||
ref int lpErrno);
|
||||
|
||||
[DllImport("Ws2_32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
static extern int WSCGetProviderPath(
|
||||
[In] ref Guid providerId,
|
||||
[Out][MarshalAs(UnmanagedType.LPWStr)] StringBuilder providerDllPath,
|
||||
[In, Out] ref int providerDllPathLength,
|
||||
[Out] out int lpErrno
|
||||
);
|
||||
|
||||
private delegate bool CheckLSPPredicate(in WSAPROTOCOL_INFO protocolInfo);
|
||||
|
||||
/// <summary>
|
||||
/// WSAPROTOCOL_INFO 结构用于存储或检索给定协议的完整信息。
|
||||
/// <see cref="https://learn.microsoft.com/zh-cn/windows/win32/api/winsock2/ns-winsock2-wsaprotocol_infoa"/>
|
||||
/// </summary>
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "<挂起>")]
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct WSAPROTOCOL_INFO
|
||||
{
|
||||
/// <summary>
|
||||
/// 描述协议提供的服务的位掩码
|
||||
/// </summary>
|
||||
public uint dwServiceFlags1;
|
||||
|
||||
/// <summary>
|
||||
/// 保留给其他协议属性定义。
|
||||
/// </summary>
|
||||
public uint dwServiceFlags2;
|
||||
|
||||
/// <summary>
|
||||
/// 保留给其他协议属性定义。
|
||||
/// </summary>
|
||||
public uint dwServiceFlags3;
|
||||
|
||||
/// <summary>
|
||||
/// 保留给其他协议属性定义。
|
||||
/// </summary>
|
||||
public uint dwServiceFlags4;
|
||||
|
||||
/// <summary>
|
||||
/// 一组标志,提供有关如何在 Winsock 目录中表示此协议的信息
|
||||
/// </summary>
|
||||
public uint dwProviderFlags;
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供商供应商) 分配给提供程序的 GUID
|
||||
/// </summary>
|
||||
public Guid ProviderId;
|
||||
|
||||
/// <summary>
|
||||
/// 由 WS2_32.DLL 为每个WSAPROTOCOL_INFO结构分配的唯 一 标识符。
|
||||
/// </summary>
|
||||
public uint dwCatalogEntryId;
|
||||
|
||||
public WSAPROTOCOLCHAIN ProtocolChain;
|
||||
|
||||
/// <summary>
|
||||
/// 协议版本标识符。
|
||||
/// </summary>
|
||||
public int iVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 要作为地址系列参数传递给 套接字 或 WSASocket 函数的
|
||||
/// </summary>
|
||||
public int iAddressFamily;
|
||||
|
||||
/// <summary>
|
||||
/// 最大地址长度(以字节为单位)。
|
||||
/// </summary>
|
||||
public int iMaxSockAddr;
|
||||
|
||||
/// <summary>
|
||||
/// 最小地址长度(以字节为单位)。
|
||||
/// </summary>
|
||||
public int iMinSockAddr;
|
||||
|
||||
public int iSocketType;
|
||||
public int iProtocol;
|
||||
public int iProtocolMaxOffset;
|
||||
|
||||
/// <summary>
|
||||
/// 目前,这些值是 BIGENDIAN 和 LITTLEENDIAN) (清单常量,分别指示值为 0 和 1 的 big-endian 或 little-endian
|
||||
/// </summary>
|
||||
public int iNetworkByteOrder;
|
||||
|
||||
public int iSecurityScheme;
|
||||
|
||||
/// <summary>
|
||||
/// 协议支持的最大消息大小(以字节为单位)
|
||||
/// </summary>
|
||||
public uint dwMessageSize;
|
||||
|
||||
public uint dwProviderReserved;
|
||||
|
||||
/// <summary>
|
||||
/// 一个字符数组,其中包含标识协议的可读名称,例如“MSAFD Tcpip [UDP/IP]”。 允许的最大字符数为 WSAPROTOCOL_LEN,定义为 255。
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||
public string szProtocol;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WSAPROTOCOLCHAIN 结构包含构成协议链的目录条目标识符的计数列表。
|
||||
/// <see cref="https://learn.microsoft.com/zh-cn/windows/win32/api/winsock2/ns-winsock2-wsaprotocolchain"/>
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WSAPROTOCOLCHAIN
|
||||
{
|
||||
/// <summary>
|
||||
/// 链的长度(以字节为单位)
|
||||
/// 将 ChainLen 设置为零表示分层协议
|
||||
/// 将 ChainLen 设置为 1 指示基本协议
|
||||
/// 将 ChainLen 设置为大于 1 表示协议链
|
||||
/// ChainEntries[MAX_PROTOCOL_CHAIN]
|
||||
/// </summary>
|
||||
public int ChainLen;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
|
||||
public uint[] ChainEntries;
|
||||
}
|
||||
|
||||
#endregion P/Invoke
|
||||
}
|
||||
@@ -0,0 +1,953 @@
|
||||
using STUN.Client;
|
||||
using STUN.StunResult;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
namespace BD.WTTS.Services.Implementation;
|
||||
|
||||
internal partial class NetworkTestService : INetworkTestService
|
||||
{
|
||||
#region 常量
|
||||
|
||||
// 默认测试地址
|
||||
const string DEFAULT_TESTSERVER_HOSTNAME = "stun.syncthing.net";
|
||||
|
||||
const int DEFAULT_PORT = 3478;
|
||||
const int DEFAULT_TLSPORT = 5349;
|
||||
|
||||
// 默认端口号
|
||||
const int DEFAULT_TESTSTUN3489_PORT = DEFAULT_PORT;
|
||||
|
||||
// 默认端口号
|
||||
const int DEFAULT_TESTSTUN5389_PORT = DEFAULT_PORT;
|
||||
|
||||
#endregion 常量
|
||||
|
||||
public NetworkTestService()
|
||||
{
|
||||
}
|
||||
|
||||
#region Ping
|
||||
|
||||
/// <summary>
|
||||
/// 将 Internet 控制消息协议 (ICMP) 回显请求消息发送到远程计算机,并等待来自该计算机的 ICMP 回送答复消息。 有关 ICMP 消息的详细说明,请参阅 RFC 792,可从 https://www.ietf.org获取。
|
||||
/// </summary>
|
||||
/// <param name="testHostNameOrAddress"></param>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<PingReply> TestPingAsync(string testHostNameOrAddress, TimeSpan timeout, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using Ping p = new Ping();
|
||||
|
||||
var reply = await p.SendPingAsync(
|
||||
testHostNameOrAddress,
|
||||
timeout,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
#endregion Ping
|
||||
|
||||
#region Open/Upload/Download Speed Test
|
||||
|
||||
public async Task<(bool Success, long? DelayMs)> TestOpenUrlAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var testClient = httpClientFunc?.Invoke() ?? new HttpClient();
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
var resp = await testClient.GetAsync(url, HttpCompletionOption.ResponseContentRead, cancellationToken);
|
||||
|
||||
watch.Stop();
|
||||
|
||||
return (true, watch.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "测试打开 URL 异常");
|
||||
return (false, default);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Success, double? Rate)> TestUploadSpeedAsync(
|
||||
string uploadServerUrl,
|
||||
byte[] uploadBytes,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using HttpClient testClient = new HttpClient();
|
||||
|
||||
return await TestUploadSpeedCoreAsync(
|
||||
() => testClient,
|
||||
BuildUploadContent,
|
||||
uploadServerUrl,
|
||||
uploadBytes,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
static MultipartFormDataContent BuildUploadContent(byte[] data)
|
||||
{
|
||||
MultipartFormDataContent content = new MultipartFormDataContent();
|
||||
|
||||
content.Add(new ByteArrayContent(data));
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Success, double? Rate)> TestDownloadSpeedAsync(string downloadUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using HttpClient testClient = new HttpClient();
|
||||
|
||||
return await TestDownloadSpeedCoreAsync(downloadUrl, () => testClient, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试上传速度
|
||||
/// </summary>
|
||||
/// <param name="httpClientFunc">自定义用于发送 http 请求的 client </param>
|
||||
/// <param name="uploadContentFunc">自定义上传内容类型</param>
|
||||
/// <param name="uploadServerUrl"></param>
|
||||
/// <param name="uploadBytes"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
static async Task<(bool Success, double? Rate)> TestUploadSpeedCoreAsync(
|
||||
Func<HttpClient> httpClientFunc,
|
||||
Func<byte[], HttpContent> uploadContentFunc,
|
||||
string uploadServerUrl,
|
||||
byte[] uploadBytes,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uploadServerUrl))
|
||||
return (false, null);
|
||||
|
||||
var uploadContent = uploadContentFunc(uploadBytes);
|
||||
try
|
||||
{
|
||||
var httpClient = httpClientFunc();
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
var resp = await httpClient.PostAsync(uploadServerUrl, uploadContent, cancellationToken);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
watch.Stop();
|
||||
|
||||
return (true, GetRate(uploadBytes.Length, watch.Elapsed.TotalSeconds));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "测试上传异常");
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试下载速度
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="httpClientFunc"></param>
|
||||
/// <returns></returns>
|
||||
static async Task<(bool Success, double? Rate)> TestDownloadSpeedCoreAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var httpClient = httpClientFunc?.Invoke() ?? new HttpClient();
|
||||
|
||||
try
|
||||
{
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
var resp = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
var bytesArray = await resp.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
|
||||
watch.Stop();
|
||||
|
||||
return (true, GetRate(bytesArray.Length, watch.Elapsed.TotalSeconds));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "测试下载异常");
|
||||
return (false, null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (httpClientFunc == null)
|
||||
httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
static double GetRate(long byteArrayLength, double seconds)
|
||||
{
|
||||
if (seconds == 0)
|
||||
throw new ArgumentException("时间不能小于0", nameof(seconds));
|
||||
|
||||
long bits = byteArrayLength * 8;
|
||||
double megabits = bits / 1_000_000.0; // 将比特转换为兆比特
|
||||
|
||||
return megabits / seconds; // 返回速率,单位是 Mbps
|
||||
}
|
||||
|
||||
#endregion Open/Upload/Download Speed Test
|
||||
|
||||
#region STUN 测试
|
||||
|
||||
#region RFC3489
|
||||
|
||||
StunClient3489? _stunClient;
|
||||
|
||||
/// <summary>
|
||||
/// 测试 STUN 客户端 基于 RFC3489
|
||||
/// </summary>
|
||||
/// <param name="testServerHostName">测试服务地址</param>
|
||||
/// <param name="testServerPort">测试服务端口</param>
|
||||
/// <param name="localIPEndPoint">本机 IP </param>
|
||||
/// <param name="force">强制刷新</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<ClassicStunResult?> TestStunClient3489Async(
|
||||
string? testServerHostName = DEFAULT_TESTSERVER_HOSTNAME,
|
||||
int? testServerPort = DEFAULT_TESTSTUN3489_PORT,
|
||||
IPEndPoint? localIPEndPoint = null,
|
||||
bool force = false,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
testServerHostName ??= DEFAULT_TESTSERVER_HOSTNAME;
|
||||
testServerPort ??= DEFAULT_TESTSTUN3489_PORT;
|
||||
|
||||
if (_stunClient == null || force)
|
||||
_stunClient = await GetStunClient3489Async(testServerHostName, testServerPort.Value, localIPEndPoint);
|
||||
|
||||
try
|
||||
{
|
||||
await _stunClient.QueryAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "TestStunClient3489Async 异常");
|
||||
return null;
|
||||
}
|
||||
|
||||
return _stunClient.State;
|
||||
}
|
||||
|
||||
private async ValueTask<StunClient3489> GetStunClient3489Async(string serverHostName, int port, IPEndPoint? localIPEndPoint = null)
|
||||
{
|
||||
IPEndPoint serverEndpoint = await GetServerIPEndPointAsync(serverHostName, port);
|
||||
|
||||
IPEndPoint localEndpoint = localIPEndPoint ?? new IPEndPoint(IPAddress.Any, default);
|
||||
|
||||
var client = new StunClient3489(
|
||||
serverEndpoint,
|
||||
localEndpoint
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private async Task<IPEndPoint> GetServerIPEndPointAsync(string hostNameOrAddress, int port)
|
||||
{
|
||||
var serverIps = await Dns.GetHostAddressesAsync(hostNameOrAddress);
|
||||
|
||||
IPEndPoint serverEndpoint = new IPEndPoint(serverIps.First(), port);
|
||||
|
||||
return serverEndpoint;
|
||||
}
|
||||
|
||||
#endregion RFC3489
|
||||
|
||||
#region RFC5389
|
||||
|
||||
/// <summary>
|
||||
/// 测试 STUN 客户端 基于 RFC5389
|
||||
/// </summary>
|
||||
/// <param name="protocol"></param>
|
||||
/// <param name="testServerHostName"></param>
|
||||
/// <param name="testServerPort"></param>
|
||||
/// <param name="localIPEndPoint"></param>
|
||||
/// <param name="force"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<StunResult5389?> TestStunClient5389Async(
|
||||
TransportProtocol protocol,
|
||||
string? testServerHostName = DEFAULT_TESTSERVER_HOSTNAME,
|
||||
int? testServerPort = DEFAULT_TESTSTUN5389_PORT,
|
||||
IPEndPoint? localIPEndPoint = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
testServerHostName ??= DEFAULT_TESTSERVER_HOSTNAME;
|
||||
testServerPort ??= DEFAULT_TESTSTUN5389_PORT;
|
||||
|
||||
IStunClient5389 client = await GetStunClient5389Async(protocol, testServerHostName, testServerPort.Value, localIPEndPoint);
|
||||
|
||||
if (client == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
await client.QueryAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "TestStunClient5389Async 异常");
|
||||
return null;
|
||||
}
|
||||
|
||||
return client.State;
|
||||
}
|
||||
|
||||
private async ValueTask<IStunClient5389> GetStunClient5389Async(TransportProtocol protocol, string serverHostName, int port, IPEndPoint? localIPEndPoint = null)
|
||||
{
|
||||
IPEndPoint serverEndpoint = await GetServerIPEndPointAsync(serverHostName, port);
|
||||
|
||||
IPEndPoint localEndpoint = localIPEndPoint ?? new IPEndPoint(IPAddress.Any, default);
|
||||
|
||||
IStunClient5389 client = protocol == TransportProtocol.Tcp
|
||||
? new StunClient5389TCP(serverEndpoint, localEndpoint)
|
||||
: new StunClient5389UDP(serverEndpoint, localEndpoint);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
#endregion RFC5389
|
||||
|
||||
#endregion STUN 测试
|
||||
|
||||
#region DNS 查询
|
||||
|
||||
/// <summary>
|
||||
/// 测试 DNS UDP
|
||||
/// </summary>
|
||||
/// <param name="testDomain">要测试的域名</param>
|
||||
/// <param name="dnsServerIp">DNS 服务地址</param>
|
||||
/// <param name="dnsServerPort">DNS 服务端口</param>
|
||||
/// <param name="dnsRecordType"> dns 测试类型 </param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(long DelayMs, IPAddress[] Address)> TestDNSAsync(
|
||||
string testDomain,
|
||||
string dnsServerIp,
|
||||
int dnsServerPort,
|
||||
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using UdpClient udpClient = new UdpClient(dnsServerIp, dnsServerPort);
|
||||
|
||||
var query = new DnsQueryUdpRequest(testDomain, dnsRecordType);
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
await udpClient.SendAsync(query, cancellationToken);
|
||||
|
||||
var receivedData = await udpClient.ReceiveAsync(cancellationToken);
|
||||
|
||||
watch.Stop();
|
||||
|
||||
var dnsQueryResponse = new DnsQueryUdpResponse(receivedData.Buffer);
|
||||
|
||||
return (watch.ElapsedMilliseconds, dnsQueryResponse.GetAddresses().ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 DNS Over Https
|
||||
/// </summary>
|
||||
/// <param name="testDomain"></param>
|
||||
/// <param name="dohServer"></param>
|
||||
/// <param name="dnsRecordType"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(long DelayMs, IPAddress[] Address)> TestDNSOverHttpsAsync(
|
||||
string testDomain,
|
||||
string dohServer,
|
||||
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
UseCookies = false,
|
||||
UseProxy = false,
|
||||
Proxy = HttpNoProxy.Instance,
|
||||
};
|
||||
using HttpClient client = new HttpClient(handler);
|
||||
|
||||
string queryUrl = $"{dohServer}?name={testDomain}&type={dnsRecordType}";
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
HttpResponseMessage? resp = null;
|
||||
try
|
||||
{
|
||||
resp = await client.GetAsync(queryUrl, HttpCompletionOption.ResponseContentRead, cancellationToken);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
finally
|
||||
{
|
||||
watch.Stop();
|
||||
}
|
||||
|
||||
var queryResp = await resp.Content.ReadFromJsonAsync<DnsQueryJsonResponse>();
|
||||
|
||||
return (watch.ElapsedMilliseconds, queryResp?.GetAddresses().ToArray() ?? Array.Empty<IPAddress>());
|
||||
}
|
||||
|
||||
#region DNS 解析定义
|
||||
|
||||
internal interface IDnsQueryResponse
|
||||
{
|
||||
List<DnsQueryAnswerRecord> Answers { get; }
|
||||
|
||||
IEnumerable<IPAddress> GetAddresses();
|
||||
}
|
||||
|
||||
internal record DnsQueryUdpResponse : DnsQueryUdpRequest, IDnsQueryResponse
|
||||
{
|
||||
public ushort AnswerCount { get; init; } // (ushort)((_origin[6] << 8) | _origin[7]);
|
||||
|
||||
public ushort AuthorityCount { get; init; } // (ushort)((_origin[8] << 8) | _origin[9]);
|
||||
|
||||
public ushort AdditionalCount { get; init; } // (ushort)((_origin[10] << 8) | _origin[11]);
|
||||
|
||||
public List<DnsQueryAnswerRecord> Answers { get; init; }
|
||||
|
||||
private readonly byte[] _origin;
|
||||
|
||||
public DnsQueryUdpResponse(byte[] origin) : base(origin)
|
||||
{
|
||||
_origin = origin;
|
||||
|
||||
AnswerCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
AuthorityCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
AdditionalCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
|
||||
// 跳过查询问题部分
|
||||
for (int i = 0; i < QuestionCount; i++)
|
||||
{
|
||||
Position = SkipName(origin, Position);
|
||||
Position += 4; // 跳过类型
|
||||
}
|
||||
|
||||
Answers = new List<DnsQueryAnswerRecord>();
|
||||
|
||||
Position = ParseRecords(origin, Position, AnswerCount, Answers);
|
||||
Position = ParseRecords(origin, Position, AuthorityCount, Answers);
|
||||
Position = ParseRecords(origin, Position, AdditionalCount, Answers);
|
||||
}
|
||||
|
||||
private static int ParseRecords(byte[] response, int position, int recordCount, List<DnsQueryAnswerRecord> answers)
|
||||
{
|
||||
for (int i = 0; i < recordCount; i++)
|
||||
{
|
||||
position = ParseName(response, position, out var parsedName);
|
||||
|
||||
int @type = (response[position++] << 8) | response[position++];
|
||||
int @class = (response[position++] << 8) | response[position++];
|
||||
long ttl = (response[position++] << 24) | (response[position++] << 16) | (response[position++] << 8) | response[position++];
|
||||
int dataLength = (response[position++] << 8) | response[position++];
|
||||
|
||||
if (@type == (int)DnsQueryAnswerRecord.DnsRecordType.A && dataLength == 4) // Type A (IPv4)
|
||||
{
|
||||
var ipAddress = new byte[4];
|
||||
Array.Copy(response, position, ipAddress, 0, 4);
|
||||
|
||||
answers.Add(
|
||||
new DnsQueryAnswerRecord.A(
|
||||
parsedName!,
|
||||
(DnsQueryAnswerRecord.DnsRecordType)@type,
|
||||
ttl,
|
||||
new IPAddress(ipAddress)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (@type == (int)DnsQueryAnswerRecord.DnsRecordType.AAAA && dataLength == 16) // Type AAAA (IPv6)
|
||||
{
|
||||
var ipAddress = new byte[16];
|
||||
Array.Copy(response, position, ipAddress, 0, 16);
|
||||
|
||||
answers.Add(
|
||||
new DnsQueryAnswerRecord.AAAA(
|
||||
parsedName!,
|
||||
(DnsQueryAnswerRecord.DnsRecordType)@type,
|
||||
ttl,
|
||||
new IPAddress(ipAddress)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.CNAME)
|
||||
{
|
||||
ParseName(response, position, out var parsedCName);
|
||||
|
||||
if (string.IsNullOrEmpty(parsedCName))
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), $"解析 DNS CName字段异常 name :{parsedName}, data length :{dataLength}");
|
||||
continue;
|
||||
}
|
||||
|
||||
answers.Add(
|
||||
new DnsQueryAnswerRecord.CName(
|
||||
parsedName!,
|
||||
(DnsQueryAnswerRecord.DnsRecordType)@type,
|
||||
ttl,
|
||||
parsedCName!
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), $"未支持的 DNS响应记录 type :{@type} ,class :{@class} name :{parsedName}, data length :{dataLength}");
|
||||
continue;
|
||||
}
|
||||
|
||||
position += dataLength;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
private static int ParseName(byte[] response, int position, out string? name)
|
||||
{
|
||||
StringBuilder nameBuilder = new StringBuilder();
|
||||
int originalPosition = position;
|
||||
bool isPointer = false;
|
||||
|
||||
// 0表示结束
|
||||
while (response[position] != 0)
|
||||
{
|
||||
// dns 协议 名称可能是 指针 或者 直接表示
|
||||
// 如果大于等于 192 表示是指针开始
|
||||
if (response[position] >= 192)
|
||||
{
|
||||
if (!isPointer)
|
||||
{
|
||||
originalPosition = position + 2;
|
||||
isPointer = true;
|
||||
}
|
||||
// 因为 00111111 可以忽略前两位 用 0x3F 掩码获取指针
|
||||
int pointer = ((response[position] & 0x3F) << 8) | response[position + 1];
|
||||
position = pointer;
|
||||
}
|
||||
else
|
||||
{
|
||||
int length = response[position];
|
||||
position++;
|
||||
nameBuilder.Append(Encoding.ASCII.GetString(response, position, length));
|
||||
position += length;
|
||||
if (response[position] != 0)
|
||||
{
|
||||
nameBuilder.Append('.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPointer)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
else
|
||||
{
|
||||
position = originalPosition;
|
||||
}
|
||||
|
||||
name = nameBuilder.ToString();
|
||||
return position;
|
||||
}
|
||||
|
||||
private static int SkipName(byte[] response, int position)
|
||||
{
|
||||
if (response[position] >= 192)
|
||||
{
|
||||
return position + 2; // Name is a pointer
|
||||
}
|
||||
|
||||
while (response[position] != 0)
|
||||
{
|
||||
position += response[position] + 1;
|
||||
}
|
||||
return position + 1;
|
||||
}
|
||||
|
||||
public IEnumerable<IPAddress> GetAddresses()
|
||||
{
|
||||
foreach (var answer in Answers)
|
||||
{
|
||||
if (answer is DnsQueryAnswerRecord.A a)
|
||||
{
|
||||
yield return a.Data;
|
||||
}
|
||||
else if (answer is DnsQueryAnswerRecord.AAAA aaaa)
|
||||
{
|
||||
yield return aaaa.Data;
|
||||
}
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
internal record DnsQueryUdpRequest
|
||||
{
|
||||
public ushort QueryId { get; init; } // (ushort)((_origin[0] << 8) | _origin[1]);
|
||||
|
||||
public ushort Flags { get; init; } //(ushort)((_origin[2] << 8) | _origin[3]);
|
||||
|
||||
public ushort QuestionCount { get; init; } // (ushort)((_origin[4] << 8) | _origin[5]);
|
||||
|
||||
public int Length => _origin.Length;
|
||||
|
||||
protected int Position { get; set; }
|
||||
|
||||
private readonly byte[] _origin;
|
||||
|
||||
public DnsQueryUdpRequest(byte[] origin)
|
||||
{
|
||||
_origin = origin;
|
||||
|
||||
Position = default;
|
||||
QueryId = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
Flags = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
QuestionCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
}
|
||||
|
||||
public DnsQueryUdpRequest(string domainName, DnsQueryAnswerRecord.DnsRecordType type = DnsQueryAnswerRecord.DnsRecordType.A)
|
||||
: this(CreateDnsQuery(domainName, type))
|
||||
{
|
||||
}
|
||||
|
||||
public static byte[] CreateDnsQuery(string domainName, DnsQueryAnswerRecord.DnsRecordType type = DnsQueryAnswerRecord.DnsRecordType.A)
|
||||
{
|
||||
// 12字节报文头部
|
||||
const int DNS_QUERY_HEADER_BYTECOUNT = 12;
|
||||
// 2字节终止符长度
|
||||
const int DNS_QUERY_END_BYTECOUNT = 2;
|
||||
// 4字节查询类型和查询类字段
|
||||
const int DNS_QUERY_QUERYTYPE_BYTECOUNT = 4;
|
||||
|
||||
// dns queryId 范围是一个16位字段,16位无符号的范围就是0-65535
|
||||
var queryId = (ushort)Random.Shared.Next(0, 65536);
|
||||
|
||||
byte[] dnsQuery = new byte[
|
||||
DNS_QUERY_HEADER_BYTECOUNT
|
||||
+ domainName.Length
|
||||
+ DNS_QUERY_END_BYTECOUNT
|
||||
+ DNS_QUERY_QUERYTYPE_BYTECOUNT
|
||||
];
|
||||
|
||||
BuildQueryHeader(queryId, dnsQuery);
|
||||
BuildBody(domainName, dnsQuery);
|
||||
|
||||
return dnsQuery;
|
||||
|
||||
void BuildQueryHeader(ushort queryId, byte[] query)
|
||||
{
|
||||
// 查询 ID 是一个 16 位(2 字节)的字段。
|
||||
// 为了将这个 16 位的整数值分配到字节数组 dnsQuery 的两个字节中,
|
||||
// 我们需要将其高 8 位和低 8 位分别存储在数组的第一个和第二个字节中
|
||||
query[0] = (byte)(queryId >> 8);
|
||||
query[1] = (byte)(queryId & 0xFF);
|
||||
|
||||
// 标志字段(Flags) 2 - 3 包含各种控制标志
|
||||
// 如查询/响应(QR)、操作码(Opcode)、授权回答(AA)、截断(TC)、递归期望(RD)、递归可用(RA)等
|
||||
query[2] = 0x01;
|
||||
|
||||
// 问题计数(QDCOUNT)4 - 5 表示查询问题部分的数量
|
||||
query[5] = 0x01;
|
||||
|
||||
// 其他查询报文中通常为 0
|
||||
// 回答记录计数(ANCOUNT),授权记录计数(NSCOUNT),附加记录计数(ARCOUNT)
|
||||
}
|
||||
|
||||
void BuildBody(string domainName, byte[] query)
|
||||
{
|
||||
// 获取域名组成部分 因为后面要根据部分长度和内容填充结果
|
||||
string[] domainParts = domainName.Split('.');
|
||||
|
||||
int currentPosition = 12;
|
||||
|
||||
// 根据 dns 查询报文 每一部分开始都要填充这一部分的长度
|
||||
foreach (var domainPart in domainParts)
|
||||
{
|
||||
// 标志当前 part 的长度
|
||||
query[currentPosition++] = (byte)domainPart.Length;
|
||||
|
||||
// 获取填充的字节
|
||||
var fillBytes = Encoding.ASCII.GetBytes(domainPart);
|
||||
|
||||
// 移动到当前查询体中
|
||||
fillBytes.CopyTo(query, currentPosition);
|
||||
|
||||
// 移动当前填充开始位置
|
||||
currentPosition += fillBytes.Length;
|
||||
}
|
||||
// 0 表示域名结束
|
||||
query[currentPosition++] = 0x00;
|
||||
|
||||
// 填充 查询类型和查询类字段
|
||||
query[currentPosition++] = 0x00;
|
||||
query[currentPosition++] = (byte)type; // 表示查询类型为 A 记录(IPv4 地址)。
|
||||
query[currentPosition++] = 0x00;
|
||||
query[currentPosition++] = 0x01; // 表示查询类为 IN(Internet 类)。
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator byte[](DnsQueryUdpRequest data)
|
||||
{
|
||||
return data._origin;
|
||||
}
|
||||
|
||||
public static implicit operator ReadOnlyMemory<byte>(DnsQueryUdpRequest data)
|
||||
{
|
||||
return data._origin;
|
||||
}
|
||||
}
|
||||
|
||||
internal record DnsQueryJsonResponse : IDnsQueryResponse
|
||||
{
|
||||
[S_JsonProperty("Status")]
|
||||
public int Status { get; init; }
|
||||
|
||||
[S_JsonProperty("TC")]
|
||||
public bool TC { get; init; }
|
||||
|
||||
[S_JsonProperty("RD")]
|
||||
public bool RD { get; init; }
|
||||
|
||||
[S_JsonProperty("RA")]
|
||||
public bool RA { get; init; }
|
||||
|
||||
[S_JsonProperty("AD")]
|
||||
public bool AD { get; init; }
|
||||
|
||||
[S_JsonProperty("CD")]
|
||||
public bool CD { get; init; }
|
||||
|
||||
[S_JsonProperty("Answer")]
|
||||
public List<DnsQueryAnswerRecord> Answers { get; init; }
|
||||
|
||||
public DnsQueryJsonResponse(
|
||||
int status,
|
||||
bool tc,
|
||||
bool rd,
|
||||
bool ra,
|
||||
bool ad,
|
||||
bool cd,
|
||||
List<DnsQueryAnswerRecord>? answers
|
||||
)
|
||||
{
|
||||
Status = status;
|
||||
TC = tc;
|
||||
RD = rd;
|
||||
RA = ra;
|
||||
AD = ad;
|
||||
CD = cd;
|
||||
Answers = answers?.ToList() ?? new();
|
||||
}
|
||||
|
||||
public IEnumerable<IPAddress> GetAddresses()
|
||||
{
|
||||
return Answers.Aggregate(Enumerable.Empty<IPAddress>(), (acc, x) =>
|
||||
{
|
||||
if (x is DnsQueryAnswerRecord.A ipv4)
|
||||
{
|
||||
return acc.Append(ipv4.Data);
|
||||
}
|
||||
else if (x is DnsQueryAnswerRecord.AAAA ipv6)
|
||||
{
|
||||
return acc.Append(ipv6.Data);
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class DnsQueryAnswerRecordConverter : System.Text.Json.Serialization.JsonConverter<DnsQueryAnswerRecord>
|
||||
{
|
||||
public override DnsQueryAnswerRecord? Read(ref Utf8JsonReader reader, Type typeToConvert, SystemTextJsonSerializerOptions options)
|
||||
{
|
||||
if (typeToConvert == typeof(DnsQueryAnswerRecord))
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
var json = JsonDocument.ParseValue(ref reader);
|
||||
|
||||
if (json.RootElement.TryGetProperty("type", out var typeElement))
|
||||
{
|
||||
var type = typeElement.GetInt32();
|
||||
|
||||
if (type == (int)DnsQueryAnswerRecord.DnsRecordType.A)
|
||||
{
|
||||
return json.RootElement.Deserialize<DnsQueryAnswerRecord.A>();
|
||||
}
|
||||
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.AAAA)
|
||||
{
|
||||
return json.RootElement.Deserialize<DnsQueryAnswerRecord.AAAA>();
|
||||
}
|
||||
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.CNAME)
|
||||
{
|
||||
return json.RootElement.Deserialize<DnsQueryAnswerRecord.CName>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DnsQueryAnswerRecord value, SystemTextJsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[System.Text.Json.Serialization.JsonConverter(typeof(DnsQueryAnswerRecordConverter))]
|
||||
[JsonDerivedType(typeof(DnsQueryAnswerRecord.A))]
|
||||
[JsonDerivedType(typeof(DnsQueryAnswerRecord.AAAA))]
|
||||
[JsonDerivedType(typeof(DnsQueryAnswerRecord.CName))]
|
||||
internal record DnsQueryAnswerRecord
|
||||
{
|
||||
[S_JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[S_JsonProperty("type")]
|
||||
public DnsRecordType Type { get; set; }
|
||||
|
||||
[S_JsonProperty("TTL")]
|
||||
public long Ttl { get; set; }
|
||||
|
||||
public DnsQueryAnswerRecord(
|
||||
string name,
|
||||
DnsRecordType type,
|
||||
long ttl
|
||||
)
|
||||
{
|
||||
Name = name;
|
||||
Type = @type;
|
||||
Ttl = ttl;
|
||||
//Data = data;
|
||||
//DataLength = data.Length;
|
||||
}
|
||||
|
||||
#region DnsQueryAnswerRecord 子类派生
|
||||
|
||||
internal record A : DnsQueryAnswerRecord
|
||||
{
|
||||
public A(string name, DnsRecordType type, long ttl, IPAddress data) : base(name, type, ttl)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
[S_JsonProperty("data")]
|
||||
[SystemTextJsonConverter(typeof(IPAddressConverter))]
|
||||
public IPAddress Data { get; set; }
|
||||
}
|
||||
|
||||
internal record AAAA : DnsQueryAnswerRecord
|
||||
{
|
||||
public AAAA(string name, DnsRecordType type, long ttl, IPAddress data) : base(name, type, ttl)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
[S_JsonProperty("data")]
|
||||
[SystemTextJsonConverter(typeof(IPAddressConverter))]
|
||||
public IPAddress Data { get; set; }
|
||||
}
|
||||
|
||||
internal record CName : DnsQueryAnswerRecord
|
||||
{
|
||||
public CName(string name, DnsRecordType type, long ttl, string data) : base(name, type, ttl)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
[S_JsonProperty("data")]
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
#endregion DnsQueryAnswerRecord 子类派生
|
||||
|
||||
class IPAddressConverter : System.Text.Json.Serialization.JsonConverter<IPAddress>
|
||||
{
|
||||
public override IPAddress? Read(ref Utf8JsonReader reader, Type typeToConvert, SystemTextJsonSerializerOptions options)
|
||||
{
|
||||
IPAddress? result = null;
|
||||
|
||||
// 目前只解析字符串
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
string? ipString = reader.GetString();
|
||||
result = !string.IsNullOrWhiteSpace(ipString) ? IPAddress.Parse(ipString) : default;
|
||||
}
|
||||
// byte 数组支持
|
||||
else if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
List<byte> bytes = new List<byte>();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
break;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
{
|
||||
bytes.Add((byte)reader.GetInt32());
|
||||
}
|
||||
else if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
if (byte.TryParse(reader.GetString(), out byte byteValue))
|
||||
{
|
||||
bytes.Add(byteValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = bytes.Count == 4 || bytes.Count == 16 ? new IPAddress(bytes.ToArray()) : default;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, IPAddress value, SystemTextJsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal enum DnsRecordClass : ushort
|
||||
{
|
||||
Internet = 1, // Internet(IN)
|
||||
Chaos = 3, // Chaos(CH)
|
||||
Hesiod = 4, // Hesiod(HS)
|
||||
QCLASSNONE = 254, // QCLASS NONE
|
||||
QCLASSANY = 255, // QCLASS * (ANY)
|
||||
}
|
||||
|
||||
// 域名系统记录类型 (省略了有些未使用的类型)
|
||||
internal enum DnsRecordType : byte
|
||||
{
|
||||
A = 1, // 指定域名对应的IPv4地址
|
||||
AAAA = 28, // 指定域名对应的IPv6地址
|
||||
CNAME = 5, // 别名记录,
|
||||
MX = 15, // 邮件交换记录,
|
||||
NS = 2, // 指定该域名由哪个DNS服务器来进行解析
|
||||
TXT = 16, // 主机名或域名的说明
|
||||
SOA = 6, // 起始授权机构
|
||||
PTR = 12, // 反向IP查询
|
||||
ANY = 255, // 所有DNS记录类型
|
||||
}
|
||||
}
|
||||
|
||||
#endregion DNS 解析定义
|
||||
|
||||
#endregion DNS 查询
|
||||
}
|
||||
@@ -87,6 +87,12 @@ partial interface IAcceleratorService // XunYou
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<ApiRsp<XunYouStartGameCode>> XY_StartGame(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 迅游加速器显示隐藏
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<ApiRsp<int>> XY_ShowWinodw(bool showHide, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
partial interface IXunYouAccelStateToFrontendCallback
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using STUN.StunResult;
|
||||
using System.Net.NetworkInformation;
|
||||
using static BD.WTTS.Services.Implementation.NetworkTestService;
|
||||
|
||||
namespace BD.WTTS.Services;
|
||||
|
||||
internal interface INetworkTestService : IHttpRequestTestService, IStunTestService
|
||||
{
|
||||
static INetworkTestService Instance => Ioc.Get<INetworkTestService>();
|
||||
|
||||
/// <summary>
|
||||
/// Ping
|
||||
/// </summary>
|
||||
/// <param name="testHostNameOrAddress"></param>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<PingReply> TestPingAsync(string testHostNameOrAddress, TimeSpan timeout, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 测试 DNS UDP
|
||||
/// </summary>
|
||||
/// <param name="testDomain"></param>
|
||||
/// <param name="dnsServerIp"></param>
|
||||
/// <param name="dnsServerPort"></param>
|
||||
/// <param name="dnsRecordType"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(long DelayMs, IPAddress[] Address)> TestDNSAsync(
|
||||
string testDomain,
|
||||
string dnsServerIp,
|
||||
int dnsServerPort,
|
||||
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 测试 DNS Over Https
|
||||
/// </summary>
|
||||
/// <param name="testDomain"></param>
|
||||
/// <param name="dohServer"></param>
|
||||
/// <param name="dnsRecordType"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(long DelayMs, IPAddress[] Address)> TestDNSOverHttpsAsync(
|
||||
string testDomain,
|
||||
string dohServer,
|
||||
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
||||
internal interface IHttpRequestTestService
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试打开网址
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="httpClientFunc"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(bool Success, long? DelayMs)> TestOpenUrlAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 测试上传速度
|
||||
/// </summary>
|
||||
/// <param name="uploadServerUrl"></param>
|
||||
/// <param name="uploadBytes"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(bool Success, double? Rate)> TestUploadSpeedAsync(
|
||||
string uploadServerUrl,
|
||||
byte[] uploadBytes,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 测试下载速度
|
||||
/// </summary>
|
||||
/// <param name="downloadUrl"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(bool Success, double? Rate)> TestDownloadSpeedAsync(string downloadUrl, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal interface IStunTestService
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试 STUN Client RFC3489
|
||||
/// </summary>
|
||||
/// <param name="testServerHostName">STUN 服务地址</param>
|
||||
/// <param name="testServerPort">STUN 服务端口</param>
|
||||
/// <param name="localIPEndPoint">本机IP</param>
|
||||
/// <param name="force">刷新测试 Client</param>
|
||||
/// <remarks></remarks>
|
||||
/// <returns></returns>
|
||||
Task<ClassicStunResult?> TestStunClient3489Async(
|
||||
string? testServerHostName = default,
|
||||
int? testServerPort = default,
|
||||
IPEndPoint? localIPEndPoint = default,
|
||||
bool force = false,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 测试 STUN Client RFC5389
|
||||
/// </summary>
|
||||
/// <param name="protocol">使用的传输协议 Tcp,Udp</param>
|
||||
/// <param name="testServerHostName">STUN 服务地址</param>
|
||||
/// <param name="testServerPort">STUN 服务端口</param>
|
||||
/// <param name="localIPEndPoint">本机IP</param>
|
||||
/// <param name="force"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<StunResult5389?> TestStunClient5389Async(
|
||||
TransportProtocol protocol,
|
||||
string? testServerHostName = default,
|
||||
int? testServerPort = default,
|
||||
IPEndPoint? localIPEndPoint = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,14 @@ public sealed partial class GameAcceleratorService
|
||||
DeleteMyGameCommand = ReactiveCommand.Create<XunYouGameViewModel>(DeleteMyGame);
|
||||
GameAcceleratorCommand = ReactiveCommand.CreateFromTask<XunYouGameViewModel>(GameAccelerator);
|
||||
GameLaunchCommand = ReactiveCommand.CreateFromTask<XunYouGameViewModel>(GameLaunch);
|
||||
InstallAcceleratorCommand = ReactiveCommand.CreateFromTask(InstallAccelerator);
|
||||
InstallAcceleratorCommand = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
if (await IWindowManager.Instance.ShowTaskDialogAsync(new MessageBoxWindowViewModel(),
|
||||
pageContent: new AcceleratorPathAskBox(), title: "游戏加速", isCancelButton: false, isOkButton: false))
|
||||
{
|
||||
await InstallAccelerator();
|
||||
}
|
||||
});
|
||||
UninstallAcceleratorCommand = ReactiveCommand.Create(UninstallAccelerator);
|
||||
AcceleratorChangeAreaCommand = ReactiveCommand.Create<XunYouGameViewModel>(AcceleratorChangeArea);
|
||||
|
||||
@@ -247,8 +254,13 @@ public sealed partial class GameAcceleratorService
|
||||
/// <summary>
|
||||
/// 设置加速游戏状态
|
||||
/// </summary>
|
||||
void SetGameStatus(XunYouGameViewModel game, int areaId = 0, int serverId = 0)
|
||||
async void SetGameStatus(XunYouGameViewModel game, int areaId = 0, int serverId = 0)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentAcceleratorGame != null && CurrentAcceleratorGame.Id == game.Id)
|
||||
{
|
||||
CurrentAcceleratorGame.IsAccelerating = false;
|
||||
@@ -295,6 +307,10 @@ public sealed partial class GameAcceleratorService
|
||||
|
||||
//加速后
|
||||
Toast.Show(ToastIcon.Success, "加速成功");
|
||||
if (ProxySettings.AutoShowWattAcceleratorWindow.Value)
|
||||
{
|
||||
_ = ShowXunYouWindow(true);
|
||||
}
|
||||
int testSpeedCallback(SpeedCallbackWrapper w)
|
||||
{
|
||||
if (CurrentAcceleratorGame != null)
|
||||
@@ -321,6 +337,11 @@ public sealed partial class GameAcceleratorService
|
||||
|
||||
public async Task GameAccelerator(XunYouGameViewModel app)
|
||||
{
|
||||
if (!XunYouSDK.IsSupported)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (app.IsAccelerating)
|
||||
return;
|
||||
|
||||
@@ -348,7 +369,7 @@ public sealed partial class GameAcceleratorService
|
||||
//}
|
||||
|
||||
if (!await IWindowManager.Instance.ShowTaskDialogAsync(new MessageBoxWindowViewModel(),
|
||||
pageContent: new AcceleratorPathAskBox(), title: "未安装加速插件", isCancelButton: true))
|
||||
pageContent: new AcceleratorPathAskBox(), title: "游戏加速", isCancelButton: false, isOkButton: false))
|
||||
{
|
||||
app.IsAccelerating = false;
|
||||
return;
|
||||
@@ -746,4 +767,14 @@ public sealed partial class GameAcceleratorService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShowXunYouWindow(bool showHide)
|
||||
{
|
||||
//if (UserService.Current.User?.WattOpenId != null)
|
||||
// await Ioc.Get<IAcceleratorService>().XY_StartEx2(
|
||||
// UserService.Current.User.WattOpenId,
|
||||
// UserService.Current.User.NickName, 0, 0, 0);
|
||||
|
||||
var result = await Ioc.Get<IAcceleratorService>().XY_ShowWinodw(showHide);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ partial class ProxyService
|
||||
IReadOnlyCollection<ScriptIPCDTO>? scripts = default;
|
||||
bool isEnableScript = ProxySettings.IsEnableScript.Value;
|
||||
bool isOnlyWorkSteamBrowser = ProxySettings.IsOnlyWorkSteamBrowser.Value;
|
||||
ushort proxyPort = ProxySettings.SystemProxyPortId.Value;
|
||||
ushort proxyPort = ProxySettings.SystemProxyPortId.Value == 0 ? ProxySettings.SystemProxyPortId.Default : ProxySettings.SystemProxyPortId.Value;
|
||||
string? proxyIp = ProxySettings.SystemProxyIp.Value;
|
||||
proxyMode = defaultProxyMode;
|
||||
bool isProxyGOG = ProxySettings.IsProxyGOG.Value;
|
||||
@@ -63,8 +63,8 @@ partial class ProxyService
|
||||
string? twoLevelAgentPassword = ProxySettings.TwoLevelAgentPassword.Value;
|
||||
string? proxyDNS = ProxySettings.ProxyMasterDns.Value;
|
||||
bool isSupportIpv6 = await RefreshIpv6Support();
|
||||
bool useDoh = ProxySettings.UseDoh2.Value;
|
||||
string? customDohAddres = ProxySettings.CustomDohAddres.Value;
|
||||
bool useDoh = ProxySettings.UseDoh.Value;
|
||||
string? customDohAddres = ProxySettings.CustomDohAddres2.Value;
|
||||
|
||||
Lazy<IPAddress> proxyIp_ = new(() => ReverseProxySettings.GetProxyIp(proxyIp));
|
||||
void SetProxyIp(IPAddress proxyIPAddress)
|
||||
@@ -206,7 +206,7 @@ partial class ProxyService
|
||||
}
|
||||
}
|
||||
|
||||
string? proxyToken = proxyDomains.Any_Nullable(s => s.ProxyType == ProxyType.ServerAccelerate) ?
|
||||
string? proxyToken = proxyDomains.Any_Nullable(s => s.ProxyType == ProxyType.ServerAccelerate || s.Items.Any_Nullable(x => x.ProxyType == ProxyType.ServerAccelerate)) ?
|
||||
await TryRequestServerSideProxyToken() : null;
|
||||
|
||||
ReverseProxySettings reverseProxySettings = new(proxyDomains, scripts,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
using Avalonia.Data;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Org.BouncyCastle.Bcpg.OpenPgp;
|
||||
using System.Linq;
|
||||
|
||||
namespace BD.WTTS.Services;
|
||||
@@ -151,6 +152,7 @@ public sealed partial class ProxyService
|
||||
|
||||
ProxyStarting = false;
|
||||
ProxyStatus = startOrStop;
|
||||
IsAnyProxyScripts = ProxyStatus && IsEnableScript;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +262,16 @@ public sealed partial class ProxyService
|
||||
[Reactive]
|
||||
public bool ProxyStatus { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public bool IsAnyProxyScripts { get; set; }
|
||||
|
||||
private bool CheckProxyScriptsEnable()
|
||||
{
|
||||
if (!ProxyScripts.Items.Any_Nullable())
|
||||
return false;
|
||||
return ProxyScripts.Items!.Any(w => !w.Disable);
|
||||
}
|
||||
|
||||
#endregion 代理状态启动退出
|
||||
|
||||
static bool IsProgramStartupRunProxy()
|
||||
@@ -454,7 +466,7 @@ public sealed partial class ProxyService
|
||||
{
|
||||
if (nodes == null)
|
||||
return Enumerable.Empty<string>();
|
||||
return nodes.Where(s => s.ThreeStateEnable != false).SelectMany(node => new[] { node.Id.ToString() }.Concat(GetAccelerateEnableAllIds(node.Items)));
|
||||
return nodes.Where(s => s.ThreeStateEnable == true).SelectMany(node => new[] { node.Id.ToString() }.Concat(GetAccelerateEnableAllIds(node.Items)));
|
||||
}
|
||||
|
||||
private void RestoreAccelerateEnableAllIds(IEnumerable<AccelerateProjectDTO> nodes, IReadOnlyCollection<string> enableItems)
|
||||
|
||||
@@ -148,18 +148,23 @@ public partial interface IProxySettings
|
||||
/// <summary>
|
||||
/// 启用 DNS over HTTPS
|
||||
/// </summary>
|
||||
bool UseDoh2 { get; set; }
|
||||
bool UseDoh { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 DNS over HTTPS 地址
|
||||
/// </summary>
|
||||
string? CustomDohAddres { get; set; }
|
||||
string? CustomDohAddres2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加速页面 Tab 选项卡选中下标
|
||||
/// </summary>
|
||||
int AcceleratorTabsSelectedIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加速后自动唤起 Watt 加速器界面
|
||||
/// </summary>
|
||||
bool AutoShowWattAcceleratorWindow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启用脚本自动检查更新的默认值
|
||||
/// </summary>
|
||||
@@ -268,16 +273,21 @@ public partial interface IProxySettings
|
||||
/// <summary>
|
||||
/// 启用 DNS over HTTPS的默认值
|
||||
/// </summary>
|
||||
const bool DefaultUseDoh2 = false;
|
||||
const bool DefaultUseDoh = true;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 DNS over HTTPS 地址的默认值
|
||||
/// </summary>
|
||||
const string? DefaultCustomDohAddres = null;
|
||||
const string? DefaultCustomDohAddres2 = null;
|
||||
|
||||
/// <summary>
|
||||
/// 加速页面 Tab 选项卡选中下标的默认值
|
||||
/// </summary>
|
||||
static readonly int DefaultAcceleratorTabsSelectedIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 加速后自动唤起 Watt 加速器界面的默认值
|
||||
/// </summary>
|
||||
const bool DefaultAutoShowWattAcceleratorWindow = true;
|
||||
|
||||
}
|
||||
|
||||
@@ -189,13 +189,13 @@ public sealed partial class ProxySettings_ : IProxySettings, ISettings, ISetting
|
||||
/// 启用 DNS over HTTPS
|
||||
/// </summary>
|
||||
[MPKey(21), MP2Key(21), JsonPropertyOrder(21)]
|
||||
public bool UseDoh2 { get; set; } = IProxySettings.DefaultUseDoh2;
|
||||
public bool UseDoh { get; set; } = IProxySettings.DefaultUseDoh;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 DNS over HTTPS 地址
|
||||
/// </summary>
|
||||
[MPKey(22), MP2Key(22), JsonPropertyOrder(22)]
|
||||
public string? CustomDohAddres { get; set; } = IProxySettings.DefaultCustomDohAddres;
|
||||
public string? CustomDohAddres2 { get; set; } = IProxySettings.DefaultCustomDohAddres2;
|
||||
|
||||
/// <summary>
|
||||
/// 加速页面 Tab 选项卡选中下标
|
||||
@@ -203,6 +203,12 @@ public sealed partial class ProxySettings_ : IProxySettings, ISettings, ISetting
|
||||
[MPKey(23), MP2Key(23), JsonPropertyOrder(23)]
|
||||
public int AcceleratorTabsSelectedIndex { get; set; } = IProxySettings.DefaultAcceleratorTabsSelectedIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 加速后自动唤起 Watt 加速器界面
|
||||
/// </summary>
|
||||
[MPKey(24), MP2Key(24), JsonPropertyOrder(24)]
|
||||
public bool AutoShowWattAcceleratorWindow { get; set; } = IProxySettings.DefaultAutoShowWattAcceleratorWindow;
|
||||
|
||||
}
|
||||
|
||||
public static partial class ProxySettings
|
||||
@@ -356,14 +362,14 @@ public static partial class ProxySettings
|
||||
/// <summary>
|
||||
/// 启用 DNS over HTTPS
|
||||
/// </summary>
|
||||
public static SettingsStructProperty<bool, ProxySettings_> UseDoh2 { get; }
|
||||
= new(DefaultUseDoh2);
|
||||
public static SettingsStructProperty<bool, ProxySettings_> UseDoh { get; }
|
||||
= new(DefaultUseDoh);
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 DNS over HTTPS 地址
|
||||
/// </summary>
|
||||
public static SettingsProperty<string, ProxySettings_> CustomDohAddres { get; }
|
||||
= new(DefaultCustomDohAddres);
|
||||
public static SettingsProperty<string, ProxySettings_> CustomDohAddres2 { get; }
|
||||
= new(DefaultCustomDohAddres2);
|
||||
|
||||
/// <summary>
|
||||
/// 加速页面 Tab 选项卡选中下标
|
||||
@@ -371,4 +377,10 @@ public static partial class ProxySettings
|
||||
public static SettingsStructProperty<int, ProxySettings_> AcceleratorTabsSelectedIndex { get; }
|
||||
= new(DefaultAcceleratorTabsSelectedIndex);
|
||||
|
||||
/// <summary>
|
||||
/// 加速后自动唤起 Watt 加速器界面
|
||||
/// </summary>
|
||||
public static SettingsStructProperty<bool, ProxySettings_> AutoShowWattAcceleratorWindow { get; }
|
||||
= new(DefaultAutoShowWattAcceleratorWindow);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using AppResources = BD.WTTS.Client.Resources.Strings;
|
||||
|
||||
using BD.WTTS.Client.Resources;
|
||||
using BD.WTTS.UI.Views.Pages;
|
||||
using AppResources = BD.WTTS.Client.Resources.Strings;
|
||||
|
||||
namespace BD.WTTS.UI.ViewModels;
|
||||
|
||||
public sealed partial class AcceleratorPageViewModel
|
||||
{
|
||||
public NetworkCheckControlViewModel NetworkCheckControlViewModel { get; } = new();
|
||||
|
||||
DateTime _initializeTime;
|
||||
readonly IHostsFileService? hostsFileService;
|
||||
readonly IPlatformService platformService = IPlatformService.Instance;
|
||||
@@ -15,6 +15,31 @@ public sealed partial class AcceleratorPageViewModel
|
||||
|
||||
public AcceleratorPageViewModel()
|
||||
{
|
||||
ProxyService.Current.WhenValueChanged(x => x.ProxyStatus)
|
||||
.Where(x => x == true)
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
// Create new ProxyEnableDomain for 加速服务 page
|
||||
var enableGroupDomain = ProxyService.Current.ProxyDomainsList
|
||||
.Where(list => list.ThreeStateEnable == true || list.ThreeStateEnable == null)
|
||||
.Select(list => new ProxyDomainGroupViewModel
|
||||
{
|
||||
Name = list.Name,
|
||||
IconUrl = list.IconUrl ?? string.Empty,
|
||||
EnableProxyDomainVMs = new(
|
||||
list.Items!
|
||||
.Where(i => i.ThreeStateEnable == true)
|
||||
.Select(i => new ProxyDomainViewModel(i.Name, i.ProxyType, "https://" + i.ListenDomainNames.Split(";")[0],
|
||||
i.Items?
|
||||
.Select(c => new ProxyDomainViewModel(c.Name, c.ProxyType, "https://" + c.ListenDomainNames.Split(';')[0]))
|
||||
.ToList()))
|
||||
.ToList()),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
EnableProxyDomainGroupVMs = enableGroupDomain.AsReadOnly();
|
||||
});
|
||||
|
||||
StartProxyCommand = ReactiveCommand.CreateFromTask(async _ =>
|
||||
{
|
||||
#if LINUX
|
||||
@@ -29,6 +54,7 @@ public sealed partial class AcceleratorPageViewModel
|
||||
if (_initializeTime > DateTime.Now.AddSeconds(-2))
|
||||
{
|
||||
Toast.Show(ToastIcon.Warning, Strings.Warning_DoNotOperateFrequently);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,7 +72,7 @@ public sealed partial class AcceleratorPageViewModel
|
||||
ProxySettingsCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
var vm = new ProxySettingsWindowViewModel();
|
||||
IWindowManager.Instance.ShowTaskDialogAsync(vm, vm.Title, pageContent: new ProxySettingsPage(), isOkButton: false);
|
||||
_ = IWindowManager.Instance.ShowTaskDialogAsync(vm, vm.Title, pageContent: new ProxySettingsPage(), isOkButton: false);
|
||||
});
|
||||
|
||||
if (IApplication.IsDesktop())
|
||||
@@ -65,10 +91,15 @@ public sealed partial class AcceleratorPageViewModel
|
||||
certificateManager.GetCerFilePathGeneratedWhenNoFileExists();
|
||||
platformService.OpenFolder(certificateManager.PfxFilePath);
|
||||
});
|
||||
OpenLogFileCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
platformService.OpenFolder(IPCSubProcessFileSystem.GetLogDirPath(Plugin.Instance.UniqueEnglishName));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#if LINUX
|
||||
|
||||
public bool EnvironmentCheck()
|
||||
{
|
||||
try
|
||||
@@ -89,6 +120,7 @@ public sealed partial class AcceleratorPageViewModel
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
public void TrustCer_OnClick()
|
||||
|
||||
+6
-2
@@ -1,4 +1,3 @@
|
||||
using BD.WTTS.Client.Resources;
|
||||
using System.Reactive;
|
||||
|
||||
namespace BD.WTTS.UI.ViewModels;
|
||||
@@ -7,6 +6,9 @@ public sealed partial class AcceleratorPageViewModel : TabItemViewModel
|
||||
{
|
||||
public override string Name => Strings.Welcome;
|
||||
|
||||
[Reactive]
|
||||
public ReadOnlyCollection<ProxyDomainGroupViewModel>? EnableProxyDomainGroupVMs { get; set; }
|
||||
|
||||
public ICommand StartProxyCommand { get; }
|
||||
|
||||
public ICommand RefreshCommand { get; }
|
||||
@@ -30,4 +32,6 @@ public sealed partial class AcceleratorPageViewModel : TabItemViewModel
|
||||
public ICommand? ProxySettingsCommand { get; }
|
||||
|
||||
public ICommand? TrustCerCommand { get; }
|
||||
}
|
||||
|
||||
public ICommand? OpenLogFileCommand { get; }
|
||||
}
|
||||
+11
-8
@@ -9,17 +9,20 @@ public sealed class AllGamesListPageViewModel : WindowViewModel
|
||||
{
|
||||
XunYouGames = new ObservableCollection<XunYouGameViewModel>();
|
||||
|
||||
Task2.InBackground(() =>
|
||||
if (XunYouSDK.IsSupported)
|
||||
{
|
||||
var games = XunYouSDK.GetAllGames();
|
||||
if (games != null)
|
||||
Task2.InBackground(() =>
|
||||
{
|
||||
XunYouGames.Clear();
|
||||
foreach (var game in games)
|
||||
var games = XunYouSDK.GetAllGames();
|
||||
if (games != null)
|
||||
{
|
||||
XunYouGames.Add(new XunYouGameViewModel(game));
|
||||
XunYouGames.Clear();
|
||||
foreach (var game in games)
|
||||
{
|
||||
XunYouGames.Add(new XunYouGameViewModel(game));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
using STUN.Enums;
|
||||
using STUN.StunResult;
|
||||
using System.Reactive;
|
||||
|
||||
namespace BD.WTTS.UI.ViewModels;
|
||||
|
||||
public partial class NetworkCheckControlViewModel : ViewModelBase
|
||||
{
|
||||
[GeneratedRegex(DomainValidationAttribute.RegPattern)]
|
||||
private static partial Regex DomainRegExp();
|
||||
|
||||
public class DomainValidationAttribute : RegularExpressionAttribute
|
||||
{
|
||||
public const string RegPattern = @"^[^/]+\.[^/]{2,}$";
|
||||
|
||||
public override string FormatErrorMessage(string name) => "请填入不带分隔符 \"/\" 的域名";
|
||||
|
||||
public DomainValidationAttribute() : base(RegPattern)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private readonly INetworkTestService _networkTestService = INetworkTestService.Instance;
|
||||
|
||||
public record NATFetchResult(string PublicEndPoint, string LocalEndPoint, string NATLevel, string NATTypeTip, bool PingResult);
|
||||
|
||||
public string DefaultTestDomain { get; } = "store.steampowered.com";
|
||||
|
||||
[Reactive]
|
||||
public string SelectedSTUNAddress { get; set; }
|
||||
|
||||
public string[] STUNAddress { get; } =
|
||||
[
|
||||
"stun.syncthing.net",
|
||||
"stun.hot-chilli.net",
|
||||
"stun.fitauto.ru",
|
||||
"stun.miwifi.com",
|
||||
];
|
||||
|
||||
[ObservableAsProperty]
|
||||
public string NATLevel { get; } = string.Empty;
|
||||
|
||||
[ObservableAsProperty]
|
||||
public string NATTypeTip { get; } = string.Empty;
|
||||
|
||||
[ObservableAsProperty]
|
||||
public string LocalEndPoint { get; } = string.Empty;
|
||||
|
||||
[ObservableAsProperty]
|
||||
public string PublicEndPoint { get; } = string.Empty;
|
||||
|
||||
[ObservableAsProperty]
|
||||
public bool PingOkVisible { get; }
|
||||
|
||||
[ObservableAsProperty]
|
||||
public bool PingErrorVisible { get; }
|
||||
|
||||
[ObservableAsProperty]
|
||||
public bool IsNATChecking { get; }
|
||||
|
||||
[ObservableAsProperty]
|
||||
public bool IsDNSChecking { get; }
|
||||
|
||||
[ObservableAsProperty]
|
||||
public bool IsIPv6Checking { get; }
|
||||
|
||||
[DomainValidation]
|
||||
[Reactive]
|
||||
public string DomainPendingTest { get; set; } = string.Empty;
|
||||
|
||||
[Reactive]
|
||||
public string DNSTestDelay { get; set; } = string.Empty;
|
||||
|
||||
[Reactive]
|
||||
public string DNSTestResult { get; set; } = string.Empty;
|
||||
|
||||
[Reactive]
|
||||
public string IPv6Address { get; set; } = string.Empty;
|
||||
|
||||
[Reactive]
|
||||
public bool IsSupportIPv6 { get; set; }
|
||||
|
||||
public ReactiveCommand<Unit, NATFetchResult> NATCheckCommand { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> DNSCheckCommand { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> IPv6CheckCommand { get; }
|
||||
|
||||
public NetworkCheckControlViewModel()
|
||||
{
|
||||
SelectedSTUNAddress = STUNAddress[0];
|
||||
|
||||
NATCheckCommand = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
var natCheckResult = await _networkTestService.TestStunClient3489Async(testServerHostName: SelectedSTUNAddress) ?? new ClassicStunResult { NatType = NatType.Unknown };
|
||||
var (netSucc, _) = await _networkTestService.TestOpenUrlAsync("https://www.baidu.com");
|
||||
|
||||
var publicEndPoint = natCheckResult.PublicEndPoint?.Address.ToString() ?? "Unknown";
|
||||
var localEndPoint = natCheckResult.LocalEndPoint?.Address.ToString() ?? "Unknown";
|
||||
|
||||
var (natLevel, natTypeTip) = natCheckResult.NatType switch
|
||||
{
|
||||
// Open
|
||||
NatType.OpenInternet or NatType.FullCone => ("开放 NAT", "您可与在其网络上具有任意 NAT 类型的用户玩多人游戏和发起多人游戏。"),
|
||||
// Moderate
|
||||
NatType.RestrictedCone or NatType.PortRestrictedCone or NatType.SymmetricUdpFirewall => ("中等 NAT", "您可与一些用户玩多人游戏;但是,并且通常你将不会被选为比赛的主持人。"),
|
||||
// Strict
|
||||
NatType.Symmetric or NatType.UdpBlocked => ("严格 NAT", "您只能与具有开放 NAT 类型的用户玩多人游戏。您不能被选为比赛的主持人。"),
|
||||
// Unknown
|
||||
NatType.Unknown or NatType.UnsupportedServer or _ => ("不可用 NAT", "如果 NAT 不可用,您将无法使用群聊天或连接到某些 Xbox 游戏的多人游戏。"),
|
||||
};
|
||||
|
||||
return new NATFetchResult(publicEndPoint, localEndPoint, natLevel, natTypeTip, netSucc);
|
||||
});
|
||||
NATCheckCommand
|
||||
.IsExecuting
|
||||
.ToPropertyEx(this, x => x.IsNATChecking);
|
||||
NATCheckCommand
|
||||
.Select(x => x.PublicEndPoint)
|
||||
.ToPropertyEx(this, x => x.PublicEndPoint);
|
||||
NATCheckCommand
|
||||
.Select(x => x.LocalEndPoint)
|
||||
.ToPropertyEx(this, x => x.LocalEndPoint);
|
||||
NATCheckCommand
|
||||
.Select(x => x.NATLevel)
|
||||
.Merge(Observable.Return("未知"))
|
||||
.ToPropertyEx(this, x => x.NATLevel);
|
||||
NATCheckCommand
|
||||
.Select(x => x.NATTypeTip)
|
||||
.Merge(Observable.Return("未知类型"))
|
||||
.ToPropertyEx(this, x => x.NATTypeTip);
|
||||
|
||||
var hidePingResultStream = NATCheckCommand
|
||||
.IsExecuting
|
||||
.Where(x => x == true)
|
||||
.Select(x => false);
|
||||
NATCheckCommand
|
||||
.Select(x => x.PingResult == true)
|
||||
.Merge(hidePingResultStream)
|
||||
.ToPropertyEx(this, x => x.PingOkVisible);
|
||||
NATCheckCommand
|
||||
.Select(x => x.PingResult == false)
|
||||
.Merge(hidePingResultStream)
|
||||
.ToPropertyEx(this, x => x.PingErrorVisible);
|
||||
|
||||
var canDNSCheck = this.WhenAnyValue(x => x.DomainPendingTest)
|
||||
.Select(domain => domain == string.Empty || DomainRegExp().IsMatch(domain));
|
||||
DNSCheckCommand = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
var testDomain = DomainPendingTest == string.Empty ? DefaultTestDomain : DomainPendingTest;
|
||||
try
|
||||
{
|
||||
long delayMs;
|
||||
IPAddress[] address;
|
||||
if (ProxySettings.UseDoh)
|
||||
{
|
||||
var configDoh = ProxySettings.CustomDohAddres2.Value ?? ProxySettingsWindowViewModel.DohAddress.FirstOrDefault() ?? string.Empty;
|
||||
(delayMs, address) = await _networkTestService.TestDNSOverHttpsAsync(testDomain, configDoh);
|
||||
}
|
||||
else
|
||||
{
|
||||
var configDns = ProxySettings.ProxyMasterDns.Value ?? string.Empty;
|
||||
(delayMs, address) = await _networkTestService.TestDNSAsync(testDomain, configDns, 53);
|
||||
}
|
||||
if (address.Length == 0)
|
||||
throw new Exception("Parsing failed. Return empty ip address.");
|
||||
|
||||
DNSTestDelay = delayMs + "ms ";
|
||||
DNSTestResult = string.Empty + address.FirstOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(AcceleratorPageViewModel), ex.ToString());
|
||||
DNSTestDelay = string.Empty;
|
||||
DNSTestResult = "error";
|
||||
}
|
||||
}, canDNSCheck);
|
||||
DNSCheckCommand
|
||||
.IsExecuting
|
||||
.ToPropertyEx(this, x => x.IsDNSChecking);
|
||||
|
||||
IPv6CheckCommand = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
var result = await IMicroServiceClient.Instance.Accelerate.GetMyIP(ipV6: true);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
IsSupportIPv6 = true;
|
||||
IPv6Address = result.Content ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsSupportIPv6 = false;
|
||||
IPv6Address = string.Empty;
|
||||
}
|
||||
});
|
||||
IPv6CheckCommand
|
||||
.IsExecuting
|
||||
.ToPropertyEx(this, x => x.IsIPv6Checking);
|
||||
|
||||
IPv6CheckCommand.Execute().Subscribe();
|
||||
}
|
||||
}
|
||||
+21
-3
@@ -14,7 +14,7 @@ public sealed class ProxySettingsWindowViewModel : WindowViewModel
|
||||
//public IEnumerable<ExternalProxyType> ProxyTypes { get; }
|
||||
// = Enum2.GetAll<ExternalProxyType>();
|
||||
|
||||
public IEnumerable<string> ProxyDNSs { get; } = new[]
|
||||
public static IEnumerable<string> ProxyDNSs { get; } = new[]
|
||||
{
|
||||
"System Default",
|
||||
PrimaryDNS_114,
|
||||
@@ -31,7 +31,7 @@ public sealed class ProxySettingsWindowViewModel : WindowViewModel
|
||||
"127.0.0.1",
|
||||
};
|
||||
|
||||
public IEnumerable<string> DohAddress { get; }
|
||||
public static IEnumerable<string> DohAddress { get; }
|
||||
= new[] {
|
||||
Dnspod_DohAddres,
|
||||
Dnspod_DohAddres2,
|
||||
@@ -44,4 +44,22 @@ public sealed class ProxySettingsWindowViewModel : WindowViewModel
|
||||
DohAddres_360,
|
||||
TUNA_DohAddres,
|
||||
};
|
||||
}
|
||||
|
||||
public void ResetSettings()
|
||||
{
|
||||
// 更改多个设置项不立即保存
|
||||
ProxySettings.SystemProxyIp.Reset(save: false);
|
||||
ProxySettings.ProxyMasterDns.Reset(save: false);
|
||||
ProxySettings.SystemProxyPortId.Reset(save: false);
|
||||
ProxySettings.ProgramStartupRunProxy.Reset(save: false);
|
||||
ProxySettings.EnableHttpProxyToHttps.Reset(save: false);
|
||||
ProxySettings.UseDoh.Reset(save: false);
|
||||
ProxySettings.CustomDohAddres2.Reset(save: false);
|
||||
ProxySettings.OnlyEnableProxyScript.Reset(save: false);
|
||||
|
||||
// 更改完成后保存一次
|
||||
ProxySettings.OnlyEnableProxyScript.Save();
|
||||
|
||||
Toast.Show(ToastIcon.Success, "重置成功");
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -10,7 +10,14 @@
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<StackPanel Spacing="6">
|
||||
<StackPanel Spacing="15">
|
||||
|
||||
<TextBlock FontSize="21" TextAlignment="Center">
|
||||
<Run Text="下载 Watt加速器" />
|
||||
<LineBreak />
|
||||
<Run Text="拒绝游戏延迟,提高稳定性" />
|
||||
</TextBlock>
|
||||
|
||||
<ui:SettingsExpander
|
||||
Description="{Binding Source={x:Static ms:GameAcceleratorSettings.WattAcceleratorDirPath}, Path=Value}"
|
||||
Header="安装路径"
|
||||
@@ -22,6 +29,16 @@
|
||||
<Button Click="SelectWattAcceleratorInstallPath" Content="选择路径" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
<TextBlock Text="需要下载 Watt 加速器插件才可使用,确定要下载吗?" />
|
||||
|
||||
<!--<TextBlock Text="需要下载 Watt 加速器插件才可使用,确定要下载吗?" />-->
|
||||
|
||||
<Button
|
||||
Margin="0,15"
|
||||
Padding="20,10"
|
||||
HorizontalAlignment="Center"
|
||||
Classes="TransparentAccent"
|
||||
Click="OKButton_Click"
|
||||
Content="立即下载"
|
||||
FontSize="14" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
+7
-1
@@ -1,9 +1,10 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public partial class AcceleratorPathAskBox : UserControl
|
||||
public partial class AcceleratorPathAskBox : ReactiveUserControl<MessageBoxWindowViewModel>
|
||||
{
|
||||
public AcceleratorPathAskBox()
|
||||
{
|
||||
@@ -30,4 +31,9 @@ public partial class AcceleratorPathAskBox : UserControl
|
||||
GameAcceleratorSettings.WattAcceleratorDirPath.Value = Path.Combine(path, "WattAccelerator");
|
||||
}
|
||||
}
|
||||
|
||||
private void OKButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
this.ViewModel?.Close?.Invoke(true);
|
||||
}
|
||||
}
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<UserControl
|
||||
x:Class="BD.WTTS.UI.Views.Pages.AcceleratorServiceStatus"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:enum="using:BD.WTTS.Enums"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="spp:AcceleratorPageViewModel"
|
||||
mc:Ignorable="d">
|
||||
<ScrollViewer
|
||||
Padding="0,0,10,0"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding EnableProxyDomainGroupVMs}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical" Spacing="{StaticResource SettingsSpacing}" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="spp:ProxyDomainGroupViewModel">
|
||||
<Border>
|
||||
<ui:SettingsExpander IsClickEnabled="False" IsExpanded="True">
|
||||
<ui:SettingsExpander.Header>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<spp:Image2
|
||||
Width="30"
|
||||
Height="30"
|
||||
VerticalAlignment="Center"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
Source="{Binding IconUrl}"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Header>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button
|
||||
HorizontalAlignment="Right"
|
||||
Click="ConnectTestButtonOnClick"
|
||||
Command="{Binding ConnectTestCommand}"
|
||||
Content="连通性测试"
|
||||
DockPanel.Dock="Right" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem Theme="{StaticResource SettingsExpanderItem2}">
|
||||
<TreeView ItemsSource="{Binding EnableProxyDomainVMs}" SelectionMode="Single">
|
||||
<TreeView.ItemTemplate>
|
||||
<TreeDataTemplate ItemsSource="{Binding Children}">
|
||||
<DockPanel Margin="8,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
|
||||
<Border
|
||||
Padding="5,0"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding ProxyType, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:ProxyType.ServerAccelerate}}"
|
||||
ToolTip.Tip="Beta 测试加速项,不稳定且随时可能调整">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0.0" Color="#43cbff" />
|
||||
<GradientStop Offset="1.0" Color="#9708cc" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Text="Beta" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Margin="0,0,4,0"
|
||||
HorizontalAlignment="Right"
|
||||
DockPanel.Dock="Right"
|
||||
Foreground="{Binding DelayColor}"
|
||||
Text="{Binding DelayMillseconds}"
|
||||
ToolTip.ShowDelay="0"
|
||||
ToolTip.Tip="此数值代表测试网页打开访问所需时间,请以实际浏览器访问网页状况为准" />
|
||||
</DockPanel>
|
||||
</TreeDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.VisualTree;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Pages;
|
||||
|
||||
public partial class AcceleratorServiceStatus : UserControl
|
||||
{
|
||||
public AcceleratorServiceStatus()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ConnectTestButtonOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
var expander = button.FindAncestorOfType<SettingsExpander>();
|
||||
if (expander != null) expander.IsExpanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,15 @@
|
||||
FontSize="18"
|
||||
IsVisible="{Binding ShowStart}" />
|
||||
|
||||
<Button
|
||||
Width="175"
|
||||
Height="42"
|
||||
Classes="accent"
|
||||
Command="{Binding ShowXunYouWindow, Source={x:Static s:GameAcceleratorService.Current}}"
|
||||
CommandParameter="True"
|
||||
Content="{x:Static spp:Strings.GameAccelerator_ShowWattAccelerator}"
|
||||
FontSize="18" />
|
||||
|
||||
<Button
|
||||
Width="150"
|
||||
Height="42"
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
<UserControl
|
||||
x:Class="BD.WTTS.UI.Views.Pages.NetworkCheck"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ms="https://steampp.net/settings"
|
||||
xmlns:s="https://steampp.net/services"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
xmlns:sys="using:System"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:DataType="spp:NetworkCheckControlViewModel"
|
||||
mc:Ignorable="d">
|
||||
<ScrollViewer
|
||||
Padding="0,0,10,0"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="8">
|
||||
<!-- NAT -->
|
||||
<ui:SettingsExpander Header="NAT 类型检测" IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<ui:FontIconSource FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<WrapPanel>
|
||||
<ui:FAComboBox
|
||||
Margin="0,0,4,4"
|
||||
IsEditable="False"
|
||||
IsEnabled="{Binding !IsNATChecking}"
|
||||
ItemsSource="{Binding STUNAddress}"
|
||||
SelectedItem="{Binding SelectedSTUNAddress, Mode=TwoWay}"
|
||||
ToolTip.Tip="STUN 服务器地址">
|
||||
<ui:FAComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="sys:String">
|
||||
<TextBlock Text="{Binding}" />
|
||||
</DataTemplate>
|
||||
</ui:FAComboBox.ItemTemplate>
|
||||
</ui:FAComboBox>
|
||||
<Button
|
||||
x:Name="NATCheckButton"
|
||||
Width="100"
|
||||
Margin="0,0,0,4"
|
||||
Command="{Binding NATCheckCommand}"
|
||||
Content="检测"
|
||||
IsEnabled="True" />
|
||||
</WrapPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
|
||||
<ui:SettingsExpanderItem Content="NAT 类型:">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Grid>
|
||||
<ui:ProgressRing
|
||||
Width="22"
|
||||
Margin="0,-4,0,-10"
|
||||
HorizontalAlignment="Right"
|
||||
IsVisible="{Binding IsNATChecking}" />
|
||||
|
||||
<DockPanel IsVisible="{Binding !IsNATChecking}">
|
||||
<SelectableTextBlock
|
||||
x:Name="NATTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="Gray"
|
||||
Text="{Binding NATLevel}" />
|
||||
<ui:FontIcon
|
||||
Margin="3,0"
|
||||
FontSize="20"
|
||||
Glyph=""
|
||||
ToolTip.ShowDelay="0">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock x:Name="NATTypeTip" Text="{Binding NATTypeTip}" />
|
||||
</ToolTip.Tip>
|
||||
</ui:FontIcon>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem Content="本机IP地址:">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Grid>
|
||||
<ui:ProgressRing
|
||||
Width="22"
|
||||
Margin="0,-4,0,-10"
|
||||
HorizontalAlignment="Right"
|
||||
IsVisible="{Binding IsNATChecking}" />
|
||||
<SelectableTextBlock
|
||||
Foreground="Gray"
|
||||
IsVisible="{Binding !IsNATChecking}"
|
||||
Text="{Binding LocalEndPoint}" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="对外IP地址:"
|
||||
ToolTip.Tip="若与您路由器 WAN IP 地址相同,则为公共网络" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Grid>
|
||||
<ui:ProgressRing
|
||||
Width="22"
|
||||
Margin="0,-4,0,-10"
|
||||
HorizontalAlignment="Right"
|
||||
IsVisible="{Binding IsNATChecking}" />
|
||||
<SelectableTextBlock
|
||||
Foreground="Gray"
|
||||
IsVisible="{Binding !IsNATChecking}"
|
||||
Text="{Binding PublicEndPoint}" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem Content="互联网连通情况:">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Grid>
|
||||
<ui:ProgressRing
|
||||
Width="22"
|
||||
Margin="0,-4,0,-10"
|
||||
HorizontalAlignment="Right"
|
||||
IsVisible="{Binding IsNATChecking}" />
|
||||
|
||||
<ui:FontIcon
|
||||
x:Name="PingOK"
|
||||
HorizontalAlignment="Right"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
FontSize="26"
|
||||
Foreground="Green"
|
||||
Glyph=""
|
||||
IsVisible="{Binding PingOkVisible}" />
|
||||
|
||||
<Grid x:Name="PingError" IsVisible="{Binding PingErrorVisible}">
|
||||
<ui:FontIcon
|
||||
Margin="3"
|
||||
HorizontalAlignment="Right"
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontSize="20"
|
||||
Foreground="Red"
|
||||
Glyph="" />
|
||||
<ui:FontIcon
|
||||
HorizontalAlignment="Right"
|
||||
FontSize="26"
|
||||
Foreground="White"
|
||||
Glyph="" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock
|
||||
Margin="-28,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Text="相关链接" />
|
||||
|
||||
<WrapPanel Margin="-10,0,0,0">
|
||||
<HyperlinkButton
|
||||
VerticalAlignment="Center"
|
||||
Content="XBOX 关于 NAT 文档"
|
||||
NavigateUri="https://support.xbox.com/zh-CN/help/hardware-network/connect-network/xbox-one-nat-error" />
|
||||
<HyperlinkButton
|
||||
VerticalAlignment="Center"
|
||||
Content="PlayStation 关于 NAT 文档"
|
||||
NavigateUri="https://manuals.playstation.net/document/ct/ps4/settings/nw_test.html" />
|
||||
<HyperlinkButton
|
||||
VerticalAlignment="Center"
|
||||
Content="Nintendo Switch 关于 NAT 文档"
|
||||
NavigateUri="https://www.nintendoswitch.com.cn/support/faq/373.html" />
|
||||
<HyperlinkButton
|
||||
VerticalAlignment="Center"
|
||||
Content="Bungie 关于 NAT 文档"
|
||||
NavigateUri="https://help.bungie.net/hc/zh-cn/articles/360049496751-%E9%AB%98%E7%BA%A7%E7%96%91%E9%9A%BE%E6%8E%92%E8%A7%A3-%E9%80%9A%E7%94%A8%E5%8D%B3%E6%8F%92%E5%8D%B3%E7%94%A8-%E7%AB%AF%E5%8F%A3%E8%BD%AC%E9%80%81%E4%B8%8E%E7%BD%91%E7%BB%9C%E5%9C%B0%E5%9D%80%E8%BD%AC%E6%8D%A2%E7%B1%BB%E5%9E%8B" />
|
||||
<HyperlinkButton
|
||||
VerticalAlignment="Center"
|
||||
Content="Activision 关于 NAT 文档"
|
||||
NavigateUri="https://support.activision.com/tw/zh/articles/port-forward-and-nat-faq" />
|
||||
<HyperlinkButton
|
||||
VerticalAlignment="Center"
|
||||
Content="RockStar Games 关于 NAT 文档"
|
||||
NavigateUri="https://support.rockstargames.com/tw/articles/206210548/%E5%A6%82%E4%BD%95%E8%A7%A3%E6%B1%BA-GTA-%E7%B7%9A%E4%B8%8A%E6%A8%A1%E5%BC%8F%E4%B8%AD%E8%88%87%E5%9A%B4%E6%A0%BC-Strict-NAT-%E9%A1%9E%E5%9E%8B%E7%9B%B8%E9%97%9C%E7%9A%84%E9%8C%AF%E8%AA%A4" />
|
||||
<HyperlinkButton
|
||||
VerticalAlignment="Center"
|
||||
Content="迅游关于 NAT 文档"
|
||||
NavigateUri="https://cs.xunyou.com/html/282/15271.shtml" />
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- DNS -->
|
||||
<ui:SettingsExpander Header="DNS 检测" IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<ui:FontIconSource FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button
|
||||
Width="100"
|
||||
Command="{Binding DNSCheckCommand}"
|
||||
Content="检测" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
|
||||
<TextBlock
|
||||
Margin="-28,0,0,-4"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Text="检测项目" />
|
||||
|
||||
<ui:SettingsExpanderItem Content="待检测的域名">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<TextBox
|
||||
Width="220"
|
||||
Text="{Binding DomainPendingTest, Mode=TwoWay}"
|
||||
Watermark="{Binding DefaultTestDomain}">
|
||||
<TextBox.Styles>
|
||||
<Style Selector="DataValidationErrors">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<DockPanel LastChildFill="True">
|
||||
<ContentControl
|
||||
Content="{Binding (DataValidationErrors.Errors)}"
|
||||
ContentTemplate="{TemplateBinding ErrorTemplate}"
|
||||
DataContext="{TemplateBinding Owner}"
|
||||
DockPanel.Dock="Right"
|
||||
IsVisible="{Binding (DataValidationErrors.HasErrors)}" />
|
||||
<ContentPresenter
|
||||
Name="PART_ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}" />
|
||||
</DockPanel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Setter Property="ErrorTemplate">
|
||||
<DataTemplate x:DataType="{x:Type x:Object}">
|
||||
<Canvas
|
||||
Width="14"
|
||||
Height="14"
|
||||
Margin="4,0,1,0"
|
||||
Background="Transparent">
|
||||
<ToolTip.Tip>
|
||||
<ItemsControl ItemsSource="{Binding}" />
|
||||
</ToolTip.Tip>
|
||||
<Path
|
||||
Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2"
|
||||
Stroke="Red"
|
||||
StrokeThickness="2" />
|
||||
</Canvas>
|
||||
</DataTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
</TextBox.Styles>
|
||||
</TextBox>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem Content="{Binding Path=Res.Settings_Proxy_CustomDNS, Mode=OneWay, Source={x:Static s:ResourceService.Current}}">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ui:FAComboBox
|
||||
Width="220"
|
||||
IsEditable="True"
|
||||
IsEnabled="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=!Value}"
|
||||
ItemsSource="{Binding Source={x:Static spp:ProxySettingsWindowViewModel.ProxyDNSs}}"
|
||||
Text="{Binding Source={x:Static ms:ProxySettings.ProxyMasterDns}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem Content="{Binding Path=Res.CommunityFix_EnableUseDoh, Mode=OneWay, Source={x:Static s:ResourceService.Current}}">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ToggleSwitch Classes="LeftContent" IsChecked="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.CommunityFix_CustomDohAddres, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ui:FAComboBox
|
||||
Width="220"
|
||||
IsEditable="True"
|
||||
IsEnabled="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=Value}"
|
||||
ItemsSource="{Binding Source={x:Static spp:ProxySettingsWindowViewModel.DohAddress}}"
|
||||
PlaceholderText="{Binding Source={x:Static s:IDnsAnalysisService.Dnspod_DohAddres2}, Mode=OneWay}"
|
||||
Text="{Binding Source={x:Static ms:ProxySettings.CustomDohAddres2}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<TextBlock
|
||||
Margin="-28,0,0,-4"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Text="测试结果" />
|
||||
|
||||
<ui:SettingsExpanderItem Content="延迟">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Grid>
|
||||
<ui:ProgressRing
|
||||
Width="22"
|
||||
Margin="0,-4,0,-10"
|
||||
IsVisible="{Binding IsDNSChecking}" />
|
||||
<SelectableTextBlock IsVisible="{Binding !IsDNSChecking}" Text="{Binding DNSTestDelay}" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem Content="解析地址">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Grid>
|
||||
<ui:ProgressRing
|
||||
Width="22"
|
||||
Margin="0,-4,0,-10"
|
||||
IsVisible="{Binding IsDNSChecking}" />
|
||||
<SelectableTextBlock IsVisible="{Binding !IsDNSChecking}" Text="{Binding DNSTestResult}" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- IPV6 -->
|
||||
<ui:SettingsExpander Header="IPv6 检测" IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<ui:FontIconSource FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button
|
||||
Width="100"
|
||||
Command="{Binding IPv6CheckCommand}"
|
||||
Content="检测" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
|
||||
<ui:SettingsExpanderItem Content="支持情况">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Grid>
|
||||
<ui:ProgressRing
|
||||
Width="22"
|
||||
Margin="0,-4,0,-10"
|
||||
IsVisible="{Binding IsIPv6Checking}" />
|
||||
<Grid IsVisible="{Binding !IsIPv6Checking}">
|
||||
<TextBlock
|
||||
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
|
||||
IsVisible="{Binding IsSupportIPv6}"
|
||||
Text="已支持 IPv6 访问" />
|
||||
<TextBlock
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
||||
IsVisible="{Binding !IsSupportIPv6}"
|
||||
Text="不支持 IPv6 访问" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem Content="IPv6 地址">
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Grid>
|
||||
<ui:ProgressRing
|
||||
Width="22"
|
||||
Margin="0,-4,0,-10"
|
||||
IsVisible="{Binding IsIPv6Checking}" />
|
||||
<SelectableTextBlock
|
||||
Foreground="Gray"
|
||||
IsVisible="{Binding !IsIPv6Checking}"
|
||||
Text="{Binding IPv6Address}" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Pages;
|
||||
|
||||
public partial class NetworkCheck : UserControl
|
||||
{
|
||||
public NetworkCheck()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using LiveChartsCore;
|
||||
using LiveChartsCore.Motion;
|
||||
using LiveChartsCore.SkiaSharpView;
|
||||
using LiveChartsCore.SkiaSharpView.Avalonia;
|
||||
using LiveChartsCore.SkiaSharpView.Drawing;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<UserControl
|
||||
x:Class="BD.WTTS.UI.Views.Controls.ProxyLog"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:AvaloniaEdit="using:AvaloniaEdit"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:s="https://steampp.net/services"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<AvaloniaEdit:TextEditor
|
||||
x:Name="LogTextbox"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
IsReadOnly="True"
|
||||
ShowLineNumbers="True" />
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,92 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using AvaloniaEdit;
|
||||
using AvaloniaEdit.TextMate;
|
||||
using Org.BouncyCastle.Math;
|
||||
using TextMateSharp.Grammars;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public partial class ProxyLog : UserControl
|
||||
{
|
||||
CancellationTokenSource cancellation = new();
|
||||
|
||||
public ProxyLog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var logTextbox = this.FindControl<TextEditor>("LogTextbox")!;
|
||||
|
||||
//Here we initialize RegistryOptions with the theme we want to use.
|
||||
var registryOptions = new TextMateSharp.Grammars.RegistryOptions(ThemeName.Dark);
|
||||
|
||||
//Initial setup of TextMate.
|
||||
var textMateInstallation = logTextbox.InstallTextMate(registryOptions);
|
||||
|
||||
//Here we are getting the language by the extension and right after that we are initializing grammar with this language.
|
||||
//And that's all 😀, you are ready to use AvaloniaEdit with syntax highlighting!
|
||||
textMateInstallation.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions.GetLanguageByExtension(".log").Id));
|
||||
|
||||
//logTextbox.Text = LogText;
|
||||
//logTextbox.ScrollToLine(logTextbox.Document.LineCount);
|
||||
|
||||
logTextbox.TextChanged += LogTextbox_TextChanged;
|
||||
|
||||
ProxyService.Current.WhenAnyValue(x => x.ProxyStatus)
|
||||
.Subscribe(x =>
|
||||
{
|
||||
if (x)
|
||||
{
|
||||
cancellation = new CancellationTokenSource();
|
||||
Task2.InBackground(FlushLogsAsync, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cancellation != null)
|
||||
{
|
||||
cancellation.Cancel();
|
||||
cancellation.Dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void LogTextbox_TextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
LogTextbox.ScrollToLine(LogTextbox.Document.LineCount);
|
||||
}
|
||||
|
||||
void FlushLogsAsync()
|
||||
{
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isAttachedToVisualTree = this.IsAttachedToVisualTree();
|
||||
if (isAttachedToVisualTree)
|
||||
{
|
||||
var logtext = IReverseProxyService.Constants.Instance.GetLogAllMessage();
|
||||
if (string.IsNullOrEmpty(logtext))
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (LogTextbox.Text.Length != logtext.Length)
|
||||
LogTextbox.Text = logtext;
|
||||
});
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<UserControl
|
||||
x:Class="BD.WTTS.UI.Views.Pages.ProxyServiceStatus"
|
||||
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"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:DataType="spp:AcceleratorPageViewModel"
|
||||
mc:Ignorable="d">
|
||||
<ScrollViewer
|
||||
Padding="0,0,10,0"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl Grid.Column="1" ItemsSource="{Binding EnableProxyScripts^, Source={x:Static s:ProxyService.Current}}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type spp:ScriptDTO}">
|
||||
<spp:AppItem
|
||||
Title="{Binding Name}"
|
||||
MaxWidth="135"
|
||||
Margin="4"
|
||||
Classes="Vertical"
|
||||
Description="{Binding AuthorName}"
|
||||
ImageMargin="20"
|
||||
ImageStretch="None"
|
||||
ToolTip.Tip="{Binding Describe}">
|
||||
|
||||
<spp:AppItem.Image>
|
||||
<spp:Image2
|
||||
FallbackSource="avares://BD.WTTS.Client.Plugins.Accelerator/UI/Assets/script.ico"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
Source="{Binding IconUrl}"
|
||||
Stretch="Uniform" />
|
||||
</spp:AppItem.Image>
|
||||
</spp:AppItem>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Pages;
|
||||
|
||||
public partial class ProxyServiceStatus : UserControl
|
||||
{
|
||||
public ProxyServiceStatus()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,15 @@
|
||||
xmlns:enum="using:BD.WTTS.Enums"
|
||||
xmlns:i="using:Avalonia.Xaml.Interactivity"
|
||||
xmlns:ia="using:Avalonia.Xaml.Interactions.Core"
|
||||
xmlns:ir="using:Avalonia.Xaml.Interactions.Responsive"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ms="https://steampp.net/settings"
|
||||
xmlns:s="https://steampp.net/services"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
xmlns:sys="using:System"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
Title="{Binding Name, Source={x:Static s:Plugin.Instance}, Mode=OneWay}"
|
||||
d:DesignHeight="450"
|
||||
d:DesignHeight="650"
|
||||
d:DesignWidth="800"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="spp:AcceleratorPageViewModel"
|
||||
@@ -236,28 +238,7 @@
|
||||
</TabStrip.ItemTemplate>
|
||||
</TabStrip>
|
||||
|
||||
<DockPanel>
|
||||
<TextBlock Text="{x:Static spp:Strings.CommunityFix_MenuName}" Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Classes.NotSupport="{Binding Source={x:Static s:ProxyService.Current}, Path=IPv6AddresString, Mode=OneWay, Converter={StaticResource IsNullConverter}}"
|
||||
Classes.Support="{Binding Source={x:Static s:ProxyService.Current}, Path=IPv6AddresString, Mode=OneWay, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}"
|
||||
Theme="{StaticResource CaptionTextBlockStyle}"
|
||||
ToolTip.Tip="{Binding Source={x:Static s:ProxyService.Current}, Path=IPv6AddresString, Mode=OneWay}">
|
||||
<TextBlock.Styles>
|
||||
<Style Selector="TextBlock.Support">
|
||||
<Setter Property="Text" Value="已支持 IPv6 访问" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.NotSupport">
|
||||
<Setter Property="Text" Value="不支持 IPv6 访问" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
</Style>
|
||||
</TextBlock.Styles>
|
||||
</TextBlock>
|
||||
</DockPanel>
|
||||
<TextBlock Text="{x:Static spp:Strings.CommunityFix_MenuName}" Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
|
||||
<ToggleSwitch
|
||||
VerticalAlignment="Center"
|
||||
@@ -304,37 +285,59 @@
|
||||
|
||||
<Separator Margin="0,8" Classes="Horizontal" />
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding SetupCertificateCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_SetupCertificate}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding DeleteCertificateCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_DeleteCertificate}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding ShowCertificateCommand}"
|
||||
Content="查看证书信息" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding OpenCertificateDirCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_OpenCertificateDir}" />
|
||||
<ItemsControl>
|
||||
<ItemsControl.Items>
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding SetupCertificateCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_SetupCertificate}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding DeleteCertificateCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_DeleteCertificate}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding ShowCertificateCommand}"
|
||||
Content="查看证书信息" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding OpenCertificateDirCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_OpenCertificateDir}" />
|
||||
</ItemsControl.Items>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<spp:FixedWrapPanel ItemsPerLine="2" Spacing="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
|
||||
<Separator Margin="0,8" Classes="Horizontal" />
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding EditHostsFileCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_EditHostsFile}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding ResetHostsFileCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_ResetHostsFile}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding OpenHostsDirCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_OpenHostsDir}" />
|
||||
<ItemsControl>
|
||||
<ItemsControl.Items>
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding EditHostsFileCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_EditHostsFile}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding ResetHostsFileCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_ResetHostsFile}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding OpenHostsDirCommand}"
|
||||
Content="{x:Static spp:Strings.CommunityFix_OpenHostsDir}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding OpenLogFileCommand}"
|
||||
Content="打开加速日志文件夹" />
|
||||
</ItemsControl.Items>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<spp:FixedWrapPanel ItemsPerLine="2" Spacing="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
|
||||
<!--<ui:FAComboBox ItemsSource="{Binding Source={x:Static ms:ProxySettings.ProxyModes}, Path=., Mode=OneWay}" SelectedItem="{Binding Source={x:Static ms:ProxySettings.ProxyMode}, Path=Value, Mode=TwoWay}">
|
||||
<ui:FAComboBox.ItemTemplate>
|
||||
@@ -344,9 +347,19 @@
|
||||
</ui:FAComboBox.ItemTemplate>
|
||||
</ui:FAComboBox>-->
|
||||
|
||||
<Separator Margin="0,8" Classes="Horizontal" />
|
||||
<StackPanel IsVisible="{x:Static spp:XunYouSDK.IsSupported}}" Spacing="10">
|
||||
<Separator Margin="0,8" Classes="Horizontal" />
|
||||
|
||||
<!--<TextBlock Text="{x:Static spp:Strings.GameAccelerator_WattAcceleratorInstallPath}" Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
<TextBlock Text="Watt 加速器相关" Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
|
||||
<ToggleSwitch
|
||||
VerticalAlignment="Center"
|
||||
Classes="NoContent"
|
||||
IsChecked="{Binding Source={x:Static ms:ProxySettings.AutoShowWattAcceleratorWindow}, Path=Value, Mode=TwoWay}"
|
||||
OffContent="{x:Static spp:Strings.GameAccelerator_SetShowWattWindow}"
|
||||
OnContent="{x:Static spp:Strings.GameAccelerator_SetShowWattWindow}" />
|
||||
|
||||
<!--<TextBlock Text="{x:Static spp:Strings.GameAccelerator_WattAcceleratorInstallPath}" Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
|
||||
<ui:SettingsExpander Description="{Binding Source={x:Static ms:GameAcceleratorSettings.WattAcceleratorDirPath}, Path=Value, Mode=OneWay}}" IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
@@ -359,74 +372,127 @@
|
||||
<Button Content="打开文件夹" />
|
||||
<Button Content="选择路径" />
|
||||
</StackPanel>
|
||||
|
||||
</ui:SettingsExpander>-->
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding InstallAcceleratorCommand, Source={x:Static s:GameAcceleratorService.Current}}"
|
||||
Content="{x:Static spp:Strings.GameAccelerator_InstallWattAccelerator}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding UninstallAcceleratorCommand, Source={x:Static s:GameAcceleratorService.Current}}"
|
||||
Content="{x:Static spp:Strings.GameAccelerator_UninstallWattAccelerator}" />
|
||||
<!--<ItemsControl>
|
||||
<ItemsControl.Items>
|
||||
</ItemsControl.Items>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<spp:FixedWrapPanel ItemsPerLine="2" Spacing="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>-->
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding ShowXunYouWindow, Source={x:Static s:GameAcceleratorService.Current}}"
|
||||
CommandParameter="True"
|
||||
Content="{x:Static spp:Strings.GameAccelerator_ShowWattAccelerator}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding InstallAcceleratorCommand, Source={x:Static s:GameAcceleratorService.Current}}"
|
||||
Content="{x:Static spp:Strings.GameAccelerator_InstallWattAccelerator}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding UninstallAcceleratorCommand, Source={x:Static s:GameAcceleratorService.Current}}"
|
||||
Content="{x:Static spp:Strings.GameAccelerator_UninstallWattAccelerator}" />
|
||||
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</spp:PageBase.PaneContent>
|
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
|
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto,*">
|
||||
<Grid.Resources>
|
||||
<sys:Double x:Key="AdaptiveWidth">900</sys:Double>
|
||||
</Grid.Resources>
|
||||
<Grid.Styles>
|
||||
<Style Selector="StackPanel.small">
|
||||
<Setter Property="Grid.Row" Value="1" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="StackPanel.wide">
|
||||
<Setter Property="Grid.Row" Value="0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TabStrip.small">
|
||||
<Setter Property="Grid.ColumnSpan" Value="2" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="TabStrip.wide">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
<TabStrip
|
||||
x:Name="AcceleratorTabs"
|
||||
VerticalAlignment="Top"
|
||||
ItemContainerTheme="{StaticResource TabItemStyle}"
|
||||
Theme="{StaticResource TabStripStyle}">
|
||||
<i:Interaction.Behaviors>
|
||||
<spp:AdaptiveBehavior SourceControl="{Binding $parent[TopLevel]}">
|
||||
<ir:AdaptiveClassSetter
|
||||
MinWidth="0"
|
||||
MaxWidth="{StaticResource AdaptiveWidth}"
|
||||
ClassName="small" />
|
||||
<ir:AdaptiveClassSetter
|
||||
MinWidth="{StaticResource AdaptiveWidth}"
|
||||
MaxWidth="Infinity"
|
||||
ClassName="wide" />
|
||||
</spp:AdaptiveBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
<TabStripItem Content="平台加速" IsVisible="{Binding Source={x:Static s:ProxyService.Current}, Path=!ProxyStatus, Mode=OneWay}">
|
||||
<WrapPanel>
|
||||
<TextBlock Text="平台加速" />
|
||||
<Border
|
||||
<!--<WrapPanel>-->
|
||||
<TextBlock Text="平台加速" />
|
||||
<!--<Border
|
||||
Margin="5,0,0,0"
|
||||
Padding="5,1"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="2">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0.0" Color="#48C6EF" />
|
||||
<GradientStop Offset="1.0" Color="#6F86D6" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0.0" Color="#48C6EF" />
|
||||
<GradientStop Offset="1.0" Color="#6F86D6" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="White"
|
||||
Text="免费" />
|
||||
</Border>
|
||||
</WrapPanel>
|
||||
</WrapPanel>-->
|
||||
</TabStripItem>
|
||||
<TabStripItem Content="{x:Static spp:Strings.CommunityFix_FlowStatistics}" IsVisible="{Binding Source={x:Static s:ProxyService.Current}, Path=ProxyStatus, Mode=OneWay}" />
|
||||
<TabStripItem Content="{x:Static spp:Strings.CommunityFix_Services}" IsVisible="{Binding Source={x:Static s:ProxyService.Current}, Path=ProxyStatus, Mode=OneWay}" />
|
||||
<TabStripItem Content="加速脚本" IsVisible="{Binding Source={x:Static s:ProxyService.Current}, Path=IsAnyProxyScripts, Mode=OneWay}" />
|
||||
<TabStripItem Content="{x:Static spp:Strings.CommunityFix_ProxyLog}" IsVisible="{Binding Source={x:Static s:ProxyService.Current}, Path=ProxyStatus, Mode=OneWay}" />
|
||||
<TabStripItem Content="{x:Static spp:Strings.CommunityFix_FlowStatistics}" IsVisible="{Binding Source={x:Static s:ProxyService.Current}, Path=ProxyStatus, Mode=OneWay}" />
|
||||
<TabStripItem Name="GameAccTab">
|
||||
<WrapPanel>
|
||||
<TextBlock Text="游戏加速" />
|
||||
<Border
|
||||
<!--<WrapPanel>-->
|
||||
<TextBlock Text="游戏加速" />
|
||||
<!--<Border
|
||||
Margin="5,0,0,0"
|
||||
Padding="5,1"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="2">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0.0" Color="#EEAD92" />
|
||||
<GradientStop Offset="1.0" Color="#6018DC" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0.0" Color="#EEAD92" />
|
||||
<GradientStop Offset="1.0" Color="#6018DC" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="White"
|
||||
Text="收费" />
|
||||
</Border>
|
||||
</WrapPanel>
|
||||
</Border>-->
|
||||
<!--</WrapPanel>-->
|
||||
</TabStripItem>
|
||||
<TabStripItem Content="网络检测" IsVisible="True" />
|
||||
<TabStripItem Content="dummy_tab_for_transisit_animation" IsVisible="False" />
|
||||
</TabStrip>
|
||||
<Carousel
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,0"
|
||||
Classes="AnimeSlide"
|
||||
SelectedIndex="{Binding #AcceleratorTabs.SelectedIndex}">
|
||||
@@ -509,84 +575,18 @@
|
||||
</spp:ContentLoader>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 加速服务 -->
|
||||
<spp:AcceleratorServiceStatus />
|
||||
|
||||
<!-- 加速脚本 -->
|
||||
<spp:ProxyServiceStatus />
|
||||
|
||||
<!-- 加速日志 -->
|
||||
<spp:ProxyLog Margin="5,0" />
|
||||
|
||||
<!-- 流量统计 -->
|
||||
<spp:ProxyChartView Margin="5,0" />
|
||||
|
||||
<!-- 加速服务 -->
|
||||
<Grid Margin="0,0,10,0" ColumnDefinitions="*,20,*">
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Padding="10"
|
||||
Classes="paper">
|
||||
<DockPanel>
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
DockPanel.Dock="Top"
|
||||
FontSize="16"
|
||||
Text="{x:Static spp:Strings.CommunityFix_AccelerationsEnable}" />
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{Binding EnableProxyDomains, Source={x:Static s:ProxyService.Current}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type spp:AccelerateProjectDTO}">
|
||||
<StackPanel Margin="10">
|
||||
<DockPanel>
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<Border
|
||||
Padding="5,0"
|
||||
HorizontalAlignment="Left"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding ProxyType, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:ProxyType.ServerAccelerate}}"
|
||||
ToolTip.Tip="Beta 测试加速项,不稳定且随时可能调整">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0.0" Color="#43cbff" />
|
||||
<GradientStop Offset="1.0" Color="#9708cc" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Text="Beta" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
<Separator Classes="Separator" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
Padding="10"
|
||||
Classes="paper">
|
||||
<DockPanel>
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
DockPanel.Dock="Top"
|
||||
FontSize="16"
|
||||
Text="{x:Static spp:Strings.CommunityFix_ScriptsEnable}" />
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{Binding EnableProxyScripts^, Source={x:Static s:ProxyService.Current}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type spp:ScriptDTO}">
|
||||
<StackPanel Margin="10">
|
||||
<DockPanel>
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
</DockPanel>
|
||||
<Separator Classes="Separator" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
|
||||
<!-- 游戏加速 -->
|
||||
<Panel x:DataType="spp:GameAcceleratorPageViewModel">
|
||||
<Panel.DataContext>
|
||||
@@ -647,20 +647,31 @@
|
||||
IsVisible="{Binding Source={x:Static s:GameAcceleratorService.Current}, Path=CurrentAcceleratorGame.IsAccelerated}" />
|
||||
</Panel>
|
||||
|
||||
<!--<TabItem Header="加速日志">
|
||||
<!-- 网络检测 -->
|
||||
<spp:NetworkCheck DataContext="{Binding NetworkCheckControlViewModel}" />
|
||||
|
||||
</TabItem>-->
|
||||
<!--<TabItem Header="AD">
|
||||
|
||||
</TabItem>-->
|
||||
<TabItem Content="dummy_tab_for_transisit_animation" />
|
||||
</Carousel>
|
||||
|
||||
<!-- Side tab of paid game spped up -->
|
||||
<StackPanel
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Right"
|
||||
IsVisible="{Binding #GameAccTab.IsSelected}"
|
||||
Orientation="Horizontal"
|
||||
Spacing="5">
|
||||
|
||||
<i:Interaction.Behaviors>
|
||||
<spp:AdaptiveBehavior SourceControl="{Binding $parent[TopLevel]}">
|
||||
<ir:AdaptiveClassSetter
|
||||
MinWidth="0"
|
||||
MaxWidth="{StaticResource AdaptiveWidth}"
|
||||
ClassName="small" />
|
||||
<ir:AdaptiveClassSetter
|
||||
MinWidth="{StaticResource AdaptiveWidth}"
|
||||
MaxWidth="Infinity"
|
||||
ClassName="wide" />
|
||||
</spp:AdaptiveBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
<Border
|
||||
Padding="5,1"
|
||||
VerticalAlignment="Center"
|
||||
@@ -732,14 +743,26 @@
|
||||
</AutoCompleteBox>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<!-- Side tab after speed up -->
|
||||
<StackPanel
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding Source={x:Static s:ProxyService.Current}, Path=ProxyStatus, Mode=OneWay}"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
|
||||
<i:Interaction.Behaviors>
|
||||
<spp:AdaptiveBehavior SourceControl="{Binding $parent[TopLevel]}">
|
||||
<ir:AdaptiveClassSetter
|
||||
MinWidth="0"
|
||||
MaxWidth="{StaticResource AdaptiveWidth}"
|
||||
ClassName="small" />
|
||||
<ir:AdaptiveClassSetter
|
||||
MinWidth="{StaticResource AdaptiveWidth}"
|
||||
MaxWidth="Infinity"
|
||||
ClassName="wide" />
|
||||
</spp:AdaptiveBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
<StackPanel.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<CompiledBinding
|
||||
@@ -762,12 +785,20 @@
|
||||
<Run Text="A:流量统计图表是用于查看实时流量数据信息,您需要先访问对应的网站,这边才会有相应的流量波动数据,请检查是否已经选择了对应的网站加速项目,如果确定有正常勾选加速项目并且访问网站依然无流量,则可能是加速的监听服务没有启用成功,尝试更换加速模式。" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
<Run FontSize="16" Text="Q:加速后对应网站打不开显示SSL错误或者Steam浏览器显示-100错误?" />
|
||||
<Run FontSize="16" Text="Q:加速后对应网站打不开显示 SSL 错误或者 Steam 浏览器显示 -100 错误?" />
|
||||
<LineBreak />
|
||||
<Run Text="A:可以尝试执行以下操作进行修复,关闭加速,点击加速设置-移除证书再安装证书,重开加速。或者加速设置-代理设置-尝试更换doh地址或者关闭doh使用dns等方案。" />
|
||||
<Run Text="A:可以尝试执行以下操作进行修复,关闭加速,点击加速设置-移除证书再安装证书,重开加速。或者加速设置 - 代理设置- 尝试更换 doh 地址或者关闭 doh 使用 dns 等方案。" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
<Run FontSize="16" Text="Q:为什么加速经常不可用?还要自己去改设置或重新安装证书?" />
|
||||
<LineBreak />
|
||||
<Run Text="A:Steam++ 的加速与游戏加速器的加速功能不同,加速是完全运行于你电脑本地的,所以会受到你电脑本地网络环境,系统环境各种因素影响,也正因如此才有这么多加速相关的选项,请合理根据情况适进行测试以及更改对应的选项以便加速能正常使用。" />
|
||||
</TextBlock>
|
||||
</ToolTip.Tip>
|
||||
<ui:FontIcon FontSize="20" Glyph="" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<ui:FontIcon FontSize="20" Glyph="" />
|
||||
<TextBlock Text="加速不能用?" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBlock Text="{x:Static spp:Strings.CommunityFix_ProxyMode}" />
|
||||
<TextBlock Text="{Binding Source={x:Static ms:ProxySettings.ProxyMode}, Path=Value, Mode=OneWay, Converter={StaticResource EnumLocalizationNameConverter}}" />
|
||||
@@ -777,7 +808,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<spp:AdControl
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Width="165"
|
||||
Height="390"
|
||||
@@ -785,4 +816,4 @@
|
||||
VerticalAlignment="Top"
|
||||
Standard="Vertical" />
|
||||
</Grid>
|
||||
</spp:PageBase>
|
||||
</spp:PageBase>
|
||||
@@ -1,8 +1,8 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using static System.Net.Mime.MediaTypeNames;
|
||||
using Avalonia.VisualTree;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Pages;
|
||||
|
||||
@@ -150,4 +150,4 @@ public partial class AcceleratorPage2 : PageBase<AcceleratorPageViewModel>
|
||||
// // Log.Error(nameof(AcceleratorPage), ex, "ForceSave fail.");
|
||||
// //}
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
mc:Ignorable="d">
|
||||
<StackPanel Margin="{StaticResource PageMargin}" Spacing="{StaticResource SettingsSpacing}">
|
||||
<ui:SettingsExpander Header="{Binding Path=Res.CommunityFix_ProxySettings, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" IsExpanded="True">
|
||||
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button Command="{Binding ResetSettings}" Content="{Binding Path=Res.Reset, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.Settings_Proxy_SystemProxyIp, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
@@ -31,6 +33,7 @@
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ui:FAComboBox
|
||||
IsEditable="True"
|
||||
IsEnabled="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=!Value}"
|
||||
ItemsSource="{Binding ProxyDNSs}"
|
||||
Text="{Binding Source={x:Static ms:ProxySettings.ProxyMasterDns}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
@@ -69,7 +72,7 @@
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.CommunityFix_EnableUseDoh, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ToggleSwitch Classes="LeftContent" IsChecked="{Binding Source={x:Static ms:ProxySettings.UseDoh2}, Path=Value, Mode=TwoWay}" />
|
||||
<ToggleSwitch Classes="LeftContent" IsChecked="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
@@ -78,9 +81,10 @@
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ui:FAComboBox
|
||||
IsEditable="True"
|
||||
IsEnabled="{Binding Source={x:Static ms:ProxySettings.UseDoh}, Path=Value}"
|
||||
ItemsSource="{Binding DohAddress}"
|
||||
PlaceholderText="{Binding Source={x:Static s:IDnsAnalysisService.Dnspod_DohAddres}, Mode=OneWay}"
|
||||
Text="{Binding Source={x:Static ms:ProxySettings.CustomDohAddres}, Path=Value, Mode=TwoWay}" />
|
||||
PlaceholderText="{Binding Source={x:Static s:IDnsAnalysisService.Dnspod_DohAddres2}, Mode=OneWay}"
|
||||
Text="{Binding Source={x:Static ms:ProxySettings.CustomDohAddres2}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
Description="{Binding Description, Source={x:Static s:Plugin.Instance}, Mode=OneWay}"
|
||||
Subtitle="{Binding Author, Source={x:Static s:Plugin.Instance}, Mode=OneWay, Converter={StaticResource StringFormatConverter}, ConverterParameter=Plugin_Author}"
|
||||
mc:Ignorable="d">
|
||||
<spp:PageBase.Styles>
|
||||
<Style Selector="spp|AppItem /template/ TextBlock#HeaderText">
|
||||
<Setter Property="Grid.ColumnSpan" Value="0" />
|
||||
</Style>
|
||||
</spp:PageBase.Styles>
|
||||
<spp:PageBase.PreviewImage>
|
||||
<ui:ImageIconSource Source="avares://BD.WTTS.Client.Plugins.Accelerator/UI/Assets/script.ico" />
|
||||
</spp:PageBase.PreviewImage>
|
||||
|
||||
@@ -16,6 +16,7 @@ partial class XunYouSDK // 12.接口 xunyou_start_ex2
|
||||
/// </summary>
|
||||
/// <param name="openid">登录 token,由调用方提供</param>
|
||||
/// <param name="nickname">账号昵称</param>
|
||||
/// <param name="show"><see cref="PInvoke.User32.WindowStyles"/></param>
|
||||
/// <param name="gameid">游戏 id,由迅游给出明确值,或传 0,如果传 0,表示只启动迅游,不自动加速。</param>
|
||||
/// <param name="area">游戏分区 id ,由游戏给出区服名和 id 对照表,或传 0,如果传 0,表示只启动迅游,不自动加速。</param>
|
||||
/// <param name="server">游戏服 id</param>
|
||||
@@ -28,6 +29,7 @@ partial class XunYouSDK // 12.接口 xunyou_start_ex2
|
||||
public static int StartEx2(
|
||||
string openid,
|
||||
string nickname,
|
||||
XunYouShowCommands show = XunYouShowCommands.SW_HIDE,
|
||||
int gameid = 0,
|
||||
int area = 0,
|
||||
int server = 0,
|
||||
@@ -51,7 +53,7 @@ partial class XunYouSDK // 12.接口 xunyou_start_ex2
|
||||
|
||||
result = xunyou_start_ex2(appId, openid, openid_len,
|
||||
userType, channel_no, channel_no.Length,
|
||||
nickname, nickname_len, XunYouShowCommands.SW_HIDE,
|
||||
nickname, nickname_len, show,
|
||||
gameid,
|
||||
area, server, xunyou_callback, ptr,
|
||||
installPath, install_path_len, areaName,
|
||||
|
||||
@@ -14,6 +14,7 @@ partial class XunYouSDK // 22.接口 xunyou_start_ex3
|
||||
/// </summary>
|
||||
/// <param name="jwt">登录 token,由调用方提供</param>
|
||||
/// <param name="nickname">账号昵称</param>
|
||||
/// <param name="show"><see cref="PInvoke.User32.WindowStyles"/></param>
|
||||
/// <param name="gameid">游戏 id,由迅游给出明确值,或传 0,如果传 0,表示只启动迅游,不自动加速。</param>
|
||||
/// <param name="area">游戏分区 id ,由游戏给出区服名和 id 对照表,或传 0,如果传 0,表示只启动迅游,不自动加速。</param>
|
||||
/// <param name="server">游戏服 id</param>
|
||||
@@ -26,6 +27,7 @@ partial class XunYouSDK // 22.接口 xunyou_start_ex3
|
||||
public static int StartEx3(
|
||||
string jwt,
|
||||
string nickname,
|
||||
XunYouShowCommands show = XunYouShowCommands.SW_HIDE,
|
||||
int gameid = 0,
|
||||
int area = 0,
|
||||
int server = 0,
|
||||
@@ -49,7 +51,7 @@ partial class XunYouSDK // 22.接口 xunyou_start_ex3
|
||||
|
||||
result = xunyou_start_ex2(appId, jwt, jwt_len,
|
||||
userType, channel_no, channel_no.Length,
|
||||
nickname, nickname_len, XunYouShowCommands.SW_HIDE,
|
||||
nickname, nickname_len, show,
|
||||
gameid,
|
||||
area, server, xunyou_callback, ptr,
|
||||
installPath, install_path_len, areaName,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Mobius;
|
||||
|
||||
partial class XunYouSDK // 25.接口 xunyou_show
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置当前启动客户端的显隐状态
|
||||
/// </summary>
|
||||
/// <param name="show">客户端显示状态(SW_HIDE)</param>
|
||||
[LibraryImport(libraryName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
|
||||
public static partial void xunyou_show(int show = 0);
|
||||
}
|
||||
-3
@@ -115,9 +115,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BD.Common.XmlnsDefinition.Avalonia">
|
||||
<Aliases>BD_Common_XmlnsDefinition_Avalonia</Aliases>
|
||||
</PackageReference>
|
||||
<ProjectReference Include="..\BD.WTTS.Client\BD.WTTS.Client.csproj" />
|
||||
<ProjectReference Include="..\BD.WTTS.Client.Avalonia\BD.WTTS.Client.Avalonia.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
|
||||
|
||||
protected sealed override string? AuthorOriginalString => null;
|
||||
|
||||
public sealed override string Description => moduleName;
|
||||
public sealed override string Description => moduleName + " 控制台功能实现";
|
||||
|
||||
public sealed override object? Icon => Resources.asf;
|
||||
|
||||
@@ -113,4 +113,22 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
|
||||
{
|
||||
yield return GetConfiguration<ASFSettings_>(directoryExists);
|
||||
}
|
||||
|
||||
public override async ValueTask OnExit()
|
||||
{
|
||||
if (ArchiSteamFarmServiceImpl.ASFProcessId.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = Process.GetProcessById(ArchiSteamFarmServiceImpl.ASFProcessId.Value);
|
||||
process.Kill();
|
||||
process.Dispose();
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// 进程已经退出
|
||||
}
|
||||
}
|
||||
await base.OnExit();
|
||||
}
|
||||
}
|
||||
|
||||
+43
-37
@@ -27,6 +27,8 @@ public partial class ArchiSteamFarmServiceImpl : ReactiveObject, IArchiSteamFarm
|
||||
{
|
||||
}
|
||||
|
||||
public static int? ASFProcessId { get; private set; }
|
||||
|
||||
public Process? ASFProcess { get; set; }
|
||||
|
||||
public event Action<string>? OnConsoleWirteLine;
|
||||
@@ -69,7 +71,7 @@ public partial class ArchiSteamFarmServiceImpl : ReactiveObject, IArchiSteamFarm
|
||||
isFirstStart = false;
|
||||
}
|
||||
|
||||
(var isSuccess, var ipcUrl) = await StartProcess();
|
||||
(var isSuccess, var ipcUrl) = await StartProcessAsync();
|
||||
if (!isSuccess && !isFirstStart)
|
||||
throw new ArgumentException();
|
||||
else
|
||||
@@ -85,30 +87,52 @@ public partial class ArchiSteamFarmServiceImpl : ReactiveObject, IArchiSteamFarm
|
||||
|
||||
#region StartProcess
|
||||
|
||||
private async Task<(bool IsSuccess, string Message)> StartProcess()
|
||||
private async Task<(bool IsSuccess, string Message)> StartProcessAsync()
|
||||
{
|
||||
var isStartSuccess = false;
|
||||
var ipcUrl = string.Empty;
|
||||
try
|
||||
|
||||
if (ASFProcess != null || string.IsNullOrEmpty(SharedInfo.ASFExecuteFilePath))
|
||||
{
|
||||
if (ASFProcess != null || string.IsNullOrEmpty(SharedInfo.ASFExecuteFilePath))
|
||||
{
|
||||
Toast.Show(ToastIcon.Error, BDStrings.ASF_SelectASFExePath);
|
||||
return (isStartSuccess, ipcUrl);
|
||||
}
|
||||
Toast.Show(ToastIcon.Error, BDStrings.ASF_SelectASFExePath);
|
||||
return (isStartSuccess, ipcUrl);
|
||||
}
|
||||
|
||||
if (ASFSettings.CheckArchiSteamFarmExe && !await CheckFileConsistence()) // 检查文件是否被篡改
|
||||
{
|
||||
Toast.Show(ToastIcon.Error, BDStrings.ASF_ExecuteFileUnsafe);
|
||||
return (isStartSuccess, ipcUrl);
|
||||
}
|
||||
if (ASFSettings.CheckArchiSteamFarmExe && !await CheckFileConsistence()) // 检查文件是否被篡改
|
||||
{
|
||||
Toast.Show(ToastIcon.Error, BDStrings.ASF_ExecuteFileUnsafe);
|
||||
return (isStartSuccess, ipcUrl);
|
||||
}
|
||||
|
||||
ipcUrl = GetIPCUrl();
|
||||
webApiService.SetIPCUrl(ipcUrl); // 设置 IPC 接口地址
|
||||
ipcUrl = GetIPCUrl();
|
||||
webApiService.SetIPCUrl(ipcUrl); // 设置 IPC 接口地址
|
||||
|
||||
KillASFProcess(); // 杀死未关闭的 ASF 进程
|
||||
KillASFProcess(); // 杀死未关闭的 ASF 进程
|
||||
|
||||
var options = new ProcessStartInfo(SharedInfo.ASFExecuteFilePath);
|
||||
ASFProcess = StartAsync(SharedInfo.ASFExecuteFilePath);
|
||||
ASFProcessId = ASFProcess?.Id;
|
||||
|
||||
ASFService.Current.ConsoleLogText = string.Empty;
|
||||
Task2.InBackground(ReadOutPutData, true);
|
||||
AppDomain.CurrentDomain.ProcessExit += ExitHandler;
|
||||
AppDomain.CurrentDomain.UnhandledException += ExitHandler;
|
||||
|
||||
ASFProcess!.ErrorDataReceived += new DataReceivedEventHandler(ExitHandler);
|
||||
ASFProcess.BeginErrorReadLine();
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
while (
|
||||
!(isStartSuccess = SocketHelper.IsUsePort(CurrentIPCPortValue)) &&
|
||||
!cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
continue;
|
||||
}
|
||||
return (isStartSuccess, ipcUrl);
|
||||
|
||||
Process? StartAsync(string fileName)
|
||||
{
|
||||
var options = new ProcessStartInfo(fileName);
|
||||
options.CreateNoWindow = true;
|
||||
options.UseShellExecute = false;
|
||||
options.RedirectStandardOutput = true;
|
||||
@@ -120,26 +144,8 @@ public partial class ArchiSteamFarmServiceImpl : ReactiveObject, IArchiSteamFarm
|
||||
options.ArgumentList.Add("--CRYPTKEY");
|
||||
options.ArgumentList.Add(EncryptionKey);
|
||||
}
|
||||
ASFProcess = Process.Start(options);
|
||||
|
||||
ASFService.Current.ConsoleLogText = string.Empty;
|
||||
Task2.InBackground(ReadOutPutData, true);
|
||||
AppDomain.CurrentDomain.ProcessExit += ExitHandler;
|
||||
AppDomain.CurrentDomain.UnhandledException += ExitHandler;
|
||||
|
||||
ASFProcess!.ErrorDataReceived += new DataReceivedEventHandler(ExitHandler);
|
||||
ASFProcess.BeginErrorReadLine();
|
||||
while (!SocketHelper.IsUsePort(CurrentIPCPortValue))
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
continue;
|
||||
}
|
||||
isStartSuccess = true;
|
||||
return Process.Start(options);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return (isStartSuccess, ipcUrl);
|
||||
}
|
||||
|
||||
private void KillASFProcess()
|
||||
@@ -306,7 +312,7 @@ public partial class ArchiSteamFarmServiceImpl : ReactiveObject, IArchiSteamFarm
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Restart()
|
||||
public async Task RestartAsync()
|
||||
{
|
||||
Toast.Show(ToastIcon.Info, AppResources.ASF_Restarting, ToastLength.Short);
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class ASFService : ReactiveObject
|
||||
{
|
||||
SteamBotsSourceList = new SourceCache<BotViewModel, string>(t => t.Bot.BotName);
|
||||
|
||||
archiSteamFarmService.OnConsoleWirteLine += OnConsoleWirteLine;
|
||||
archiSteamFarmService.OnConsoleWirteLine += OnConsoleWriteLine;
|
||||
|
||||
ASFSettings.ConsoleMaxLine.Subscribe(x =>
|
||||
{
|
||||
@@ -58,9 +58,9 @@ public sealed class ASFService : ReactiveObject
|
||||
});
|
||||
}
|
||||
|
||||
void OnConsoleWirteLine(string message)
|
||||
void OnConsoleWriteLine(string message)
|
||||
{
|
||||
MainThread2.InvokeOnMainThreadAsync(() =>
|
||||
MainThread2.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
//ConsoleLogBuilder.Append(message); // message 包含换行
|
||||
//var text = ConsoleLogBuilder.ToString();
|
||||
@@ -74,7 +74,7 @@ public sealed class ASFService : ReactiveObject
|
||||
{
|
||||
return;
|
||||
}
|
||||
archiSteamFarmService.ShellMessageInput(input);
|
||||
_ = archiSteamFarmService.ShellMessageInput(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -156,8 +156,20 @@ public sealed class ASFService : ReactiveObject
|
||||
|
||||
public void OpenFolder(string tag)
|
||||
{
|
||||
if (!Enum.TryParse<EPathFolder>(tag, true, out var folderASFPath)) return;
|
||||
var folderASFPathValue = folderASFPath.GetFolderPath();
|
||||
if (string.IsNullOrEmpty(ASFSettings.ArchiSteamFarmExePath.Value))
|
||||
{
|
||||
Toast.Show(ToastIcon.Error, BDStrings.ASF_SetExePathFirst);
|
||||
return;
|
||||
}
|
||||
|
||||
string folderASFPathValue = string.Empty;
|
||||
if (!Enum.TryParse<EPathFolder>(tag, true, out var folderASFPath) ||
|
||||
!Path.Exists(folderASFPathValue = folderASFPath.GetFolderPath()))
|
||||
{
|
||||
Toast.Show(ToastIcon.Error, BDStrings.ASF_SelectASFExePath);
|
||||
return;
|
||||
}
|
||||
|
||||
IPlatformService.Instance.OpenFolder(folderASFPathValue);
|
||||
}
|
||||
|
||||
@@ -287,7 +299,7 @@ public sealed class ASFService : ReactiveObject
|
||||
Toast.Show(ToastIcon.Success, string.Format(AppResources.LocalAuth_ImportSuccessTip_, num));
|
||||
}
|
||||
|
||||
public async Task SelectASFProgramLocation()
|
||||
public async Task SelectASFProgramLocationAsync()
|
||||
{
|
||||
AvaloniaFilePickerFileTypeFilter fileTypes = new AvaloniaFilePickerFileTypeFilter.Item[] {
|
||||
new("ArchiSteamFarm") {
|
||||
@@ -302,5 +314,77 @@ public sealed class ASFService : ReactiveObject
|
||||
ASFSettings.ArchiSteamFarmExePath.Value = path;
|
||||
}, fileTypes);
|
||||
}
|
||||
|
||||
public async void DownloadASFAsync(string variant = "win-x64", IProgress<float>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assetName = $"{nameof(ASF)}-{variant}.zip";
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", IHttpPlatformHelperService.Instance.UserAgent);
|
||||
using var latestReleaseStream = await httpClient.GetStreamAsync("https://api.github.com/repos/JustArchiNET/ArchiSteamFarm/releases/latest", cancellationToken);
|
||||
using var element = await JsonDocument.ParseAsync(latestReleaseStream, cancellationToken: cancellationToken);
|
||||
if (element.RootElement.TryGetProperty("assets", out var assets) &&
|
||||
assets.EnumerateArray().FirstOrDefault(s => s.GetProperty("name").ValueEquals(assetName))
|
||||
.TryGetProperty("browser_download_url", out var downloadUrl))
|
||||
{
|
||||
var tag_name = element.RootElement.GetProperty("tag_name").GetString();
|
||||
var downloadSavingPath = Path.Combine(Plugin.Instance.AppDataDirectory, $"ASF-{tag_name}", assetName);
|
||||
var downloadSavingDir = Path.GetDirectoryName(downloadSavingPath)!;
|
||||
Directory.CreateDirectory(downloadSavingDir);
|
||||
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, downloadUrl.GetString());
|
||||
var result = await httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
using var contentStream = await result.Content.ReadAsStreamAsync(cancellationToken);
|
||||
|
||||
byte batch = 0;
|
||||
long readThisBatch = 0;
|
||||
long batchIncreaseSize = result.Content.Headers.ContentLength.GetValueOrDefault() / 100;
|
||||
await using (FileStream fileStream = new(downloadSavingPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true))
|
||||
{
|
||||
ArrayPool<byte> bytePool = ArrayPool<byte>.Shared;
|
||||
byte[] buffer = bytePool.Rent(4096);
|
||||
|
||||
try
|
||||
{
|
||||
while (contentStream.CanRead)
|
||||
{
|
||||
int read = await contentStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
|
||||
|
||||
if (read == 0)
|
||||
break;
|
||||
|
||||
await fileStream.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
|
||||
if ((progress == null) || (batchIncreaseSize == 0) || (batch >= 99))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
readThisBatch += read;
|
||||
|
||||
while ((readThisBatch >= batchIncreaseSize) && (batch < 99))
|
||||
{
|
||||
readThisBatch -= batchIncreaseSize;
|
||||
progress.Report(++batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
bytePool.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
ZipFile.ExtractToDirectory(downloadSavingPath, downloadSavingDir);
|
||||
ASFSettings.ArchiSteamFarmExePath.Value = Path.Combine(downloadSavingDir, "ArchiSteamFarm.exe");
|
||||
progress?.Report(100);
|
||||
Toast.Show(ToastIcon.Success, "ASF 下载成功");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Toast.LogAndShowT(ex, "ASF 文件下载异常");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+16
-1
@@ -4,8 +4,23 @@ public partial class ArchiSteamFarmExePathSettingsPageViewModel : ViewModelBase
|
||||
{
|
||||
public ICommand SelectProgramPath { get; }
|
||||
|
||||
public ICommand DownloadASF { get; }
|
||||
|
||||
public ArchiSteamFarmExePathSettingsPageViewModel()
|
||||
{
|
||||
SelectProgramPath = ReactiveCommand.Create(ASFService.Current.SelectASFProgramLocation);
|
||||
SelectProgramPath = ReactiveCommand.Create(ASFService.Current.SelectASFProgramLocationAsync);
|
||||
DownloadASF = ReactiveCommand.Create(() =>
|
||||
{
|
||||
if (INotificationService.Instance.IsSupportNotifyDownload)
|
||||
{
|
||||
var progress = INotificationService.Instance.NotifyDownload(() => "开始下载 ASF ", NotificationType.NewVersion);
|
||||
ASFService.Current.DownloadASFAsync(progress: progress);
|
||||
Toast.Show(ToastIcon.Info, "等待后台下载文件", ToastLength.Short);
|
||||
}
|
||||
else
|
||||
{
|
||||
Toast.Show(ToastIcon.Error, "当前平台无法调用下载通知,请自行下载", ToastLength.Short);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -9,7 +9,7 @@ public sealed partial class ArchiSteamFarmPlusPageViewModel : ViewModelBase
|
||||
|
||||
public ArchiSteamFarmPlusPageViewModel()
|
||||
{
|
||||
SelectASFExePath = ReactiveCommand.CreateFromTask(ASFService.Current.SelectASFProgramLocation);
|
||||
SelectASFExePath = ReactiveCommand.CreateFromTask(ASFService.Current.SelectASFProgramLocationAsync);
|
||||
|
||||
OpenWebUIConsole = ReactiveCommand.Create(() => ASFService.Current.OpenBrowser(null));
|
||||
|
||||
@@ -18,12 +18,12 @@ public sealed partial class ArchiSteamFarmPlusPageViewModel : ViewModelBase
|
||||
RunOrStop = ReactiveCommand.Create(RunOrStopASF);
|
||||
}
|
||||
|
||||
public override void Activation()
|
||||
public override async void Activation()
|
||||
{
|
||||
base.Activation();
|
||||
if (string.IsNullOrEmpty(ASFSettings.ArchiSteamFarmExePath.Value))
|
||||
{
|
||||
MainThread2.InvokeOnMainThreadAsync(async () =>
|
||||
await MainThread2.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
var model = new ArchiSteamFarmExePathSettingsPageViewModel();
|
||||
await IWindowManager.Instance.ShowTaskDialogAsync(model,
|
||||
@@ -44,7 +44,16 @@ public sealed partial class ArchiSteamFarmPlusPageViewModel : ViewModelBase
|
||||
StartOrStopASF_Click();
|
||||
}
|
||||
|
||||
public static void StartOrStopASF_Click(bool? startOrStop = null) => Task.Run(async () =>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
ASFService.Current.StopASFAsync().Wait();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
public void StartOrStopASF_Click(bool? startOrStop = null) => _ = Task.Run(async () =>
|
||||
{
|
||||
var s = ASFService.Current;
|
||||
if (!s.IsASFRuning)
|
||||
|
||||
+11
@@ -26,5 +26,16 @@
|
||||
<Button Command="{Binding SelectProgramPath}" Content="选择路径" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
<ui:SettingsExpander
|
||||
Description="下载并设置 ASF 路径"
|
||||
Header="还没有下载过ASF?请点击下载"
|
||||
IsClickEnabled="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<ui:FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button Command="{Binding DownloadASF}" Content="一键下载" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
@@ -32,7 +32,9 @@ static class SharedInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
return Path.GetDirectoryName(ASFSettingsExecuteFilePath) ?? string.Empty;
|
||||
return !string.IsNullOrWhiteSpace(ASFSettingsExecuteFilePath)
|
||||
? Path.GetDirectoryName(ASFSettingsExecuteFilePath) ?? string.Empty
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,10 +63,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="BD.Common.XmlnsDefinition.Avalonia">
|
||||
<Aliases>BD_Common_XmlnsDefinition_Avalonia</Aliases>
|
||||
</PackageReference>-->
|
||||
<PackageReference Include="ReactiveUI.Fody" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -89,12 +89,12 @@ public sealed partial class SteamTradePageViewModel : WindowViewModel
|
||||
if (_steamClient.Session == null || _steamAuthenticator == null)
|
||||
return false;
|
||||
|
||||
// token 依旧有效 直接返回
|
||||
if (!_steamClient.Session.IsAccessTokenExpired())
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
// token 依旧有效 直接返回
|
||||
if (!_steamClient.Session.IsAccessTokenExpired())
|
||||
return true;
|
||||
|
||||
// 是否有 refresh token
|
||||
string? refreshToken = _steamClient.Session?.RefreshToken;
|
||||
|
||||
@@ -109,6 +109,12 @@ public sealed partial class SteamTradePageViewModel : WindowViewModel
|
||||
// 使用原先获取的具体派生类型 修改数据
|
||||
_steamAuthenticator.SessionData = session.ToString();
|
||||
|
||||
// 重置当前 client
|
||||
if (!ResetCurrentClient())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 通过原始令牌引用保存信息
|
||||
await AuthenticatorHelper.SaveAuthenticator(_authenticatorDto)
|
||||
.ConfigureAwait(false);
|
||||
@@ -204,6 +210,30 @@ public sealed partial class SteamTradePageViewModel : WindowViewModel
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
bool ResetCurrentClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_steamAuthenticator == null)
|
||||
return false;
|
||||
|
||||
_steamClient?.Dispose();
|
||||
|
||||
// 重新生成 client
|
||||
_steamClient = _steamAuthenticator
|
||||
.GetClient(ResourceService.GetCurrentCultureSteamLanguageName());
|
||||
|
||||
if (_steamClient == null)
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async Task<IEnumerable<SteamTradeConfirmationModel>?> GetConfirmations(WSteamClient steamClient)
|
||||
{
|
||||
IsLoading = true;
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
x:DataType="spp:AuthenticatorItemModel"
|
||||
Classes.IsActive="{Binding IsSelected}"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="spp|AppItem /template/ TextBlock#HeaderText">
|
||||
<Setter Property="Grid.ColumnSpan" Value="0" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<spp:AppItem
|
||||
Title="{Binding AuthName}"
|
||||
Description="{Binding AuthData.Created, StringFormat={}{0:yyyy-MM-dd HH:mm:ss}}"
|
||||
|
||||
@@ -26,6 +26,8 @@ public sealed partial class PlatformAccount : ReactiveObject
|
||||
|
||||
public bool IsRegDeleteOnClear { get; set; }
|
||||
|
||||
public bool AllFilesRequired { get; set; }
|
||||
|
||||
public string? ExeExtraArgs { get; set; }
|
||||
|
||||
public string? DefaultExePath { get; set; }
|
||||
|
||||
+10
-8
@@ -575,14 +575,16 @@ Windows Registry Editor Version 5.00
|
||||
// FILE OR FOLDER
|
||||
if (PathHelper.HandleFileOrFolder(accFile, savedFile, localCachePath, false, platform.FolderPath)) continue;
|
||||
|
||||
// Could not find file/folder
|
||||
Toast.Show(ToastIcon.Error, AppResources.Error_CannotFindAccountFile_.Format(accFile));
|
||||
|
||||
return false;
|
||||
|
||||
// TODO: Run some action that can be specified in the Platforms.json file
|
||||
// Add for the start, and end of this function -- To allow 'plugins'?
|
||||
// Use reflection?
|
||||
if (platform.AllFilesRequired)
|
||||
{
|
||||
// Could not find file/folder
|
||||
Toast.Show(ToastIcon.Error, AppResources.Error_CannotFindAccountFile_.Format(accFile));
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warn(nameof(CurrnetUserAdd), $"{accFile} Could not find file/folder");
|
||||
}
|
||||
}
|
||||
|
||||
JTokenHelper.SaveRegJson(regJson, platform.RegJsonPath(name));
|
||||
|
||||
+3
-2
@@ -46,7 +46,7 @@ public sealed class SteamPlatformSwitcher : IPlatformSwitcher
|
||||
}
|
||||
else
|
||||
{
|
||||
await steamService.SetSteamCurrentUserAsync(string.Empty);
|
||||
await ClearCurrentLoginUser(platform);
|
||||
foreach (var user in users)
|
||||
{
|
||||
user.MostRecent = false;
|
||||
@@ -62,7 +62,7 @@ public sealed class SteamPlatformSwitcher : IPlatformSwitcher
|
||||
return true;
|
||||
}
|
||||
|
||||
async ValueTask<bool> IPlatformSwitcher.ClearCurrentLoginUser(PlatformAccount platform)
|
||||
public async ValueTask<bool> ClearCurrentLoginUser(PlatformAccount platform)
|
||||
{
|
||||
await steamService.SetSteamCurrentUserAsync(string.Empty);
|
||||
return true;
|
||||
@@ -82,6 +82,7 @@ public sealed class SteamPlatformSwitcher : IPlatformSwitcher
|
||||
|
||||
public async ValueTask NewUserLogin(PlatformAccount platform)
|
||||
{
|
||||
await ClearCurrentLoginUser(platform);
|
||||
await SwapToAccount(null, platform);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"BackupFileTypesIgnore": null,
|
||||
"BackupFileTypesInclude": null,
|
||||
"UniqueIdPath": "REG:HKCU\\Software\\Epic Games\\Unreal Engine\\Identifiers:AccountId",
|
||||
"UniqueIdType": 2
|
||||
"UniqueIdType": 2,
|
||||
"AllFilesRequired": true
|
||||
},
|
||||
{
|
||||
"FullName": "Ubisoft",
|
||||
@@ -41,6 +42,7 @@
|
||||
"%LocalAppData%\\Ubisoft Game Launcher\\user.dat": "user.dat"
|
||||
},
|
||||
"ClearPaths": [ "SAME_AS_LOGIN_FILES" ],
|
||||
"ProfilePicPath": "%Platform_Folder%\\cache\\avatars\\%UniqueId%_256.png",
|
||||
"CachePaths": [
|
||||
"%Platform_Folder%\\cache\\achievements",
|
||||
"%Platform_Folder%\\cache\\assets",
|
||||
@@ -102,7 +104,9 @@
|
||||
],
|
||||
"ClearPaths": [ "SAME_AS_LOGIN_FILES" ],
|
||||
"LoginFiles": {
|
||||
"%LocalAppData%\\Riot Games\\Riot Client\\Data\\Cookies\\": "Cookies",
|
||||
"%LocalAppData%\\Riot Games\\Riot Client\\Data\\Cookies": "Cookies",
|
||||
"%LocalAppData%\\Riot Games\\Riot Client\\Data\\Sessions": "Sessions",
|
||||
"%LocalAppData%\\Riot Games\\Riot Client\\Data\\RiotClientPrivateSettings.yaml": "RiotClientPrivateSettings.yaml",
|
||||
"%LocalAppData%\\Riot Games\\Riot Client\\Data\\RiotGamesPrivateSettings.yaml": "RiotGamesPrivateSettings.yaml",
|
||||
"%LocalAppData%\\Riot Games\\Riot Client\\Config\\RiotClientSettings.yaml": "RiotClientSettings.yaml",
|
||||
"%LocalAppData%\\Riot Games\\Riot Client\\WattToolkit.id": "WattToolkit.id"
|
||||
@@ -150,6 +154,74 @@
|
||||
},
|
||||
"IsExitBeforeInteract": true,
|
||||
"UniqueIdPath": "%AppData%\\Battle.net\\Battle.net.config::$.Client.SavedAccountNames",
|
||||
"UniqueIdType": 5
|
||||
"UniqueIdType": 5,
|
||||
"AllFilesRequired": true
|
||||
},
|
||||
{
|
||||
"FullName": "GOG Galaxy",
|
||||
"Icon": "GOGGalaxy",
|
||||
"Platform": 64,
|
||||
"PlatformIds": [ "g", "gog", "gog2" ],
|
||||
"DefaultExePath": "%ProgramFiles(x86)%\\GOG Galaxy\\GalaxyClient.exe",
|
||||
"ExesToEnd": [ "GalaxyClient.exe", "GOG Galaxy Notifications Renderer.exe", "GalaxyClientService.exe", "GalaxyCommunication.exe" ],
|
||||
"ClearPaths": [ "SAME_AS_LOGIN_FILES" ],
|
||||
"LoginFiles": {
|
||||
"%LocalAppData%\\GOG.com\\Galaxy\\Configuration\\config.json": "LocalAppData\\config.json",
|
||||
"REG:HKCU\\Software\\GOG.com\\Galaxy\\settings:username": "userId",
|
||||
"REG:HKCU\\Software\\GOG.com\\Galaxy:refreshToken": "refreshToken",
|
||||
"REG:HKCU\\Software\\GOG.com\\Galaxy\\settings:userId": "UNIQUE_ID",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\Cache\\index": "ProgramData\\Cache\\index",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\Cache\\data_*": "ProgramData\\Cache\\",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\Code Cache\\js\\index-dir\\the-real-index": "ProgramData\\Code Cache\\js\\index-dir\\the-real-index",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\GPUCache\\index": "ProgramData\\GPUCache\\index",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\Local Storage\\leveldb": "ProgramData\\Local Storage\\leveldb\\",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\Session Storage\\*.log": "ProgramData\\Session Storage\\",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\Session Storage\\*.ldb": "ProgramData\\Session Storage\\",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\Cookies": "ProgramData\\Cookies",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\storage\\*": "ProgramData\\storage\\",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\config.json": "ProgramData\\config.json"
|
||||
},
|
||||
"ProfilePicPath": "%ProgramData%\\GOG.com\\Galaxy\\webcache\\%UniqueId%\\user\\%LARGEST%.jpg",
|
||||
"CachePaths": [
|
||||
"C:\\Program Files (x86)\\GOG Galaxy\\GPUCache",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\Cache",
|
||||
"%ProgramData%\\GOG.com\\Galaxy\\webcache\\common\\GPUCache"
|
||||
],
|
||||
"BackupPaths": [
|
||||
"%LocalAppData%\\GOG.com\\Galaxy\\Configuration\\"
|
||||
],
|
||||
"UniqueIdFile": "HKCU\\Software\\GOG.com\\Galaxy\\settings:userId",
|
||||
"UniqueIdType": 2
|
||||
},
|
||||
{
|
||||
"FullName": "Discord",
|
||||
"Icon": "Discord",
|
||||
"Platform": 32768,
|
||||
"PlatformIds": [ "d", "discord" ],
|
||||
"DefaultExePath": "%LocalAppData%\\Discord\\Update.exe",
|
||||
"ExeExtraArgs": "--processStart Discord.exe",
|
||||
"ExesToEnd": [ "Discord.exe" ],
|
||||
"PathListToClear": [ "SAME_AS_LOGIN_FILES" ],
|
||||
"LoginFiles": {
|
||||
"%AppData%\\discord\\settings.json": "settings.json",
|
||||
"%AppData%\\discord\\Network Persistent State": "Network Persistent State",
|
||||
"%AppData%\\discord\\Preferences": "Preferences",
|
||||
"%AppData%\\discord\\TransportSecurity": "TransportSecurity",
|
||||
"%AppData%\\discord\\Cache\\index": "Cache\\index",
|
||||
"%AppData%\\discord\\Cache\\data_*": "Cache\\",
|
||||
"%AppData%\\discord\\Code Cache\\js\\index-dir\\the-real-index": "Code Cache\\js\\index-dir\\the-real-index",
|
||||
"%AppData%\\discord\\GPUCache\\index": "GPUCache\\index",
|
||||
"%AppData%\\discord\\Local Storage\\leveldb": "Local Storage\\leveldb\\",
|
||||
"%AppData%\\discord\\Session Storage\\*.log": "Session Storage\\",
|
||||
"%AppData%\\discord\\Session Storage\\*.ldb": "Session Storage\\",
|
||||
"%AppData%\\discord\\blob_storage": "blob_storage\\",
|
||||
"%AppData%\\discord\\WattToolkit.id": "WattToolkit.id"
|
||||
},
|
||||
"IsExitBeforeInteract": true,
|
||||
"CachePaths": [
|
||||
"%AppData%\\discord\\Cache\\f_*"
|
||||
],
|
||||
"UniqueIdFile": "%AppData%\\discord\\WattToolkit.id",
|
||||
"UniqueIdType": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user