diff --git a/v2rayN/ServiceLib/Enums/EViewAction.cs b/v2rayN/ServiceLib/Enums/EViewAction.cs index 075439fd..fba340dc 100644 --- a/v2rayN/ServiceLib/Enums/EViewAction.cs +++ b/v2rayN/ServiceLib/Enums/EViewAction.cs @@ -23,6 +23,7 @@ public enum EViewAction RoutingRuleDetailsWindow, AddServerWindow, AddServer2Window, + AddGroupServerWindow, DNSSettingWindow, RoutingSettingWindow, OptionSettingWindow, diff --git a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs index 7a35b671..4812a0d2 100644 --- a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs @@ -19,10 +19,21 @@ public class ProfileGroupItemManager await InitData(); } - public Task> GetProfileGroupItemList() + // Read-only getters: do not create or mark dirty + public bool TryGet(string indexId, out ProfileGroupItem? item) { - var bag = new ConcurrentBag(_items.Values); - return Task.FromResult(bag); + item = null; + if (string.IsNullOrWhiteSpace(indexId)) + { + return false; + } + + return _items.TryGetValue(indexId, out item); + } + + public ProfileGroupItem? GetOrDefault(string indexId) + { + return string.IsNullOrWhiteSpace(indexId) ? null : (_items.TryGetValue(indexId, out var v) ? v : null); } private async Task InitData() @@ -50,7 +61,7 @@ public class ProfileGroupItemManager { if (string.IsNullOrEmpty(indexId)) { - throw new ArgumentNullException(nameof(indexId)); + indexId = Utils.GetGuid(false); } return _items.GetOrAdd(indexId, AddProfileGroupItem); diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index eddaa0a7..d91ee1f2 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -672,6 +672,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Child Configuration 的本地化字符串。 + /// + public static string menuAddChildServer { + get { + return ResourceManager.GetString("menuAddChildServer", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration Configuration 的本地化字符串。 /// @@ -699,6 +708,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Policy Group Configuration 的本地化字符串。 + /// + public static string menuAddPolicyGroupServer { + get { + return ResourceManager.GetString("menuAddPolicyGroupServer", resourceCulture); + } + } + + /// + /// 查找类似 Add Proxy Chain Configuration 的本地化字符串。 + /// + public static string menuAddProxyChainServer { + get { + return ResourceManager.GetString("menuAddProxyChainServer", resourceCulture); + } + } + /// /// 查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。 /// @@ -1320,6 +1347,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Remove Child Configuration 的本地化字符串。 + /// + public static string menuRemoveChildServer { + get { + return ResourceManager.GetString("menuRemoveChildServer", resourceCulture); + } + } + /// /// 查找类似 Remove duplicate Configurations 的本地化字符串。 /// @@ -1995,6 +2031,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please Add At Least One Configuration 的本地化字符串。 + /// + public static string PleaseAddAtLeastOneServer { + get { + return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture); + } + } + /// /// 查找类似 Please fill Remarks 的本地化字符串。 /// @@ -2373,6 +2418,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group 的本地化字符串。 + /// + public static string TbConfigTypePolicyGroup { + get { + return ResourceManager.GetString("TbConfigTypePolicyGroup", resourceCulture); + } + } + + /// + /// 查找类似 Proxy Chain 的本地化字符串。 + /// + public static string TbConfigTypeProxyChain { + get { + return ResourceManager.GetString("TbConfigTypeProxyChain", resourceCulture); + } + } + /// /// 查找类似 Confirm 的本地化字符串。 /// @@ -2643,6 +2706,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Most Stable 的本地化字符串。 + /// + public static string TbLeastLoad { + get { + return ResourceManager.GetString("TbLeastLoad", resourceCulture); + } + } + + /// + /// 查找类似 Lowest Latency 的本地化字符串。 + /// + public static string TbLeastPing { + get { + return ResourceManager.GetString("TbLeastPing", resourceCulture); + } + } + /// /// 查找类似 Address (IPv4, IPv6) 的本地化字符串。 /// @@ -2697,6 +2778,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group Type 的本地化字符串。 + /// + public static string TbPolicyGroupType { + get { + return ResourceManager.GetString("TbPolicyGroupType", resourceCulture); + } + } + /// /// 查找类似 Port 的本地化字符串。 /// @@ -2769,6 +2859,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Random 的本地化字符串。 + /// + public static string TbRandom { + get { + return ResourceManager.GetString("TbRandom", resourceCulture); + } + } + /// /// 查找类似 v2ray Full Config Template 的本地化字符串。 /// @@ -2832,6 +2931,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Round Robin 的本地化字符串。 + /// + public static string TbRoundRobin { + get { + return ResourceManager.GetString("TbRoundRobin", resourceCulture); + } + } + /// /// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 14b1ff6f..acf0230b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1512,4 +1512,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index fb8c6a11..2b23db1e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1512,4 +1512,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 5b145da3..8202e1c5 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1512,4 +1512,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 27c2d0a3..9550f28d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1512,4 +1512,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index a269c0a5..ae24f0b4 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1509,4 +1509,40 @@ 默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效 + + 请至少添加一个配置文件 + + + 策略组 + + + 链式代理 + + + 最低延迟 + + + 随机 + + + 负载均衡 + + + 最稳定 + + + 策略组类型 + + + 添加策略组配置文件 + + + 添加链式代理配置文件 + + + 添加子配置文件 + + + 删除子配置文件 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 81a01cfc..fdaa2d7c 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1509,4 +1509,40 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs index 5d10160c..2a7f13f3 100644 --- a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -1,4 +1,5 @@ using System.Reactive; +using DynamicData.Binding; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -9,9 +10,203 @@ public class AddGroupServerViewModel : MyReactiveObject [Reactive] public ProfileItem SelectedSource { get; set; } + [Reactive] + public ProfileItem SelectedChild { get; set; } + [Reactive] public IList SelectedChildren { get; set; } - //[Reactive] - //public + [Reactive] + public string? CoreType { get; set; } + + [Reactive] + public string? PolicyGroupType { get; set; } + + public IObservableCollection ChildItemsObs { get; } = new ObservableCollectionExtended(); + + //public ReactiveCommand AddCmd { get; } + public ReactiveCommand RemoveCmd { get; } + public ReactiveCommand MoveTopCmd { get; } + public ReactiveCommand MoveUpCmd { get; } + public ReactiveCommand MoveDownCmd { get; } + public ReactiveCommand MoveBottomCmd { get; } + + public ReactiveCommand SaveCmd { get; } + + public AddGroupServerViewModel(ProfileItem profileItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + RemoveCmd = ReactiveCommand.CreateFromTask(async () => + { + await ChildRemoveAsync(); + }); + MoveTopCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Top); + }); + MoveUpCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Up); + }); + MoveDownCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Down); + }); + MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Bottom); + }); + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveServerAsync(); + }); + + SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem); + + CoreType = (SelectedSource?.CoreType ?? ECoreType.Xray).ToString(); + + ProfileGroupItemManager.Instance.TryGet(profileItem.IndexId, out var profileGroup); + PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch + { + EMultipleLoad.LeastPing => ResUI.TbLeastPing, + EMultipleLoad.Random => ResUI.TbRandom, + EMultipleLoad.RoundRobin => ResUI.TbRoundRobin, + EMultipleLoad.LeastLoad => ResUI.TbLeastLoad, + _ => ResUI.TbLeastPing, + }; + + _ = Init(); + } + + public async Task Init() + { + var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId); + if (childItemMulti != null) + { + var childIndexIds = childItemMulti.ChildItems.IsNullOrEmpty() ? new List() : Utils.String2List(childItemMulti.ChildItems); + foreach (var item in childIndexIds) + { + var child = await AppManager.Instance.GetProfileItem(item); + if (child == null) + { + continue; + } + ChildItemsObs.Add(child); + } + } + } + + public async Task ChildRemoveAsync() + { + if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + ChildItemsObs.Remove(SelectedChild); + await Task.CompletedTask; + } + + public async Task MoveServer(EMove eMove) + { + if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + var index = ChildItemsObs.IndexOf(SelectedChild); + if (index < 0) + { + return; + } + switch (eMove) + { + case EMove.Top: + if (index == 0) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(0, SelectedChild); + break; + case EMove.Up: + if (index == 0) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(index - 1, SelectedChild); + break; + case EMove.Down: + if (index == ChildItemsObs.Count - 1) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(index + 1, SelectedChild); + break; + case EMove.Bottom: + if (index == ChildItemsObs.Count - 1) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Add(SelectedChild); + break; + default: + break; + } + await Task.CompletedTask; + } + + private async Task SaveServerAsync() + { + var remarks = SelectedSource.Remarks; + if (remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); + return; + } + if (ChildItemsObs.Count == 0) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer); + return; + } + SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) || + SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup)) + { + return; + } + var childIndexIds = new List(); + foreach (var item in ChildItemsObs) + { + if (item.IndexId.IsNotEmpty()) + { + childIndexIds.Add(item.IndexId); + } + } + SelectedSource.Address = Utils.List2String(childIndexIds); + var profileGroup = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource.IndexId); + profileGroup.ChildItems = Utils.List2String(childIndexIds); + profileGroup.MultipleLoad = PolicyGroupType switch + { + var s when s == ResUI.TbLeastPing => EMultipleLoad.LeastPing, + var s when s == ResUI.TbRandom => EMultipleLoad.Random, + var s when s == ResUI.TbRoundRobin => EMultipleLoad.RoundRobin, + var s when s == ResUI.TbLeastLoad => EMultipleLoad.LeastLoad, + _ => EMultipleLoad.LeastPing, + }; + if (await ConfigHandler.AddGroupServerCommon(_config, SelectedSource, profileGroup, true) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } } diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index aedf8bbf..06ca3184 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -23,6 +23,8 @@ public class MainWindowViewModel : MyReactiveObject public ReactiveCommand AddWireguardServerCmd { get; } public ReactiveCommand AddAnytlsServerCmd { get; } public ReactiveCommand AddCustomServerCmd { get; } + public ReactiveCommand AddPolicyGroupServerCmd { get; } + public ReactiveCommand AddProxyChainServerCmd { get; } public ReactiveCommand AddServerViaClipboardCmd { get; } public ReactiveCommand AddServerViaScanCmd { get; } public ReactiveCommand AddServerViaImageCmd { get; } @@ -122,6 +124,14 @@ public class MainWindowViewModel : MyReactiveObject { await AddServerAsync(true, EConfigType.Custom); }); + AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.PolicyGroup); + }); + AddProxyChainServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.ProxyChain); + }); AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () => { await AddServerViaClipboardAsync(null); @@ -341,6 +351,10 @@ public class MainWindowViewModel : MyReactiveObject { ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); } + else if (eConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + } else { ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index b36c933c..636f92b3 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -500,6 +500,10 @@ public class ProfilesViewModel : MyReactiveObject { ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); } + else if (eConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + } else { ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); diff --git a/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml b/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml new file mode 100644 index 00000000..41428b3b --- /dev/null +++ b/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml @@ -0,0 +1,216 @@ + + + + +