diff --git a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml
new file mode 100644
index 00000000..225c6418
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs
new file mode 100644
index 00000000..0b9a919f
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs
@@ -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
+{
+ 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
+ {
+ 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 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(this);
+ if (result == true)
+ {
+ var profile = await selectWindow.ProfileItem;
+ if (profile != null && ViewModel != null)
+ {
+ ViewModel.ChildItemsObs.Add(profile);
+ }
+ }
+ }
+}
diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml
index 91476dd7..b66a98a4 100644
--- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml
@@ -35,6 +35,8 @@
+
+
diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
index 6c56cefc..82d33833 100644
--- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
@@ -83,6 +83,8 @@ public partial class MainWindow : WindowBase
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);
@@ -207,6 +209,11 @@ public partial class MainWindow : WindowBase
return false;
return await new AddServer2Window((ProfileItem)obj).ShowDialog(this);
+ case EViewAction.AddGroupServerWindow:
+ if (obj is null)
+ return false;
+ return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog(this);
+
case EViewAction.DNSSettingWindow:
return await new DNSSettingWindow().ShowDialog(this);
diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs
index 88763851..a147921f 100644
--- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs
@@ -167,6 +167,11 @@ public partial class ProfilesView : ReactiveUserControl
return false;
return await new AddServer2Window((ProfileItem)obj).ShowDialog(_window);
+ case EViewAction.AddGroupServerWindow:
+ if (obj is null)
+ return false;
+ return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog(_window);
+
case EViewAction.ShareServer:
if (obj is null)
return false;