Add custom outbound support (#9817)

* Add custom outbound support

* Add test

* Add inner fmt support

* Full config to outbounds

* Rename to `Outbound`

* AI optimized

* Fix

* Add bind interface placeholder

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
This commit is contained in:
DHR60
2026-08-05 06:25:16 +00:00
committed by GitHub
parent ccf18ba0fb
commit eff584597f
43 changed files with 1364 additions and 367 deletions
@@ -103,57 +103,102 @@
HorizontalAlignment="Left"
MaxDropDownHeight="1000" />
<TextBlock
<Separator
Grid.Row="4"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbDisplayLog}" />
<StackPanel
Grid.Row="4"
Grid.Column="1"
Margin="{StaticResource Margin4}"
Orientation="Horizontal">
Grid.ColumnSpan="3"
Margin="{StaticResource Margin4}" />
<Grid
x:Name="gridCustomServer"
Grid.Row="5"
Grid.Column="0"
Grid.ColumnSpan="3"
ColumnDefinitions="Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbDisplayLog}" />
<StackPanel
Grid.Row="0"
Grid.Column="1"
Margin="{StaticResource Margin4}"
Orientation="Horizontal">
<ToggleSwitch
x:Name="togDisplayLog"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TipDisplayLog}" />
</StackPanel>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort}" />
<TextBox
x:Name="txtPreSocksPort"
Grid.Row="1"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<StackPanel
Grid.Row="2"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="{StaticResource Margin4}">
<TextBlock
Width="500"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TipPreSocksPort}"
TextWrapping="Wrap" />
<TextBlock
Width="500"
Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.CustomServerTips}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<Grid
x:Name="gridCustomOutbound"
Grid.Row="5"
Grid.Column="0"
Grid.ColumnSpan="3"
ColumnDefinitions="Auto,Auto,Auto"
IsVisible="False"
RowDefinitions="Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="Sing-Box Endpoint" />
<ToggleSwitch
x:Name="togDisplayLog"
x:Name="togSingBoxEndpoint"
Grid.Row="0"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TipDisplayLog}" />
</StackPanel>
<TextBlock
Grid.Row="5"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort}" />
<TextBox
x:Name="txtPreSocksPort"
Grid.Row="5"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<StackPanel
Grid.Row="6"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="{StaticResource Margin4}">
<TextBlock
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Width="500"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TipPreSocksPort}"
Text="{x:Static resx:ResUI.TbCustomOutboundTip}"
TextWrapping="Wrap" />
<TextBlock
Width="500"
Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.CustomServerTips}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Grid>
</ScrollViewer>
</DockPanel>
@@ -12,15 +12,19 @@ public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => Close();
cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
this.WhenActivated(disposables =>
{
this.WhenAnyValue(v => v.ViewModel.SelectedSource)
.KeepNotNull()
.Subscribe(InitializeData)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Address, v => v.txtAddress.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.DisplayLog, v => v.togDisplayLog.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsSingboxEndpoint, v => v.togSingBoxEndpoint.IsChecked).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.BrowseServerCmd, v => v.btnBrowse).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.EditServerCmd, v => v.btnEdit).DisposeWith(disposables);
@@ -34,6 +38,24 @@ public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
});
}
private void InitializeData(ProfileItem profileItem)
{
if (profileItem.ConfigType is EConfigType.Custom)
{
Title = ResUI.menuAddCustomServer;
cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList();
gridCustomServer.IsVisible = true;
gridCustomOutbound.IsVisible = false;
}
else if (profileItem.ConfigType is EConfigType.Outbound)
{
Title = ResUI.menuAddCustomOutboundServer;
cmbCoreType.ItemsSource = Global.CoreTypes;
gridCustomServer.IsVisible = false;
gridCustomOutbound.IsVisible = true;
}
}
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
txtRemarks.Focus();
@@ -33,9 +33,11 @@
Header="{x:Static resx:ResUI.menuAddServerViaScan}"
InputGesture="Ctrl+S" />
<MenuItem x:Name="menuAddServerViaImage" Header="{x:Static resx:ResUI.menuAddServerViaImage}" />
<Separator />
<MenuItem x:Name="menuAddCustomServer" Header="{x:Static resx:ResUI.menuAddCustomServer}" />
<MenuItem x:Name="menuAddPolicyGroupServer" Header="{x:Static resx:ResUI.menuAddPolicyGroupServer}" />
<MenuItem x:Name="menuAddProxyChainServer" Header="{x:Static resx:ResUI.menuAddProxyChainServer}" />
<MenuItem x:Name="menuAddCustomOutboundServer" Header="{x:Static resx:ResUI.menuAddCustomOutboundServer}" />
<Separator />
<MenuItem x:Name="menuAddVmessServer" Header="{x:Static resx:ResUI.menuAddVmessServer}" />
<MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" />
@@ -47,6 +47,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables);
+28 -15
View File
@@ -34,7 +34,7 @@
</StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid ColumnDefinitions="Auto,400,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,400,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
@@ -69,8 +69,8 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
TextWrapping="Wrap"
PlaceholderText="{x:Static resx:ResUI.SubUrlTips}" />
PlaceholderText="{x:Static resx:ResUI.SubUrlTips}"
TextWrapping="Wrap" />
<Button
Grid.Row="2"
Grid.Column="2"
@@ -100,8 +100,8 @@
VerticalAlignment="Center"
Classes="TextArea"
MinLines="4"
TextWrapping="Wrap"
PlaceholderText="{x:Static resx:ResUI.SubUrlTips}" />
PlaceholderText="{x:Static resx:ResUI.SubUrlTips}"
TextWrapping="Wrap" />
</StackPanel>
</Flyout>
</Button.Flyout>
@@ -179,8 +179,8 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
TextWrapping="Wrap"
PlaceholderText="{x:Static resx:ResUI.SubUrlTips}" />
PlaceholderText="{x:Static resx:ResUI.SubUrlTips}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="8"
@@ -241,31 +241,44 @@
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
<TextBox
x:Name="txtPreSocksPort"
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
<ComboBox
x:Name="cmbCustomCoreType"
Grid.Row="11"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}"
PlaceholderText="{x:Static resx:ResUI.TipPreSocksPort}" />
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="12"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
<TextBox
x:Name="txtPreSocksPort"
Grid.Row="12"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
PlaceholderText="{x:Static resx:ResUI.TipPreSocksPort}"
ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" />
<TextBlock
Grid.Row="13"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvMemo}" />
<TextBox
x:Name="txtMemo"
Grid.Row="12"
Grid.Row="13"
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</Grid>
</ScrollViewer>
</DockPanel>
@@ -12,6 +12,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
btnCancel.Click += (s, e) => Close();
cmbConvertTarget.ItemsSource = Global.SubConvertTargets;
cmbCustomCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
this.WhenActivated(disposables =>
{
@@ -28,6 +29,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.NextProfile, v => v.txtNextProfile.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.SelectedValue).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SelectPrevProfileCmd, v => v.btnSelectPrevProfile).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SelectNextProfileCmd, v => v.btnSelectNextProfile).DisposeWith(disposables);