This commit is contained in:
DHR60 2025-09-11 12:10:59 +08:00
parent 8c094dd976
commit 6166b6c0e3
5 changed files with 312 additions and 0 deletions

View file

@ -0,0 +1,149 @@
<Window
x:Class="v2rayN.Desktop.Views.AddGroupServerWindow"
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:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="{x:Static resx:ResUI.menuServers}"
Width="900"
Height="700"
x:DataType="vms:AddGroupServerViewModel"
ShowInTaskbar="False"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin4}"
HorizontalAlignment="Center"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
</StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid RowDefinitions="Auto,*,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid
Grid.Row="0"
ColumnDefinitions="180,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.menuServers}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRemarks}" />
<TextBox
x:Name="txtRemarks"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCoreType}" />
<ComboBox
x:Name="cmbCoreType"
Grid.Row="2"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<Grid
x:Name="gridPolicyGroup"
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="3"
ColumnDefinitions="180,Auto,Auto">
<TextBlock
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPolicyGroupType}" />
<ComboBox
x:Name="cmbPolicyGroupType"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
</Grid>
</Grid>
<DataGrid
x:Name="lstChild"
Grid.Row="1"
AutoGenerateColumns="False"
BorderThickness="1"
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="False"
GridLinesVisibility="All"
HeadersVisibility="Column"
IsReadOnly="True"
ItemsSource="{Binding ChildItemsObs}"
SelectionMode="Extended">
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuAddChildServer" Header="{x:Static resx:ResUI.menuAddChildServer}" />
<MenuItem x:Name="menuRemoveChildServer" Header="{x:Static resx:ResUI.menuRemoveChildServer}" />
<MenuItem x:Name="menuSelectAllChild" Header="{x:Static resx:ResUI.menuSelectAll}" />
<Separator />
<MenuItem x:Name="menuMoveTop" Header="{x:Static resx:ResUI.menuMoveTop}" />
<MenuItem x:Name="menuMoveUp" Header="{x:Static resx:ResUI.menuMoveUp}" />
<MenuItem x:Name="menuMoveDown" Header="{x:Static resx:ResUI.menuMoveDown}" />
<MenuItem x:Name="menuMoveBottom" Header="{x:Static resx:ResUI.menuMoveBottom}" />
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn
Width="150"
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Port}"
Header="{x:Static resx:ResUI.LvPort}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Network}"
Header="{x:Static resx:ResUI.LvTransportProtocol}" />
<DataGridTextColumn
Width="100"
Binding="{Binding StreamSecurity}"
Header="{x:Static resx:ResUI.LvTLS}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</ScrollViewer>
</DockPanel>
</Window>

View file

@ -0,0 +1,149 @@
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using ReactiveUI;
using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views;
public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
{
public AddGroupServerWindow()
{
InitializeComponent();
}
public AddGroupServerWindow(ProfileItem profileItem)
{
InitializeComponent();
this.Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close();
ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Global.CoreTypes;
cmbPolicyGroupType.ItemsSource = new List<string>
{
ResUI.TbLeastPing,
ResUI.TbRandom,
ResUI.TbRoundRobin,
ResUI.TbLeastLoad,
};
switch (profileItem.ConfigType)
{
case EConfigType.PolicyGroup:
this.Title = ResUI.TbConfigTypePolicyGroup;
break;
case EConfigType.ProxyChain:
this.Title = ResUI.TbConfigTypeProxyChain;
gridPolicyGroup.IsVisible = false;
break;
}
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
// Context menu actions that require custom logic (Add, SelectAll)
menuAddChildServer.Click += MenuAddChild_Click;
menuSelectAllChild.Click += (s, e) => lstChild.SelectAll();
// Keyboard shortcuts when focus is within grid
this.AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
lstChild.LoadingRow += LstChild_LoadingRow;
}
private void LstChild_LoadingRow(object? sender, DataGridRowEventArgs e)
{
e.Row.Header = $" {e.Row.Index + 1}";
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
break;
}
return await Task.FromResult(true);
}
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
txtRemarks.Focus();
}
private void AddGroupServerWindow_KeyDown(object? sender, KeyEventArgs e)
{
if (!lstChild.IsKeyboardFocusWithin)
return;
if ((e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Meta)) != 0)
{
if (e.Key == Key.A)
{
lstChild.SelectAll();
e.Handled = true;
}
}
else
{
switch (e.Key)
{
case Key.T:
ViewModel?.MoveServer(EMove.Top);
e.Handled = true;
break;
case Key.U:
ViewModel?.MoveServer(EMove.Up);
e.Handled = true;
break;
case Key.D:
ViewModel?.MoveServer(EMove.Down);
e.Handled = true;
break;
case Key.B:
ViewModel?.MoveServer(EMove.Bottom);
e.Handled = true;
break;
case Key.Delete:
ViewModel?.ChildRemoveAsync();
e.Handled = true;
break;
}
}
}
private async void MenuAddChild_Click(object? sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true)
{
var profile = await selectWindow.ProfileItem;
if (profile != null && ViewModel != null)
{
ViewModel.ChildItemsObs.Add(profile);
}
}
}
}

View file

@ -35,6 +35,8 @@
<MenuItem x:Name="menuAddServerViaScan" Header="{x:Static resx:ResUI.menuAddServerViaScan}" />
<MenuItem x:Name="menuAddServerViaImage" Header="{x:Static resx:ResUI.menuAddServerViaImage}" />
<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}" />
<Separator />
<MenuItem x:Name="menuAddVmessServer" Header="{x:Static resx:ResUI.menuAddVmessServer}" />
<MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" />

View file

@ -85,6 +85,8 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).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);
this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables);
@ -203,6 +205,11 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
return false;
return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(this);
case EViewAction.AddGroupServerWindow:
if (obj is null)
return false;
return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog<bool>(this);
case EViewAction.DNSSettingWindow:
return await new DNSSettingWindow().ShowDialog<bool>(this);

View file

@ -169,6 +169,11 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
return false;
return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(_window);
case EViewAction.AddGroupServerWindow:
if (obj is null)
return false;
return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog<bool>(_window);
case EViewAction.ShareServer:
if (obj is null)
return false;